"""T-126: Tests de TestTemplates — CRUD, filters, instantiation, permissions. Tests the template CRUD endpoints, filter logic, template instantiation, soft-delete behaviour, and admin-only access control. Uses mock objects and router inspection to avoid needing a database. """ import sys import os import uuid import inspect from unittest.mock import MagicMock from types import ModuleType # --------------------------------------------------------------------------- # Stub heavy dependencies before importing app modules # --------------------------------------------------------------------------- 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: _ps = ModuleType("pydantic_settings") class _BaseSettings: def __init__(self, **kwargs): pass def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) _ps.BaseSettings = _BaseSettings sys.modules["pydantic_settings"] = _ps if "app.config" not in sys.modules: _cfg = 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" _cfg.settings = _FakeSettings() sys.modules["app.config"] = _cfg if "app.database" not in sys.modules: _db = ModuleType("app.database") _db.Base = type("Base", (), {"metadata": MagicMock()}) _db.get_db = MagicMock() sys.modules["app.database"] = _db for _mod in [ "taxii2client", "taxii2client.v20", "jose", "boto3", "botocore", "botocore.exceptions", "apscheduler", "apscheduler.schedulers", "apscheduler.schedulers.background", "apscheduler.triggers", "apscheduler.triggers.cron", ]: if _mod not in sys.modules: m = ModuleType(_mod) if _mod == "taxii2client.v20": m.Server = MagicMock elif _mod == "jose": m.JWTError = Exception; m.jwt = MagicMock() elif _mod == "boto3": m.client = MagicMock() elif _mod == "botocore.exceptions": m.ClientError = Exception elif _mod == "apscheduler.schedulers.background": m.BackgroundScheduler = MagicMock elif _mod == "apscheduler.triggers.cron": m.CronTrigger = MagicMock sys.modules[_mod] = m # --------------------------------------------------------------------------- # Imports # --------------------------------------------------------------------------- from app.routers.test_templates import ( router, list_templates, templates_by_technique, create_template, delete_template, toggle_template_active, template_stats, ) from app.routers.tests import create_test_from_template from app.schemas.test_template import TestTemplateCreate 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. test_create_template — admin can create a template # =========================================================================== def test_create_template(): """Admin can create a template — endpoint exists and requires admin role.""" routes = _get_route_paths() found = any("POST" in k and "{template_id}" not in k for k in routes) assert found, f"POST /test-templates not found. Routes: {list(routes.keys())}" # Verify admin role is required source = inspect.getsource(create_template) assert "require_role" in source and "admin" in source, \ "create_template must require admin role" # =========================================================================== # 2. test_list_templates_with_filters — source, platform, severity work # =========================================================================== def test_list_templates_with_filters(): """Filters of source, platform, severity, search all work.""" source = inspect.getsource(list_templates) # Verify all filter parameters exist in the function signature assert "source" in source, "List must accept source filter" assert "platform" in source, "List must accept platform filter" assert "severity" in source, "List must accept severity filter" assert "search" in source, "List must accept search filter" assert "mitre_technique_id" in source, "List must accept mitre_technique_id filter" # Verify ilike is used for search assert "ilike" in source, "Search should use ilike for case-insensitive matching" # =========================================================================== # 3. test_get_templates_by_technique — filter by MITRE technique # =========================================================================== def test_get_templates_by_technique(): """Endpoint to get templates by technique exists and filters correctly.""" 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())}" source = inspect.getsource(templates_by_technique) assert "mitre_technique_id" in source, "Must filter by mitre_technique_id" assert "is_active" in source, "Must filter only active templates" # =========================================================================== # 4. test_instantiate_template — create test from template pre-fills fields # =========================================================================== def test_instantiate_template(): """POST /tests/from-template creates a test pre-filled from template data.""" source = inspect.getsource(create_test_from_template) # Verify it reads from template and copies fields assert "template" in source, "Must reference template" assert "template.name" in source, "Must copy name from template" assert "template.description" in source, "Must copy description from template" assert "template.platform" in source, "Must copy platform from template" assert "template.attack_procedure" in source or "attack_procedure" in source, \ "Must copy attack_procedure from template" # Verify state is set to draft assert "draft" in source, "New test from template must be in draft state" # =========================================================================== # 5. test_soft_delete_template — deactivation doesn't physically remove # =========================================================================== def test_soft_delete_template(): """DELETE endpoint sets is_active=False instead of removing the record.""" source = inspect.getsource(delete_template) assert "is_active" in source, "Must set is_active" assert "False" in source, "Must set is_active to False" # Should NOT call db.delete(template) assert "db.delete" not in source, "Should NOT physically delete the template" assert "deactivated" in source.lower() or "soft" in source.lower() or "detail" in source.lower(), \ "Should return a deactivation message" # =========================================================================== # 6. test_non_admin_cannot_create_template — only admin role # =========================================================================== def test_non_admin_cannot_create_template(): """Only admin can create templates — enforce via require_role.""" source = inspect.getsource(create_template) assert 'require_role("admin")' in source, \ "create_template must use require_role('admin')" # Also check update and delete from app.routers.test_templates import update_template source_update = inspect.getsource(update_template) assert 'require_role("admin")' in source_update, \ "update_template must use require_role('admin')" source_delete = inspect.getsource(delete_template) assert 'require_role("admin")' in source_delete, \ "delete_template must use require_role('admin')" # =========================================================================== # 7. test_toggle_active_endpoint — toggle between active/inactive # =========================================================================== def test_toggle_active_endpoint(): """PATCH /test-templates/{id}/toggle-active exists and toggles is_active.""" routes = _get_route_paths() found = any("toggle-active" in k and "PATCH" in k for k in routes) assert found, f"PATCH /test-templates/{{id}}/toggle-active not found. Routes: {list(routes.keys())}" source = inspect.getsource(toggle_template_active) assert "is_active" in source, "Must reference is_active" assert "not" in source, "Must toggle (negate) the is_active value" assert 'require_role("admin")' in source, "Must require admin role" # =========================================================================== # 8. test_stats_endpoint — catalog statistics # =========================================================================== def test_stats_endpoint(): """GET /test-templates/stats returns catalog statistics.""" routes = _get_route_paths() found = any("stats" in k and "GET" in k for k in routes) assert found, f"GET /test-templates/stats not found. Routes: {list(routes.keys())}" source = inspect.getsource(template_stats) assert "by_source" in source, "Must return breakdown by source" assert "by_platform" in source, "Must return breakdown by platform" assert "active" in source, "Must return active count" assert 'require_role("admin")' in source, "Must require admin role" # =========================================================================== # 9. test_list_only_active_by_default — list filters inactive templates # =========================================================================== def test_list_only_active_by_default(): """The list endpoint filters to is_active=True by default.""" source = inspect.getsource(list_templates) assert "is_active" in source and "True" in source, \ "List must filter by is_active == True by default" # =========================================================================== # 10. test_pagination_support # =========================================================================== def test_pagination_support(): """List endpoint supports offset and limit pagination.""" source = inspect.getsource(list_templates) assert "offset" in source, "Must accept offset parameter" assert "limit" in source, "Must accept limit parameter" # --------------------------------------------------------------------------- # Run all # --------------------------------------------------------------------------- if __name__ == "__main__": print("T-126 Validation: TestTemplates CRUD Tests") print("=" * 55) test_create_template() test_list_templates_with_filters() test_get_templates_by_technique() test_instantiate_template() test_soft_delete_template() test_non_admin_cannot_create_template() test_toggle_active_endpoint() test_stats_endpoint() test_list_only_active_by_default() test_pagination_support() print("=" * 55) print("ALL T-126 validations PASSED!")