- Add test_test_entity.py with 46 pure unit tests covering the full domain entity - Fix _FakeSettings in 11 test files (REPORT_TEMPLATES_DIR, JIRA, TEMPO) - Fix stale db.commit assertions to db.flush after UoW refactor - Add missing mock fields for TestEntity.from_orm compatibility - Make database.py skip pool args for SQLite in test environment - Disable slowapi rate limiter in test client fixture - Inject test engine into app.database to fix threading errors - Update role assertions to match current require_any_role policy - Mark 6 legacy V1 endpoint tests as xfail (replaced by V2 workflow)
209 lines
7.8 KiB
Python
209 lines
7.8 KiB
Python
"""Validation tests for T-111: TestTemplates CRUD Router.
|
|
|
|
Tests the router structure, endpoint presence, and filter logic.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import uuid
|
|
from unittest.mock import MagicMock
|
|
from types import ModuleType
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Stubs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
backend_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
if backend_dir not in sys.path:
|
|
sys.path.insert(0, backend_dir)
|
|
|
|
if "pydantic_settings" not in sys.modules:
|
|
pydantic_settings_mock = ModuleType("pydantic_settings")
|
|
class _BaseSettings:
|
|
def __init__(self, **kwargs): pass
|
|
def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs)
|
|
pydantic_settings_mock.BaseSettings = _BaseSettings
|
|
sys.modules["pydantic_settings"] = pydantic_settings_mock
|
|
|
|
if "app.config" not in sys.modules:
|
|
config_mod = ModuleType("app.config")
|
|
class _FakeSettings:
|
|
DATABASE_URL = "sqlite:///:memory:"
|
|
SECRET_KEY = "test"
|
|
ALGORITHM = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
|
MINIO_ENDPOINT = "localhost:9000"
|
|
MINIO_ACCESS_KEY = "test"
|
|
MINIO_SECRET_KEY = "test"
|
|
MINIO_BUCKET = "test"
|
|
REPORT_TEMPLATES_DIR = "app/templates/reports"
|
|
REPORT_OUTPUT_DIR = "/tmp/aegis_reports"
|
|
COMPANY_NAME = "Test Org"
|
|
COMPANY_LOGO_PATH = "app/templates/reports/assets/logo.png"
|
|
JIRA_ENABLED = False
|
|
JIRA_URL = ""
|
|
JIRA_USERNAME = ""
|
|
JIRA_API_TOKEN = ""
|
|
JIRA_IS_CLOUD = True
|
|
JIRA_DEFAULT_PROJECT = ""
|
|
JIRA_ISSUE_TYPE_TEST = "Task"
|
|
JIRA_ISSUE_TYPE_CAMPAIGN = "Epic"
|
|
TEMPO_ENABLED = False
|
|
TEMPO_API_TOKEN = ""
|
|
TEMPO_DEFAULT_WORK_TYPE = "Red Team"
|
|
NVD_API_KEY = ""
|
|
STALE_THRESHOLD_DAYS = 365
|
|
CORS_ORIGINS = "http://localhost:3000"
|
|
SCORING_WEIGHT_TESTS = 40
|
|
SCORING_WEIGHT_DETECTION_RULES = 20
|
|
SCORING_WEIGHT_D3FEND = 15
|
|
SCORING_WEIGHT_FRESHNESS = 15
|
|
SCORING_WEIGHT_PLATFORM_DIVERSITY = 10
|
|
config_mod.settings = _FakeSettings()
|
|
sys.modules["app.config"] = config_mod
|
|
|
|
if "app.database" not in sys.modules:
|
|
db_mod = ModuleType("app.database")
|
|
db_mod.Base = type("Base", (), {"metadata": MagicMock()})
|
|
db_mod.get_db = MagicMock()
|
|
sys.modules["app.database"] = db_mod
|
|
|
|
for mod_name in [
|
|
"taxii2client", "taxii2client.v20",
|
|
"jose", "boto3", "botocore", "botocore.exceptions",
|
|
"apscheduler", "apscheduler.schedulers",
|
|
"apscheduler.schedulers.background",
|
|
"apscheduler.triggers", "apscheduler.triggers.cron",
|
|
]:
|
|
if mod_name not in sys.modules:
|
|
m = ModuleType(mod_name)
|
|
if mod_name == "taxii2client.v20": m.Server = MagicMock
|
|
elif mod_name == "jose": m.JWTError = Exception; m.jwt = MagicMock()
|
|
elif mod_name == "boto3": m.client = MagicMock()
|
|
elif mod_name == "botocore.exceptions": m.ClientError = Exception
|
|
elif mod_name == "apscheduler.schedulers.background": m.BackgroundScheduler = MagicMock
|
|
elif mod_name == "apscheduler.triggers.cron": m.CronTrigger = MagicMock
|
|
sys.modules[mod_name] = m
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Imports
|
|
# ---------------------------------------------------------------------------
|
|
|
|
from app.routers.test_templates import router
|
|
import inspect
|
|
|
|
|
|
def _get_route_paths():
|
|
routes = {}
|
|
for route in router.routes:
|
|
path = getattr(route, "path", "")
|
|
methods = getattr(route, "methods", set())
|
|
for method in methods:
|
|
routes[f"{method} {path}"] = route
|
|
return routes
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. GET /test-templates returns paginated list
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_endpoint_exists():
|
|
routes = _get_route_paths()
|
|
found = any("GET" in k and (k.endswith(" ") or k == "GET " or k == "GET /")
|
|
for k in routes) or any("GET" in k and "{template_id}" not in k and "by-technique" not in k for k in routes)
|
|
assert found, f"GET /test-templates not found. Routes: {list(routes.keys())}"
|
|
print(" [PASS] GET /test-templates returns paginated list")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. GET /test-templates?source=atomic_red_team filters by source
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_has_source_filter():
|
|
from app.routers.test_templates import list_templates
|
|
source = inspect.getsource(list_templates)
|
|
assert "source" in source and "filter" in source.lower()
|
|
print(" [PASS] GET /test-templates?source=atomic_red_team filters by source")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. GET /test-templates?platform=windows filters by platform
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_has_platform_filter():
|
|
from app.routers.test_templates import list_templates
|
|
source = inspect.getsource(list_templates)
|
|
assert "platform" in source and "filter" in source.lower()
|
|
print(" [PASS] GET /test-templates?platform=windows filters by platform")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. GET /test-templates/by-technique/T1059.001 returns technique templates
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_by_technique_endpoint():
|
|
routes = _get_route_paths()
|
|
found = any("by-technique" in k and "GET" in k for k in routes)
|
|
assert found, f"GET /test-templates/by-technique/{{mitre_id}} not found. Routes: {list(routes.keys())}"
|
|
print(" [PASS] GET /test-templates/by-technique/{mitre_id} endpoint exists")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. POST /test-templates only accessible by admin
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_create_admin_only():
|
|
from app.routers.test_templates import create_template
|
|
source = inspect.getsource(create_template)
|
|
assert "require_any_role" in source or "require_role" in source
|
|
print(" [PASS] POST /test-templates only accessible by admin")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. DELETE /test-templates/{id} does soft delete (is_active=False)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_soft_delete():
|
|
from app.routers.test_templates import delete_template
|
|
source = inspect.getsource(delete_template)
|
|
assert "is_active" in source and "False" in source
|
|
print(" [PASS] DELETE /test-templates/{id} does soft delete (is_active=False)")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Search filter looks in name and description
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_search_filter():
|
|
from app.routers.test_templates import list_templates
|
|
source = inspect.getsource(list_templates)
|
|
assert "search" in source
|
|
assert "name" in source and "description" in source
|
|
assert "ilike" in source
|
|
print(" [PASS] Search filter searches in name and description")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run all
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if __name__ == "__main__":
|
|
print("T-111 Validation: TestTemplates CRUD Router")
|
|
print("=" * 50)
|
|
test_list_endpoint_exists()
|
|
test_list_has_source_filter()
|
|
test_list_has_platform_filter()
|
|
test_by_technique_endpoint()
|
|
test_create_admin_only()
|
|
test_soft_delete()
|
|
test_search_filter()
|
|
print("=" * 50)
|
|
print("ALL T-111 validations PASSED!")
|