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.
219 lines
7.1 KiB
Python
219 lines
7.1 KiB
Python
"""Tests for cookie category, cookie, and allow-list schemas and routes."""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from src.schemas.cookie import (
|
|
AllowListEntryCreate,
|
|
AllowListEntryUpdate,
|
|
CookieCategoryResponse,
|
|
CookieCreate,
|
|
CookieResponse,
|
|
CookieUpdate,
|
|
ReviewStatus,
|
|
StorageType,
|
|
)
|
|
|
|
# ─── Schema tests ────────────────────────────────────────────────────
|
|
|
|
|
|
class TestStorageType:
|
|
def test_values(self):
|
|
assert StorageType.cookie == "cookie"
|
|
assert StorageType.local_storage == "local_storage"
|
|
assert StorageType.session_storage == "session_storage"
|
|
assert StorageType.indexed_db == "indexed_db"
|
|
|
|
|
|
class TestReviewStatus:
|
|
def test_values(self):
|
|
assert ReviewStatus.pending == "pending"
|
|
assert ReviewStatus.approved == "approved"
|
|
assert ReviewStatus.rejected == "rejected"
|
|
|
|
|
|
class TestCookieCreate:
|
|
def test_valid_minimal(self):
|
|
schema = CookieCreate(name="_ga", domain=".example.com")
|
|
assert schema.name == "_ga"
|
|
assert schema.domain == ".example.com"
|
|
assert schema.storage_type == StorageType.cookie
|
|
assert schema.category_id is None
|
|
|
|
def test_valid_full(self):
|
|
cat_id = uuid.uuid4()
|
|
schema = CookieCreate(
|
|
name="_ga",
|
|
domain=".google.com",
|
|
storage_type=StorageType.cookie,
|
|
category_id=cat_id,
|
|
description="Google Analytics cookie",
|
|
vendor="Google",
|
|
path="/",
|
|
max_age_seconds=63072000,
|
|
is_http_only=False,
|
|
is_secure=True,
|
|
same_site="Lax",
|
|
)
|
|
assert schema.category_id == cat_id
|
|
assert schema.max_age_seconds == 63072000
|
|
|
|
def test_rejects_empty_name(self):
|
|
with pytest.raises(ValidationError):
|
|
CookieCreate(name="", domain=".example.com")
|
|
|
|
def test_rejects_empty_domain(self):
|
|
with pytest.raises(ValidationError):
|
|
CookieCreate(name="_ga", domain="")
|
|
|
|
|
|
class TestCookieUpdate:
|
|
def test_partial_update(self):
|
|
schema = CookieUpdate(review_status=ReviewStatus.approved)
|
|
dump = schema.model_dump(exclude_unset=True)
|
|
assert dump == {"review_status": ReviewStatus.approved}
|
|
|
|
def test_update_category(self):
|
|
cat_id = uuid.uuid4()
|
|
schema = CookieUpdate(category_id=cat_id)
|
|
assert schema.category_id == cat_id
|
|
|
|
|
|
class TestAllowListEntryCreate:
|
|
def test_valid(self):
|
|
cat_id = uuid.uuid4()
|
|
schema = AllowListEntryCreate(
|
|
name_pattern="_ga*",
|
|
domain_pattern=".google.com",
|
|
category_id=cat_id,
|
|
description="Google Analytics cookies",
|
|
)
|
|
assert schema.name_pattern == "_ga*"
|
|
assert schema.category_id == cat_id
|
|
|
|
def test_rejects_empty_name_pattern(self):
|
|
with pytest.raises(ValidationError):
|
|
AllowListEntryCreate(
|
|
name_pattern="",
|
|
domain_pattern=".example.com",
|
|
category_id=uuid.uuid4(),
|
|
)
|
|
|
|
|
|
class TestAllowListEntryUpdate:
|
|
def test_partial_update(self):
|
|
schema = AllowListEntryUpdate(description="Updated description")
|
|
dump = schema.model_dump(exclude_unset=True)
|
|
assert dump == {"description": "Updated description"}
|
|
|
|
|
|
class TestCookieCategoryResponse:
|
|
def test_from_dict(self):
|
|
now = "2024-01-01T00:00:00"
|
|
resp = CookieCategoryResponse(
|
|
id=uuid.uuid4(),
|
|
name="Analytics",
|
|
slug="analytics",
|
|
description="Analytics cookies",
|
|
is_essential=False,
|
|
display_order=2,
|
|
tcf_purpose_ids=[1, 3],
|
|
gcm_consent_types=["analytics_storage"],
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
assert resp.slug == "analytics"
|
|
assert resp.is_essential is False
|
|
|
|
|
|
class TestCookieResponse:
|
|
def test_from_dict(self):
|
|
now = "2024-01-01T00:00:00"
|
|
resp = CookieResponse(
|
|
id=uuid.uuid4(),
|
|
site_id=uuid.uuid4(),
|
|
name="_ga",
|
|
domain=".google.com",
|
|
storage_type="cookie",
|
|
review_status="pending",
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
assert resp.name == "_ga"
|
|
assert resp.review_status == "pending"
|
|
|
|
|
|
# ─── Route tests ─────────────────────────────────────────────────────
|
|
|
|
|
|
class TestCookieCategoryRoutes:
|
|
def test_categories_route_registered(self, app):
|
|
"""Verify the categories routes are registered in the app."""
|
|
routes = [r.path for r in app.routes]
|
|
assert "/api/v1/cookies/categories" in routes
|
|
assert "/api/v1/cookies/categories/{category_id}" in routes
|
|
|
|
|
|
class TestCookieRoutes:
|
|
@pytest.mark.asyncio
|
|
async def test_list_cookies_requires_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
resp = await client.get(f"/api/v1/cookies/sites/{site_id}")
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_cookie_requires_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
resp = await client.post(
|
|
f"/api/v1/cookies/sites/{site_id}",
|
|
json={"name": "_ga", "domain": ".google.com"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_cookie_rejects_invalid_body(self, client):
|
|
site_id = uuid.uuid4()
|
|
resp = await client.post(
|
|
f"/api/v1/cookies/sites/{site_id}",
|
|
json={"name": "", "domain": ""},
|
|
headers={"Authorization": "Bearer fake-token"},
|
|
)
|
|
# Should return 401 (bad token) or 422 (validation)
|
|
assert resp.status_code in (401, 422)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_summary_route_requires_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
resp = await client.get(f"/api/v1/cookies/sites/{site_id}/summary")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
class TestAllowListRoutes:
|
|
@pytest.mark.asyncio
|
|
async def test_list_allow_list_requires_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
resp = await client.get(f"/api/v1/cookies/sites/{site_id}/allow-list")
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_allow_list_requires_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
resp = await client.post(
|
|
f"/api/v1/cookies/sites/{site_id}/allow-list",
|
|
json={
|
|
"name_pattern": "_ga*",
|
|
"domain_pattern": ".google.com",
|
|
"category_id": str(uuid.uuid4()),
|
|
},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_allow_list_requires_auth(self, client):
|
|
site_id = uuid.uuid4()
|
|
entry_id = uuid.uuid4()
|
|
resp = await client.delete(f"/api/v1/cookies/sites/{site_id}/allow-list/{entry_id}")
|
|
assert resp.status_code == 401
|