187 lines
6.8 KiB
Python
187 lines
6.8 KiB
Python
|
|
"""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"]
|