Files
consentos/apps/api/tests/test_compliance.py
James Cottrill fbf26453f2 feat: initial public release
ConsentOS — a privacy-first cookie consent management platform.

Self-hosted, source-available alternative to OneTrust, Cookiebot, and
CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google
Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant
architecture with role-based access, configuration cascade
(system → org → group → site → region), dark-pattern detection in
the scanner, and a tamper-evident consent record audit trail.

This is the initial public release. Prior development history is
retained internally.

See README.md for the feature list, architecture overview, and
quick-start instructions. Licensed under the Elastic Licence 2.0 —
self-host freely; do not resell as a managed service.
2026-04-14 09:18:18 +00:00

598 lines
21 KiB
Python

"""Tests for the compliance rule engine and router."""
import uuid
import pytest
from httpx import ASGITransport, AsyncClient
from src.schemas.compliance import (
ComplianceCheckResponse,
ComplianceIssue,
Framework,
FrameworkResult,
Severity,
)
from src.services.compliance import (
CCPA_RULES,
CNIL_RULES,
EPRIVACY_RULES,
FRAMEWORK_RULES,
GDPR_RULES,
LGPD_RULES,
SiteContext,
calculate_overall_score,
run_compliance_check,
run_framework_check,
)
# ── SiteContext defaults ──────────────────────────────────────────────
class TestSiteContext:
def test_default_values(self):
ctx = SiteContext()
assert ctx.blocking_mode == "opt_in"
assert ctx.tcf_enabled is False
assert ctx.gcm_enabled is True
assert ctx.consent_expiry_days == 365
assert ctx.has_reject_button is True
assert ctx.has_granular_choices is True
assert ctx.has_cookie_wall is False
assert ctx.pre_ticked_boxes is False
def test_custom_values(self):
ctx = SiteContext(
blocking_mode="opt_out",
consent_expiry_days=180,
privacy_policy_url="https://example.com/privacy",
)
assert ctx.blocking_mode == "opt_out"
assert ctx.consent_expiry_days == 180
assert ctx.privacy_policy_url == "https://example.com/privacy"
# ── GDPR rules ────────────────────────────────────────────────────────
class TestGDPRRules:
def test_compliant_site(self):
ctx = SiteContext(
blocking_mode="opt_in",
has_reject_button=True,
has_granular_choices=True,
has_cookie_wall=False,
pre_ticked_boxes=False,
privacy_policy_url="https://example.com/privacy",
uncategorised_cookies=0,
)
result = run_framework_check(Framework.GDPR, ctx)
assert result.score == 100
assert result.status == "compliant"
assert len(result.issues) == 0
assert result.rules_passed == result.rules_checked
def test_opt_out_mode_fails(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert any(i.rule_id == "gdpr_opt_in" for i in result.issues)
assert result.status == "non_compliant"
def test_informational_mode_fails(self):
ctx = SiteContext(
blocking_mode="informational",
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert any(i.rule_id == "gdpr_opt_in" for i in result.issues)
def test_no_reject_button_fails(self):
ctx = SiteContext(
has_reject_button=False,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert any(i.rule_id == "gdpr_reject_button" for i in result.issues)
def test_no_granular_consent_fails(self):
ctx = SiteContext(
has_granular_choices=False,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert any(i.rule_id == "gdpr_granular" for i in result.issues)
def test_cookie_wall_fails(self):
ctx = SiteContext(
has_cookie_wall=True,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert any(i.rule_id == "gdpr_cookie_wall" for i in result.issues)
def test_pre_ticked_fails(self):
ctx = SiteContext(
pre_ticked_boxes=True,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert any(i.rule_id == "gdpr_pre_ticked" for i in result.issues)
def test_no_privacy_policy_warns(self):
ctx = SiteContext(privacy_policy_url=None)
result = run_framework_check(Framework.GDPR, ctx)
policy_issues = [i for i in result.issues if i.rule_id == "gdpr_privacy_policy"]
assert len(policy_issues) == 1
assert policy_issues[0].severity == Severity.WARNING
def test_uncategorised_cookies_warns(self):
ctx = SiteContext(
uncategorised_cookies=5,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
uncat_issues = [i for i in result.issues if i.rule_id == "gdpr_uncategorised"]
assert len(uncat_issues) == 1
assert "5" in uncat_issues[0].message
def test_multiple_failures_accumulate(self):
ctx = SiteContext(
blocking_mode="opt_out",
has_reject_button=False,
has_granular_choices=False,
has_cookie_wall=True,
pre_ticked_boxes=True,
privacy_policy_url=None,
uncategorised_cookies=3,
)
result = run_framework_check(Framework.GDPR, ctx)
assert result.score == 0 # Capped at 0
assert result.status == "non_compliant"
assert len(result.issues) >= 5
# ── CNIL rules ────────────────────────────────────────────────────────
class TestCNILRules:
def test_compliant_site(self):
ctx = SiteContext(
blocking_mode="opt_in",
has_reject_button=True,
has_granular_choices=True,
privacy_policy_url="https://example.com/privacy",
consent_expiry_days=180,
)
result = run_framework_check(Framework.CNIL, ctx)
assert result.score == 100
assert result.status == "compliant"
def test_consent_expiry_too_long(self):
ctx = SiteContext(
consent_expiry_days=365,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.CNIL, ctx)
assert any(i.rule_id == "cnil_reconsent" for i in result.issues)
def test_consent_expiry_at_limit(self):
ctx = SiteContext(
consent_expiry_days=182,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.CNIL, ctx)
assert not any(i.rule_id == "cnil_reconsent" for i in result.issues)
def test_cookie_lifetime_too_long(self):
ctx = SiteContext(
consent_expiry_days=400,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.CNIL, ctx)
assert any(i.rule_id == "cnil_cookie_lifetime" for i in result.issues)
def test_cookie_lifetime_at_limit(self):
ctx = SiteContext(
consent_expiry_days=395,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.CNIL, ctx)
assert not any(i.rule_id == "cnil_cookie_lifetime" for i in result.issues)
def test_inherits_gdpr_rules(self):
"""CNIL should check all GDPR rules plus CNIL-specific ones."""
assert len(CNIL_RULES) > len(GDPR_RULES)
def test_reject_first_layer(self):
ctx = SiteContext(
has_reject_button=False,
consent_expiry_days=180,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.CNIL, ctx)
assert any(i.rule_id == "cnil_reject_first_layer" for i in result.issues)
# ── CCPA rules ────────────────────────────────────────────────────────
class TestCCPARules:
def test_compliant_site(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url="https://example.com/privacy",
banner_config={"show_do_not_sell_link": True},
)
result = run_framework_check(Framework.CCPA, ctx)
assert result.score == 100
assert result.status == "compliant"
def test_opt_in_also_acceptable(self):
ctx = SiteContext(
blocking_mode="opt_in",
privacy_policy_url="https://example.com/privacy",
banner_config={"show_do_not_sell_link": True},
)
result = run_framework_check(Framework.CCPA, ctx)
assert not any(i.rule_id == "ccpa_opt_out" for i in result.issues)
def test_informational_mode_passes_ccpa(self):
"""CCPA opt-out check passes for informational (it's not 'informational')."""
ctx = SiteContext(
blocking_mode="informational",
privacy_policy_url="https://example.com/privacy",
banner_config={"show_do_not_sell_link": True},
)
result = run_framework_check(Framework.CCPA, ctx)
# informational is not in ("opt_out", "opt_in"), so it fails
assert any(i.rule_id == "ccpa_opt_out" for i in result.issues)
def test_no_do_not_sell_link(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url="https://example.com/privacy",
banner_config={},
)
result = run_framework_check(Framework.CCPA, ctx)
assert any(i.rule_id == "ccpa_do_not_sell" for i in result.issues)
def test_no_banner_config_fails_dns(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url="https://example.com/privacy",
banner_config=None,
)
result = run_framework_check(Framework.CCPA, ctx)
assert any(i.rule_id == "ccpa_do_not_sell" for i in result.issues)
def test_no_privacy_policy_warns(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url=None,
banner_config={"show_do_not_sell_link": True},
)
result = run_framework_check(Framework.CCPA, ctx)
assert any(i.rule_id == "ccpa_privacy_policy" for i in result.issues)
# ── ePrivacy rules ────────────────────────────────────────────────────
class TestEPrivacyRules:
def test_compliant_site(self):
ctx = SiteContext(blocking_mode="opt_in")
result = run_framework_check(Framework.EPRIVACY, ctx)
assert result.score == 100
assert result.status == "compliant"
def test_opt_out_passes(self):
ctx = SiteContext(blocking_mode="opt_out")
result = run_framework_check(Framework.EPRIVACY, ctx)
assert not any(i.rule_id == "eprivacy_consent" for i in result.issues)
def test_informational_fails(self):
ctx = SiteContext(blocking_mode="informational")
result = run_framework_check(Framework.EPRIVACY, ctx)
assert any(i.rule_id == "eprivacy_consent" for i in result.issues)
# ── LGPD rules ────────────────────────────────────────────────────────
class TestLGPDRules:
def test_compliant_site(self):
ctx = SiteContext(
blocking_mode="opt_in",
privacy_policy_url="https://example.com/privacy",
has_granular_choices=True,
)
result = run_framework_check(Framework.LGPD, ctx)
assert result.score == 100
assert result.status == "compliant"
def test_informational_fails(self):
ctx = SiteContext(
blocking_mode="informational",
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.LGPD, ctx)
assert any(i.rule_id == "lgpd_consent_basis" for i in result.issues)
def test_no_privacy_policy_warns(self):
ctx = SiteContext(privacy_policy_url=None)
result = run_framework_check(Framework.LGPD, ctx)
assert any(i.rule_id == "lgpd_data_controller" for i in result.issues)
def test_no_granular_warns(self):
ctx = SiteContext(
has_granular_choices=False,
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.LGPD, ctx)
assert any(i.rule_id == "lgpd_granular" for i in result.issues)
def test_opt_out_passes(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.LGPD, ctx)
assert not any(i.rule_id == "lgpd_consent_basis" for i in result.issues)
# ── Engine orchestration ──────────────────────────────────────────────
class TestComplianceEngine:
def test_run_all_frameworks(self):
ctx = SiteContext(
blocking_mode="opt_in",
privacy_policy_url="https://example.com/privacy",
has_reject_button=True,
has_granular_choices=True,
consent_expiry_days=180,
)
results = run_compliance_check(ctx)
assert len(results) == 5
frameworks = {r.framework for r in results}
assert frameworks == {
Framework.GDPR,
Framework.CNIL,
Framework.CCPA,
Framework.EPRIVACY,
Framework.LGPD,
}
def test_run_specific_frameworks(self):
ctx = SiteContext()
results = run_compliance_check(ctx, [Framework.GDPR, Framework.CCPA])
assert len(results) == 2
assert results[0].framework == Framework.GDPR
assert results[1].framework == Framework.CCPA
def test_run_single_framework(self):
ctx = SiteContext()
results = run_compliance_check(ctx, [Framework.EPRIVACY])
assert len(results) == 1
assert results[0].framework == Framework.EPRIVACY
def test_empty_frameworks_list_runs_all(self):
ctx = SiteContext(
privacy_policy_url="https://example.com/privacy",
consent_expiry_days=180,
)
results = run_compliance_check(ctx, None)
assert len(results) == 5
class TestScoring:
def test_perfect_score(self):
result = FrameworkResult(
framework=Framework.GDPR,
score=100,
status="compliant",
rules_checked=7,
rules_passed=7,
)
assert calculate_overall_score([result]) == 100
def test_zero_score(self):
result = FrameworkResult(
framework=Framework.GDPR,
score=0,
status="non_compliant",
rules_checked=7,
rules_passed=0,
)
assert calculate_overall_score([result]) == 0
def test_average_across_frameworks(self):
results = [
FrameworkResult(
framework=Framework.GDPR,
score=100,
status="compliant",
rules_checked=7,
rules_passed=7,
),
FrameworkResult(
framework=Framework.CCPA,
score=50,
status="partial",
rules_checked=3,
rules_passed=1,
),
]
assert calculate_overall_score(results) == 75
def test_empty_results(self):
assert calculate_overall_score([]) == 100
def test_critical_issues_deduct_20(self):
ctx = SiteContext(
blocking_mode="opt_out",
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
# opt_out causes one critical issue (gdpr_opt_in) → -20 points
assert result.score == 80
def test_warning_issues_deduct_5(self):
ctx = SiteContext(
blocking_mode="opt_in",
privacy_policy_url=None,
uncategorised_cookies=0,
)
result = run_framework_check(Framework.GDPR, ctx)
# Missing privacy policy is a warning → -5 points
assert result.score == 95
def test_score_floors_at_zero(self):
ctx = SiteContext(
blocking_mode="opt_out",
has_reject_button=False,
has_granular_choices=False,
has_cookie_wall=True,
pre_ticked_boxes=True,
privacy_policy_url=None,
uncategorised_cookies=10,
)
result = run_framework_check(Framework.GDPR, ctx)
assert result.score == 0
def test_status_non_compliant_with_critical(self):
ctx = SiteContext(blocking_mode="opt_out")
result = run_framework_check(Framework.GDPR, ctx)
assert result.status == "non_compliant"
def test_status_partial_with_warnings_only(self):
ctx = SiteContext(
blocking_mode="opt_in",
privacy_policy_url=None,
)
result = run_framework_check(Framework.GDPR, ctx)
assert result.status == "partial"
def test_status_compliant_with_no_issues(self):
ctx = SiteContext(
blocking_mode="opt_in",
privacy_policy_url="https://example.com/privacy",
)
result = run_framework_check(Framework.GDPR, ctx)
assert result.status == "compliant"
# ── Framework registry ────────────────────────────────────────────────
class TestFrameworkRegistry:
def test_all_frameworks_registered(self):
assert Framework.GDPR in FRAMEWORK_RULES
assert Framework.CNIL in FRAMEWORK_RULES
assert Framework.CCPA in FRAMEWORK_RULES
assert Framework.EPRIVACY in FRAMEWORK_RULES
assert Framework.LGPD in FRAMEWORK_RULES
def test_each_framework_has_rules(self):
for fw, rules in FRAMEWORK_RULES.items():
assert len(rules) > 0, f"{fw} has no rules"
def test_rule_ids_are_unique_per_framework(self):
for fw, rules in FRAMEWORK_RULES.items():
ids = [r.rule_id for r in rules]
assert len(ids) == len(set(ids)), f"Duplicate rule IDs in {fw}"
def test_gdpr_rule_count(self):
assert len(GDPR_RULES) == 7
def test_cnil_includes_gdpr_rules(self):
gdpr_ids = {r.rule_id for r in GDPR_RULES}
cnil_ids = {r.rule_id for r in CNIL_RULES}
assert gdpr_ids.issubset(cnil_ids)
def test_ccpa_rule_count(self):
assert len(CCPA_RULES) == 3
def test_eprivacy_rule_count(self):
assert len(EPRIVACY_RULES) == 2
def test_lgpd_rule_count(self):
assert len(LGPD_RULES) == 3
# ── Router tests ──────────────────────────────────────────────────────
class TestComplianceRouter:
@pytest.fixture
def app(self):
from src.main import create_app
return create_app()
@pytest.fixture
async def client(self, app):
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
async def test_list_frameworks(self, client):
resp = await client.get("/api/v1/compliance/frameworks")
assert resp.status_code == 200
data = resp.json()
assert len(data) == 5
ids = {fw["id"] for fw in data}
assert ids == {"gdpr", "cnil", "ccpa", "eprivacy", "lgpd"}
async def test_check_requires_auth(self, client):
resp = await client.post(f"/api/v1/compliance/check/{uuid.uuid4()}")
assert resp.status_code == 401
# ── Schema tests ──────────────────────────────────────────────────────
class TestSchemas:
def test_compliance_issue_schema(self):
issue = ComplianceIssue(
rule_id="test_rule",
severity=Severity.CRITICAL,
message="Test message",
recommendation="Test recommendation",
)
assert issue.rule_id == "test_rule"
assert issue.severity == Severity.CRITICAL
def test_framework_result_schema(self):
result = FrameworkResult(
framework=Framework.GDPR,
score=85,
status="partial",
rules_checked=7,
rules_passed=5,
)
assert result.framework == Framework.GDPR
assert result.score == 85
def test_compliance_check_response_schema(self):
response = ComplianceCheckResponse(
site_id="test-id",
results=[],
overall_score=100,
)
assert response.overall_score == 100
def test_severity_values(self):
assert Severity.CRITICAL == "critical"
assert Severity.WARNING == "warning"
assert Severity.INFO == "info"
def test_framework_values(self):
assert Framework.GDPR == "gdpr"
assert Framework.CNIL == "cnil"
assert Framework.CCPA == "ccpa"
assert Framework.EPRIVACY == "eprivacy"
assert Framework.LGPD == "lgpd"