Files
breakpilot-compliance/backend-compliance/tests/test_canonical_control_routes.py
Benjamin Admin 050f353192
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 40s
CI/CD / test-python-backend-compliance (push) Successful in 41s
CI/CD / test-python-document-crawler (push) Successful in 26s
CI/CD / test-python-dsms-gateway (push) Successful in 23s
CI/CD / validate-canonical-controls (push) Successful in 18s
CI/CD / deploy-hetzner (push) Successful in 2m26s
feat(canonical-controls): Canonical Control Library — rechtssichere Security Controls
Eigenstaendig formulierte Security Controls mit unabhaengiger Taxonomie
und Open-Source-Verankerung (OWASP, NIST, ENISA). Keine BSI-Nomenklatur.

- Migration 044: 5 DB-Tabellen (frameworks, controls, sources, licenses, mappings)
- 10 Seed Controls mit 39 Open-Source-Referenzen
- License Gate: Quellen-Berechtigungspruefung (analysis/excerpt/embeddings/product)
- Too-Close-Detektor: 5 Metriken (exact-phrase, token-overlap, ngram, embedding, LCS)
- REST API: 8 Endpoints unter /v1/canonical/
- Go Loader mit Multi-Index (ID, domain, severity, framework)
- Frontend: Control Library Browser + Provenance Wiki
- CI/CD: validate-controls.py Job (schema, no-leak, open-anchors)
- 67 Tests (8 Go + 59 Python), alle PASS
- MkDocs Dokumentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 19:55:06 +01:00

226 lines
7.5 KiB
Python

