Files
consentos/apps/api/tests/test_routers_config.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

297 lines
11 KiB
Python

"""Unit tests for config router — mocked database."""
import uuid
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from src.main import create_app
from src.services.auth import create_access_token
ORG_ID = uuid.uuid4()
USER_ID = uuid.uuid4()
def _auth_headers(role="owner"):
token = create_access_token(
user_id=USER_ID, organisation_id=ORG_ID, role=role, email="admin@test.com"
)
return {"Authorization": f"Bearer {token}"}
def _mock_config(**overrides):
config = MagicMock(spec=[])
config.id = overrides.get("id", uuid.uuid4())
config.site_id = overrides.get("site_id", uuid.uuid4())
config.blocking_mode = overrides.get("blocking_mode", "opt_in")
config.tcf_enabled = overrides.get("tcf_enabled", False)
config.tcf_publisher_cc = overrides.get("tcf_publisher_cc")
config.gpp_enabled = overrides.get("gpp_enabled", True)
config.gpp_supported_apis = overrides.get("gpp_supported_apis", ["usnat"])
config.gpc_enabled = overrides.get("gpc_enabled", True)
default_jurisdictions = ["US-CA", "US-CO", "US-CT", "US-TX", "US-MT"]
config.gpc_jurisdictions = overrides.get("gpc_jurisdictions", default_jurisdictions)
config.gpc_global_honour = overrides.get("gpc_global_honour", False)
config.gcm_enabled = overrides.get("gcm_enabled", True)
config.gcm_default = overrides.get("gcm_default")
config.banner_config = overrides.get("banner_config", {})
config.regional_modes = overrides.get("regional_modes")
config.privacy_policy_url = overrides.get("privacy_policy_url")
config.scan_schedule_cron = overrides.get("scan_schedule_cron")
config.scan_max_pages = overrides.get("scan_max_pages", 50)
config.consent_expiry_days = overrides.get("consent_expiry_days", 365)
config.created_at = datetime.now(UTC)
config.updated_at = datetime.now(UTC)
return config
def _mock_db_sequence(*results):
session = AsyncMock()
mock_results = []
for r in results:
result = MagicMock()
result.scalar_one_or_none.return_value = r
mock_results.append(result)
session.execute = AsyncMock(side_effect=mock_results)
return session
@pytest.fixture
def mock_app():
return create_app()
async def _client(app, mock_session):
from src.db import get_db
async def _override():
yield mock_session
app.dependency_overrides[get_db] = _override
transport = ASGITransport(app=app)
return AsyncClient(transport=transport, base_url="http://test")
class TestPublicSiteConfig:
@pytest.mark.asyncio
async def test_get_public_config(self, mock_app):
config = _mock_config()
db = _mock_db_sequence(config)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/config/sites/{config.site_id}")
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_get_public_config_not_found(self, mock_app):
db = _mock_db_sequence(None)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/config/sites/{uuid.uuid4()}")
assert resp.status_code == 404
class TestResolvedConfig:
@pytest.mark.asyncio
async def test_get_resolved_config(self, mock_app):
config = _mock_config()
# Resolved endpoint does 4 queries: config, site org_id, org_config, site group_id
db = _mock_db_sequence(config, ORG_ID, None, None)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/config/sites/{config.site_id}/resolved")
assert resp.status_code == 200
data = resp.json()
assert "site_id" in data
assert "blocking_mode" in data
@pytest.mark.asyncio
async def test_get_resolved_config_with_region(self, mock_app):
config = _mock_config(regional_modes={"EU": "opt_in", "US": "opt_out"})
db = _mock_db_sequence(config, ORG_ID, None, None)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/config/sites/{config.site_id}/resolved?region=EU")
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_get_resolved_config_not_found(self, mock_app):
db = _mock_db_sequence(None)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/config/sites/{uuid.uuid4()}/resolved")
assert resp.status_code == 404
class TestPublishConfig:
@pytest.mark.asyncio
async def test_publish_config_success(self, mock_app):
config = _mock_config()
# Publish does: config query, org_config query, group_id, active A/B test query
db = _mock_db_sequence(config, None, None, None)
mock_result = MagicMock()
mock_result.success = True
mock_result.path = "/cdn/site-config.json"
mock_result.published_at = datetime.now(UTC).isoformat()
with patch(
"src.routers.config.publish_site_config",
new_callable=AsyncMock,
return_value=mock_result,
):
async with await _client(mock_app, db) as client:
resp = await client.post(
f"/api/v1/config/sites/{config.site_id}/publish",
headers=_auth_headers(),
)
assert resp.status_code == 200
data = resp.json()
assert data["published"] is True
@pytest.mark.asyncio
async def test_publish_config_failure(self, mock_app):
config = _mock_config()
db = _mock_db_sequence(config, None, None, None)
mock_result = MagicMock()
mock_result.success = False
mock_result.error = "Disk full"
with patch(
"src.routers.config.publish_site_config",
new_callable=AsyncMock,
return_value=mock_result,
):
async with await _client(mock_app, db) as client:
resp = await client.post(
f"/api/v1/config/sites/{config.site_id}/publish",
headers=_auth_headers(),
)
assert resp.status_code == 500
@pytest.mark.asyncio
async def test_publish_config_not_found(self, mock_app):
db = _mock_db_sequence(None)
async with await _client(mock_app, db) as client:
resp = await client.post(
f"/api/v1/config/sites/{uuid.uuid4()}/publish",
headers=_auth_headers(),
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_publish_requires_admin(self, mock_app):
db = _mock_db_sequence()
async with await _client(mock_app, db) as client:
resp = await client.post(
f"/api/v1/config/sites/{uuid.uuid4()}/publish",
headers=_auth_headers(role="viewer"),
)
assert resp.status_code == 403
class TestGeoResolvedConfig:
@pytest.mark.asyncio
async def test_get_geo_resolved_config_with_header(self, mock_app):
config = _mock_config(
regional_modes={"EU": "opt_in", "US": "opt_out", "DEFAULT": "informational"},
)
# Geo-resolved does: config, site org_id, org_config, site group_id
db = _mock_db_sequence(config, ORG_ID, None, None)
async with await _client(mock_app, db) as client:
resp = await client.get(
f"/api/v1/config/sites/{config.site_id}/geo-resolved",
headers={"cf-ipcountry": "DE"},
)
assert resp.status_code == 200
data = resp.json()
assert data["blocking_mode"] == "opt_in"
assert data["detected_country"] == "DE"
assert data["detected_region"] == "EU"
@pytest.mark.asyncio
async def test_get_geo_resolved_config_us(self, mock_app):
config = _mock_config(
regional_modes={"EU": "opt_in", "US-CA": "opt_out", "DEFAULT": "informational"},
)
db = _mock_db_sequence(config, ORG_ID, None, None)
with patch(
"src.routers.config.detect_region",
new_callable=AsyncMock,
) as mock_detect:
from src.services.geoip import GeoResult
mock_detect.return_value = GeoResult(country_code="US", region="US-CA")
async with await _client(mock_app, db) as client:
resp = await client.get(
f"/api/v1/config/sites/{config.site_id}/geo-resolved",
)
assert resp.status_code == 200
data = resp.json()
assert data["blocking_mode"] == "opt_out"
assert data["detected_region"] == "US-CA"
@pytest.mark.asyncio
async def test_get_geo_resolved_config_not_found(self, mock_app):
db = _mock_db_sequence(None)
async with await _client(mock_app, db) as client:
resp = await client.get(
f"/api/v1/config/sites/{uuid.uuid4()}/geo-resolved",
)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_get_geo_resolved_config_no_region_detected(self, mock_app):
config = _mock_config(
regional_modes={"EU": "opt_in", "DEFAULT": "informational"},
)
db = _mock_db_sequence(config, ORG_ID, None, None)
with patch(
"src.routers.config.detect_region",
new_callable=AsyncMock,
) as mock_detect:
from src.services.geoip import GeoResult
mock_detect.return_value = GeoResult(country_code=None, region=None)
async with await _client(mock_app, db) as client:
resp = await client.get(
f"/api/v1/config/sites/{config.site_id}/geo-resolved",
)
assert resp.status_code == 200
data = resp.json()
assert data["detected_country"] is None
assert data["detected_region"] is None
class TestVisitorGeo:
@pytest.mark.asyncio
async def test_get_visitor_geo_with_header(self, mock_app):
db = _mock_db_sequence()
async with await _client(mock_app, db) as client:
resp = await client.get(
"/api/v1/config/geo",
headers={"cf-ipcountry": "GB"},
)
assert resp.status_code == 200
data = resp.json()
assert data["country_code"] == "GB"
assert data["region"] == "GB"
@pytest.mark.asyncio
async def test_get_visitor_geo_no_headers(self, mock_app):
db = _mock_db_sequence()
with patch(
"src.routers.config.detect_region",
new_callable=AsyncMock,
) as mock_detect:
from src.services.geoip import GeoResult
mock_detect.return_value = GeoResult(country_code=None, region=None)
async with await _client(mock_app, db) as client:
resp = await client.get("/api/v1/config/geo")
assert resp.status_code == 200
data = resp.json()
assert data["country_code"] is None
assert data["region"] is None