feat(tests): richer server-side filters + true total count for GET /tests
GET /tests caps limit at 200, so any page relying on len(results) for a "total" count (e.g. Validated Tests) silently under-reports once there are more matches than the page size — the 200 shown was a page cap, not the real total. - Adds GET /tests/count, sharing its query-building with list_tests() via a new _build_test_query() so the two can't drift apart. - Adds technique_search (free-text on MITRE ID or name), attack_success, detection_result, and validated_from/validated_to filters, so callers aren't limited to state/technique_id/platform/creator.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
"""Tests for the expanded GET /tests filters and the new GET /tests/count.
|
||||
|
||||
Block: Validated Tests page couldn't show a true total once there were
|
||||
more than `limit` (max 200) matching tests, and had no server-side way to
|
||||
filter by technique/platform/date/outcome beyond a client-side name search.
|
||||
count_tests() shares its query-building with list_tests() (_build_test_query)
|
||||
so the two can't silently drift out of sync.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.models.enums import AttackSuccessResult, TestResult, TestState
|
||||
from app.models.technique import Technique
|
||||
from app.models.test import Test
|
||||
from app.services.test_crud_service import count_tests, list_tests
|
||||
|
||||
|
||||
def _seed(db, owner_id, *, mitre_id, name, technique_name=None, state=TestState.validated, **overrides):
|
||||
tech = Technique(mitre_id=mitre_id, name=technique_name or f"Technique {mitre_id}", tactic="execution", platforms=["windows"])
|
||||
db.add(tech)
|
||||
db.flush()
|
||||
test = Test(technique_id=tech.id, name=name, state=state, created_by=owner_id, **overrides)
|
||||
db.add(test)
|
||||
db.commit()
|
||||
db.refresh(test)
|
||||
return test
|
||||
|
||||
|
||||
def test_count_tests_matches_list_tests_total_beyond_page_size(db, red_lead_user):
|
||||
for i in range(5):
|
||||
_seed(db, red_lead_user.id, mitre_id=f"T900{i}", name=f"Validated {i}")
|
||||
|
||||
total = count_tests(db, state="validated")
|
||||
page = list_tests(db, state="validated", limit=2)
|
||||
|
||||
assert total == 5
|
||||
assert len(page) == 2
|
||||
|
||||
|
||||
def test_technique_search_matches_mitre_id_or_name(db, red_lead_user):
|
||||
_seed(db, red_lead_user.id, mitre_id="T1059.001", name="PowerShell test", technique_name="Command Line Interface")
|
||||
_seed(db, red_lead_user.id, mitre_id="T1055", name="Process Injection test", technique_name="Process Injection")
|
||||
|
||||
by_id = list_tests(db, technique_search="T1059")
|
||||
by_name = list_tests(db, technique_search="injection")
|
||||
|
||||
assert [t.name for t in by_id] == ["PowerShell test"]
|
||||
assert [t.name for t in by_name] == ["Process Injection test"]
|
||||
|
||||
|
||||
def test_attack_success_and_detection_result_filters(db, red_lead_user):
|
||||
_seed(
|
||||
db, red_lead_user.id, mitre_id="T9010", name="Successful and detected",
|
||||
attack_success=AttackSuccessResult.successful, detection_result=TestResult.detected,
|
||||
)
|
||||
_seed(
|
||||
db, red_lead_user.id, mitre_id="T9011", name="Successful and not detected",
|
||||
attack_success=AttackSuccessResult.successful, detection_result=TestResult.not_detected,
|
||||
)
|
||||
|
||||
results = list_tests(db, attack_success="successful", detection_result="detected")
|
||||
|
||||
assert [t.name for t in results] == ["Successful and detected"]
|
||||
|
||||
|
||||
def test_validated_date_range_filter(db, red_lead_user):
|
||||
old = _seed(db, red_lead_user.id, mitre_id="T9020", name="Old validation")
|
||||
old.blue_validated_at = datetime.utcnow() - timedelta(days=30)
|
||||
recent = _seed(db, red_lead_user.id, mitre_id="T9021", name="Recent validation")
|
||||
recent.blue_validated_at = datetime.utcnow() - timedelta(days=1)
|
||||
db.commit()
|
||||
|
||||
results = list_tests(db, validated_from=datetime.utcnow() - timedelta(days=7))
|
||||
|
||||
assert [t.name for t in results] == ["Recent validation"]
|
||||
|
||||
|
||||
def test_count_tests_endpoint_returns_true_total(api, db, red_lead_user, auth_headers):
|
||||
for i in range(3):
|
||||
_seed(db, red_lead_user.id, mitre_id=f"T903{i}", name=f"Counted {i}")
|
||||
|
||||
resp = api("get", "/api/v1/tests/count?state=validated", auth_headers)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 3
|
||||
Reference in New Issue
Block a user