fix: multiple test-workflow bugs — draft wipe, dark date pickers, Jira status strings, Tempo gate, hold timer
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- Fix the red/blue draft form losing unsaved fields (e.g. execution_start_time) whenever an evidence upload refetches the test — the hydration effect now runs once per test, not on every refetch.
- Add color-scheme: dark so native date/time picker popups match the app theme instead of rendering white.
- Fix _STATE_TO_JIRA_STATUS: real Jira statuses are 'To Do' and 'Red Team test review', not 'To-Do' and 'RT Test Review' — confirmed live against the actual workflow transitions, which is why Submit to Blue Team never moved the ticket past 'In Progress'.
- Tempo worklog sync was silently blocked by TEMPO_ENABLED defaulting to False with no admin-facing toggle, even after configuring a Tempo token via Settings. Now bypassed once an admin or personal token is actually configured, mirroring Jira's DB-backed enabled flag.
- Hold now actually pauses the running phase timer (paused_at), and Resume accumulates the held duration into red/blue_paused_seconds — previously the timer kept counting through a hold.
This commit is contained in:
kitos
2026-07-10 15:24:19 +02:00
parent 997b38d333
commit be2a3fdced
8 changed files with 180 additions and 13 deletions
+90
View File
@@ -0,0 +1,90 @@
"""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 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
+3 -3
View File
@@ -119,11 +119,11 @@ def test_push_hold_event_sets_blocked(mock_get_client, mock_configured, db):
@pytest.mark.parametrize(
"new_state,expected_status",
[
("draft", "To-Do"),
("draft", "To Do"),
("red_executing", "In Progress"),
("red_review", "RT Test Review"),
("red_review", "Red Team test review"),
("blue_evaluating", "Queued Blue Team"),
("blue_review", "Blue Team Test Review"),
("blue_review", "Blue Team test review"),
("in_review", "Validation"),
("validated", "Done"),
("rejected", "Rejected"),
+42
View File
@@ -12,13 +12,55 @@ from app.services import tempo_service
def test_auto_log_test_worklog_skips_when_disabled(monkeypatch, db):
"""No env kill-switch, no admin token, no personal token — must no-op."""
monkeypatch.setattr("app.services.tempo_service.settings.TEMPO_ENABLED", False)
test = MagicMock()
test.id = uuid4()
user = MagicMock()
user.tempo_api_token = None
assert tempo_service.auto_log_test_worklog(db, test, user, "red_team_execution") is None
@patch("tempoapiclient.client_v4.Tempo")
def test_auto_log_test_worklog_proceeds_with_admin_token_even_when_disabled(
mock_tempo_cls, monkeypatch, db, admin_user
):
"""Regression: TEMPO_ENABLED defaults False with no UI toggle — an admin
who configures a Tempo token via Settings must not be silently blocked."""
monkeypatch.setattr("app.services.tempo_service.settings.TEMPO_ENABLED", False)
from app.models.system_config import SystemConfig
db.add(SystemConfig(key="tempo.admin_token", value="admin-tempo-token"))
test_id = uuid4()
link = JiraLink(
entity_type=JiraLinkEntityType.test,
entity_id=test_id,
jira_issue_key="TST-11",
jira_issue_id="10011",
created_by=admin_user.id,
)
db.add(link)
admin_user.tempo_api_token = None # relying on the admin token, not a personal one
admin_user.jira_account_id = "jira-account-123"
db.commit()
mock_tempo_instance = MagicMock()
mock_tempo_instance.create_worklog.return_value = {"id": "wl-2"}
mock_tempo_cls.return_value = mock_tempo_instance
test = MagicMock()
test.id = test_id
test.name = "Phishing simulation"
test.red_started_at = datetime(2026, 5, 18, 10, 0, 0)
test.updated_at = datetime(2026, 5, 18, 12, 0, 0)
test.created_at = test.updated_at
result = tempo_service.auto_log_test_worklog(db, test, admin_user, "red_team_execution")
assert result == {"id": "wl-2"}
mock_tempo_instance.create_worklog.assert_called_once()
def test_get_tempo_client_raises_when_disabled(monkeypatch):
monkeypatch.setattr("app.services.tempo_service.settings.TEMPO_ENABLED", False)
with pytest.raises(InvalidOperationError, match="not enabled"):