"""Simulation workflow / state machine tests.""" from __future__ import annotations 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": "Op Gamma", "start_date": "2026-06-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": "Workflow Sim"}, ) assert resp.status_code == 201 return resp.get_json() def _transition(client: FlaskClient, token: str, sid: int, to: str): return client.post( f"/api/simulations/{sid}/transition", headers=_h(token), json={"to": to}, ) # --------------------------------------------------------------------------- # Valid transitions # --------------------------------------------------------------------------- def test_transition_to_review_required_from_pending( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) assert sim["status"] == "pending" resp = _transition(client, redteam_token, sim["id"], "review_required") assert resp.status_code == 200 assert resp.get_json()["status"] == "review_required" def test_transition_to_review_required_from_in_progress( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) # Auto-advance to in_progress client.patch( f"/api/simulations/{sim['id']}", headers=_h(redteam_token), json={"description": "started"}, ) resp = _transition(client, redteam_token, sim["id"], "review_required") assert resp.status_code == 200 assert resp.get_json()["status"] == "review_required" def test_transition_to_done_by_redteam( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _transition(client, redteam_token, sim["id"], "review_required") resp = _transition(client, redteam_token, sim["id"], "done") assert resp.status_code == 200 assert resp.get_json()["status"] == "done" def test_transition_to_done_by_soc( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _transition(client, redteam_token, sim["id"], "review_required") resp = _transition(client, soc_token, sim["id"], "done") assert resp.status_code == 200 assert resp.get_json()["status"] == "done" def test_transition_to_done_by_admin( client: FlaskClient, admin_token: str ) -> None: eng = _make_engagement(client, admin_token) sim = _make_sim(client, admin_token, eng["id"]) _transition(client, admin_token, sim["id"], "review_required") resp = _transition(client, admin_token, sim["id"], "done") assert resp.status_code == 200 assert resp.get_json()["status"] == "done" # --------------------------------------------------------------------------- # Invalid transitions (AC-11.3) # --------------------------------------------------------------------------- def test_transition_done_from_pending_rejected( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) resp = _transition(client, redteam_token, sim["id"], "done") assert resp.status_code == 409 def test_transition_to_pending_rejected( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _transition(client, redteam_token, sim["id"], "review_required") resp = _transition(client, redteam_token, sim["id"], "pending") assert resp.status_code == 409 def test_transition_to_in_progress_rejected( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) resp = _transition(client, redteam_token, sim["id"], "in_progress") assert resp.status_code == 409 def test_transition_unknown_status_rejected( client: FlaskClient, redteam_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) resp = _transition(client, redteam_token, sim["id"], "nonexistent") assert resp.status_code == 409 def test_transition_review_required_from_done_is_reopen( client: FlaskClient, redteam_token: str ) -> None: """done → review_required is the Reopen path, now allowed (AC-18.2).""" eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) _transition(client, redteam_token, sim["id"], "review_required") _transition(client, redteam_token, sim["id"], "done") resp = _transition(client, redteam_token, sim["id"], "review_required") assert resp.status_code == 200 assert resp.get_json()["status"] == "review_required" # --------------------------------------------------------------------------- # RBAC by role # --------------------------------------------------------------------------- def test_soc_cannot_transition_to_review_required( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) resp = _transition(client, soc_token, sim["id"], "review_required") assert resp.status_code == 403 def test_soc_cannot_transition_to_done_from_pending( client: FlaskClient, redteam_token: str, soc_token: str ) -> None: eng = _make_engagement(client, redteam_token) sim = _make_sim(client, redteam_token, eng["id"]) resp = _transition(client, soc_token, sim["id"], "done") assert resp.status_code == 409 def test_transition_simulation_404(client: FlaskClient, redteam_token: str) -> None: resp = _transition(client, redteam_token, 9999, "review_required") assert resp.status_code == 404