"""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