"""Superadmin session timeout integration tests. Validates the absolute-only timeout policy for superadmin sessions. Superadmin sessions do NOT have idle timeout — only absolute timeout. """ import pytest import uuid from datetime import datetime, timedelta, timezone from tests.integration.client.base import ApiError # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def assert_success(response: dict, message_contains: str = "") -> dict: """Assert that an api_response-wrapped payload succeeded.""" data = response.get("data", {}) assert response.get("success") is not False, ( f"Expected success but got error: {response.get('message')}" ) if message_contains: assert message_contains.lower() in response.get("message", "").lower(), ( f"Expected message to contain '{message_contains}' but got: {response.get('message')}" ) return data def _get_session_row(integration_app, token: str): """Look up the Session model row for a given bearer token.""" from gatehouse_app.models.user.session import Session with integration_app.app_context(): return Session.query.filter_by(token=token).first() def _touch_session(integration_app, session_id: str, **updates): """Directly update columns on a Session row. Only use this to simulate the passage of time — never to assert internal state. """ from gatehouse_app.models.user.session import Session with integration_app.app_context(): sess = Session.query.get(session_id) for attr, value in updates.items(): setattr(sess, attr, value) from gatehouse_app import db db.session.commit() # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def superadmin_credentials(integration_app): """Create a superadmin and return login credentials.""" from gatehouse_app.services.superadmin_auth_service import SuperadminAuthService email = f"admin_{uuid.uuid4().hex[:8]}@gatehouse.local" password = "SuperAdmin123!" with integration_app.app_context(): sa = SuperadminAuthService.create_superadmin( email=email, credential=password, full_name="Test Superadmin", ) return {"id": str(sa.id), "email": email, "password": password} @pytest.fixture def logged_in_superadmin(integration_client, superadmin_credentials, integration_app): """Log in as superadmin and return session metadata. Returns dict with ``superadmin``, ``token``, ``session_id``, ``session_row``. """ creds = superadmin_credentials resp = integration_client.post( "/api/v1/superadmin/auth/login", data={"email": creds["email"], "password": creds["password"]}, ) data = assert_success(resp) token = data["token"] session_row = _get_session_row(integration_app, token) assert session_row is not None, "Session row should exist after superadmin login" return { "superadmin": creds, "token": token, "session_id": session_row.id, "session_row": session_row, } # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestSuperadminSessionTimeouts: """Absolute-only timeout behavior for superadmin sessions.""" def test_superadmin_session_valid_before_timeout( self, integration_client, logged_in_superadmin, ): """SA-SESS-01 — Fresh superadmin session is accepted.""" integration_client.set_token(logged_in_superadmin["token"]) result = integration_client.get("/api/v1/superadmin/auth/me") data = assert_success(result) assert "superadmin" in data def test_absolute_timeout_rejects_superadmin( self, integration_client, logged_in_superadmin, integration_app, ): """SA-SESS-02 — Superadmin session rejected after absolute timeout. Push ``created_at`` far into the past. The session must be rejected even though ``last_activity_at`` is fresh. """ _touch_session( integration_app, logged_in_superadmin["session_id"], created_at=datetime.now(timezone.utc) - timedelta(days=1), last_activity_at=datetime.now(timezone.utc), ) integration_client.set_token(logged_in_superadmin["token"]) with pytest.raises(ApiError) as exc_info: integration_client.get("/api/v1/superadmin/auth/me") assert exc_info.value.status_code == 401 def test_idle_timeout_does_NOT_reject_superadmin( self, integration_client, logged_in_superadmin, integration_app, ): """SA-SESS-03 — Superadmin sessions have NO idle timeout. Push ``last_activity_at`` far into the past but keep ``created_at`` recent. The session should still be valid because superadmin sessions only use absolute timeout. """ _touch_session( integration_app, logged_in_superadmin["session_id"], last_activity_at=datetime.now(timezone.utc) - timedelta(hours=1), ) integration_client.set_token(logged_in_superadmin["token"]) result = integration_client.get("/api/v1/superadmin/auth/me") data = assert_success(result) assert "superadmin" in data def test_revoked_superadmin_session_rejected( self, integration_client, logged_in_superadmin, ): """SA-SESS-04 — Revoked superadmin session is rejected.""" integration_client.set_token(logged_in_superadmin["token"]) # Logout revokes the session integration_client.post("/api/v1/superadmin/auth/logout") integration_client.clear_token() # Try using the old token integration_client.set_token(logged_in_superadmin["token"]) with pytest.raises(ApiError) as exc_info: integration_client.get("/api/v1/superadmin/auth/me") assert exc_info.value.status_code == 401 def test_superadmin_session_has_owner_type( self, integration_app, logged_in_superadmin, ): """SA-SESS-05 — Superadmin session row has owner_type='superadmin'.""" from gatehouse_app.models.user.session import Session from gatehouse_app.utils.constants import SessionType with integration_app.app_context(): sess = Session.query.get(logged_in_superadmin["session_id"]) assert sess is not None assert sess.owner_type == SessionType.SUPERADMIN assert sess.owner_id == logged_in_superadmin["superadmin"]["id"]