refactor(backend): enforce route→service→repo layered architecture

- introduce custom repository exception hierarchy (DuplicateEntryError,
  IntegrityConstraintError, InvalidInputError) replacing raw ValueError
- eliminate all direct repository imports and raw SQL from route layer
- add UserService, SessionService, OrganizationService to service layer
- add get_stats/get_org_distribution service methods replacing admin inline SQL
- fix timing side-channel in authenticate_user via dummy bcrypt check
- replace SHA-256 client secret fallback with explicit InvalidClientError
- replace assert with InvalidGrantError in authorization code exchange
- replace N+1 token revocation loops with bulk UPDATE statements
- rename oauth account token fields (drop misleading 'encrypted' suffix)
- add Alembic migration 0003 for token field column rename
- add 45 new service/repository tests; 975 passing, 94% coverage
This commit is contained in:
2026-02-27 09:32:57 +01:00
parent 0646c96b19
commit 98b455fdc3
62 changed files with 2933 additions and 1728 deletions

View File

@@ -537,7 +537,7 @@ class TestOrganizationExceptionHandlers:
):
"""Test generic exception handler in get_my_organizations (covers lines 81-83)."""
with patch(
"app.crud.organization.organization.get_user_organizations_with_details",
"app.api.routes.organizations.organization_service.get_user_organizations_with_details",
side_effect=Exception("Database connection lost"),
):
# The exception handler logs and re-raises, so we expect the exception
@@ -554,7 +554,7 @@ class TestOrganizationExceptionHandlers:
):
"""Test generic exception handler in get_organization (covers lines 124-128)."""
with patch(
"app.crud.organization.organization.get",
"app.api.routes.organizations.organization_service.get_organization",
side_effect=Exception("Database timeout"),
):
with pytest.raises(Exception, match="Database timeout"):
@@ -569,7 +569,7 @@ class TestOrganizationExceptionHandlers:
):
"""Test generic exception handler in get_organization_members (covers lines 170-172)."""
with patch(
"app.crud.organization.organization.get_organization_members",
"app.api.routes.organizations.organization_service.get_organization_members",
side_effect=Exception("Connection pool exhausted"),
):
with pytest.raises(Exception, match="Connection pool exhausted"):
@@ -591,11 +591,11 @@ class TestOrganizationExceptionHandlers:
admin_token = login_response.json()["access_token"]
with patch(
"app.crud.organization.organization.get",
"app.api.routes.organizations.organization_service.get_organization",
return_value=test_org_with_user_admin,
):
with patch(
"app.crud.organization.organization.update",
"app.api.routes.organizations.organization_service.update_organization",
side_effect=Exception("Write lock timeout"),
):
with pytest.raises(Exception, match="Write lock timeout"):