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.
This commit is contained in:
296
apps/api/tests/test_routers_config.py
Normal file
296
apps/api/tests/test_routers_config.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user