diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28c03a8f..f8077662 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - python-version: ['3.10'] # Only supported one at the moment + python-version: ['3.10', '3.11'] steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24054bd4..e1bf4c0d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - python-version: ['3.10'] # Only supported one at the moment + python-version: ['3.10', '3.11'] steps: diff --git a/api/main.py b/api/main.py index 68998414..9e16a586 100644 --- a/api/main.py +++ b/api/main.py @@ -10,7 +10,7 @@ import os import re -from typing import List +from typing import List, Union from fastapi import ( Depends, FastAPI, @@ -387,7 +387,7 @@ async def get_user_groups(request: Request): return paginated_resp -@app.get('/group/{group_id}', response_model=UserGroup, +@app.get('/group/{group_id}', response_model=Union[UserGroup, None], response_model_by_alias=False) async def get_group(group_id: str): """Get user group information from the provided group id""" @@ -448,7 +448,7 @@ async def translate_null_query_params(query_params: dict): return translated -@app.get('/node/{node_id}', response_model=Node, +@app.get('/node/{node_id}', response_model=Union[Node, None], response_model_by_alias=False) async def get_node(node_id: str): """Get node information from the provided node id""" diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 9209bd8b..9b9b43d3 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,9 +1,10 @@ # SPDX-License-Identifier: LGPL-2.1-or-later # -# Copyright (C) 2021 Collabora Limited +# Copyright (C) 2021-2024 Collabora Limited # Author: Guillaume Tucker +# Author: Jeny Sadadia -FROM python:3.10 +FROM python:3.11 MAINTAINER "KernelCI TSC" RUN apt-get update && apt-get install --no-install-recommends -y git diff --git a/docker/api/requirements-dev.txt b/docker/api/requirements-dev.txt index e55054e9..89504fd8 100644 --- a/docker/api/requirements-dev.txt +++ b/docker/api/requirements-dev.txt @@ -1,3 +1,3 @@ -r requirements-tests.txt pycodestyle==2.8.0 -pylint==2.12.2 +pylint==3.1.0 diff --git a/docker/api/requirements.txt b/docker/api/requirements.txt index 52106284..5e5c6788 100644 --- a/docker/api/requirements.txt +++ b/docker/api/requirements.txt @@ -1,14 +1,13 @@ cloudevents==1.9.0 -fastapi[all]==0.68.1 +fastapi[all]==0.99.1 fastapi-pagination==0.9.3 fastapi-users[beanie, oauth]==10.4.0 fastapi-versioning==0.10.0 MarkupSafe==2.0.1 -motor==2.5.1 +motor==3.3.2 passlib==1.7.4 pydantic==1.10.5 pymongo-migrate==0.11.0 python-jose[cryptography]==3.3.0 -pyyaml==5.3.1 redis==5.0.1 -uvicorn[standard]==0.13.4 +uvicorn[standard]==0.29.0 diff --git a/pyproject.toml b/pyproject.toml index 3978c93d..62742351 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,19 +13,18 @@ requires-python = ">=3.10" license = {text = "LGPL-2.1-or-later"} dependencies = [ "cloudevents == 1.9.0", - "fastapi[all] == 0.68.1", + "fastapi[all] == 0.99.1", "fastapi-pagination == 0.9.3", "fastapi-users[beanie, oauth] == 10.4.0", "fastapi-versioning == 0.10.0", "MarkupSafe == 2.0.1", - "motor == 2.5.1", + "motor == 3.3.2", "passlib == 1.7.4", "pydantic == 1.10.5", "pymongo-migrate == 0.11.0", "python-jose[cryptography] == 3.3.0", - "pyyaml == 5.3.1", "redis == 5.0.1", - "uvicorn[standard] == 0.13.4", + "uvicorn[standard] == 0.29.0", ] [project.optional-dependencies] @@ -42,7 +41,7 @@ tests = [ dev = [ "kernelci-api[tests]", "pycodestyle == 2.8.0", - "pylint == 2.12.2", + "pylint == 3.1.0", ] [project.urls] diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 10d6ab09..b86a818a 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -10,8 +10,6 @@ from httpx import AsyncClient from motor.motor_asyncio import AsyncIOMotorClient -from fastapi.testclient import TestClient - from api.main import versioned_app from kernelci.api.models import Node, Regression @@ -30,12 +28,6 @@ 'offset', } -@pytest.fixture(scope='session') -def test_client(): - """Fixture to get FastAPI Test client instance""" - with TestClient(app=versioned_app, base_url=BASE_URL) as client: - yield client - @pytest.fixture(scope='session') async def test_async_client(): diff --git a/tests/e2e_tests/test_count_handler.py b/tests/e2e_tests/test_count_handler.py index b65c6bcf..8508a47f 100644 --- a/tests/e2e_tests/test_count_handler.py +++ b/tests/e2e_tests/test_count_handler.py @@ -10,33 +10,35 @@ import pytest +@pytest.mark.asyncio @pytest.mark.dependency( depends=[ 'tests/e2e_tests/test_pipeline.py::test_node_pipeline'], scope='session') -def test_count_nodes(test_client): +async def test_count_nodes(test_async_client): """ Test Case : Test KernelCI API GET /count endpoint Expected Result : HTTP Response Code 200 OK Total number of nodes available """ - response = test_client.get("count") + response = await test_async_client.get("count") assert response.status_code == 200 assert response.json() >= 0 +@pytest.mark.asyncio @pytest.mark.dependency( depends=[ 'tests/e2e_tests/test_pipeline.py::test_node_pipeline'], scope='session') -def test_count_nodes_matching_attributes(test_client): +async def test_count_nodes_matching_attributes(test_async_client): """ Test Case : Test KernelCI API GET /count endpoint with attributes Expected Result : HTTP Response Code 200 OK Number of nodes matching attributes """ - response = test_client.get("count?name=checkout") + response = await test_async_client.get("count?name=checkout") assert response.status_code == 200 assert response.json() >= 0 diff --git a/tests/e2e_tests/test_root_handler.py b/tests/e2e_tests/test_root_handler.py index 75d842e2..56a3cbc2 100644 --- a/tests/e2e_tests/test_root_handler.py +++ b/tests/e2e_tests/test_root_handler.py @@ -5,9 +5,12 @@ """End-to-end test functions for KernelCI API root handler""" +import pytest -def test_root_endpoint(test_client): + +@pytest.mark.asyncio +async def test_root_endpoint(test_async_client): """Test root handler""" - response = test_client.get("/latest") + response = await test_async_client.get("/") assert response.status_code == 200 assert response.json() == {"message": "KernelCI API"} diff --git a/tests/e2e_tests/test_subscribe_handler.py b/tests/e2e_tests/test_subscribe_handler.py index f9a8ee72..11ddef79 100644 --- a/tests/e2e_tests/test_subscribe_handler.py +++ b/tests/e2e_tests/test_subscribe_handler.py @@ -9,18 +9,19 @@ import pytest +@pytest.mark.asyncio @pytest.mark.dependency( depends=['tests/e2e_tests/test_user_creation.py::test_create_regular_user'], scope='session') @pytest.mark.order(3) -def test_subscribe_node_channel(test_client): +async def test_subscribe_node_channel(test_async_client): """ Test Case : Test KernelCI API '/subscribe' endpoint with 'node' channel Expected Result : HTTP Response Code 200 OK JSON with subscription 'id' and 'channel' keys """ - response = test_client.post( + response = await test_async_client.post( "subscribe/node", headers={ "Authorization": f"Bearer {pytest.BEARER_TOKEN}" # pylint: disable=no-member @@ -32,18 +33,19 @@ def test_subscribe_node_channel(test_client): assert response.json().get('channel') == 'node' +@pytest.mark.asyncio @pytest.mark.dependency( depends=['tests/e2e_tests/test_user_creation.py::test_create_regular_user'], scope='session') @pytest.mark.order(3) -def test_subscribe_test_channel(test_client): +async def test_subscribe_test_channel(test_async_client): """ Test Case : Test KernelCI API '/subscribe' endpoint with 'test_channel' Expected Result : HTTP Response Code 200 OK JSON with subscription 'id' and 'channel' keys """ - response = test_client.post( + response = await test_async_client.post( "subscribe/test_channel", headers={ "Authorization": f"Bearer {pytest.BEARER_TOKEN}" # pylint: disable=no-member @@ -55,11 +57,12 @@ def test_subscribe_test_channel(test_client): assert response.json().get('channel') == 'test_channel' +@pytest.mark.asyncio @pytest.mark.dependency( depends=['tests/e2e_tests/test_user_creation.py::test_create_regular_user'], scope='session') @pytest.mark.order(3) -def test_subscribe_user_group_channel(test_client): +async def test_subscribe_user_group_channel(test_async_client): """ Test Case : Test KernelCI API '/subscribe' endpoint with 'user_group' channel @@ -67,7 +70,7 @@ def test_subscribe_user_group_channel(test_client): HTTP Response Code 200 OK JSON with subscription 'id' and 'channel' keys """ - response = test_client.post( + response = await test_async_client.post( "subscribe/user_group", headers={ "Authorization": f"Bearer {pytest.BEARER_TOKEN}" # pylint: disable=no-member diff --git a/tests/e2e_tests/test_unsubscribe_handler.py b/tests/e2e_tests/test_unsubscribe_handler.py index a787da4d..62932b7f 100644 --- a/tests/e2e_tests/test_unsubscribe_handler.py +++ b/tests/e2e_tests/test_unsubscribe_handler.py @@ -9,18 +9,19 @@ import pytest +@pytest.mark.asyncio @pytest.mark.dependency( depends=[ 'tests/e2e_tests/test_subscribe_handler.py::test_subscribe_node_channel'], scope='session') @pytest.mark.order("last") -def test_unsubscribe_node_channel(test_client): +async def test_unsubscribe_node_channel(test_async_client): """ Test Case : Test KernelCI API '/unsubscribe' endpoint with 'node' channel Expected Result : HTTP Response Code 200 OK """ - response = test_client.post( + response = await test_async_client.post( f"unsubscribe/{pytest.node_channel_subscription_id}", # pylint: disable=no-member headers={ "Authorization": f"Bearer {pytest.BEARER_TOKEN}" # pylint: disable=no-member @@ -29,18 +30,19 @@ def test_unsubscribe_node_channel(test_client): assert response.status_code == 200 +@pytest.mark.asyncio @pytest.mark.dependency( depends=[ 'tests/e2e_tests/test_subscribe_handler.py::test_subscribe_test_channel'], scope='session') @pytest.mark.order("last") -def test_unsubscribe_test_channel(test_client): +async def test_unsubscribe_test_channel(test_async_client): """ Test Case : Test KernelCI API '/unsubscribe' endpoint with 'test_channel' Expected Result : HTTP Response Code 200 OK """ - response = test_client.post( + response = await test_async_client.post( f"unsubscribe/{pytest.test_channel_subscription_id}", # pylint: disable=no-member headers={ "Authorization": f"Bearer {pytest.BEARER_TOKEN}" # pylint: disable=no-member diff --git a/tests/e2e_tests/test_user_creation.py b/tests/e2e_tests/test_user_creation.py index 69844b73..e8fd222e 100644 --- a/tests/e2e_tests/test_user_creation.py +++ b/tests/e2e_tests/test_user_creation.py @@ -118,8 +118,9 @@ async def test_create_regular_user(test_async_client): pytest.BEARER_TOKEN = response.json()['access_token'] +@pytest.mark.asyncio @pytest.mark.dependency(depends=["test_create_regular_user"]) -def test_whoami(test_client): +async def test_whoami(test_async_client): """ Test Case : Test KernelCI API /whoami endpoint Expected Result : @@ -127,7 +128,7 @@ def test_whoami(test_client): JSON with 'id', 'email', username', 'groups', 'is_superuser' 'is_verified' and 'is_active' keys """ - response = test_client.get( + response = await test_async_client.get( "whoami", headers={ "Accept": "application/json", @@ -140,8 +141,9 @@ def test_whoami(test_client): assert response.json()['username'] == 'test_user' +@pytest.mark.asyncio @pytest.mark.dependency(depends=["test_create_regular_user"]) -def test_create_user_negative(test_client): +async def test_create_user_negative(test_async_client): """ Test Case : Test KernelCI API /user/register endpoint when requested with regular user's bearer token. @@ -149,7 +151,7 @@ def test_create_user_negative(test_client): HTTP Response Code 403 Forbidden JSON with 'detail' key denoting 'Forbidden' error """ - response = test_client.post( + response = await test_async_client.post( "user/register", headers={ "Accept": "application/json", diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index f029382a..4447b1b9 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -11,7 +11,6 @@ """pytest fixtures for KernelCI API""" from unittest.mock import AsyncMock -import asyncio import fakeredis.aioredis from fastapi.testclient import TestClient from fastapi import Request, HTTPException, status @@ -91,17 +90,16 @@ def mock_get_current_admin_user(request: Request): is_verified=True ) - +# Mock dependency callables for getting current user app.dependency_overrides[get_current_user] = mock_get_current_user app.dependency_overrides[get_current_superuser] = mock_get_current_admin_user -@pytest.fixture +@pytest.fixture(scope='session') def test_client(): """Fixture to get FastAPI Test client instance""" - # Mock dependency callables for getting current user with TestClient(app=versioned_app, base_url=BASE_URL) as client: - return client + yield client @pytest.fixture @@ -113,20 +111,6 @@ async def test_async_client(): await versioned_app.router.shutdown() -@pytest.fixture -def event_loop(): - """Create an instance of the default event loop for each test case. - This is a workaround to prevent the default event loop to be closed by - async pubsub tests. It was causing other tests unable to run. - The issue has already been reported here: - https://github.com/pytest-dev/pytest-asyncio/issues/371 - """ - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - yield loop - loop.close() - - @pytest.fixture def mock_db_create(mocker): """Mocks async call to Database class method used to create object"""