"""Settings routes (/api/settings) are gated behind the panel token. Under cloud_auth the panel reaches these routes with a CEO session cookie; the gate (``require_panel_token``) accepts the cookie OR a valid CEO HMAC token. In dev mode a missing token is a no-op (existing behavior). """ from __future__ import annotations from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from roboco.agents_config import issue_panel_token from roboco.api import deps as _deps from roboco.api.auth.backend import SESSION_COOKIE_NAME from roboco.api.routes.settings import router as settings_router from roboco.db.base import get_db if TYPE_CHECKING: from collections.abc import AsyncIterator _SECRET = "test-secret-for-settings-routes" _HTTP_200 = 200 _HTTP_401 = 401 @pytest_asyncio.fixture async def settings_client( monkeypatch: pytest.MonkeyPatch, ) -> AsyncIterator[AsyncClient]: monkeypatch.setenv("ROBOCO_AGENT_AUTH_SECRET", _SECRET) fake_service = MagicMock() fake_service.all = AsyncMock(return_value={}) monkeypatch.setattr( "roboco.api.routes.settings.get_settings_service", lambda _db: fake_service, ) async def _fake_db() -> AsyncIterator[object]: yield object() app = FastAPI() app.include_router(settings_router, prefix="/api/settings") app.dependency_overrides[get_db] = _fake_db async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: yield client app.dependency_overrides.clear() @pytest.mark.asyncio async def test_settings_rejects_no_credential_under_cloud_auth( settings_client: AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(_deps.settings, "cloud_auth_enabled", True) monkeypatch.delenv("ROBOCO_AGENT_AUTH_REQUIRED", raising=False) r = await settings_client.get("/api/settings") assert r.status_code == _HTTP_401 @pytest.mark.asyncio async def test_settings_accepts_valid_token_under_cloud_auth( settings_client: AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(_deps.settings, "cloud_auth_enabled", True) r = await settings_client.get( "/api/settings", headers={"X-Agent-Token": issue_panel_token()} ) assert r.status_code == _HTTP_200 @pytest.mark.asyncio async def test_settings_accepts_valid_cookie_under_cloud_auth( settings_client: AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(_deps.settings, "cloud_auth_enabled", True) with patch( "roboco.api.deps.resolve_session_user", new=AsyncMock(return_value=MagicMock()), ): r = await settings_client.get( "/api/settings", cookies={SESSION_COOKIE_NAME: "valid"} ) assert r.status_code == _HTTP_200 @pytest.mark.asyncio async def test_settings_dev_mode_no_token_passes( settings_client: AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(_deps.settings, "cloud_auth_enabled", False) monkeypatch.delenv("ROBOCO_AGENT_AUTH_REQUIRED", raising=False) r = await settings_client.get("/api/settings") assert r.status_code == _HTTP_200 @pytest.mark.asyncio async def test_settings_put_rejects_no_credential_under_cloud_auth( settings_client: AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(_deps.settings, "cloud_auth_enabled", True) monkeypatch.delenv("ROBOCO_AGENT_AUTH_REQUIRED", raising=False) r = await settings_client.put("/api/settings/some.key", json={"value": "x"}) assert r.status_code == _HTTP_401