feat(backend): enforce campaign scheduling, add reviewer queue filter, tactic order, duplicate-test counts

- start_execution now blocks a test from starting before its campaign's
  start_date, closing a gap where the field was persisted but never
  enforced
- GET /tests gains a reviewer_id "my reviews" filter for the red_review/
  blue_review lead-gate stage, distinct from pending_validation_side
  (which only ever covered the later in_review stage and ignored
  assignment entirely)
- get_coverage_by_tactic now returns all 14 MITRE tactics in canonical
  kill-chain order instead of an alphabetical, partial list — the
  regular Dashboard and Executive Dashboard both consume this endpoint
  and previously disagreed on order/completeness
- test-template listing now includes existing_test_count per technique
  so the catalog can warn before creating a likely-duplicate test
This commit is contained in:
kitos
2026-07-08 08:46:09 +02:00
parent 21c6febd22
commit 1c8fa436ab
12 changed files with 333 additions and 3 deletions
@@ -0,0 +1,76 @@
"""Tests for the campaign start_date scheduling gate on test execution.
Block 3 fix: `Campaign.start_date` was persisted and displayed but never
actually enforced — a test belonging to a campaign scheduled for the future
could still be started immediately. `start_execution` now blocks this.
"""
from datetime import datetime, timedelta
import pytest
from app.domain.errors import BusinessRuleViolation
from app.models.campaign import Campaign, CampaignTest
from app.models.enums import TestState
from app.models.technique import Technique
from app.models.test import Test
from app.services.test_workflow_service import start_execution
def _seed_test_in_campaign(db, owner_id, start_date):
tech = Technique(mitre_id="T1059.003", name="Windows Command Shell", tactic="execution", platforms=["windows"])
db.add(tech)
db.flush()
campaign = Campaign(name="Scheduled Campaign", type="custom", status="active", created_by=owner_id, start_date=start_date)
db.add(campaign)
db.flush()
test = Test(technique_id=tech.id, name="Scheduled test", state=TestState.draft, created_by=owner_id)
db.add(test)
db.flush()
db.add(CampaignTest(campaign_id=campaign.id, test_id=test.id, order_index=0))
db.commit()
db.refresh(test)
return test
def test_start_execution_blocked_before_campaign_start_date(db, red_tech_user):
future = datetime.utcnow() + timedelta(days=7)
test = _seed_test_in_campaign(db, red_tech_user.id, future)
with pytest.raises(BusinessRuleViolation):
start_execution(db, test, red_tech_user)
def test_start_execution_allowed_after_campaign_start_date(db, red_tech_user):
past = datetime.utcnow() - timedelta(days=1)
test = _seed_test_in_campaign(db, red_tech_user.id, past)
result = start_execution(db, test, red_tech_user)
assert result.state == TestState.red_executing
def test_start_execution_allowed_for_standalone_test(db, red_tech_user):
"""A test with no campaign at all is never gated by this check."""
tech = Technique(mitre_id="T1059.004", name="Unix Shell", tactic="execution", platforms=["linux"])
db.add(tech)
db.flush()
test = Test(technique_id=tech.id, name="Standalone test", state=TestState.draft, created_by=red_tech_user.id)
db.add(test)
db.commit()
db.refresh(test)
result = start_execution(db, test, red_tech_user)
assert result.state == TestState.red_executing
def test_start_execution_allowed_when_campaign_has_no_start_date(db, red_tech_user):
test = _seed_test_in_campaign(db, red_tech_user.id, None)
result = start_execution(db, test, red_tech_user)
assert result.state == TestState.red_executing