"""End-to-end smoke: login → create engagement → list engagement (sprint 1). Uses the testcontainers Postgres scaffold + Flask test client. Each test seeds a single RT-lead user and signs in over the session-cookie surface the frontend will consume. """ from __future__ import annotations from uuid import UUID import pytest from mimic.auth.password import hash_password from mimic.db.models import Group, User, UserGroup from mimic.db.types import UserType from mimic.rbac.matrix import GROUP_PERMISSIONS, GroupName, Permission pytestmark = pytest.mark.integration def _seed_rt_lead( db, email: str = "lead@example.org", password: str = "lead-secret-1", # noqa: S107 ) -> UUID: """Create an rt_lead user + the rt_lead group + the membership link.""" group = db.session.query(Group).filter_by(name=GroupName.RT_LEAD.value).first() if group is None: group = Group(name=GroupName.RT_LEAD.value, description="Red team lead") db.session.add(group) db.session.flush() user = User( email=email, display_name="Lead", type=UserType.RT_LEAD, local_password_hash=hash_password(password, rounds=4), ) db.session.add(user) db.session.flush() db.session.add(UserGroup(user_id=user.id, group_id=group.id, engagement_id=None)) db.session.commit() return user.id def test_login_then_create_and_list_engagement(app, client) -> None: from mimic.extensions import db # noqa: PLC0415 (must follow app fixture) with app.app_context(): _seed_rt_lead(db) # 1. /me before login → 401, uniform envelope. response = client.get("/api/v1/auth/me") assert response.status_code == 401 body = response.get_json() assert body == {"error": "not_authenticated", "message": "no active session"} # 2. login → 200, body shape matches CurrentUser, session cookie set. response = client.post( "/api/v1/auth/login", json={"username": "lead@example.org", "password": "lead-secret-1"}, ) assert response.status_code == 200 user_payload = response.get_json() assert user_payload["username"] == "lead@example.org" assert user_payload["role"] == "rt_lead" assert Permission.ENGAGEMENT_CREATE.value in user_payload["permissions"] assert sorted(user_payload["permissions"]) == sorted( p.value for p in GROUP_PERMISSIONS[GroupName.RT_LEAD] ) # 3. /me after login → 200, same shape. response = client.get("/api/v1/auth/me") assert response.status_code == 200 assert response.get_json()["username"] == "lead@example.org" # 4. POST /engagements → 201, created_by_id is current user. response = client.post( "/api/v1/engagements/", json={"client_name": "Acme Demo", "c2_type": "mythic"}, ) assert response.status_code == 201 engagement = response.get_json() assert engagement["client_name"] == "Acme Demo" # 5. GET /engagements lists it (RT lead sees everything). response = client.get("/api/v1/engagements/") assert response.status_code == 200 listing = response.get_json() assert any(e["id"] == engagement["id"] for e in listing) # 6. logout → 204; subsequent /me → 401. response = client.post("/api/v1/auth/logout") assert response.status_code == 204 response = client.get("/api/v1/auth/me") assert response.status_code == 401 def test_login_rejects_bad_credentials(app, client) -> None: from mimic.extensions import db # noqa: PLC0415 with app.app_context(): _seed_rt_lead(db, email="bob@example.org", password="hunter2") # Wrong password. response = client.post( "/api/v1/auth/login", json={"username": "bob@example.org", "password": "wrong"}, ) assert response.status_code == 401 assert response.get_json() == { "error": "invalid_credentials", "message": "invalid username or password", } # Unknown user — same uniform message (no enumeration leak). response = client.post( "/api/v1/auth/login", json={"username": "ghost@example.org", "password": "hunter2"}, ) assert response.status_code == 401 assert response.get_json() == { "error": "invalid_credentials", "message": "invalid username or password", } def test_logout_without_session_returns_401(app, client) -> None: response = client.post("/api/v1/auth/logout") assert response.status_code == 401 assert response.get_json() == {"error": "not_authenticated", "message": "no active session"} def test_engagements_route_accepts_both_slash_variants(app, client) -> None: """strict_slashes=False: `/engagements` and `/engagements/` both match the same handler — no 308 redirect that would drop the session cookie on some browsers (frontend fix request).""" from mimic.extensions import db # noqa: PLC0415 with app.app_context(): _seed_rt_lead(db, email="dual@example.org", password="dual-slash-1") client.post( "/api/v1/auth/login", json={"username": "dual@example.org", "password": "dual-slash-1"}, ) no_slash = client.get("/api/v1/engagements") with_slash = client.get("/api/v1/engagements/") assert no_slash.status_code == 200 assert with_slash.status_code == 200 assert no_slash.get_json() == with_slash.get_json() def test_engagement_create_validation_error_returns_uniform_envelope(app, client) -> None: """Pydantic 422 must flow through the global handler with `details` carrying the per-field error list (frontend uses it for form mapping).""" from mimic.extensions import db # noqa: PLC0415 with app.app_context(): _seed_rt_lead(db, email="form@example.org", password="form-secret-1") client.post( "/api/v1/auth/login", json={"username": "form@example.org", "password": "form-secret-1"}, ) response = client.post("/api/v1/engagements", json={"description": "no client name"}) assert response.status_code == 422 body = response.get_json() assert body["error"] == "validation_error" assert body["message"] == "request failed" assert isinstance(body["details"], list) locs = [tuple(entry["loc"]) for entry in body["details"]] assert ("client_name",) in locs def test_unknown_route_returns_uniform_404(app, client) -> None: response = client.get("/api/v1/does-not-exist") assert response.status_code == 404 body = response.get_json() assert body["error"] == "not_found" assert "message" in body def test_bad_json_body_returns_uniform_400(app, client) -> None: from mimic.extensions import db # noqa: PLC0415 with app.app_context(): _seed_rt_lead(db, email="raw@example.org", password="raw-secret-1") client.post( "/api/v1/auth/login", json={"username": "raw@example.org", "password": "raw-secret-1"}, ) response = client.post( "/api/v1/engagements", data="not-json", content_type="application/json", ) assert response.status_code == 400 body = response.get_json() assert body["error"] == "bad_request" assert body["message"] == "JSON body required"