From 436f27ef093f2e29c8b7b51269eaa770a54678c5 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 16:26:47 +0200 Subject: [PATCH 01/14] Add test deps --- pyproject.toml | 2 ++ uv.lock | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index cbe7252..5142a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ dependencies = [ "paho-mqtt>=2.1.0", "pyjwt[crypto]>=2.12.1", "pwdlib[argon2]>=0.3.0", + "pytest>=9.0.3", + "requests>=2.33.1", ] [tool.uv.sources] diff --git a/uv.lock b/uv.lock index 0285ad4..be8057a 100644 --- a/uv.lock +++ b/uv.lock @@ -544,7 +544,9 @@ dependencies = [ { name = "poetry" }, { name = "pwdlib", extra = ["argon2"] }, { name = "pyjwt", extra = ["crypto"] }, + { name = "pytest" }, { name = "python-desfire" }, + { name = "requests" }, { name = "sqlmodel" }, ] @@ -555,7 +557,9 @@ requires-dist = [ { name = "poetry", specifier = ">=2.3.4" }, { name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.12.1" }, + { name = "pytest", specifier = ">=9.0.3" }, { name = "python-desfire", git = "https://github.com/waza-ari/python-desfire" }, + { name = "requests", specifier = ">=2.33.1" }, { name = "sqlmodel", specifier = ">=0.0.38" }, ] @@ -658,6 +662,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "installer" version = "0.7.0" @@ -908,6 +921,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "poetry" version = "2.3.4" @@ -1137,6 +1159,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "python-desfire" version = "0.1.6" From 4e2467c45b831208f7118431e89cfcf7c76eda60 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 16:27:06 +0200 Subject: [PATCH 02/14] Add ai generated tests --- test/conftest.py | 126 +++++++++++++++ test/test_main.py | 24 +++ test/test_models.py | 94 +++++++++++ test/test_services/test_aa_manager.py | 192 ++++++++++++++++++++++ test/test_services/test_auth.py | 195 +++++++++++++++++++++++ test/test_services/test_card_manager.py | 66 ++++++++ test/test_services/test_database.py | 64 ++++++++ test/test_services/test_group_manager.py | 68 ++++++++ test/test_services/test_user_manager.py | 150 +++++++++++++++++ 9 files changed, 979 insertions(+) create mode 100644 test/conftest.py create mode 100644 test/test_main.py create mode 100644 test/test_models.py create mode 100644 test/test_services/test_aa_manager.py create mode 100644 test/test_services/test_auth.py create mode 100644 test/test_services/test_card_manager.py create mode 100644 test/test_services/test_database.py create mode 100644 test/test_services/test_group_manager.py create mode 100644 test/test_services/test_user_manager.py diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..f572437 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,126 @@ +import pytest +from fastapi.testclient import TestClient +from sqlmodel import Session, create_engine, SQLModel +from sqlalchemy.orm import sessionmaker + +from app.main import app +from app.model.models import UserDB, Card, GroupDB, AccessAuthorizationDB, Timetable, AaGroupLink +from app.services.database import get_session + +# Use in-memory SQLite for testing +TEST_SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" + +engine = create_engine(TEST_SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +@pytest.fixture(scope="function") +def db_session(): + """Create a fresh database session for each test.""" + SQLModel.metadata.create_all(engine) + session = TestingSessionLocal() + try: + yield session + finally: + session.close() + SQLModel.metadata.drop_all(engine) + + +@pytest.fixture(scope="function") +def client(db_session): + """Create a test client with a database session override.""" + def override_get_session(): + try: + yield db_session + finally: + pass + + app.dependency_overrides[get_session] = override_get_session + with TestClient(app) as test_client: + yield test_client + app.dependency_overrides.clear() + + +@pytest.fixture +def admin_user(db_session): + """Create an admin user for testing.""" + from app.services.auth import get_password_hash + admin = UserDB( + name="admin", + passwordhash=get_password_hash("admin123"), + is_admin=True + ) + db_session.add(admin) + db_session.commit() + db_session.refresh(admin) + return admin + + +@pytest.fixture +def regular_user(db_session): + """Create a regular user for testing.""" + from app.services.auth import get_password_hash + user = UserDB( + name="user", + passwordhash=get_password_hash("user123"), + is_admin=False + ) + db_session.add(user) + db_session.commit() + db_session.refresh(user) + return user + + +@pytest.fixture +def auth_headers(client, admin_user): + """Get authentication headers for admin user.""" + response = client.post( + "/token", + data={"username": admin_user.name, "password": "admin123"} + ) + token = response.json()["access_token"] + return {"Authorization": f"Bearer {token}"} + + +@pytest.fixture +def user_auth_headers(client, regular_user): + """Get authentication headers for regular user.""" + response = client.post( + "/token", + data={"username": regular_user.name, "password": "user123"} + ) + token = response.json()["access_token"] + return {"Authorization": f"Bearer {token}"} + + +@pytest.fixture +def test_group(db_session): + """Create a test group.""" + group = GroupDB(name="Test Group") + db_session.add(group) + db_session.commit() + db_session.refresh(group) + return group + + +@pytest.fixture +def test_card(db_session, test_group): + """Create a test card.""" + card = Card(uuid="test-uuid-123", group_id=test_group.id) + db_session.add(card) + db_session.commit() + db_session.refresh(card) + return card + + +@pytest.fixture +def test_aa(db_session): + """Create a test access authorization.""" + aa = AccessAuthorizationDB( + name="Test AA", + is_active=True + ) + db_session.add(aa) + db_session.commit() + db_session.refresh(aa) + return aa diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..877df3e --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,24 @@ +def test_app_startup(client): + """Test that the application starts up correctly.""" + response = client.get("/") + # Application should respond (even if it's a 404) + assert response.status_code in [404, 200] + + +def test_health_check(client): + """Test basic health check endpoint if it exists.""" + # Note: This would require adding a health check endpoint + pass + + +def test_router_includes(): + """Test that all routers are included in the app.""" + from app.main import app + routes = [route.path for route in app.routes] + + # Check that router prefixes are present + assert any("/users" in route for route in routes) + assert any("/cards" in route for route in routes) + assert any("/groups" in route for route in routes) + assert any("/aa" in route for route in routes) + assert any("/token" in route for route in routes) diff --git a/test/test_models.py b/test/test_models.py new file mode 100644 index 0000000..42f805f --- /dev/null +++ b/test/test_models.py @@ -0,0 +1,94 @@ +import pytest +from app.model.models import ( + UserBase, UserResponse, UserCreate, UserDB, UserUpdate, + GroupBase, GroupCreate, GroupDB, GroupResponse, + AccessAuthorizationBase, AccessAuthorizationCreate, + AccessAuthorizationDB, AccessAuthorizationResponse, AccessAuthorizationUpdate, + Card, Timetable, TimetableCreate, Token, TokenData, AaGroupLink +) + + +def test_user_models(): + """Test user model creation and validation.""" + # Test UserBase + user_base = UserBase(name="Test User", email="test@example.com", is_admin=False) + assert user_base.name == "Test User" + assert user_base.email == "test@example.com" + assert user_base.is_admin is False + + # Test UserCreate + user_create = UserCreate(name="New User", email="new@example.com", password="secret123") + assert user_create.password == "secret123" + + # Test UserUpdate + user_update = UserUpdate(name="Updated Name") + assert user_update.name == "Updated Name" + assert user_update.email is None + + +def test_group_models(): + """Test group model creation and validation.""" + # Test GroupBase + group_base = GroupBase(name="Test Group") + assert group_base.name == "Test Group" + + # Test GroupCreate + group_create = GroupCreate(name="New Group") + assert group_create.name == "New Group" + + +def test_access_authorization_models(): + """Test access authorization model creation and validation.""" + # Test AccessAuthorizationBase + aa_base = AccessAuthorizationBase(name="Test AA", is_active=True) + assert aa_base.name == "Test AA" + assert aa_base.is_active is True + + # Test AccessAuthorizationCreate with timetables + timetable_create = TimetableCreate(weekday=1, starttime="08:00", duration=60) + aa_create = AccessAuthorizationCreate( + name="New AA", + is_active=False, + timetables=[timetable_create] + ) + assert aa_create.name == "New AA" + assert aa_create.is_active is False + assert len(aa_create.timetables) == 1 + + +def test_card_model(): + """Test card model creation and validation.""" + card = Card(uuid="test-uuid", group_id=1) + assert card.uuid == "test-uuid" + assert card.group_id == 1 + + +def test_timetable_models(): + """Test timetable model creation and validation.""" + # Test TimetableBase with valid values + timetable = TimetableCreate(weekday=1, starttime="09:00", duration=120) + assert timetable.weekday == 1 + assert timetable.starttime == "09:00" + assert timetable.duration == 120 + + # Test boundary values + max_duration = TimetableCreate(weekday=7, starttime="23:59", duration=1439) + assert max_duration.duration == 1439 + assert max_duration.weekday == 7 + + +def test_token_models(): + """Test token model creation and validation.""" + token = Token(access_token="test-token", token_type="bearer") + assert token.access_token == "test-token" + assert token.token_type == "bearer" + + token_data = TokenData(username="testuser") + assert token_data.username == "testuser" + + +def test_aa_group_link_model(): + """Test many-to-many relationship link model.""" + link = AaGroupLink(group_id=1, accessauth_id=2) + assert link.group_id == 1 + assert link.accessauth_id == 2 diff --git a/test/test_services/test_aa_manager.py b/test/test_services/test_aa_manager.py new file mode 100644 index 0000000..3d655b9 --- /dev/null +++ b/test/test_services/test_aa_manager.py @@ -0,0 +1,192 @@ +import pytest +from fastapi import status + + +def test_create_access_auth(client, auth_headers): + """Test creating a new access authorization.""" + aa_data = { + "name": "New AA", + "is_active": True, + "timetables": [ + {"weekday": 1, "starttime": "08:00", "duration": 60}, + {"weekday": 2, "starttime": "09:00", "duration": 90} + ] + } + + response = client.post("/aa/", json=aa_data, headers=auth_headers) + assert response.status_code == 200 + + data = response.json() + assert data["name"] == "New AA" + assert data["is_active"] is True + assert "id" in data + assert len(data["timetables"]) == 2 + + +def test_get_all_access_auths(client, auth_headers, test_aa): + """Test retrieving all access authorizations.""" + response = client.get("/aa/", headers=auth_headers) + assert response.status_code == 200 + + aa_list = response.json() + assert len(aa_list) >= 1 + + aa_names = [aa["name"] for aa in aa_list] + assert test_aa.name in aa_names + + +def test_get_access_auth_by_id(client, auth_headers, test_aa): + """Test retrieving a specific access authorization by ID.""" + response = client.get(f"/aa/{test_aa.id}", headers=auth_headers) + assert response.status_code == 200 + + data = response.json() + assert data["id"] == test_aa.id + assert data["name"] == test_aa.name + + +def test_get_nonexistent_access_auth(client, auth_headers): + """Test retrieving a non-existent access authorization.""" + response = client.get("/aa/99999", headers=auth_headers) + assert response.status_code == 404 + + +def test_assign_access_auth_to_group(client, auth_headers, test_group, test_aa): + """Test assigning an access authorization to a group.""" + response = client.put( + f"/aa/assign/{test_group.id}/{test_aa.id}", + headers=auth_headers + ) + assert response.status_code == 200 + + data = response.json() + assert data["id"] == test_group.id + # The AA should now be in the group's accessauths + # Note: The response model might not include the full relationship + + +def test_assign_already_assigned_access_auth(client, auth_headers, test_group, test_aa): + """Test assigning an already assigned access authorization.""" + # First assignment + client.put(f"/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers) + + # Second assignment should indicate it's already assigned + response = client.put( + f"/aa/assign/{test_group.id}/{test_aa.id}", + headers=auth_headers + ) + # According to the code, this returns 200 with "already assigned" message + assert response.status_code == 200 + + +def test_unassign_access_auth_from_group(client, auth_headers, test_group, test_aa): + """Test unassigning an access authorization from a group.""" + # First assign + client.put(f"/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers) + + # Then unassign + response = client.put( + f"/aa/unassign/{test_group.id}/{test_aa.id}", + headers=auth_headers + ) + assert response.status_code == 200 + + +def test_unassign_nonexistent_assignment(client, auth_headers, test_group, test_aa): + """Test unassigning a non-existent assignment.""" + response = client.put( + f"/aa/unassign/{test_group.id}/{test_aa.id}", + headers=auth_headers + ) + # According to the code, this returns 200 with "not assigned" message + assert response.status_code == 200 + + +def test_assign_to_nonexistent_group(client, auth_headers, test_aa): + """Test assigning an AA to a non-existent group.""" + response = client.put(f"/aa/assign/99999/{test_aa.id}", headers=auth_headers) + assert response.status_code == 404 + + +def test_assign_nonexistent_aa(client, auth_headers, test_group): + """Test assigning a non-existent AA to a group.""" + response = client.put(f"/aa/assign/{test_group.id}/99999", headers=auth_headers) + assert response.status_code == 404 + + +def test_update_access_auth(client, auth_headers, test_aa): + """Test updating an access authorization.""" + update_data = { + "name": "Updated AA", + "is_active": False + } + + response = client.patch( + f"/aa/{test_aa.id}", + json=update_data, + headers=auth_headers + ) + assert response.status_code == 200 + + data = response.json() + assert data["name"] == "Updated AA" + assert data["is_active"] is False + + +def test_update_access_auth_with_timetables(client, auth_headers, test_aa): + """Test updating an access authorization with new timetables.""" + update_data = { + "timetables": [ + {"weekday": 5, "starttime": "10:00", "duration": 120} + ] + } + + response = client.patch( + f"/aa/{test_aa.id}", + json=update_data, + headers=auth_headers + ) + assert response.status_code == 200 + + +def test_update_nonexistent_access_auth(client, auth_headers): + """Test updating a non-existent access authorization.""" + update_data = {"name": "Updated"} + response = client.patch("/aa/99999", json=update_data, headers=auth_headers) + assert response.status_code == 404 + + +def test_delete_access_auth(client, auth_headers, test_aa): + """Test deleting an access authorization.""" + response = client.delete(f"/aa/{test_aa.id}", headers=auth_headers) + assert response.status_code == 200 + assert "deleted successfully" in response.json()["message"].lower() + + # Verify AA is deleted + response = client.get(f"/aa/{test_aa.id}", headers=auth_headers) + assert response.status_code == 404 + + +def test_delete_nonexistent_access_auth(client, auth_headers): + """Test deleting a non-existent access authorization.""" + response = client.delete("/aa/99999", headers=auth_headers) + assert response.status_code == 404 + + +def test_aa_operations_by_non_admin(client, test_aa, user_auth_headers): + """Test that non-admin users cannot perform AA operations.""" + # Try to create an AA + response = client.post( + "/aa/", + json={"name": "test", "is_active": True, "timetables": []}, + headers=user_auth_headers + ) + assert response.status_code == 403 + + # Try to get all AAs + response = client.get("/aa/", headers=user_auth_headers) + assert response.status_code == 403 + + # Try to assign AA + response = client.put(f"/aa/assign/1/{test_aa.id}", headers=user_auth_headers) + assert response.status_code == 403 diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py new file mode 100644 index 0000000..353b753 --- /dev/null +++ b/test/test_services/test_auth.py @@ -0,0 +1,195 @@ +import pytest +from datetime import datetime, timedelta, timezone +from fastapi import HTTPException, status +from app.services.auth import ( + verify_password, get_password_hash, get_user, authenticate_user, + create_access_token, get_current_user, auth_is_admin, create_first_user +) +from app.model.models import UserDB +from jwt.exceptions import InvalidTokenError + + +def test_password_hashing(): + """Test password hashing and verification.""" + password = "test_password_123" + + # Hash password + hashed = get_password_hash(password) + assert hashed != password + assert len(hashed) > 0 + + # Verify correct password + assert verify_password(password, hashed) is True + + # Verify incorrect password + assert verify_password("wrong_password", hashed) is False + + +def test_get_user(db_session): + """Test get_user function.""" + from app.services.auth import get_password_hash + + # Create a user + user = UserDB(name="testuser", passwordhash=get_password_hash("password")) + db_session.add(user) + db_session.commit() + + # Get existing user + retrieved_user = get_user(db_session, "testuser") + assert retrieved_user is not None + assert retrieved_user.name == "testuser" + + # Try to get non-existent user + with pytest.raises(HTTPException) as exc_info: + get_user(db_session, "nonexistent") + assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + + +def test_authenticate_user(db_session): + """Test user authentication.""" + from app.services.auth import get_password_hash + + # Create a user + user = UserDB(name="authuser", passwordhash=get_password_hash("correctpass")) + db_session.add(user) + db_session.commit() + + # Authenticate with correct credentials + authenticated = authenticate_user(db_session, "authuser", "correctpass") + assert authenticated is not False + assert authenticated.name == "authuser" + + # Authenticate with wrong password + authenticated = authenticate_user(db_session, "authuser", "wrongpass") + assert authenticated is False + + # Authenticate non-existent user + authenticated = authenticate_user(db_session, "nonexistent", "password") + assert authenticated is False + + +def test_create_access_token(): + """Test JWT token creation.""" + data = {"sub": "testuser"} + + # Create token with default expiration + token = create_access_token(data) + assert isinstance(token, str) + assert len(token) > 0 + + # Create token with custom expiration + custom_expire = timedelta(hours=1) + token = create_access_token(data, expires_delta=custom_expire) + assert isinstance(token, str) + + +def test_get_current_user(db_session, admin_user): + """Test getting current user from token.""" + from app.services.auth import create_access_token, get_current_user + + # Create token for admin user + token = create_access_token(data={"sub": admin_user.name}) + + # Get user from token + user = get_current_user(token=token) + assert user is not None + assert user.name == admin_user.name + assert user.id == admin_user.id + + # Test invalid token + with pytest.raises(HTTPException) as exc_info: + get_current_user(token="invalid_token") + assert exc_info.value.status_code == status.HTTP_401_UNAUTHORIZED + + # Test expired token (create token with past expiration) + past_expire = timedelta(minutes=-100) + expired_token = create_access_token(data={"sub": admin_user.name}, expires_delta=past_expire) + + with pytest.raises(HTTPException) as exc_info: + get_current_user(token=expired_token) + assert exc_info.value.status_code == status.HTTP_401_UNAUTHORIZED + + +def test_auth_is_admin(db_session, admin_user, regular_user): + """Test admin authorization check.""" + from app.services.auth import create_access_token, auth_is_admin + + # Create token for admin user + admin_token = create_access_token(data={"sub": admin_user.name}) + + # Admin should pass + result = auth_is_admin(token=admin_token) + assert result is True + + # Create token for regular user + user_token = create_access_token(data={"sub": regular_user.name}) + + # Regular user should fail + with pytest.raises(HTTPException) as exc_info: + auth_is_admin(token=user_token) + assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN + + +def test_create_first_user(db_session): + """Test automatic creation of first admin user.""" + # Clear any existing users + db_session.exec(select(UserDB)).all() + for user in db_session.exec(select(UserDB)).all(): + db_session.delete(user) + db_session.commit() + + # Create first user + result = create_first_user() + assert result is not None + assert result.name == "admin" + assert result.is_admin is True + + # Verify user exists in database + user = db_session.exec(select(UserDB).where(UserDB.name == "admin")).first() + assert user is not None + assert user.is_admin is True + + # Test that it doesn't create another admin if one exists + second_result = create_first_user() + assert second_result is None # Should print "Admin user already exists" + + +def test_token_endpoint(client, admin_user): + """Test the token endpoint for login.""" + # Test successful login + response = client.post( + "/token", + data={"username": admin_user.name, "password": "admin123"} + ) + assert response.status_code == 200 + data = response.json() + assert "access_token" in data + assert data["token_type"] == "bearer" + + # Test failed login with wrong password + response = client.post( + "/token", + data={"username": admin_user.name, "password": "wrongpassword"} + ) + assert response.status_code == 401 + + # Test failed login with non-existent user + response = client.post( + "/token", + data={"username": "nonexistent", "password": "password"} + ) + assert response.status_code == 401 + + +def test_test_login_endpoint(client, admin_user, auth_headers): + """Test the test login endpoint.""" + # Test with valid token + response = client.get("/test/login", headers=auth_headers) + assert response.status_code == 200 + data = response.json() + assert data["name"] == admin_user.name + assert data["is_admin"] is True + + # Test without token + response = client.get("/test/login") + assert response.status_code == 401 diff --git a/test/test_services/test_card_manager.py b/test/test_services/test_card_manager.py new file mode 100644 index 0000000..e39db7a --- /dev/null +++ b/test/test_services/test_card_manager.py @@ -0,0 +1,66 @@ +import pytest +from fastapi import status + + +def test_add_card(client, auth_headers, test_group): + """Test adding a card to a group.""" + response = client.post(f"/cards/{test_group.id}", headers=auth_headers) + assert response.status_code == 200 + + data = response.json() + assert "id" in data + assert "uuid" in data + assert data["group_id"] == test_group.id + assert len(data["uuid"]) > 0 # UUID should be generated + + +def test_add_card_to_nonexistent_group(client, auth_headers): + """Test adding a card to a non-existent group.""" + response = client.post("/cards/99999", headers=auth_headers) + # This might succeed and create a card with a non-existent group_id + # or fail depending on foreign key constraints + # For now, let's assume it might fail + # assert response.status_code == 404 + + +def test_delete_card(client, auth_headers, test_card): + """Test deleting a card.""" + response = client.delete(f"/cards/{test_card.id}", headers=auth_headers) + assert response.status_code == 200 + assert "deleted successfully" in response.json()["message"].lower() + + +def test_delete_nonexistent_card(client, auth_headers): + """Test deleting a non-existent card.""" + response = client.delete("/cards/99999", headers=auth_headers) + assert response.status_code == 404 + + +def test_get_cards_for_group(client, auth_headers, test_group, test_card): + """Test getting all cards for a group.""" + response = client.get(f"/cards/{test_group.id}", headers=auth_headers) + assert response.status_code == 200 + + cards = response.json() + assert len(cards) >= 1 + assert any(card["id"] == test_card.id for card in cards) + + +def test_get_cards_for_nonexistent_group(client, auth_headers): + """Test getting cards for a non-existent group.""" + response = client.get("/cards/99999", headers=auth_headers) + assert response.status_code == 200 + + cards = response.json() + assert len(cards) == 0 # Empty list for non-existent group + + +def test_card_operations_by_non_admin(client, test_group, user_auth_headers): + """Test that non-admin users cannot perform card operations.""" + # Try to add a card + response = client.post(f"/cards/{test_group.id}", headers=user_auth_headers) + assert response.status_code == 403 + + # Try to get cards + response = client.get(f"/cards/{test_group.id}", headers=user_auth_headers) + assert response.status_code == 403 diff --git a/test/test_services/test_database.py b/test/test_services/test_database.py new file mode 100644 index 0000000..8496032 --- /dev/null +++ b/test/test_services/test_database.py @@ -0,0 +1,64 @@ +import pytest +from sqlmodel import Session, select +from app.services.database import create_db_and_tables, get_session, add_and_refresh +from app.model.models import UserDB, GroupDB, Card + + +def test_create_db_and_tables(): + """Test database and tables creation.""" + # This is primarily an integration test + from sqlalchemy import inspect + from app.services.database import engine + + create_db_and_tables() + inspector = inspect(engine) + + # Check that tables exist + tables = inspector.get_table_names() + assert "userdb" in tables + assert "groupdb" in tables + assert "card" in tables + assert "accessauthorizationdb" in tables + assert "timetable" in tables + assert "aagrouplink" in tables + + +def test_get_session(db_session): + """Test database session generator.""" + # Test that we can get a session + session_gen = get_session() + session = next(session_gen) + + assert isinstance(session, Session) + + # Test that session works + user = UserDB(name="Test", passwordhash="hash") + session.add(user) + session.commit() + + retrieved_user = session.get(UserDB, user.id) + assert retrieved_user is not None + assert retrieved_user.name == "Test" + + # Clean up generator + try: + next(session_gen) + except StopIteration: + pass + + +def test_add_and_refresh(db_session): + """Test add_and_refresh helper function.""" + user = UserDB(name="Test User", passwordhash="hashed") + + # Add user + result = add_and_refresh(db_session, user) + + # Assert that user is now in database with ID + assert result.id is not None + assert result.name == "Test User" + + # Verify in database + db_user = db_session.get(UserDB, result.id) + assert db_user is not None + assert db_user.name == "Test User" diff --git a/test/test_services/test_group_manager.py b/test/test_services/test_group_manager.py new file mode 100644 index 0000000..f4460b0 --- /dev/null +++ b/test/test_services/test_group_manager.py @@ -0,0 +1,68 @@ +import pytest +from fastapi import status + + +def test_create_group(client, auth_headers): + """Test creating a new group.""" + group_data = {"name": "New Test Group"} + + response = client.post("/groups/", json=group_data, headers=auth_headers) + assert response.status_code == 200 + + data = response.json() + assert data["name"] == "New Test Group" + assert "id" in data + + +def test_create_duplicate_group(client, auth_headers, test_group): + """Test creating a group with a duplicate name.""" + group_data = {"name": test_group.name} + + response = client.post("/groups/", json=group_data, headers=auth_headers) + # This should fail due to unique constraint + assert response.status_code == 422 # Validation error + + +def test_get_groups(client, auth_headers, test_group): + """Test retrieving all groups.""" + response = client.get("/groups/", headers=auth_headers) + assert response.status_code == 200 + + groups = response.json() + assert len(groups) >= 1 + + group_names = [group["name"] for group in groups] + assert test_group.name in group_names + + +def test_delete_group(client, auth_headers, test_group): + """Test deleting a group.""" + response = client.delete(f"/groups/{test_group.id}", headers=auth_headers) + assert response.status_code == 200 + assert "deleted successfully" in response.json()["message"].lower() + + # Verify group is deleted + response = client.get("/groups/", headers=auth_headers) + groups = response.json() + assert not any(group["id"] == test_group.id for group in groups) + + +def test_delete_nonexistent_group(client, auth_headers): + """Test deleting a non-existent group.""" + response = client.delete("/groups/99999", headers=auth_headers) + assert response.status_code == 404 + + +def test_group_operations_by_non_admin(client, user_auth_headers): + """Test that non-admin users cannot perform group operations.""" + # Try to create a group + response = client.post( + "/groups/", + json={"name": "test"}, + headers=user_auth_headers + ) + assert response.status_code == 403 + + # Try to get groups + response = client.get("/groups/", headers=user_auth_headers) + assert response.status_code == 403 diff --git a/test/test_services/test_user_manager.py b/test/test_services/test_user_manager.py new file mode 100644 index 0000000..c62e366 --- /dev/null +++ b/test/test_services/test_user_manager.py @@ -0,0 +1,150 @@ +import pytest +from fastapi import status + + +def test_create_user(client, auth_headers): + """Test creating a new user.""" + user_data = { + "name": "newuser", + "email": "newuser@example.com", + "is_admin": False, + "password": "newpassword123" + } + + response = client.post("/users/", json=user_data, headers=auth_headers) + assert response.status_code == 200 + + data = response.json() + assert data["name"] == "newuser" + assert data["email"] == "newuser@example.com" + assert data["is_admin"] is False + assert "id" in data + assert "passwordhash" not in data # Password hash should not be in response + + +def test_create_user_unauthorized(client): + """Test creating a user without admin credentials.""" + user_data = { + "name": "unauthorized_user", + "email": "unauthorized@example.com", + "password": "password123" + } + + response = client.post("/users/", json=user_data) + assert response.status_code == 403 + + +def test_get_users(client, auth_headers, admin_user, regular_user): + """Test retrieving all users.""" + response = client.get("/users/", headers=auth_headers) + assert response.status_code == 200 + + users = response.json() + assert len(users) >= 2 # At least admin_user and regular_user + + user_names = [user["name"] for user in users] + assert admin_user.name in user_names + assert regular_user.name in user_names + + +def test_get_user_by_id(client, auth_headers, regular_user): + """Test retrieving a specific user by ID.""" + response = client.get(f"/users/{regular_user.id}", headers=auth_headers) + assert response.status_code == 200 + + data = response.json() + assert data["id"] == regular_user.id + assert data["name"] == regular_user.name + + +def test_get_nonexistent_user(client, auth_headers): + """Test retrieving a non-existent user.""" + response = client.get("/users/99999", headers=auth_headers) + assert response.status_code == 404 + assert "not found" in response.json()["detail"].lower() + + +def test_update_user(client, auth_headers, regular_user): + """Test updating a user.""" + update_data = { + "name": "updated_name", + "email": "updated@example.com" + } + + response = client.patch( + f"/users/{regular_user.id}", + json=update_data, + headers=auth_headers + ) + assert response.status_code == 200 + + data = response.json() + assert data["name"] == "updated_name" + assert data["email"] == "updated@example.com" + # Unchanged fields should remain the same + assert data["is_admin"] == regular_user.is_admin + + +def test_update_user_password(client, auth_headers, regular_user): + """Test updating a user's password.""" + update_data = { + "password": "new_password_456" + } + + response = client.patch( + f"/users/{regular_user.id}", + json=update_data, + headers=auth_headers + ) + assert response.status_code == 200 + + # Verify password can be used for login + login_response = client.post( + "/token", + data={"username": regular_user.name, "password": "new_password_456"} + ) + assert login_response.status_code == 200 + + +def test_update_nonexistent_user(client, auth_headers): + """Test updating a non-existent user.""" + update_data = {"name": "updated"} + response = client.patch("/users/99999", json=update_data, headers=auth_headers) + assert response.status_code == 404 + + +def test_delete_user(client, auth_headers, regular_user): + """Test deleting a user.""" + response = client.delete(f"/users/{regular_user.id}", headers=auth_headers) + assert response.status_code == 200 + assert "deleted successfully" in response.json()["message"].lower() + + # Verify user is deleted + response = client.get(f"/users/{regular_user.id}", headers=auth_headers) + assert response.status_code == 404 + + +def test_delete_nonexistent_user(client, auth_headers): + """Test deleting a non-existent user.""" + response = client.delete("/users/99999", headers=auth_headers) + assert response.status_code == 404 + + +def test_user_operations_by_non_admin(client, user_auth_headers): + """Test that non-admin users cannot perform admin operations.""" + # Try to create a user + response = client.post( + "/users/", + json={"name": "test", "password": "pass"}, + headers=user_auth_headers + ) + assert response.status_code == 403 + + # Try to get users + response = client.get("/users/", headers=user_auth_headers) + assert response.status_code == 403 + + # Try to delete the admin user (if ID is known) + # This would require knowing the admin user ID + # response = client.delete(f"/users/{admin_id}", headers=user_auth_headers) + # assert response.status_code == 403 From aafcdcc6de95b0e647f5860ecea93757e20d9971 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 16:27:25 +0200 Subject: [PATCH 03/14] Needed to load --- app/controllers/__init__.py | 0 app/services/__init__.py | 0 test/__init__.py | 0 test/test_services/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/controllers/__init__.py create mode 100644 app/services/__init__.py create mode 100644 test/__init__.py create mode 100644 test/test_services/__init__.py diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_services/__init__.py b/test/test_services/__init__.py new file mode 100644 index 0000000..e69de29 From 235420bc3ecc3a64b3160359c9b10970896230cc Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:02:40 +0200 Subject: [PATCH 04/14] Fix testing harness --- test/conftest.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index f572437..bf850df 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,38 +2,31 @@ import pytest from fastapi.testclient import TestClient from sqlmodel import Session, create_engine, SQLModel from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool from app.main import app from app.model.models import UserDB, Card, GroupDB, AccessAuthorizationDB, Timetable, AaGroupLink from app.services.database import get_session # Use in-memory SQLite for testing -TEST_SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" - -engine = create_engine(TEST_SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) -TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +TEST_SQLALCHEMY_DATABASE_URL = "sqlite://" +engine = create_engine(TEST_SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, poolclass=StaticPool) @pytest.fixture(scope="function") def db_session(): """Create a fresh database session for each test.""" SQLModel.metadata.create_all(engine) - session = TestingSessionLocal() - try: + with Session(engine) as session: yield session - finally: - session.close() - SQLModel.metadata.drop_all(engine) + SQLModel.metadata.drop_all(engine) @pytest.fixture(scope="function") def client(db_session): """Create a test client with a database session override.""" def override_get_session(): - try: - yield db_session - finally: - pass + yield db_session app.dependency_overrides[get_session] = override_get_session with TestClient(app) as test_client: From 0337a90f15c4d3dd7b8d88540e27676afc42d154 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:03:33 +0200 Subject: [PATCH 05/14] Merge get_user into authenticate_user, it was only doing one db call --- app/services/auth.py | 8 +------- test/test_services/test_auth.py | 23 +---------------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/app/services/auth.py b/app/services/auth.py index cd2e0be..3ac5199 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -26,14 +26,8 @@ def verify_password(plain_password, hashed_password): def get_password_hash(password): return password_hash.hash(password) -def get_user(db, username: str): - user = db.exec(select(UserDB).where(UserDB.name == username)).first() - if user is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Username not found in get_user, this shouldn't happen") - return user - def authenticate_user(db, username: str, password: str): - user = get_user(db, username) + user = db.exec(select(UserDB).where(UserDB.name == username)).first() if not user: return False if not verify_password(password, user.passwordhash): diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py index 353b753..f82e694 100644 --- a/test/test_services/test_auth.py +++ b/test/test_services/test_auth.py @@ -2,7 +2,7 @@ import pytest from datetime import datetime, timedelta, timezone from fastapi import HTTPException, status from app.services.auth import ( - verify_password, get_password_hash, get_user, authenticate_user, + verify_password, get_password_hash, authenticate_user, create_access_token, get_current_user, auth_is_admin, create_first_user ) from app.model.models import UserDB @@ -24,27 +24,6 @@ def test_password_hashing(): # Verify incorrect password assert verify_password("wrong_password", hashed) is False - -def test_get_user(db_session): - """Test get_user function.""" - from app.services.auth import get_password_hash - - # Create a user - user = UserDB(name="testuser", passwordhash=get_password_hash("password")) - db_session.add(user) - db_session.commit() - - # Get existing user - retrieved_user = get_user(db_session, "testuser") - assert retrieved_user is not None - assert retrieved_user.name == "testuser" - - # Try to get non-existent user - with pytest.raises(HTTPException) as exc_info: - get_user(db_session, "nonexistent") - assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND - - def test_authenticate_user(db_session): """Test user authentication.""" from app.services.auth import get_password_hash From 5c2e58d5d0b367e67421170d671dbb2e88c98123 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:10:40 +0200 Subject: [PATCH 06/14] Fix deprecation warning by changing on_event to asynccontesxtmanager --- app/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 69dfcf8..3d46e18 100644 --- a/app/main.py +++ b/app/main.py @@ -1,17 +1,21 @@ from fastapi import FastAPI from fastapi.security import OAuth2PasswordBearer +from contextlib import asynccontextmanager from .controllers import userManager, cardManager, groupManager, aaManager from .services.database import create_db_and_tables from .services.auth import token_router, create_first_user oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -app = FastAPI() -@app.on_event("startup") -def on_startup(): +@asynccontextmanager +async def lifespan(app: FastAPI): create_db_and_tables() create_first_user() print("Database created and tables initialized.") + yield + +app = FastAPI(lifespan=lifespan) + app.include_router(token_router) From a820431707efc4798b695c65e64ec39ad52fc397 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:12:13 +0200 Subject: [PATCH 07/14] Fix accessToken with custom expiration --- app/services/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/auth.py b/app/services/auth.py index 3ac5199..c62b228 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -39,7 +39,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.now(timezone.utc) + expires_delta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt From e4b405cdbdf16884edf3dc67319da63edb099b8d Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:16:26 +0200 Subject: [PATCH 08/14] Revert "Merge get_user into authenticate_user, it was only doing one db call" ups, das war ja doch von mehr verwendet... This reverts commit 0337a90f15c4d3dd7b8d88540e27676afc42d154. --- app/services/auth.py | 8 +++++++- test/test_services/test_auth.py | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/services/auth.py b/app/services/auth.py index c62b228..596b6ba 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -26,8 +26,14 @@ def verify_password(plain_password, hashed_password): def get_password_hash(password): return password_hash.hash(password) -def authenticate_user(db, username: str, password: str): +def get_user(db, username: str): user = db.exec(select(UserDB).where(UserDB.name == username)).first() + if user is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Username not found in get_user, this shouldn't happen") + return user + +def authenticate_user(db, username: str, password: str): + user = get_user(db, username) if not user: return False if not verify_password(password, user.passwordhash): diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py index f82e694..353b753 100644 --- a/test/test_services/test_auth.py +++ b/test/test_services/test_auth.py @@ -2,7 +2,7 @@ import pytest from datetime import datetime, timedelta, timezone from fastapi import HTTPException, status from app.services.auth import ( - verify_password, get_password_hash, authenticate_user, + verify_password, get_password_hash, get_user, authenticate_user, create_access_token, get_current_user, auth_is_admin, create_first_user ) from app.model.models import UserDB @@ -24,6 +24,27 @@ def test_password_hashing(): # Verify incorrect password assert verify_password("wrong_password", hashed) is False + +def test_get_user(db_session): + """Test get_user function.""" + from app.services.auth import get_password_hash + + # Create a user + user = UserDB(name="testuser", passwordhash=get_password_hash("password")) + db_session.add(user) + db_session.commit() + + # Get existing user + retrieved_user = get_user(db_session, "testuser") + assert retrieved_user is not None + assert retrieved_user.name == "testuser" + + # Try to get non-existent user + with pytest.raises(HTTPException) as exc_info: + get_user(db_session, "nonexistent") + assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + + def test_authenticate_user(db_session): """Test user authentication.""" from app.services.auth import get_password_hash From 6daf2345be8ef9c8e9c17a9b97d138afa46e34b5 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:18:31 +0200 Subject: [PATCH 09/14] Error handling in following functions --- app/services/auth.py | 2 -- test/test_services/test_auth.py | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/services/auth.py b/app/services/auth.py index 596b6ba..adeb210 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -28,8 +28,6 @@ def get_password_hash(password): def get_user(db, username: str): user = db.exec(select(UserDB).where(UserDB.name == username)).first() - if user is None: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Username not found in get_user, this shouldn't happen") return user def authenticate_user(db, username: str, password: str): diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py index 353b753..9b490b8 100644 --- a/test/test_services/test_auth.py +++ b/test/test_services/test_auth.py @@ -40,9 +40,8 @@ def test_get_user(db_session): assert retrieved_user.name == "testuser" # Try to get non-existent user - with pytest.raises(HTTPException) as exc_info: - get_user(db_session, "nonexistent") - assert exc_info.value.status_code == status.HTTP_404_NOT_FOUND + retrieved_user = get_user(db_session, "nonexistent") + assert retrieved_user is None def test_authenticate_user(db_session): From 46e883200e4a9bab4205f4ceb014a565d4eeb113 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Sat, 16 May 2026 17:53:42 +0200 Subject: [PATCH 10/14] Fix get_current_user and auth_is_admin creating their own db session instead of getting from get_session --- app/services/auth.py | 45 ++++++++++++++++++--------------- test/test_services/test_auth.py | 4 +-- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/app/services/auth.py b/app/services/auth.py index adeb210..add7d9c 100644 --- a/app/services/auth.py +++ b/app/services/auth.py @@ -48,28 +48,33 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt -def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - with Session(engine) as db: - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"} - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except InvalidTokenError: +def get_current_user( + token: Annotated[str, Depends(oauth2_scheme)], + db: Session = Depends(get_session), + ): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"} + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username = payload.get("sub") + if username is None: raise credentials_exception - user = get_user(db, username=token_data.username) - if user is None: - raise credentials_exception - return user + token_data = TokenData(username=username) + except InvalidTokenError: + raise credentials_exception + user = get_user(db, username=token_data.username) + if user is None: + raise credentials_exception + return user -def auth_is_admin(token: str = Depends(oauth2_scheme)): - user = get_current_user(token=token) +def auth_is_admin( + token: str = Depends(oauth2_scheme), + db: Session = Depends(get_session), + ): + user = get_current_user(token=token, db=db) if not user.is_admin: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py index 9b490b8..2990134 100644 --- a/test/test_services/test_auth.py +++ b/test/test_services/test_auth.py @@ -117,7 +117,7 @@ def test_auth_is_admin(db_session, admin_user, regular_user): admin_token = create_access_token(data={"sub": admin_user.name}) # Admin should pass - result = auth_is_admin(token=admin_token) + result = auth_is_admin(token=admin_token, db=db_session) assert result is True # Create token for regular user @@ -125,7 +125,7 @@ def test_auth_is_admin(db_session, admin_user, regular_user): # Regular user should fail with pytest.raises(HTTPException) as exc_info: - auth_is_admin(token=user_token) + auth_is_admin(token=user_token, db=db_session) assert exc_info.value.status_code == status.HTTP_403_FORBIDDEN From 1caffff30d0cc2489b9ac3af201ef5fc7c6906fb Mon Sep 17 00:00:00 2001 From: ahtlon Date: Mon, 18 May 2026 21:03:05 +0200 Subject: [PATCH 11/14] Vscode test suite --- .vscode/settings.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b2b8866 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "test" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file From 56c8d38cde2853fa96ad7020afefa2592ffe4cb7 Mon Sep 17 00:00:00 2001 From: ahtlon Date: Mon, 18 May 2026 21:03:47 +0200 Subject: [PATCH 12/14] almost all tests run now --- app/controllers/groupManager.py | 5 ++++- test/test_services/test_auth.py | 2 +- test/test_services/test_group_manager.py | 2 +- test/test_services/test_user_manager.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controllers/groupManager.py b/app/controllers/groupManager.py index f54e021..8cf4706 100644 --- a/app/controllers/groupManager.py +++ b/app/controllers/groupManager.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, status from sqlmodel import Session, select from typing import List @@ -16,6 +16,9 @@ def get_groups(*, db: Session = Depends(get_session), admin: bool = Depends(auth @group_router.post("/", response_model=GroupResponse) def create_group(*, db: Session = Depends(get_session), group: GroupCreate, admin: bool = Depends(auth_is_admin)): db_group = GroupDB.model_validate(group) + group = db.exec(select(GroupDB).where(GroupDB.name == db_group.name)).first() + if group is not None: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Group already exists!") return add_and_refresh(db, db_group) @group_router.delete("/{group_id}") diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py index 2990134..c2acfc8 100644 --- a/test/test_services/test_auth.py +++ b/test/test_services/test_auth.py @@ -90,7 +90,7 @@ def test_get_current_user(db_session, admin_user): token = create_access_token(data={"sub": admin_user.name}) # Get user from token - user = get_current_user(token=token) + user = get_current_user(token=token, db=db_session) assert user is not None assert user.name == admin_user.name assert user.id == admin_user.id diff --git a/test/test_services/test_group_manager.py b/test/test_services/test_group_manager.py index f4460b0..c40e2dd 100644 --- a/test/test_services/test_group_manager.py +++ b/test/test_services/test_group_manager.py @@ -20,7 +20,7 @@ def test_create_duplicate_group(client, auth_headers, test_group): response = client.post("/groups/", json=group_data, headers=auth_headers) # This should fail due to unique constraint - assert response.status_code == 422 # Validation error + assert response.status_code == 409 # Validation error def test_get_groups(client, auth_headers, test_group): diff --git a/test/test_services/test_user_manager.py b/test/test_services/test_user_manager.py index c62e366..e66e832 100644 --- a/test/test_services/test_user_manager.py +++ b/test/test_services/test_user_manager.py @@ -31,7 +31,7 @@ def test_create_user_unauthorized(client): } response = client.post("/users/", json=user_data) - assert response.status_code == 403 + assert response.status_code == 401 def test_get_users(client, auth_headers, admin_user, regular_user): From 3e86fe223e15e8ae6e684216f04e0bf7b921518a Mon Sep 17 00:00:00 2001 From: ahtlon Date: Mon, 18 May 2026 21:04:17 +0200 Subject: [PATCH 13/14] Explain problem; will fix later maybe --- test/test_services/test_auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_services/test_auth.py b/test/test_services/test_auth.py index c2acfc8..a890459 100644 --- a/test/test_services/test_auth.py +++ b/test/test_services/test_auth.py @@ -131,7 +131,9 @@ def test_auth_is_admin(db_session, admin_user, regular_user): def test_create_first_user(db_session): """Test automatic creation of first admin user.""" + #Currently broken because this uses the prod db because of how i wrote the create_first_user function # Clear any existing users + from sqlmodel import select db_session.exec(select(UserDB)).all() for user in db_session.exec(select(UserDB)).all(): db_session.delete(user) From 3246081b81d44a8bf326512789bc7f2f9a0c8a2e Mon Sep 17 00:00:00 2001 From: ahtlon Date: Mon, 18 May 2026 21:15:12 +0200 Subject: [PATCH 14/14] Add pytest-cov --- pyproject.toml | 1 + uv.lock | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5142a6f..1e2b0aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "pwdlib[argon2]>=0.3.0", "pytest>=9.0.3", "requests>=2.33.1", + "pytest-cov>=7.1.0", ] [tool.uv.sources] diff --git a/uv.lock b/uv.lock index be8057a..a379a7f 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "coverage" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/76/b7c66ee3c66e1b0f9d894c8125983aa0c03fb2336f2fd16559f9c966157f/coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef", size = 219990, upload-time = "2026-05-10T18:00:38.887Z" }, + { url = "https://files.pythonhosted.org/packages/b3/af/e567cbad5ba69c013a50146dfa886dc7193361fda77521f51274ff620e1b/coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66", size = 220365, upload-time = "2026-05-10T18:00:40.864Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/9ad575d505b4d805b254febc8a5b338a2efe278f8786e56ff1cb8413f9c3/coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b", size = 251363, upload-time = "2026-05-10T18:00:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b5370068b2f57787454592ed7dcd1002f0f1703b7db1fa30f6a325a4ca6e/coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca", size = 253961, upload-time = "2026-05-10T18:00:44.079Z" }, + { url = "https://files.pythonhosted.org/packages/29/1e/51adf17738976e8f2b85ddef7b7aa12a0838b056c92f175941d8862767c1/coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7", size = 255193, upload-time = "2026-05-10T18:00:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7b/5bfd7ac1df3b881c2ac7a5cbc99c7609e6296c402f5ef587cd81c6f355b3/coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2", size = 257326, upload-time = "2026-05-10T18:00:47.173Z" }, + { url = "https://files.pythonhosted.org/packages/7d/38/1d37d316b174fad3843a1d76dbdfe4398771c9ecd0515935dd9ece9cd627/coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367", size = 251582, upload-time = "2026-05-10T18:00:49.152Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/746704f95980ba220214e1a41e18cec5aea80a898eaa53c51bf2d645ff36/coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9", size = 253325, upload-time = "2026-05-10T18:00:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b9/bbe87206d9687b192352f893797825b5f5b15ecd3aa9c68fbff0c074d77b/coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087", size = 251291, upload-time = "2026-05-10T18:00:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/46/57/b8cdb12ac0d73ef0243218bd5e22c9df8f92edab8018213a86aec67c5324/coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef", size = 255448, upload-time = "2026-05-10T18:00:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d4/5002019538b2036ce3c84340f54d2fd5100d55b0a6b0894eee56128d03c7/coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52", size = 251110, upload-time = "2026-05-10T18:00:56.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/20c5009477660f084e6ed60bc02a91894b8e234e617e86ecfd9aaf78e27b/coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe", size = 252885, upload-time = "2026-05-10T18:00:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ab/3cf6427ac9c1f1db747dbb1ce71dde47984876d4c2cfd018a3fef0a78d4d/coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae", size = 222539, upload-time = "2026-05-10T18:00:59.581Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/9228523e80321c2cb4880d1f589bc0171f2f71432c35118ad04dc01decce/coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e", size = 223344, upload-time = "2026-05-10T18:01:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/99/118daa192f95e3a6cb2740100fbf8797cda1734b4134ef0b5d501a7fa8f3/coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96", size = 221966, upload-time = "2026-05-10T18:01:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f1/a46cc0c013be170216253184a32366d7cbdb9252feaec866b05c2d12a894/coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90", size = 220679, upload-time = "2026-05-10T18:01:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/64/8c/9c30a3d311a34177fa432995be7fbfc64477d8bac5630bd38055b1c9b424/coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1", size = 221033, upload-time = "2026-05-10T18:01:07.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/3fb5e06c3badefd0c1b47e2044fdca67f8220a4ec2e7fcfb476aa0a67c6c/coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd", size = 262333, upload-time = "2026-05-10T18:01:08.903Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e6/fbc322325c7294d3e22c1ad6b79e45d0806b25228c8e5842aed6d8169aa7/coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc", size = 264410, upload-time = "2026-05-10T18:01:10.531Z" }, + { url = "https://files.pythonhosted.org/packages/08/92/c497b264bec1673c47cc77e26f760fcda4654cabf1f39546d1a23a3b8c35/coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426", size = 266836, upload-time = "2026-05-10T18:01:12.19Z" }, + { url = "https://files.pythonhosted.org/packages/78/fc/045da320987f401af5d2815d351e8aa799aec859f60e29f445e3089eeedb/coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899", size = 267974, upload-time = "2026-05-10T18:01:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/227b1e379497fb7a4fc3286e620f80c8a1e7cec66d45695a01639eb1af65/coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b", size = 261578, upload-time = "2026-05-10T18:01:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f5/3570342900f2acea31d33ff1590c5d8bac1a8e1a2e1c6d34a5d5e61de681/coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90", size = 264394, upload-time = "2026-05-10T18:01:17.607Z" }, + { url = "https://files.pythonhosted.org/packages/16/29/de1bbc01c935b28f89b1dc3db85b011c055e843a8e5e3b83141c3f80af7f/coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f", size = 262022, upload-time = "2026-05-10T18:01:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/35/95/f53890b0bf2fc10ab168e05d38869215e73ca24c4cb521c3bb0eb62fe16b/coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d", size = 265732, upload-time = "2026-05-10T18:01:21.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ea/c919e259081dd2bdf0e43b87209709ba7ec2e4117c2a7f5185379c43463c/coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47", size = 260921, upload-time = "2026-05-10T18:01:23.533Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2c/c2831889705a81dc5d1c6ca12e4d8e9b95dfc146d153488a6c0ea685d28e/coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477", size = 263109, upload-time = "2026-05-10T18:01:25.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a9/2fcae5003cac3d63fe344d2166243c2756935f48420863c5272b240d550b/coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab", size = 223212, upload-time = "2026-05-10T18:01:27.157Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/18e94d7b14b9b398164197114a587a04ab7c9fdbe1d237eef57311c5e883/coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917", size = 224272, upload-time = "2026-05-10T18:01:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/db/56/4f14fad782b035c81c4ffd09159e7103d42bb1d93ac8496d04b90a11b7da/coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8", size = 222530, upload-time = "2026-05-10T18:01:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" }, + { url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" }, + { url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" }, + { url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" }, + { url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" }, + { url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" }, +] + [[package]] name = "crashtest" version = "0.4.1" @@ -545,6 +614,7 @@ dependencies = [ { name = "pwdlib", extra = ["argon2"] }, { name = "pyjwt", extra = ["crypto"] }, { name = "pytest" }, + { name = "pytest-cov" }, { name = "python-desfire" }, { name = "requests" }, { name = "sqlmodel" }, @@ -558,6 +628,7 @@ requires-dist = [ { name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.12.1" }, { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, { name = "python-desfire", git = "https://github.com/waza-ari/python-desfire" }, { name = "requests", specifier = ">=2.33.1" }, { name = "sqlmodel", specifier = ">=0.0.38" }, @@ -1175,6 +1246,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + [[package]] name = "python-desfire" version = "0.1.6"