From 391e9da0c41d737b5cc556bb53103078f41c23db Mon Sep 17 00:00:00 2001 From: ribardej Date: Tue, 21 Oct 2025 15:36:49 +0200 Subject: [PATCH 1/6] feat(tests): Implemented basic tests and github workflow --- .github/workflows/run-tests.yml | 53 ++++++++++++++ 7project/backend/pyproject.toml | 2 + 7project/backend/tests/conftest.py | 22 ++++++ 7project/backend/tests/test_e2e_auth_flow.py | 15 ++++ .../backend/tests/test_integration_app.py | 18 +++++ .../backend/tests/test_unit_user_service.py | 55 ++++++++++++++ requirements.txt | 72 +++++++++++++++++++ 7 files changed, 237 insertions(+) create mode 100644 .github/workflows/run-tests.yml create mode 100644 7project/backend/pyproject.toml create mode 100644 7project/backend/tests/conftest.py create mode 100644 7project/backend/tests/test_e2e_auth_flow.py create mode 100644 7project/backend/tests/test_integration_app.py create mode 100644 7project/backend/tests/test_unit_user_service.py create mode 100644 requirements.txt diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..92e73bf --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,53 @@ +name: Run Python Tests + +# ----------------- +# --- Triggers ---- +# ----------------- +# This section defines when the workflow will run. +on: + # Run on every push to the 'main' branch + push: + branches: [ "main", "30-create-tests-and-set-up-a-github-pipeline" ] + # Also run on every pull request that targets the 'main' branch + pull_request: + branches: [ "main" ] + +# ----------------- +# ------ Jobs ----- +# ----------------- +# A workflow is made up of one or more jobs that can run in parallel or sequentially. +jobs: + # A descriptive name for your job + build-and-test: + # Specifies the virtual machine to run the job on. 'ubuntu-latest' is a common and cost-effective choice. + runs-on: ubuntu-latest + + # ----------------- + # ----- Steps ----- + # ----------------- + # A sequence of tasks that will be executed as part of the job. + steps: + # Step 1: Check out your repository's code + # This action allows the workflow to access your code. + - name: Check out repository code + uses: actions/checkout@v4 + + # Step 2: Set up the Python environment + # This action installs a specific version of Python on the runner. + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' # Use the Python version that matches your project + + # Step 3: Install project dependencies + # Runs shell commands to install the libraries listed in your requirements.txt. + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + # Step 4: Run your tests! + # Executes the pytest command to run your test suite. + - name: Run tests with pytest + run: pytest + working-directory: ./7project/backend \ No newline at end of file diff --git a/7project/backend/pyproject.toml b/7project/backend/pyproject.toml new file mode 100644 index 0000000..ef504fe --- /dev/null +++ b/7project/backend/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pytest.ini_options] +pythonpath = "." \ No newline at end of file diff --git a/7project/backend/tests/conftest.py b/7project/backend/tests/conftest.py new file mode 100644 index 0000000..1c6dca7 --- /dev/null +++ b/7project/backend/tests/conftest.py @@ -0,0 +1,22 @@ +import sys +import types +import pytest +from fastapi.testclient import TestClient + +# Stub sentry_sdk to avoid optional dependency issues during import of app +stub = types.ModuleType("sentry_sdk") +stub.init = lambda *args, **kwargs: None +sys.modules.setdefault("sentry_sdk", stub) + +# Import the FastAPI application +from app.app import fastApi as app # noqa: E402 + + +@pytest.fixture(scope="session") +def fastapi_app(): + return app + + +@pytest.fixture(scope="session") +def client(fastapi_app): + return TestClient(fastapi_app, raise_server_exceptions=True) diff --git a/7project/backend/tests/test_e2e_auth_flow.py b/7project/backend/tests/test_e2e_auth_flow.py new file mode 100644 index 0000000..0cd6fbb --- /dev/null +++ b/7project/backend/tests/test_e2e_auth_flow.py @@ -0,0 +1,15 @@ +from fastapi import status + + +def test_e2e_minimal_auth_flow(client): + # 1) Service is alive + alive = client.get("/") + assert alive.status_code == status.HTTP_200_OK + + # 2) Attempt to login without payload should fail fast (validation error) + login = client.post("/auth/jwt/login") + assert login.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_422_UNPROCESSABLE_ENTITY) + + # 3) Protected endpoint should not be accessible without token + me = client.get("/users/me") + assert me.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN) diff --git a/7project/backend/tests/test_integration_app.py b/7project/backend/tests/test_integration_app.py new file mode 100644 index 0000000..6c8733d --- /dev/null +++ b/7project/backend/tests/test_integration_app.py @@ -0,0 +1,18 @@ +from fastapi import status +import pytest + + +def test_root_ok(client): + resp = client.get("/") + assert resp.status_code == status.HTTP_200_OK + assert resp.json() == {"status": "ok"} + + +def test_authenticated_route_requires_auth(client): + resp = client.get("/authenticated-route") + assert resp.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN) + + +def test_sentry_debug_raises_exception(client): + with pytest.raises(ZeroDivisionError): + client.get("/sentry-debug") diff --git a/7project/backend/tests/test_unit_user_service.py b/7project/backend/tests/test_unit_user_service.py new file mode 100644 index 0000000..d9b57a9 --- /dev/null +++ b/7project/backend/tests/test_unit_user_service.py @@ -0,0 +1,55 @@ +import types +import asyncio +import pytest + +from app.services import user_service + + +def test_get_oauth_provider_known_unknown(): + # Known providers should return a provider instance + bankid = user_service.get_oauth_provider("BankID") + mojeid = user_service.get_oauth_provider("MojeID") + assert bankid is not None + assert mojeid is not None + + # Unknown should return None + assert user_service.get_oauth_provider("DoesNotExist") is None + + +def test_get_jwt_strategy_lifetime(): + strategy = user_service.get_jwt_strategy() + assert strategy is not None + # Basic smoke check: strategy has a lifetime set to 3600 + assert getattr(strategy, "lifetime_seconds", None) in (3600,) + + +@pytest.mark.asyncio +async def test_on_after_request_verify_enqueues_email(monkeypatch): + calls = {} + + def fake_enqueue_email(to: str, subject: str, body: str): + calls.setdefault("emails", []).append({ + "to": to, + "subject": subject, + "body": body, + }) + + # Patch the enqueue_email used inside user_service + monkeypatch.setattr(user_service, "enqueue_email", fake_enqueue_email) + + class DummyUser: + def __init__(self, email): + self.email = email + + mgr = user_service.UserManager(user_db=None) # user_db not needed for this method + user = DummyUser("test@example.com") + + # Call the hook + await mgr.on_after_request_verify(user, token="abc123", request=None) + + # Verify one email has been enqueued with expected content + assert len(calls.get("emails", [])) == 1 + email = calls["emails"][0] + assert email["to"] == "test@example.com" + assert "ověření účtu" in email["subject"].lower() + assert "abc123" in email["body"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f97951f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,72 @@ +aio-pika==9.5.6 +aiormq==6.8.1 +aiosqlite==0.21.0 +alembic==1.16.5 +amqp==5.3.1 +annotated-types==0.7.0 +anyio==4.11.0 +argon2-cffi==23.1.0 +argon2-cffi-bindings==25.1.0 +asyncmy==0.2.9 +bcrypt==4.3.0 +billiard==4.2.2 +celery==5.5.3 +certifi==2025.10.5 +cffi==2.0.0 +click==8.1.8 +click-didyoumean==0.3.1 +click-plugins==1.1.1.2 +click-repl==0.3.0 +cryptography==46.0.1 +dnspython==2.7.0 +email_validator==2.2.0 +exceptiongroup==1.3.0 +fastapi==0.117.1 +fastapi-users==14.0.1 +fastapi-users-db-sqlalchemy==7.0.0 +greenlet==3.2.4 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.6.4 +httpx==0.28.1 +httpx-oauth==0.16.1 +idna==3.10 +iniconfig==2.3.0 +kombu==5.5.4 +makefun==1.16.0 +Mako==1.3.10 +MarkupSafe==3.0.2 +multidict==6.6.4 +packaging==25.0 +pamqp==3.3.0 +pluggy==1.6.0 +prompt_toolkit==3.0.52 +propcache==0.3.2 +pwdlib==0.2.1 +pycparser==2.23 +pydantic==2.11.9 +pydantic_core==2.33.2 +Pygments==2.19.2 +PyJWT==2.10.1 +PyMySQL==1.1.2 +pytest==8.4.2 +pytest-asyncio==1.2.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +PyYAML==6.0.2 +six==1.17.0 +sniffio==1.3.1 +SQLAlchemy==2.0.43 +starlette==0.48.0 +tomli==2.2.1 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +tzdata==2025.2 +uvicorn==0.37.0 +uvloop==0.21.0 +vine==5.1.0 +watchfiles==1.1.0 +wcwidth==0.2.14 +websockets==15.0.1 +yarl==1.20.1 From 76eb2cce415a3518c8b21898853cf46b38119f5f Mon Sep 17 00:00:00 2001 From: ribardej Date: Tue, 21 Oct 2025 15:44:33 +0200 Subject: [PATCH 2/6] feat(tests): Added tests to PR and Prod workflows --- .github/workflows/deploy-pr.yaml | 23 +++++++++++++++++++++++ .github/workflows/deploy-prod.yaml | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/.github/workflows/deploy-pr.yaml b/.github/workflows/deploy-pr.yaml index c59f484..56938bd 100644 --- a/.github/workflows/deploy-pr.yaml +++ b/.github/workflows/deploy-pr.yaml @@ -9,6 +9,29 @@ permissions: pull-requests: write jobs: + test: + name: Run Python Tests + if: github.event.action != 'closed' + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests with pytest + run: pytest + working-directory: ./7project/backend + build: if: github.event.action != 'closed' name: Build and push image (reusable) diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 37f703c..15e5354 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -21,6 +21,29 @@ concurrency: cancel-in-progress: false jobs: + test: + name: Run Python Tests + if: github.event.action != 'closed' + runs-on: ubuntu-latest + + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests with pytest + run: pytest + working-directory: ./7project/backend + build: name: Build and push image (reusable) uses: ./.github/workflows/build-image.yaml From 40d07677bd0da15ecf891b93291fe29d562ffe32 Mon Sep 17 00:00:00 2001 From: Dejan Ribarovski Date: Tue, 21 Oct 2025 15:57:23 +0200 Subject: [PATCH 3/6] Potential fix for code scanning alert no. 11: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/run-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 92e73bf..b71a6d1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,4 +1,6 @@ name: Run Python Tests +permissions: + contents: read # ----------------- # --- Triggers ---- From 6d8a6a55c0986b7a393e1a76df3fc883b07ebdf7 Mon Sep 17 00:00:00 2001 From: ribardej Date: Wed, 22 Oct 2025 14:37:48 +0200 Subject: [PATCH 4/6] fix(backend): refactored app to fastApi to avoid CORS errors --- 7project/backend/app/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index 652ef21..0cc6912 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -23,8 +23,8 @@ sentry_sdk.init( send_default_pii=True, ) -app = FastAPI() fastApi = FastAPI() +app = fastApi # CORS for frontend dev server fastApi.add_middleware( @@ -99,4 +99,4 @@ async def authenticated_route(user: User = Depends(current_active_verified_user) @fastApi.get("/sentry-debug") async def trigger_error(): - division_by_zero = 1 / 0 \ No newline at end of file + division_by_zero = 1 / 0 From aade88beb90af39dca6b442be0fc0855949517df Mon Sep 17 00:00:00 2001 From: ribardej Date: Wed, 22 Oct 2025 14:48:56 +0200 Subject: [PATCH 5/6] fix(backend): added a default value for FRONTEND_DOMAIN_SCHEME so the tests dont crash --- 7project/backend/app/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/7project/backend/app/app.py b/7project/backend/app/app.py index aebe333..ae50a62 100644 --- a/7project/backend/app/app.py +++ b/7project/backend/app/app.py @@ -75,7 +75,7 @@ fastApi.include_router( auth_backend, "SECRET", associate_by_email=True, - redirect_url=os.getenv("FRONTEND_DOMAIN_SCHEME") + "/auth/mojeid/callback", + redirect_url=os.getenv("FRONTEND_DOMAIN_SCHEME", "http://localhost:3000") + "/auth/mojeid/callback", ), prefix="/auth/mojeid", tags=["auth"], @@ -87,7 +87,7 @@ fastApi.include_router( auth_backend, "SECRET", associate_by_email=True, - redirect_url=os.getenv("FRONTEND_DOMAIN_SCHEME") + "/auth/bankid/callback", + redirect_url=os.getenv("FRONTEND_DOMAIN_SCHEME", "http://localhost:3000") + "/auth/bankid/callback", ), prefix="/auth/bankid", tags=["auth"], From e78b8c2e6b147fb42cfececb227a014b01a197ff Mon Sep 17 00:00:00 2001 From: ribardej Date: Wed, 22 Oct 2025 14:53:45 +0200 Subject: [PATCH 6/6] fix(tests): fixed a service test and one warning regarding 422 status --- 7project/backend/tests/test_e2e_auth_flow.py | 2 +- 7project/backend/tests/test_unit_user_service.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/7project/backend/tests/test_e2e_auth_flow.py b/7project/backend/tests/test_e2e_auth_flow.py index 0cd6fbb..e622f9c 100644 --- a/7project/backend/tests/test_e2e_auth_flow.py +++ b/7project/backend/tests/test_e2e_auth_flow.py @@ -8,7 +8,7 @@ def test_e2e_minimal_auth_flow(client): # 2) Attempt to login without payload should fail fast (validation error) login = client.post("/auth/jwt/login") - assert login.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_422_UNPROCESSABLE_ENTITY) + assert login.status_code in (status.HTTP_400_BAD_REQUEST, status.HTTP_422_UNPROCESSABLE_CONTENT) # 3) Protected endpoint should not be accessible without token me = client.get("/users/me") diff --git a/7project/backend/tests/test_unit_user_service.py b/7project/backend/tests/test_unit_user_service.py index d9b57a9..7e89962 100644 --- a/7project/backend/tests/test_unit_user_service.py +++ b/7project/backend/tests/test_unit_user_service.py @@ -20,7 +20,7 @@ def test_get_jwt_strategy_lifetime(): strategy = user_service.get_jwt_strategy() assert strategy is not None # Basic smoke check: strategy has a lifetime set to 3600 - assert getattr(strategy, "lifetime_seconds", None) in (3600,) + assert getattr(strategy, "lifetime_seconds", None) in (604800,) @pytest.mark.asyncio