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:
@@ -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
|
||||
Reference in New Issue
Block a user