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.
133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""Tests for site and site config CRUD endpoints and schemas."""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from src.schemas.site import (
|
|
BlockingMode,
|
|
SiteConfigCreate,
|
|
SiteConfigResponse,
|
|
SiteConfigUpdate,
|
|
SiteCreate,
|
|
SiteResponse,
|
|
SiteUpdate,
|
|
)
|
|
|
|
|
|
class TestSiteSchemas:
|
|
def test_create_valid(self):
|
|
site = SiteCreate(domain="example.com", display_name="Example Site")
|
|
assert site.domain == "example.com"
|
|
|
|
def test_create_empty_domain_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
SiteCreate(domain="", display_name="Test")
|
|
|
|
def test_update_partial(self):
|
|
update = SiteUpdate(display_name="New Name")
|
|
data = update.model_dump(exclude_unset=True)
|
|
assert data == {"display_name": "New Name"}
|
|
|
|
def test_response_from_attributes(self):
|
|
now = "2026-01-01T00:00:00Z"
|
|
resp = SiteResponse(
|
|
id=uuid.uuid4(),
|
|
organisation_id=uuid.uuid4(),
|
|
domain="example.com",
|
|
display_name="Example",
|
|
is_active=True,
|
|
additional_domains=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
assert resp.is_active
|
|
|
|
|
|
class TestSiteConfigSchemas:
|
|
def test_create_defaults(self):
|
|
config = SiteConfigCreate()
|
|
assert config.blocking_mode == BlockingMode.OPT_IN
|
|
assert config.gcm_enabled is True
|
|
assert config.tcf_enabled is False
|
|
assert config.scan_max_pages == 50
|
|
assert config.consent_expiry_days == 365
|
|
|
|
def test_create_with_regional_modes(self):
|
|
config = SiteConfigCreate(
|
|
regional_modes={"EU": "opt_in", "US-CA": "opt_out", "DEFAULT": "opt_in"}
|
|
)
|
|
assert config.regional_modes["EU"] == "opt_in"
|
|
|
|
def test_scan_max_pages_bounds(self):
|
|
with pytest.raises(ValidationError):
|
|
SiteConfigCreate(scan_max_pages=0)
|
|
with pytest.raises(ValidationError):
|
|
SiteConfigCreate(scan_max_pages=1001)
|
|
|
|
def test_consent_expiry_bounds(self):
|
|
with pytest.raises(ValidationError):
|
|
SiteConfigCreate(consent_expiry_days=0)
|
|
with pytest.raises(ValidationError):
|
|
SiteConfigCreate(consent_expiry_days=731)
|
|
|
|
def test_update_partial(self):
|
|
update = SiteConfigUpdate(blocking_mode=BlockingMode.OPT_OUT)
|
|
data = update.model_dump(exclude_unset=True)
|
|
assert data == {"blocking_mode": "opt_out"}
|
|
|
|
def test_response_from_attributes(self):
|
|
now = "2026-01-01T00:00:00Z"
|
|
resp = SiteConfigResponse(
|
|
id=uuid.uuid4(),
|
|
site_id=uuid.uuid4(),
|
|
blocking_mode="opt_in",
|
|
regional_modes=None,
|
|
tcf_enabled=False,
|
|
tcf_publisher_cc=None,
|
|
gcm_enabled=True,
|
|
gcm_default=None,
|
|
banner_config=None,
|
|
privacy_policy_url=None,
|
|
scan_schedule_cron=None,
|
|
scan_max_pages=50,
|
|
consent_expiry_days=365,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
assert resp.blocking_mode == "opt_in"
|
|
|
|
def test_display_mode_in_banner_config(self):
|
|
"""Display mode is stored inside banner_config, not as a top-level field."""
|
|
config = SiteConfigCreate(
|
|
banner_config={"displayMode": "overlay"},
|
|
)
|
|
assert config.banner_config["displayMode"] == "overlay"
|
|
|
|
|
|
class TestEnums:
|
|
def test_blocking_modes(self):
|
|
assert BlockingMode.OPT_IN == "opt_in"
|
|
assert BlockingMode.OPT_OUT == "opt_out"
|
|
assert BlockingMode.INFORMATIONAL == "informational"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestSiteRoutesRegistered:
|
|
async def test_site_routes_exist(self, client):
|
|
response = await client.get("/openapi.json")
|
|
paths = response.json()["paths"]
|
|
assert "/api/v1/sites/" in paths
|
|
assert "/api/v1/sites/{site_id}" in paths
|
|
assert "/api/v1/sites/{site_id}/config" in paths
|
|
|
|
async def test_site_endpoints_require_auth(self, client):
|
|
response = await client.get("/api/v1/sites/")
|
|
assert response.status_code == 401
|
|
|
|
async def test_site_config_endpoints_require_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
response = await client.get(f"/api/v1/sites/{site_id}/config")
|
|
assert response.status_code == 401
|