1b7fd6fb36
canHold checked role OR role instead of matching the current phase, so a red_tech still saw (and could call) Hold/Resume on a test already sitting in blue_evaluating. Fixed on both frontend and backend — the API had the same gap, not just the button visibility.
151 lines
4.6 KiB
Python
151 lines
4.6 KiB
Python
"""Tests for POST /tests/{id}/hold and /resume — must pause/resume the phase timer."""
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from app.models.test import Test
|
|
from app.models.technique import Technique
|
|
from app.models.enums import TestState
|
|
|
|
|
|
def _seed_technique(db) -> Technique:
|
|
technique = Technique(
|
|
mitre_id="T9999", name="Test Technique", tactic="execution", platforms=["linux"],
|
|
)
|
|
db.add(technique)
|
|
db.commit()
|
|
db.refresh(technique)
|
|
return technique
|
|
|
|
|
|
def _seed_executing_test(db, technique, created_by) -> Test:
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Holdable test",
|
|
created_by=created_by,
|
|
state=TestState.red_executing,
|
|
red_started_at=datetime.utcnow() - timedelta(minutes=10),
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
db.refresh(test)
|
|
return test
|
|
|
|
|
|
def _seed_blue_evaluating_test(db, technique, created_by) -> Test:
|
|
test = Test(
|
|
technique_id=technique.id,
|
|
name="Holdable test (blue)",
|
|
created_by=created_by,
|
|
state=TestState.blue_evaluating,
|
|
blue_started_at=datetime.utcnow() - timedelta(minutes=10),
|
|
)
|
|
db.add(test)
|
|
db.commit()
|
|
db.refresh(test)
|
|
return test
|
|
|
|
|
|
def test_hold_pauses_the_running_timer(client, db, red_tech_headers, red_tech_user):
|
|
technique = _seed_technique(db)
|
|
test = _seed_executing_test(db, technique, red_tech_user.id)
|
|
assert test.paused_at is None
|
|
|
|
resp = client.post(
|
|
f"/api/v1/tests/{test.id}/hold",
|
|
json={"reason": "waiting on lab access"},
|
|
headers=red_tech_headers,
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.json()["paused_at"] is not None
|
|
|
|
db.refresh(test)
|
|
assert test.paused_at is not None
|
|
|
|
|
|
def test_resume_clears_pause_and_accumulates_seconds(client, db, red_tech_headers, red_tech_user):
|
|
technique = _seed_technique(db)
|
|
test = _seed_executing_test(db, technique, red_tech_user.id)
|
|
|
|
resp = client.post(
|
|
f"/api/v1/tests/{test.id}/hold",
|
|
json={"reason": "waiting on lab access"},
|
|
headers=red_tech_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
resp = client.post(f"/api/v1/tests/{test.id}/resume", headers=red_tech_headers)
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.json()["paused_at"] is None
|
|
|
|
db.refresh(test)
|
|
assert test.paused_at is None
|
|
assert test.red_paused_seconds >= 0
|
|
|
|
|
|
def test_hold_does_not_double_pause_an_already_paused_timer(client, db, red_tech_headers, red_tech_user):
|
|
"""If the operator already paused the timer manually, holding must not
|
|
overwrite paused_at (which would reset the elapsed-pause calculation)."""
|
|
technique = _seed_technique(db)
|
|
test = _seed_executing_test(db, technique, red_tech_user.id)
|
|
|
|
resp = client.post(f"/api/v1/tests/{test.id}/pause-timer", headers=red_tech_headers)
|
|
assert resp.status_code == 200
|
|
db.refresh(test)
|
|
manual_pause_time = test.paused_at
|
|
|
|
resp = client.post(
|
|
f"/api/v1/tests/{test.id}/hold",
|
|
json={"reason": "waiting on lab access"},
|
|
headers=red_tech_headers,
|
|
)
|
|
assert resp.status_code == 200
|
|
|
|
db.refresh(test)
|
|
assert test.paused_at == manual_pause_time
|
|
|
|
|
|
def test_red_tech_cannot_hold_a_test_in_blue_evaluating(
|
|
client, db, red_tech_headers, red_tech_user,
|
|
):
|
|
"""Once a test has moved into Blue's queue, only blue_tech may hold it —
|
|
a red_tech is no longer involved at this stage."""
|
|
technique = _seed_technique(db)
|
|
test = _seed_blue_evaluating_test(db, technique, red_tech_user.id)
|
|
|
|
resp = client.post(
|
|
f"/api/v1/tests/{test.id}/hold",
|
|
json={"reason": "waiting on lab access"},
|
|
headers=red_tech_headers,
|
|
)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_blue_tech_can_hold_a_test_in_blue_evaluating(
|
|
client, db, blue_tech_headers, red_tech_user,
|
|
):
|
|
technique = _seed_technique(db)
|
|
test = _seed_blue_evaluating_test(db, technique, red_tech_user.id)
|
|
|
|
resp = client.post(
|
|
f"/api/v1/tests/{test.id}/hold",
|
|
json={"reason": "waiting on EDR access"},
|
|
headers=blue_tech_headers,
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
def test_blue_tech_cannot_resume_a_test_that_was_held_during_red_executing(
|
|
client, db, api, red_tech_headers, blue_tech_headers, red_tech_user,
|
|
):
|
|
technique = _seed_technique(db)
|
|
test = _seed_executing_test(db, technique, red_tech_user.id)
|
|
|
|
resp = api(
|
|
"post", f"/api/v1/tests/{test.id}/hold", red_tech_headers,
|
|
json={"reason": "waiting on lab access"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
resp = api("post", f"/api/v1/tests/{test.id}/resume", blue_tech_headers)
|
|
assert resp.status_code == 403
|