Loading src/open_exposure_gateway/adapters/database/repos/qod_sessions.py +7 −1 Changes for src/open_exposure_gateway/adapters/database/repos/qod_sessions.py: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ from datetime import datetime from typing import Any from uuid import UUID from sqlalchemy import literal_column, or_, select, type_coerce from sqlalchemy import and_, literal_column, or_, select, type_coerce from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.asyncio import AsyncSession Loading Loading @@ -44,7 +44,13 @@ class SqlQodSessionRepository(QodSessionRepository): .join(OperationRow, OperationRow.operation_id == QodSessionRow.operation_id) .where( OperationRow.app_provider_id == app_provider_id, or_( QodSessionRow.state.in_(_ACTIVE_STATES), and_( QodSessionRow.state == QodSessionState.UNAVAILABLE, QodSessionRow.status_info == "NETWORK_TERMINATED", ), ), or_(*identifier_matches), ) .limit(1) Loading tests/integration/test_postgres.py +10 −0 Changes for tests/integration/test_postgres.py: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -602,7 +602,17 @@ async def test_qod_session_repo_find_active_by_device_scopes_by_identifier_state is None ) saved.state = QodSessionState.UNAVAILABLE saved.status_info = "NETWORK_TERMINATED" await repo.save(saved) assert await repo.find_active_by_device({"phoneNumber": "+123456789"}, "tenant-a") is not None saved.status_info = "DURATION_EXPIRED" await repo.save(saved) assert await repo.find_active_by_device({"phoneNumber": "+123456789"}, "tenant-a") is None saved.state = QodSessionState.DELETED saved.status_info = None await repo.save(saved) assert await repo.find_active_by_device({"phoneNumber": "+123456789"}, "tenant-a") is None Loading tests/unit/fakes.py +4 −1 Changes for tests/unit/fakes.py: 4 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -607,7 +607,10 @@ class FakeQodSessionRepository(QodSessionRepository): ) -> QodSession | None: active_states = (QodSessionState.REQUESTED, QodSessionState.AVAILABLE) for row in self.rows.values(): if row.state not in active_states or row.device is None: holds_resources = row.state in active_states or ( row.state == QodSessionState.UNAVAILABLE and row.status_info == "NETWORK_TERMINATED" ) if not holds_resources or row.device is None: continue operation = self._operation_repo.rows.get(row.operation_id) if operation is None or operation.app_provider_id != app_provider_id: Loading tests/unit/test_qod_service.py +26 −11 Changes for tests/unit/test_qod_service.py: 26 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -566,25 +566,39 @@ class TestCreateSessionConflict: ) async def _set_state( self, qod_session_repo: FakeQodSessionRepository, session_id: UUID, state: QodSessionState self, qod_session_repo: FakeQodSessionRepository, session_id: UUID, state: QodSessionState, status_info: Literal["DURATION_EXPIRED", "NETWORK_TERMINATED"] | None = None, ) -> None: stored = await qod_session_repo.get_by_id(session_id) assert stored is not None await qod_session_repo.save(stored.model_copy(update={"state": state})) await qod_session_repo.save( stored.model_copy(update={"state": state, "status_info": status_info}) ) @pytest.mark.parametrize("state", [QodSessionState.REQUESTED, QodSessionState.AVAILABLE]) async def test_rejects_second_session_for_an_active_device( @pytest.mark.parametrize( ("state", "status_info"), [ (QodSessionState.REQUESTED, None), (QodSessionState.AVAILABLE, None), (QodSessionState.UNAVAILABLE, "NETWORK_TERMINATED"), ], ) async def test_rejects_second_session_while_the_device_holds_resources( self, service: QualityOnDemandService, operation_repo: FakeOperationRepository, qod_session_repo: FakeQodSessionRepository, publisher: AsyncMock, state: QodSessionState, status_info: Literal["DURATION_EXPIRED", "NETWORK_TERMINATED"] | None, ) -> None: first = await service.create_session( request=_make_request(), tenant_id="tenant-1", app_provider_id="provider-1" ) await self._set_state(qod_session_repo, first.sessionId, state) await self._set_state(qod_session_repo, first.sessionId, state, status_info) with pytest.raises(ConflictException) as exc_info: await service.create_session( Loading @@ -599,12 +613,12 @@ class TestCreateSessionConflict: assert publisher.publish.await_count == 1 @pytest.mark.parametrize( "state", ("state", "status_info"), [ QodSessionState.UNAVAILABLE, QodSessionState.DELETION_REQUESTED, QodSessionState.DELETED, QodSessionState.ERROR, (QodSessionState.UNAVAILABLE, "DURATION_EXPIRED"), (QodSessionState.DELETION_REQUESTED, None), (QodSessionState.DELETED, None), (QodSessionState.ERROR, None), ], ) async def test_allows_new_session_once_the_previous_one_is_no_longer_active( Loading @@ -612,11 +626,12 @@ class TestCreateSessionConflict: service: QualityOnDemandService, qod_session_repo: FakeQodSessionRepository, state: QodSessionState, status_info: Literal["DURATION_EXPIRED", "NETWORK_TERMINATED"] | None, ) -> None: first = await service.create_session( request=_make_request(), tenant_id="tenant-1", app_provider_id="provider-1" ) await self._set_state(qod_session_repo, first.sessionId, state) await self._set_state(qod_session_repo, first.sessionId, state, status_info) second = await service.create_session( request=_make_request(), tenant_id="tenant-1", app_provider_id="provider-1" Loading Loading
src/open_exposure_gateway/adapters/database/repos/qod_sessions.py +7 −1 Changes for src/open_exposure_gateway/adapters/database/repos/qod_sessions.py: 7 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ from datetime import datetime from typing import Any from uuid import UUID from sqlalchemy import literal_column, or_, select, type_coerce from sqlalchemy import and_, literal_column, or_, select, type_coerce from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.ext.asyncio import AsyncSession Loading Loading @@ -44,7 +44,13 @@ class SqlQodSessionRepository(QodSessionRepository): .join(OperationRow, OperationRow.operation_id == QodSessionRow.operation_id) .where( OperationRow.app_provider_id == app_provider_id, or_( QodSessionRow.state.in_(_ACTIVE_STATES), and_( QodSessionRow.state == QodSessionState.UNAVAILABLE, QodSessionRow.status_info == "NETWORK_TERMINATED", ), ), or_(*identifier_matches), ) .limit(1) Loading
tests/integration/test_postgres.py +10 −0 Changes for tests/integration/test_postgres.py: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -602,7 +602,17 @@ async def test_qod_session_repo_find_active_by_device_scopes_by_identifier_state is None ) saved.state = QodSessionState.UNAVAILABLE saved.status_info = "NETWORK_TERMINATED" await repo.save(saved) assert await repo.find_active_by_device({"phoneNumber": "+123456789"}, "tenant-a") is not None saved.status_info = "DURATION_EXPIRED" await repo.save(saved) assert await repo.find_active_by_device({"phoneNumber": "+123456789"}, "tenant-a") is None saved.state = QodSessionState.DELETED saved.status_info = None await repo.save(saved) assert await repo.find_active_by_device({"phoneNumber": "+123456789"}, "tenant-a") is None Loading
tests/unit/fakes.py +4 −1 Changes for tests/unit/fakes.py: 4 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -607,7 +607,10 @@ class FakeQodSessionRepository(QodSessionRepository): ) -> QodSession | None: active_states = (QodSessionState.REQUESTED, QodSessionState.AVAILABLE) for row in self.rows.values(): if row.state not in active_states or row.device is None: holds_resources = row.state in active_states or ( row.state == QodSessionState.UNAVAILABLE and row.status_info == "NETWORK_TERMINATED" ) if not holds_resources or row.device is None: continue operation = self._operation_repo.rows.get(row.operation_id) if operation is None or operation.app_provider_id != app_provider_id: Loading
tests/unit/test_qod_service.py +26 −11 Changes for tests/unit/test_qod_service.py: 26 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -566,25 +566,39 @@ class TestCreateSessionConflict: ) async def _set_state( self, qod_session_repo: FakeQodSessionRepository, session_id: UUID, state: QodSessionState self, qod_session_repo: FakeQodSessionRepository, session_id: UUID, state: QodSessionState, status_info: Literal["DURATION_EXPIRED", "NETWORK_TERMINATED"] | None = None, ) -> None: stored = await qod_session_repo.get_by_id(session_id) assert stored is not None await qod_session_repo.save(stored.model_copy(update={"state": state})) await qod_session_repo.save( stored.model_copy(update={"state": state, "status_info": status_info}) ) @pytest.mark.parametrize("state", [QodSessionState.REQUESTED, QodSessionState.AVAILABLE]) async def test_rejects_second_session_for_an_active_device( @pytest.mark.parametrize( ("state", "status_info"), [ (QodSessionState.REQUESTED, None), (QodSessionState.AVAILABLE, None), (QodSessionState.UNAVAILABLE, "NETWORK_TERMINATED"), ], ) async def test_rejects_second_session_while_the_device_holds_resources( self, service: QualityOnDemandService, operation_repo: FakeOperationRepository, qod_session_repo: FakeQodSessionRepository, publisher: AsyncMock, state: QodSessionState, status_info: Literal["DURATION_EXPIRED", "NETWORK_TERMINATED"] | None, ) -> None: first = await service.create_session( request=_make_request(), tenant_id="tenant-1", app_provider_id="provider-1" ) await self._set_state(qod_session_repo, first.sessionId, state) await self._set_state(qod_session_repo, first.sessionId, state, status_info) with pytest.raises(ConflictException) as exc_info: await service.create_session( Loading @@ -599,12 +613,12 @@ class TestCreateSessionConflict: assert publisher.publish.await_count == 1 @pytest.mark.parametrize( "state", ("state", "status_info"), [ QodSessionState.UNAVAILABLE, QodSessionState.DELETION_REQUESTED, QodSessionState.DELETED, QodSessionState.ERROR, (QodSessionState.UNAVAILABLE, "DURATION_EXPIRED"), (QodSessionState.DELETION_REQUESTED, None), (QodSessionState.DELETED, None), (QodSessionState.ERROR, None), ], ) async def test_allows_new_session_once_the_previous_one_is_no_longer_active( Loading @@ -612,11 +626,12 @@ class TestCreateSessionConflict: service: QualityOnDemandService, qod_session_repo: FakeQodSessionRepository, state: QodSessionState, status_info: Literal["DURATION_EXPIRED", "NETWORK_TERMINATED"] | None, ) -> None: first = await service.create_session( request=_make_request(), tenant_id="tenant-1", app_provider_id="provider-1" ) await self._set_state(qod_session_repo, first.sessionId, state) await self._set_state(qod_session_repo, first.sessionId, state, status_info) second = await service.create_session( request=_make_request(), tenant_id="tenant-1", app_provider_id="provider-1" Loading