""" Offline unit tests for SchedulerClient (Phase D review batch). Pins the runtime prefetch-registration fixes: - SchedulerClient sends Authorization: Bearer on its own calls (the Scheduler's /tasks endpoints are auth-guarded; a bare client 401s and registration fails silently at consolidation time) - register_volatile_fetch task config is actually executable by the Scheduler's rest_api_executor: * JSON body under config["payload"] (a "body" key is silently ignored) * user as a QUERY parameter (the /volatile/fetch endpoints use RequiredUserQuery; a body user would 422) * an auth block with the ${LIBRARY_API_KEY} placeholder so the scheduled POST passes library-desk's verify_api_key """ import httpx import pytest from src.clients.scheduler_client import ( LIBRARY_API_KEY_PLACEHOLDER, SchedulerClient, ) def _mock_transport(seen): def handler(request: httpx.Request) -> httpx.Response: seen.append(request) path = request.url.path if request.method == "GET" and path.startswith("/tasks/"): return httpx.Response(404) # task does not exist yet if request.method == "POST" and path == "/tasks": return httpx.Response(200, json={"ok": True}) return httpx.Response(200, json={}) return httpx.MockTransport(handler) @pytest.fixture def seen_requests(): return [] @pytest.fixture def client(seen_requests, monkeypatch): """SchedulerClient whose real _get_client builds against a MockTransport.""" import src.clients.scheduler_client as mod real_async_client = httpx.AsyncClient def client_factory(**kwargs): kwargs["transport"] = _mock_transport(seen_requests) return real_async_client(**kwargs) monkeypatch.setattr(mod.httpx, "AsyncClient", client_factory) return SchedulerClient(base_url="http://scheduler.test:8090", api_key="sched-key") @pytest.mark.unit class TestSchedulerApiAuth: @pytest.mark.asyncio async def test_bearer_auth_sent_on_all_calls(self, client, seen_requests): ok = await client.register_volatile_fetch( namespace="weather", key="rotterdam", user="llm_tester", schedule={"minute": 0}, ) assert ok is True assert seen_requests, "no HTTP calls were made" for request in seen_requests: assert request.headers.get("Authorization") == "Bearer sched-key" def test_missing_api_key_is_flagged(self, caplog): with caplog.at_level("WARNING"): SchedulerClient(base_url="http://scheduler.test:8090") assert any("without an API key" in r.message for r in caplog.records) @pytest.mark.unit class TestRegisterVolatileFetchConfig: @pytest.mark.asyncio async def _registered_config(self, client, seen_requests, user="llm_tester"): ok = await client.register_volatile_fetch( namespace="weather", key="rotterdam", user=user, schedule={"minute": 0, "hour": -1}, ) assert ok is True create = next( r for r in seen_requests if r.method == "POST" and r.url.path == "/tasks" ) import json return json.loads(create.content)["config"] @pytest.mark.asyncio async def test_body_key_not_used_payload_is(self, client, seen_requests): config = await self._registered_config(client, seen_requests) # rest_api_executor only reads config["payload"]; "body" is ignored assert "body" not in config assert config["payload"] == {} @pytest.mark.asyncio async def test_user_is_query_parameter(self, client, seen_requests): config = await self._registered_config(client, seen_requests) assert config["url"] == ( "http://library-desk:8089/volatile/fetch/weather/rotterdam" "?user=llm_tester" ) @pytest.mark.asyncio async def test_auth_block_uses_placeholder(self, client, seen_requests): config = await self._registered_config(client, seen_requests) assert config["auth"] == { "type": "bearer", "token": LIBRARY_API_KEY_PLACEHOLDER, } # never the raw key, never in plain headers (not substituted there) assert "Authorization" not in config.get("headers", {})