"""Integration tests for the public runtime config endpoint. Covers GET /api/v1/config/public and the SELF_SERVE_ENABLED interaction with the existing /auth/register invite-code gate. """ from __future__ import annotations import pytest from httpx import AsyncClient from app.core.config import settings class TestConfigPublic: """GET /api/v1/config/public — anonymous, no auth.""" @pytest.mark.asyncio async def test_get_config_public_returns_self_serve_flag( self, client: AsyncClient, monkeypatch: pytest.MonkeyPatch ): """Endpoint reflects the current SELF_SERVE_ENABLED setting and the configured OAuth providers, with no auth required.""" # Default-off: SELF_SERVE_ENABLED is False unless explicitly set. monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", None) monkeypatch.setattr(settings, "MS_CLIENT_ID", None) response = await client.get("/api/v1/config/public") assert response.status_code == 200 body = response.json() assert body == {"self_serve_enabled": False, "oauth_providers": []} # Flip it on, with both OAuth providers configured. monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", True) monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", "google-test-id") monkeypatch.setattr(settings, "MS_CLIENT_ID", "ms-test-id") response = await client.get("/api/v1/config/public") assert response.status_code == 200 body = response.json() assert body["self_serve_enabled"] is True assert body["oauth_providers"] == ["google", "microsoft"] # Only Microsoft configured. monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", None) monkeypatch.setattr(settings, "MS_CLIENT_ID", "ms-test-id") response = await client.get("/api/v1/config/public") assert response.status_code == 200 assert response.json()["oauth_providers"] == ["microsoft"] @pytest.mark.asyncio async def test_get_config_public_returns_true_for_internal_tester( self, client: AsyncClient, auth_headers: dict, test_user: dict, monkeypatch: pytest.MonkeyPatch, ): """Authenticated user whose email is on INTERNAL_TESTER_EMAILS sees self_serve_enabled=True even when the global flag is off.""" monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", None) monkeypatch.setattr(settings, "MS_CLIENT_ID", None) monkeypatch.setattr(settings, "INTERNAL_TESTER_EMAILS", [test_user["email"].lower()]) response = await client.get("/api/v1/config/public", headers=auth_headers) assert response.status_code == 200 assert response.json()["self_serve_enabled"] is True @pytest.mark.asyncio async def test_get_config_public_returns_false_for_non_tester_when_global_off( self, client: AsyncClient, auth_headers: dict, monkeypatch: pytest.MonkeyPatch, ): """Authenticated user NOT on the allowlist sees the global flag — prevents accidental opt-in via stale credentials or empty allowlist.""" monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", None) monkeypatch.setattr(settings, "MS_CLIENT_ID", None) monkeypatch.setattr(settings, "INTERNAL_TESTER_EMAILS", ["someone-else@example.com"]) response = await client.get("/api/v1/config/public", headers=auth_headers) assert response.status_code == 200 assert response.json()["self_serve_enabled"] is False @pytest.mark.asyncio async def test_get_config_public_anonymous_ignores_allowlist( self, client: AsyncClient, monkeypatch: pytest.MonkeyPatch ): """Anonymous callers always see the global flag — the allowlist is keyed on authenticated identity, not request content.""" monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) monkeypatch.setattr(settings, "GOOGLE_CLIENT_ID", None) monkeypatch.setattr(settings, "MS_CLIENT_ID", None) monkeypatch.setattr(settings, "INTERNAL_TESTER_EMAILS", ["anon-tester@example.com"]) response = await client.get("/api/v1/config/public") assert response.status_code == 200 assert response.json()["self_serve_enabled"] is False class TestRegisterInviteCodeGate: """Regression + new-behavior tests for /auth/register vs SELF_SERVE_ENABLED.""" @pytest.mark.asyncio async def test_register_invite_code_required_when_self_serve_disabled( self, client: AsyncClient, monkeypatch: pytest.MonkeyPatch ): """Pre-self-serve behavior: REQUIRE_INVITE_CODE=True without an invite code (and no account-invite) must still 400.""" monkeypatch.setattr(settings, "REQUIRE_INVITE_CODE", True) monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) response = await client.post( "/api/v1/auth/register", json={ "email": "no-invite@example.com", "password": "SecurePass123!", "name": "No Invite", }, ) assert response.status_code == 400 assert "invite code is required" in response.json()["detail"].lower() @pytest.mark.asyncio async def test_register_invite_code_optional_when_self_serve_enabled( self, client: AsyncClient, monkeypatch: pytest.MonkeyPatch ): """Self-serve on: registration succeeds with no invite code even when REQUIRE_INVITE_CODE is True. The user, personal account, and a Pro trial subscription are all created.""" monkeypatch.setattr(settings, "REQUIRE_INVITE_CODE", True) monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", True) response = await client.post( "/api/v1/auth/register", json={ "email": "self-serve@example.com", "password": "SecurePass123!", "name": "Self Serve", }, ) assert response.status_code == 201, response.text body = response.json() assert body["email"] == "self-serve@example.com" assert body["account_role"] == "owner" assert "account_id" in body @pytest.mark.asyncio async def test_register_invite_code_optional_for_internal_tester( self, client: AsyncClient, monkeypatch: pytest.MonkeyPatch ): """SELF_SERVE_ENABLED is False but the registering email is on INTERNAL_TESTER_EMAILS — registration should succeed without an invite code, matching the per-email soft-cutover behavior.""" monkeypatch.setattr(settings, "REQUIRE_INVITE_CODE", True) monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) monkeypatch.setattr( settings, "INTERNAL_TESTER_EMAILS", ["tester@example.com"] ) response = await client.post( "/api/v1/auth/register", json={ "email": "tester@example.com", "password": "SecurePass123!", "name": "Internal Tester", }, ) assert response.status_code == 201, response.text body = response.json() assert body["email"] == "tester@example.com" assert body["account_role"] == "owner" @pytest.mark.asyncio async def test_register_blocked_for_non_tester_when_self_serve_disabled( self, client: AsyncClient, monkeypatch: pytest.MonkeyPatch ): """Registering with an email NOT on the allowlist still 400s when self-serve is off and no invite code is provided. Prevents the allowlist from leaking to public users.""" monkeypatch.setattr(settings, "REQUIRE_INVITE_CODE", True) monkeypatch.setattr(settings, "SELF_SERVE_ENABLED", False) monkeypatch.setattr( settings, "INTERNAL_TESTER_EMAILS", ["other@example.com"] ) response = await client.post( "/api/v1/auth/register", json={ "email": "outsider@example.com", "password": "SecurePass123!", "name": "Outsider", }, ) assert response.status_code == 400 assert "invite code is required" in response.json()["detail"].lower()