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

231 lines
8.2 KiB
Python

"""Unit tests for consent router — mocked database."""
import uuid
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock
import pytest
from httpx import ASGITransport, AsyncClient
from src.main import create_app
def _mock_consent_record(**overrides):
"""Build a mock ConsentRecord ORM object."""
record = MagicMock()
record.id = overrides.get("id", uuid.uuid4())
record.site_id = overrides.get("site_id", uuid.uuid4())
record.visitor_id = overrides.get("visitor_id", "visitor-123")
record.ip_hash = "abc123"
record.user_agent_hash = "def456"
record.action = overrides.get("action", "accept_all")
record.categories_accepted = overrides.get("categories_accepted", ["necessary"])
record.categories_rejected = overrides.get("categories_rejected", [])
record.tc_string = overrides.get("tc_string")
record.gcm_state = overrides.get("gcm_state")
record.gpp_string = overrides.get("gpp_string")
record.gpc_detected = overrides.get("gpc_detected")
record.gpc_honoured = overrides.get("gpc_honoured")
record.page_url = overrides.get("page_url")
record.country_code = overrides.get("country_code")
record.region_code = overrides.get("region_code")
record.consented_at = overrides.get("consented_at", datetime.now(UTC))
return record
def _mock_db(scalar_one_or_none=None):
session = AsyncMock()
result = MagicMock()
result.scalar_one_or_none.return_value = scalar_one_or_none
session.execute.return_value = result
_added_objects = []
def _fake_add(obj):
_added_objects.append(obj)
session.add = MagicMock(side_effect=_fake_add)
async def _fake_flush():
"""Simulate DB flush — populate server-side defaults."""
for obj in _added_objects:
if getattr(obj, "id", None) is None:
obj.id = uuid.uuid4()
if hasattr(obj, "consented_at") and getattr(obj, "consented_at", None) is None:
obj.consented_at = datetime.now(UTC)
if hasattr(obj, "created_at") and getattr(obj, "created_at", None) is None:
obj.created_at = datetime.now(UTC)
if hasattr(obj, "updated_at") and getattr(obj, "updated_at", None) is None:
obj.updated_at = datetime.now(UTC)
session.flush = AsyncMock(side_effect=_fake_flush)
session.refresh = AsyncMock()
return session
@pytest.fixture
def mock_app():
return create_app()
async def _client(app, mock_session):
from src.db import get_db
from src.services.dependencies import get_current_user, require_role
user = MagicMock()
user.organisation_id = uuid.uuid4()
user.role = "owner"
async def _override():
yield mock_session
app.dependency_overrides[get_db] = _override
app.dependency_overrides[get_current_user] = lambda: user
def _override_require_role(*_roles):
return lambda: user
app.dependency_overrides[require_role] = _override_require_role
transport = ASGITransport(app=app)
return AsyncClient(transport=transport, base_url="http://test")
class TestRecordConsent:
@pytest.mark.asyncio
async def test_record_consent_success(self, mock_app):
db = _mock_db()
async with await _client(mock_app, db) as client:
resp = await client.post(
"/api/v1/consent/",
json={
"site_id": str(uuid.uuid4()),
"visitor_id": "visitor-123",
"action": "accept_all",
"categories_accepted": ["necessary", "analytics"],
"categories_rejected": [],
},
)
assert resp.status_code == 201
@pytest.mark.asyncio
async def test_record_consent_reject_all(self, mock_app):
db = _mock_db()
async with await _client(mock_app, db) as client:
resp = await client.post(
"/api/v1/consent/",
json={
"site_id": str(uuid.uuid4()),
"visitor_id": "visitor-456",
"action": "reject_all",
"categories_accepted": ["necessary"],
"categories_rejected": ["analytics", "marketing"],
},
)
assert resp.status_code == 201
@pytest.mark.asyncio
async def test_record_consent_custom(self, mock_app):
db = _mock_db()
async with await _client(mock_app, db) as client:
resp = await client.post(
"/api/v1/consent/",
json={
"site_id": str(uuid.uuid4()),
"visitor_id": "visitor-789",
"action": "custom",
"categories_accepted": ["necessary", "analytics"],
"categories_rejected": ["marketing"],
},
)
assert resp.status_code == 201
@pytest.mark.asyncio
async def test_record_consent_invalid_action(self, mock_app):
db = _mock_db()
async with await _client(mock_app, db) as client:
resp = await client.post(
"/api/v1/consent/",
json={
"site_id": str(uuid.uuid4()),
"visitor_id": "visitor-000",
"action": "invalid_action",
"categories_accepted": ["necessary"],
"categories_rejected": [],
},
)
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_record_consent_empty_visitor_id(self, mock_app):
db = _mock_db()
async with await _client(mock_app, db) as client:
resp = await client.post(
"/api/v1/consent/",
json={
"site_id": str(uuid.uuid4()),
"visitor_id": "",
"action": "accept_all",
"categories_accepted": ["necessary"],
"categories_rejected": [],
},
)
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_record_consent_with_optional_fields(self, mock_app):
db = _mock_db()
async with await _client(mock_app, db) as client:
resp = await client.post(
"/api/v1/consent/",
json={
"site_id": str(uuid.uuid4()),
"visitor_id": "visitor-opt",
"action": "accept_all",
"categories_accepted": ["necessary"],
"categories_rejected": [],
"tc_string": "CPXxRAAAA",
"gcm_state": {"analytics_storage": "granted"},
"page_url": "https://example.com",
"country_code": "GB",
"region_code": "ENG",
},
)
assert resp.status_code == 201
class TestGetConsent:
@pytest.mark.asyncio
async def test_get_consent_found(self, mock_app):
record = _mock_consent_record()
db = _mock_db(scalar_one_or_none=record)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/consent/{record.id}")
assert resp.status_code == 200
@pytest.mark.asyncio
async def test_get_consent_not_found(self, mock_app):
db = _mock_db(scalar_one_or_none=None)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/consent/{uuid.uuid4()}")
assert resp.status_code == 404
class TestVerifyConsent:
@pytest.mark.asyncio
async def test_verify_consent_valid(self, mock_app):
record = _mock_consent_record()
db = _mock_db(scalar_one_or_none=record)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/consent/verify/{record.id}")
assert resp.status_code == 200
data = resp.json()
assert data["valid"] is True
@pytest.mark.asyncio
async def test_verify_consent_not_found(self, mock_app):
db = _mock_db(scalar_one_or_none=None)
async with await _client(mock_app, db) as client:
resp = await client.get(f"/api/v1/consent/verify/{uuid.uuid4()}")
assert resp.status_code == 404