"""Sprint 4 — done read-only + Reopen tests (AC-18).""" from __future__ import annotations import pytest from flask.testing import FlaskClient from backend.tests.conftest import auth_headers as _h def _make_engagement(client: FlaskClient, token: str) -> dict: resp = client.post( "/api/engagements", headers=_h(token), json={"name": "Eng", "start_date": "2026-01-01"}, ) assert resp.status_code == 201 return resp.get_json() def _make_sim(client: FlaskClient, token: str, eid: int) -> dict: resp = client.post( f"/api/engagements/{eid}/simulations", headers=_h(token), json={"name": "Sim"}, ) assert resp.status_code == 201 return resp.get_json() def _advance_to_done(client: FlaskClient, redteam_token: str, soc_token: str, sid: int) -> None: client.post( f"/api/simulations/{sid}/transition", headers=_h(redteam_token), json={"to": "review_required"}, ) client.post( f"/api/simulations/{sid}/transition", headers=_h(soc_token), json={"to": "done"}, ) def _patch(client: FlaskClient, token: str, sid: int, payload: dict): return client.patch( f"/api/simulations/{sid}", headers=_h(token), json=payload, ) def _transition(client: FlaskClient, token: str, sid: int, to: str): return client.post( f"/api/simulations/{sid}/transition", headers=_h(token), json={"to": to}, ) # --------------------------------------------------------------------------- # AC-18.1 — PATCH on done → 409 for all roles # --------------------------------------------------------------------------- def test_patch_done_sim_admin_returns_409( client: FlaskClient, redteam_token: str, soc_token: str, admin_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _patch(client, admin_token, sim["id"], {"name": "renamed"}) assert resp.status_code == 409 assert resp.get_json()["error"] == "simulation is done — reopen first" def test_patch_done_sim_redteam_returns_409( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _patch(client, redteam_token, sim["id"], {"description": "x"}) assert resp.status_code == 409 def test_patch_done_sim_soc_returns_409( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _patch(client, soc_token, sim["id"], {"soc_comment": "afterthought"}) assert resp.status_code == 409 # --------------------------------------------------------------------------- # AC-18.2 — Reopen: done → review_required, all 3 roles # --------------------------------------------------------------------------- @pytest.mark.parametrize("role", ["redteam", "soc", "admin"]) def test_reopen_done_sim_allowed_for_all_roles( client: FlaskClient, redteam_token: str, soc_token: str, admin_token: str, role: str, ) -> None: token = {"redteam": redteam_token, "soc": soc_token, "admin": admin_token}[role] eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _transition(client, token, sim["id"], "review_required") assert resp.status_code == 200 assert resp.get_json()["status"] == "review_required" # --------------------------------------------------------------------------- # AC-18.3 — Other transitions from done → 409 # --------------------------------------------------------------------------- def test_transition_done_to_done_rejected( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _transition(client, redteam_token, sim["id"], "done") assert resp.status_code == 409 def test_transition_done_to_in_progress_rejected( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _transition(client, redteam_token, sim["id"], "in_progress") assert resp.status_code == 409 def test_transition_done_to_pending_rejected( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) resp = _transition(client, redteam_token, sim["id"], "pending") assert resp.status_code == 409 # --------------------------------------------------------------------------- # After reopen, PATCH is allowed again # --------------------------------------------------------------------------- def test_patch_allowed_after_reopen( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _advance_to_done(client, redteam_token, soc_token, sim["id"]) _transition(client, redteam_token, sim["id"], "review_required") resp = _patch(client, soc_token, sim["id"], {"soc_comment": "re-reviewed"}) assert resp.status_code == 200 assert resp.get_json()["soc_comment"] == "re-reviewed" # --------------------------------------------------------------------------- # AC-18.3 — Normal review_required path (pending/in_progress) unchanged # --------------------------------------------------------------------------- def test_transition_review_required_from_in_progress_still_needs_redteam( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) # Auto-advance to in_progress. _patch(client, redteam_token, sim["id"], {"description": "active"}) resp = _transition(client, soc_token, sim["id"], "review_required") assert resp.status_code == 403