""" Tests for the compliance templates feature. Covers: - app/models.py – ComplianceTemplate model - app/utils/compliance_service.py – service functions (seed, evaluate, apply) - app/api/compliance.py – REST API endpoints - app/views/compliance.py – admin view route """ from unittest.mock import Mock, patch import pytest from fastapi import status from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool from app.database import Base, get_db from app.models import ComplianceTemplate from app.utils.compliance_service import ( COMPLIANCE_TEMPLATES, apply_template, evaluate_template_status, get_all_templates, get_compliance_summary, get_template_by_name, seed_compliance_templates, ) # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- @pytest.fixture() def ct_engine(): """In-memory SQLite engine for compliance template tests.""" engine = create_engine( "sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) Base.metadata.create_all(bind=engine) yield engine Base.metadata.drop_all(bind=engine) @pytest.fixture() def ct_session(ct_engine): """DB session scoped to one test.""" Session = sessionmaker(bind=ct_engine) session = Session() yield session session.close() @pytest.fixture() def ct_client(ct_engine): """TestClient with in-memory DB and admin override.""" from app.api.compliance import _require_admin from app.main import app def override_db(): Session = sessionmaker(bind=ct_engine) session = Session() try: yield session finally: session.close() def override_admin(): return {"email": "admin@test.com", "is_admin": True} app.dependency_overrides[get_db] = override_db app.dependency_overrides[_require_admin] = override_admin with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client: yield client app.dependency_overrides.clear() @pytest.fixture() def seeded_session(ct_session): """Session with compliance templates already seeded.""" seed_compliance_templates(ct_session) return ct_session @pytest.fixture() def seeded_client(ct_engine): """TestClient with seeded compliance templates.""" from app.api.compliance import _require_admin from app.main import app Session = sessionmaker(bind=ct_engine) session = Session() seed_compliance_templates(session) session.close() def override_db(): session = Session() try: yield session finally: session.close() def override_admin(): return {"email": "admin@test.com", "is_admin": True} app.dependency_overrides[get_db] = override_db app.dependency_overrides[_require_admin] = override_admin with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client: yield client app.dependency_overrides.clear() # --------------------------------------------------------------------------- # Model tests # --------------------------------------------------------------------------- @pytest.mark.unit class TestComplianceTemplateModel: """Tests for the ComplianceTemplate database model.""" def test_create_template(self, ct_session): """Test creating a compliance template.""" template = ComplianceTemplate( name="test_template", display_name="Test Template", description="A test compliance template", settings_json='{"auth_enabled": "True"}', enabled=False, status="not_applied", ) ct_session.add(template) ct_session.commit() assert template.id is not None assert template.name == "test_template" assert template.display_name == "Test Template" assert template.enabled is False assert template.status == "not_applied" def test_unique_name_constraint(self, ct_session): """Test that template names must be unique.""" t1 = ComplianceTemplate( name="unique_test", display_name="First", settings_json="{}", ) ct_session.add(t1) ct_session.commit() t2 = ComplianceTemplate( name="unique_test", display_name="Second", settings_json="{}", ) ct_session.add(t2) with pytest.raises(IntegrityError): ct_session.commit() ct_session.rollback() def test_default_values(self, ct_session): """Test default column values.""" template = ComplianceTemplate( name="defaults_test", display_name="Defaults", settings_json="{}", ) ct_session.add(template) ct_session.commit() assert template.enabled is False assert template.status == "not_applied" assert template.applied_at is None assert template.applied_by is None # --------------------------------------------------------------------------- # Service tests # --------------------------------------------------------------------------- @pytest.mark.unit class TestComplianceService: """Tests for compliance_service utility functions.""" def test_seed_creates_templates(self, ct_session): """Test that seeding creates all three compliance templates.""" seed_compliance_templates(ct_session) templates = ct_session.query(ComplianceTemplate).all() names = {t.name for t in templates} assert "gdpr" in names assert "hipaa" in names assert "soc2" in names assert len(templates) == 3 def test_seed_is_idempotent(self, ct_session): """Test that seeding twice does not create duplicates.""" seed_compliance_templates(ct_session) seed_compliance_templates(ct_session) templates = ct_session.query(ComplianceTemplate).all() assert len(templates) == 3 def test_seed_updates_display_name(self, ct_session): """Test that re-seeding updates display_name but preserves state.""" seed_compliance_templates(ct_session) gdpr = ct_session.query(ComplianceTemplate).filter_by(name="gdpr").first() gdpr.enabled = True ct_session.commit() seed_compliance_templates(ct_session) gdpr = ct_session.query(ComplianceTemplate).filter_by(name="gdpr").first() assert gdpr.enabled is True # User state preserved def test_get_all_templates(self, seeded_session): """Test getting all templates.""" result = get_all_templates(seeded_session) assert len(result) == 3 for t in result: assert "id" in t assert "name" in t assert "display_name" in t assert "checks" in t assert "check_count" in t def test_get_template_by_name_exists(self, seeded_session): """Test retrieving an existing template by name.""" result = get_template_by_name(seeded_session, "gdpr") assert result is not None assert result.name == "gdpr" def test_get_template_by_name_missing(self, seeded_session): """Test retrieving a non-existent template.""" result = get_template_by_name(seeded_session, "nonexistent") assert result is None @patch("app.utils.settings_service.get_all_settings_from_db") def test_evaluate_template_compliant(self, mock_settings, seeded_session): """Test evaluation when all checks pass.""" mock_settings.return_value = { "auth_enabled": "True", "sentry_send_default_pii": "False", "security_headers_enabled": "True", "security_header_hsts_enabled": "True", "security_header_csp_enabled": "True", "security_header_x_frame_options_enabled": "True", "enable_deduplication": "True", } result = evaluate_template_status(seeded_session, "gdpr") assert result["status"] == "compliant" assert result["passed"] == result["total"] assert result["failed"] == 0 @patch("app.utils.settings_service.get_all_settings_from_db") def test_evaluate_template_non_compliant(self, mock_settings, seeded_session): """Test evaluation when no checks pass.""" mock_settings.return_value = {} with patch("app.config.settings") as mock_app: mock_app.auth_enabled = False mock_app.sentry_send_default_pii = True mock_app.security_headers_enabled = False mock_app.security_header_hsts_enabled = False mock_app.security_header_csp_enabled = False mock_app.security_header_x_frame_options_enabled = False mock_app.enable_deduplication = False result = evaluate_template_status(seeded_session, "gdpr") assert result["status"] in ("non_compliant", "partial") assert result["failed"] > 0 def test_evaluate_unknown_template(self, seeded_session): """Test evaluation of a non-existent template name.""" result = evaluate_template_status(seeded_session, "unknown") assert result["status"] == "unknown" assert result["total"] == 0 @patch("app.utils.settings_service.save_setting_to_db") @patch("app.utils.settings_service.get_all_settings_from_db") def test_apply_template_success(self, mock_get_settings, mock_save, seeded_session): """Test successfully applying a template.""" mock_save.return_value = True mock_get_settings.return_value = { "auth_enabled": "True", "sentry_send_default_pii": "False", "security_headers_enabled": "True", "security_header_hsts_enabled": "True", "security_header_csp_enabled": "True", "security_header_x_frame_options_enabled": "True", "enable_deduplication": "True", } result = apply_template(seeded_session, "gdpr", applied_by="test@admin.com") assert result["success"] is True assert result["template"] == "gdpr" assert "applied_settings" in result # Verify template record updated gdpr = seeded_session.query(ComplianceTemplate).filter_by(name="gdpr").first() assert gdpr.enabled is True assert gdpr.applied_by == "test@admin.com" assert gdpr.applied_at is not None def test_apply_unknown_template(self, seeded_session): """Test applying a non-existent template.""" result = apply_template(seeded_session, "nonexistent") assert result["success"] is False assert "error" in result @patch("app.utils.settings_service.get_all_settings_from_db") def test_get_compliance_summary(self, mock_settings, seeded_session): """Test compliance summary across all templates.""" mock_settings.return_value = {} result = get_compliance_summary(seeded_session) assert "overall_status" in result assert "total_checks" in result assert "total_passed" in result assert "total_failed" in result assert "templates" in result assert len(result["templates"]) == 3 def test_compliance_templates_have_checks(self): """Test that all built-in templates have compliance checks.""" for name, defn in COMPLIANCE_TEMPLATES.items(): assert "checks" in defn, f"Template {name} missing checks" assert len(defn["checks"]) > 0, f"Template {name} has no checks" for check in defn["checks"]: assert "key" in check assert "expected" in check assert "label" in check assert "description" in check def test_compliance_templates_have_settings(self): """Test that all built-in templates have settings to apply.""" for name, defn in COMPLIANCE_TEMPLATES.items(): assert "settings" in defn, f"Template {name} missing settings" assert len(defn["settings"]) > 0, f"Template {name} has no settings" # --------------------------------------------------------------------------- # API tests # --------------------------------------------------------------------------- @pytest.mark.integration class TestComplianceAPI: """Tests for compliance API endpoints.""" def test_list_templates(self, seeded_client): """Test GET /api/compliance/templates.""" resp = seeded_client.get("/api/compliance/templates") assert resp.status_code == status.HTTP_200_OK data = resp.json() assert isinstance(data, list) assert len(data) == 3 names = {t["name"] for t in data} assert names == {"gdpr", "hipaa", "soc2"} def test_get_single_template(self, seeded_client): """Test GET /api/compliance/templates/gdpr.""" resp = seeded_client.get("/api/compliance/templates/gdpr") assert resp.status_code == status.HTTP_200_OK data = resp.json() assert data["name"] == "gdpr" assert "display_name" in data assert "checks" in data def test_get_nonexistent_template(self, seeded_client): """Test GET /api/compliance/templates/unknown returns 404.""" resp = seeded_client.get("/api/compliance/templates/unknown") assert resp.status_code == status.HTTP_404_NOT_FOUND def test_get_template_status(self, seeded_client): """Test GET /api/compliance/templates/gdpr/status.""" resp = seeded_client.get("/api/compliance/templates/gdpr/status") assert resp.status_code == status.HTTP_200_OK data = resp.json() assert "status" in data assert "total" in data assert "passed" in data assert "failed" in data assert "check_results" in data def test_apply_template(self, seeded_client): """Test POST /api/compliance/templates/gdpr/apply.""" resp = seeded_client.post("/api/compliance/templates/gdpr/apply") assert resp.status_code == status.HTTP_200_OK data = resp.json() assert data["success"] is True assert data["template"] == "gdpr" assert "applied_settings" in data def test_apply_nonexistent_template(self, seeded_client): """Test POST /api/compliance/templates/unknown/apply returns 404.""" resp = seeded_client.post("/api/compliance/templates/unknown/apply") assert resp.status_code == status.HTTP_404_NOT_FOUND def test_compliance_summary(self, seeded_client): """Test GET /api/compliance/summary.""" resp = seeded_client.get("/api/compliance/summary") assert resp.status_code == status.HTTP_200_OK data = resp.json() assert "overall_status" in data assert "total_checks" in data assert "templates" in data assert len(data["templates"]) == 3 def test_templates_require_admin(self, ct_engine): """Test that endpoints require admin access.""" from app.main import app def override_db(): Session = sessionmaker(bind=ct_engine) session = Session() try: yield session finally: session.close() app.dependency_overrides[get_db] = override_db # Do NOT override _require_admin so it checks session with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client: resp = client.get("/api/compliance/templates") assert resp.status_code == status.HTTP_403_FORBIDDEN app.dependency_overrides.clear() # --------------------------------------------------------------------------- # View tests # --------------------------------------------------------------------------- @pytest.mark.unit class TestComplianceView: """Tests for the compliance view route.""" @patch("app.views.compliance.templates") @patch("app.views.compliance.settings") @pytest.mark.asyncio async def test_compliance_page_admin(self, mock_settings, mock_templates): """Test compliance page renders for admin users.""" from app.views.compliance import compliance_page mock_settings.version = "1.0.0" mock_request = Mock() mock_request.session = {"user": {"id": "admin", "is_admin": True}} await compliance_page(mock_request) mock_templates.TemplateResponse.assert_called_once() call_args = mock_templates.TemplateResponse.call_args assert call_args[0][0] == "compliance.html" @pytest.mark.asyncio async def test_compliance_page_non_admin_redirects(self): """Test compliance page redirects non-admin users.""" from app.views.compliance import compliance_page mock_request = Mock() mock_request.session = {"user": {"id": "user1", "is_admin": False}} result = await compliance_page(mock_request) assert result.status_code == 302 # --------------------------------------------------------------------------- # Config / settings metadata tests # --------------------------------------------------------------------------- @pytest.mark.unit class TestComplianceConfig: """Tests for compliance configuration settings.""" def test_compliance_enabled_default(self): """Test that compliance_enabled defaults to True.""" from app.config import settings assert hasattr(settings, "compliance_enabled") assert settings.compliance_enabled is True def test_compliance_enabled_in_setting_metadata(self): """Test that compliance_enabled has SETTING_METADATA entry.""" from app.utils.settings_service import SETTING_METADATA assert "compliance_enabled" in SETTING_METADATA meta = SETTING_METADATA["compliance_enabled"] assert meta["category"] == "Feature Flags" assert meta["type"] == "boolean"