- SimulationTemplate model + migration 0005 (CREATE TABLE + name index) - 5 CRUD endpoints under /api/templates (admin|redteam only, SOC 403) - POST /api/engagements/<eid>/simulations extended with optional template_id - serialize_template() reusing _enrich_techniques/_enrich_tactics helpers - IntegrityError → 409 for duplicate name on both POST and PATCH - 28 new tests (CRUD, RBAC, dedup, instantiation, migration round-trip) - 221 tests pass; ruff clean; mypy clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
240 lines
7.8 KiB
Python
240 lines
7.8 KiB
Python
"""SimulationTemplate CRUD: list, create, get, patch, delete + RBAC + dedup."""
|
|
from __future__ import annotations
|
|
|
|
from flask.testing import FlaskClient
|
|
|
|
from backend.app.extensions import db
|
|
from backend.app.models import User
|
|
from backend.app.models.simulation_template import SimulationTemplate
|
|
from backend.tests.conftest import auth_headers as _h # noqa: E402
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_template(client: FlaskClient, token: str, **kw) -> dict:
|
|
payload = {"name": "Template Alpha", **kw}
|
|
resp = client.post("/api/templates", headers=_h(token), json=payload)
|
|
assert resp.status_code == 201, resp.get_json()
|
|
return resp.get_json()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# List
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_templates_empty(client: FlaskClient, admin_token: str) -> None:
|
|
resp = client.get("/api/templates", headers=_h(admin_token))
|
|
assert resp.status_code == 200
|
|
assert resp.get_json() == []
|
|
|
|
|
|
def test_list_templates_soc_forbidden(client: FlaskClient, soc_token: str) -> None:
|
|
resp = client.get("/api/templates", headers=_h(soc_token))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_list_templates_unauthenticated(client: FlaskClient) -> None:
|
|
resp = client.get("/api/templates")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_create_template_as_admin(
|
|
client: FlaskClient, admin_user: User, admin_token: str
|
|
) -> None:
|
|
body = _make_template(
|
|
client,
|
|
admin_token,
|
|
description="desc",
|
|
commands="cmd",
|
|
prerequisites="prereq",
|
|
)
|
|
assert body["name"] == "Template Alpha"
|
|
assert body["description"] == "desc"
|
|
assert body["commands"] == "cmd"
|
|
assert body["prerequisites"] == "prereq"
|
|
assert body["techniques"] == []
|
|
assert body["tactics"] == []
|
|
assert body["created_by"] == {"id": admin_user.id, "username": "admin1"}
|
|
assert body["id"] is not None
|
|
|
|
|
|
def test_create_template_as_redteam(
|
|
client: FlaskClient, redteam_user: User, redteam_token: str
|
|
) -> None:
|
|
body = _make_template(client, redteam_token)
|
|
assert body["created_by"]["username"] == "redteam1"
|
|
|
|
|
|
def test_create_template_soc_forbidden(client: FlaskClient, soc_token: str) -> None:
|
|
resp = client.post(
|
|
"/api/templates", headers=_h(soc_token), json={"name": "T"}
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_create_template_missing_name(client: FlaskClient, admin_token: str) -> None:
|
|
resp = client.post("/api/templates", headers=_h(admin_token), json={})
|
|
assert resp.status_code == 400
|
|
assert "name" in resp.get_json()["error"]
|
|
|
|
|
|
def test_create_template_duplicate_name_409(
|
|
client: FlaskClient, admin_token: str
|
|
) -> None:
|
|
_make_template(client, admin_token)
|
|
resp = client.post(
|
|
"/api/templates", headers=_h(admin_token), json={"name": "Template Alpha"}
|
|
)
|
|
assert resp.status_code == 409
|
|
assert "already exists" in resp.get_json()["error"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Get single
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_template(client: FlaskClient, admin_token: str) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.get(f"/api/templates/{created['id']}", headers=_h(admin_token))
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["id"] == created["id"]
|
|
|
|
|
|
def test_get_template_not_found(client: FlaskClient, admin_token: str) -> None:
|
|
resp = client.get("/api/templates/9999", headers=_h(admin_token))
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_get_template_soc_forbidden(
|
|
client: FlaskClient, admin_token: str, soc_token: str
|
|
) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.get(f"/api/templates/{created['id']}", headers=_h(soc_token))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Patch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_patch_template_name(client: FlaskClient, admin_token: str) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.patch(
|
|
f"/api/templates/{created['id']}",
|
|
headers=_h(admin_token),
|
|
json={"name": "Renamed"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json()["name"] == "Renamed"
|
|
assert resp.get_json()["updated_at"] is not None
|
|
|
|
|
|
def test_patch_template_empty_name_rejected(
|
|
client: FlaskClient, admin_token: str
|
|
) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.patch(
|
|
f"/api/templates/{created['id']}",
|
|
headers=_h(admin_token),
|
|
json={"name": ""},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_patch_template_unknown_field_rejected(
|
|
client: FlaskClient, admin_token: str
|
|
) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.patch(
|
|
f"/api/templates/{created['id']}",
|
|
headers=_h(admin_token),
|
|
json={"bogus_field": "x"},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert "unknown fields" in resp.get_json()["error"]
|
|
|
|
|
|
def test_patch_template_duplicate_name_409(
|
|
client: FlaskClient, admin_token: str
|
|
) -> None:
|
|
_make_template(client, admin_token, name="T1")
|
|
t2 = _make_template(client, admin_token, name="T2")
|
|
resp = client.patch(
|
|
f"/api/templates/{t2['id']}",
|
|
headers=_h(admin_token),
|
|
json={"name": "T1"},
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
def test_patch_template_soc_forbidden(
|
|
client: FlaskClient, admin_token: str, soc_token: str
|
|
) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.patch(
|
|
f"/api/templates/{created['id']}",
|
|
headers=_h(soc_token),
|
|
json={"name": "X"},
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_patch_template_not_found(client: FlaskClient, admin_token: str) -> None:
|
|
resp = client.patch(
|
|
"/api/templates/9999", headers=_h(admin_token), json={"name": "X"}
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Delete
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_delete_template(
|
|
client: FlaskClient, app, admin_token: str
|
|
) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.delete(f"/api/templates/{created['id']}", headers=_h(admin_token))
|
|
assert resp.status_code == 204
|
|
with app.app_context():
|
|
assert db.session.get(SimulationTemplate, created["id"]) is None
|
|
|
|
|
|
def test_delete_template_not_found(client: FlaskClient, admin_token: str) -> None:
|
|
resp = client.delete("/api/templates/9999", headers=_h(admin_token))
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_delete_template_soc_forbidden(
|
|
client: FlaskClient, admin_token: str, soc_token: str
|
|
) -> None:
|
|
created = _make_template(client, admin_token)
|
|
resp = client.delete(f"/api/templates/{created['id']}", headers=_h(soc_token))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# List returns ordered by name
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_templates_ordered_by_name(
|
|
client: FlaskClient, admin_token: str
|
|
) -> None:
|
|
for name in ("Zebra", "Alpha", "Midpoint"):
|
|
_make_template(client, admin_token, name=name)
|
|
body = client.get("/api/templates", headers=_h(admin_token)).get_json()
|
|
names = [t["name"] for t in body]
|
|
assert names == sorted(names)
|