Commit 4bf7ab8d authored by George Papathanail's avatar George Papathanail
Browse files

fix: return 409 while a NETWORK_TERMINATED session still holds the device

parent 65284f73
Loading
Loading
Loading
Loading
Loading
+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
@@ -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

@@ -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)
+10 −0
Changes for tests/integration/test_postgres.py: 10 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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

+4 −1
Changes for tests/unit/fakes.py: 4 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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:
+26 −11
Changes for tests/unit/test_qod_service.py: 26 added lines, 11 removed lines.
Original line number Diff line number Diff line
@@ -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(
@@ -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(
@@ -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"