"""Tests for Canonical Control Library routes (canonical_control_routes.py)."""
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime, timezone
from compliance.api.canonical_control_routes import (
FrameworkResponse,
ControlResponse,
SimilarityCheckRequest,
SimilarityCheckResponse,
_control_row,
)
class TestFrameworkResponse:
"""Tests for FrameworkResponse model."""
def test_basic_creation(self):
resp = FrameworkResponse(
id="uuid-1",
framework_id="bp_security_v1",
name="BreakPilot Security Controls",
version="1.0",
release_state="draft",
created_at="2026-03-12T00:00:00+00:00",
updated_at="2026-03-12T00:00:00+00:00",
)
assert resp.framework_id == "bp_security_v1"
assert resp.version == "1.0"
def test_optional_fields(self):
resp = FrameworkResponse(
id="uuid-1",
framework_id="test",
name="Test",
version="1.0",
release_state="draft",
created_at="2026-03-12T00:00:00+00:00",
updated_at="2026-03-12T00:00:00+00:00",
)
assert resp.description is None
assert resp.owner is None
assert resp.policy_version is None
class TestControlResponse:
"""Tests for ControlResponse model."""
def test_full_control(self):
resp = ControlResponse(
id="uuid-1",
framework_id="uuid-fw",
control_id="AUTH-001",
title="Multi-Factor Authentication",
objective="Require MFA for privileged access.",
rationale="Passwords alone are insufficient.",
scope={"platforms": ["web"]},
requirements=["MFA for admin accounts"],
test_procedure=["Test admin login without MFA"],
evidence=[{"type": "config", "description": "MFA config"}],
severity="high",
open_anchors=[{"framework": "OWASP ASVS", "ref": "V2.8", "url": "https://owasp.org"}],
release_state="draft",
tags=["mfa", "auth"],
created_at="2026-03-12T00:00:00+00:00",
updated_at="2026-03-12T00:00:00+00:00",
)
assert resp.control_id == "AUTH-001"
assert resp.severity == "high"
assert len(resp.open_anchors) == 1
def test_optional_numeric_fields(self):
resp = ControlResponse(
id="uuid-1",
framework_id="uuid-fw",
control_id="NET-001",
title="TLS",
objective="Encrypt traffic.",
rationale="Prevent eavesdropping.",
scope={},
requirements=[],
test_procedure=[],
evidence=[],
severity="high",
open_anchors=[],
release_state="draft",
tags=[],
created_at="2026-03-12T00:00:00+00:00",
updated_at="2026-03-12T00:00:00+00:00",
)
assert resp.risk_score is None
assert resp.implementation_effort is None
assert resp.evidence_confidence is None
class TestSimilarityCheckRequest:
"""Tests for SimilarityCheckRequest model."""
def test_valid_request(self):
req = SimilarityCheckRequest(
source_text="Die Anwendung muss MFA implementieren.",
candidate_text="Multi-factor authentication is required.",
)
assert req.source_text == "Die Anwendung muss MFA implementieren."
assert req.candidate_text == "Multi-factor authentication is required."
def test_empty_strings(self):
req = SimilarityCheckRequest(source_text="", candidate_text="")
assert req.source_text == ""
class TestSimilarityCheckResponse:
"""Tests for SimilarityCheckResponse model."""
def test_pass_status(self):
resp = SimilarityCheckResponse(
max_exact_run=2,
token_overlap=0.05,
ngram_jaccard=0.03,
embedding_cosine=0.45,
lcs_ratio=0.12,
status="PASS",
details={
"max_exact_run": "PASS",
"token_overlap": "PASS",
"ngram_jaccard": "PASS",
"embedding_cosine": "PASS",
"lcs_ratio": "PASS",
},
)
assert resp.status == "PASS"
def test_fail_status(self):
resp = SimilarityCheckResponse(
max_exact_run=15,
token_overlap=0.35,
ngram_jaccard=0.20,
embedding_cosine=0.95,
lcs_ratio=0.55,
status="FAIL",
details={
"max_exact_run": "FAIL",
"token_overlap": "FAIL",
"ngram_jaccard": "FAIL",
"embedding_cosine": "FAIL",
"lcs_ratio": "FAIL",
},
)
assert resp.status == "FAIL"
class TestControlRowConversion:
"""Tests for _control_row helper."""
def _make_row(self, **overrides):
now = datetime.now(timezone.utc)
defaults = {
"id": "uuid-ctrl-1",
"framework_id": "uuid-fw-1",
"control_id": "AUTH-001",
"title": "Multi-Factor Authentication",
"objective": "Require MFA.",
"rationale": "Passwords insufficient.",
"scope": {"platforms": ["web", "mobile"]},
"requirements": ["Req 1", "Req 2"],
"test_procedure": ["Test 1"],
"evidence": [{"type": "config", "description": "MFA config"}],
"severity": "high",
"risk_score": 8.5,
"implementation_effort": "m",
"evidence_confidence": 0.85,
"open_anchors": [
{"framework": "OWASP ASVS", "ref": "V2.8", "url": "https://owasp.org"},
],
"release_state": "draft",
"tags": ["mfa"],
"created_at": now,
"updated_at": now,
}
defaults.update(overrides)
mock = MagicMock()
for key, value in defaults.items():
setattr(mock, key, value)
return mock
def test_basic_conversion(self):
row = self._make_row()
result = _control_row(row)
assert result["control_id"] == "AUTH-001"
assert result["severity"] == "high"
assert result["risk_score"] == 8.5
assert result["implementation_effort"] == "m"
assert result["evidence_confidence"] == 0.85
assert len(result["open_anchors"]) == 1
def test_null_numeric_fields(self):
row = self._make_row(risk_score=None, evidence_confidence=None, implementation_effort=None)
result = _control_row(row)
assert result["risk_score"] is None
assert result["evidence_confidence"] is None
assert result["implementation_effort"] is None
def test_empty_tags(self):
row = self._make_row(tags=None)
result = _control_row(row)
assert result["tags"] == []
def test_empty_tags_list(self):
row = self._make_row(tags=[])
result = _control_row(row)
assert result["tags"] == []
def test_timestamp_format(self):
now = datetime(2026, 3, 12, 10, 30, 0, tzinfo=timezone.utc)
row = self._make_row(created_at=now, updated_at=now)
result = _control_row(row)
assert "2026-03-12" in result["created_at"]
assert "10:30" in result["created_at"]
def test_none_timestamps(self):
row = self._make_row(created_at=None, updated_at=None)
result = _control_row(row)
assert result["created_at"] is None
assert result["updated_at"] is None