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:
James Cottrill
2026-04-13 14:20:15 +00:00
commit fbf26453f2
341 changed files with 62807 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
"""Tests for dark pattern detection — mocks Playwright."""
from unittest.mock import AsyncMock
import pytest
from src.dark_pattern_detector import (
check_button_prominence,
check_cookie_wall,
check_pre_ticked_boxes,
detect_dark_patterns,
)
class TestCheckButtonProminence:
@pytest.mark.asyncio
async def test_no_accept_button_returns_empty(self) -> None:
page = AsyncMock()
page.query_selector_all = AsyncMock(return_value=[])
issues = await check_button_prominence(page)
assert issues == []
@pytest.mark.asyncio
async def test_missing_reject_button_flagged(self) -> None:
# Accept button visible, reject not found
accept_el = AsyncMock()
accept_el.is_visible = AsyncMock(return_value=True)
accept_el.evaluate = AsyncMock(
return_value={
"width": 200,
"height": 40,
"area": 8000,
"backgroundColor": "rgb(37, 99, 235)",
"color": "rgb(255, 255, 255)",
"fontSize": 16,
"fontWeight": "600",
"padding": "8px 16px",
"text": "Accept All",
"visible": True,
}
)
call_count = 0
async def _mock_query(selector):
nonlocal call_count
call_count += 1
# First batch of calls = accept selectors, return button
# Remaining calls = reject selectors, return empty
if "Accept" in selector or "Allow" in selector or "accept" in selector:
return [accept_el]
return []
page = AsyncMock()
page.query_selector_all = _mock_query
issues = await check_button_prominence(page)
assert any(i.pattern == "missing_reject_button" for i in issues)
@pytest.mark.asyncio
async def test_unequal_button_size_flagged(self) -> None:
accept_el = AsyncMock()
accept_el.is_visible = AsyncMock(return_value=True)
accept_el.evaluate = AsyncMock(
return_value={
"width": 300,
"height": 50,
"area": 15000,
"fontSize": 18,
"fontWeight": "700",
"text": "Accept All",
"visible": True,
}
)
reject_el = AsyncMock()
reject_el.is_visible = AsyncMock(return_value=True)
reject_el.evaluate = AsyncMock(
return_value={
"width": 100,
"height": 30,
"area": 3000,
"fontSize": 12,
"fontWeight": "400",
"text": "Reject",
"visible": True,
}
)
async def _mock_query(selector):
if "Accept" in selector or "Allow" in selector or "accept" in selector:
return [accept_el]
if "Reject" in selector or "Decline" in selector or "reject" in selector:
return [reject_el]
return []
page = AsyncMock()
page.query_selector_all = _mock_query
issues = await check_button_prominence(page)
assert any(i.pattern == "unequal_button_size" for i in issues)
class TestCheckPreTickedBoxes:
@pytest.mark.asyncio
async def test_no_pre_ticked_returns_empty(self) -> None:
page = AsyncMock()
page.evaluate = AsyncMock(return_value=[])
issues = await check_pre_ticked_boxes(page)
assert issues == []
@pytest.mark.asyncio
async def test_pre_ticked_non_essential_flagged(self) -> None:
page = AsyncMock()
page.evaluate = AsyncMock(
return_value=[
{"name": "analytics", "label": "Analytics Cookies"},
{"name": "marketing", "label": "Marketing Cookies"},
]
)
issues = await check_pre_ticked_boxes(page)
assert len(issues) == 1
assert issues[0].pattern == "pre_ticked_checkboxes"
assert issues[0].severity == "critical"
class TestCheckCookieWall:
@pytest.mark.asyncio
async def test_no_wall_returns_empty(self) -> None:
page = AsyncMock()
page.evaluate = AsyncMock(return_value=False)
issues = await check_cookie_wall(page)
assert issues == []
@pytest.mark.asyncio
async def test_wall_detected(self) -> None:
page = AsyncMock()
page.evaluate = AsyncMock(return_value=True)
issues = await check_cookie_wall(page)
assert len(issues) == 1
assert issues[0].pattern == "cookie_wall"
assert issues[0].severity == "critical"
class TestDetectDarkPatterns:
@pytest.mark.asyncio
async def test_no_banner_returns_empty(self) -> None:
page = AsyncMock()
page.url = "https://example.com/"
page.query_selector_all = AsyncMock(return_value=[])
result = await detect_dark_patterns(page)
assert result.banner_found is False
assert result.issues == []