79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
|
|
spec = importlib.util.spec_from_file_location("incident_exercise", Path(__file__).resolve().parents[1] / "deploy" / "incident_exercise.py")
|
|
exercise = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(exercise)
|
|
|
|
|
|
def valid_record(gate="B"):
|
|
required = exercise.GATE_C_SCENARIOS if gate == "C" else exercise.GATE_B_SCENARIOS
|
|
return {
|
|
"schemaVersion": 1,
|
|
"system": "guestops-incident-exercise",
|
|
"targetGate": gate,
|
|
"dataClassification": "synthetic-only",
|
|
"releaseCommit": "a" * 40,
|
|
"releaseRecordSha256": "b" * 64,
|
|
"environment": "https://sandbox-guestops.futuresens.co.uk",
|
|
"operator": "Exercise operator",
|
|
"incidentCommander": "Incident commander",
|
|
"reviewedBy": "Independent reviewer",
|
|
"startedAt": "2026-09-29T09:00:00Z",
|
|
"endedAt": "2026-09-29T10:00:00Z",
|
|
"reviewedAt": "2026-09-29T11:00:00Z",
|
|
"targetsMinutes": {"detection": 10, "containment": 20, "recovery": 60},
|
|
"observedMinutes": {"detection": 5, "containment": 15, "recovery": 45},
|
|
"scenarios": [
|
|
{"id": scenario, "status": "pass", "evidence": [f"restricted-ticket-{index}"]}
|
|
for index, scenario in enumerate(sorted(required), 1)
|
|
],
|
|
"postExerciseState": {
|
|
"externalWrites": "disabled", "faqMode": "off", "unresolvedOperations": 0,
|
|
},
|
|
}
|
|
|
|
|
|
class IncidentExerciseTests(unittest.TestCase):
|
|
def test_gate_b_and_c_complete_records_pass(self):
|
|
exercise.validate(valid_record("B"))
|
|
exercise.validate(valid_record("C"))
|
|
|
|
def test_gate_specific_scenarios_are_required(self):
|
|
record = valid_record("C")
|
|
record["scenarios"].pop()
|
|
with self.assertRaisesRegex(ValueError, "exact incident scenario"):
|
|
exercise.validate(record)
|
|
|
|
def test_timings_must_meet_preagreed_targets(self):
|
|
record = valid_record()
|
|
record["observedMinutes"]["containment"] = 21
|
|
with self.assertRaisesRegex(ValueError, "Observed containment"):
|
|
exercise.validate(record)
|
|
record = valid_record()
|
|
record["targetsMinutes"]["recovery"] = True
|
|
with self.assertRaisesRegex(ValueError, "recovery target"):
|
|
exercise.validate(record)
|
|
|
|
def test_independent_people_and_safe_evidence_are_required(self):
|
|
record = valid_record()
|
|
record["reviewedBy"] = record["operator"]
|
|
with self.assertRaisesRegex(ValueError, "different people"):
|
|
exercise.validate(record)
|
|
record = valid_record()
|
|
record["scenarios"][0]["evidence"] = ["guest@example.invalid"]
|
|
with self.assertRaisesRegex(ValueError, "safe opaque"):
|
|
exercise.validate(record)
|
|
|
|
def test_safe_post_exercise_state_is_required(self):
|
|
record = valid_record()
|
|
record["postExerciseState"]["externalWrites"] = "enabled"
|
|
with self.assertRaisesRegex(ValueError, "writes disabled"):
|
|
exercise.validate(record)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|