from __future__ import annotations import logging from botocore.exceptions import ClientError from fastapi.testclient import TestClient from nexus_claude_api.config import Settings from nexus_claude_api.nexus_client import _map_client_error from nexus_claude_api.server import create_app class FakeNexusClient: def converse(self, request: dict, *, correlation_id: str | None = None) -> dict: assert correlation_id assert request["messages"][0]["content"][0]["text"] == "Hello" return { "ResponseMetadata": {"RequestId": "req-route"}, "output": {"message": {"content": [{"text": "Hi"}]}}, "stopReason": "end_turn", "usage": {"inputTokens": 3, "outputTokens": 1}, } def converse_stream(self, request: dict, *, correlation_id: str | None = None): assert correlation_id assert request["messages"][0]["content"][0]["text"] == "Hello" return [ {"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "Hi"}}}, {"contentBlockStop": {"contentBlockIndex": 0}}, {"messageStop": {"stopReason": "end_turn"}}, ] class AccessDeniedNexusClient: def converse(self, request: dict, *, correlation_id: str | None = None) -> dict: raise _map_client_error( ClientError( { "Error": { "Code": "AccessDeniedException", "Message": "not allowed", }, "ResponseMetadata": { "HTTPStatusCode": 403, "RequestId": "req-denied", }, }, "Converse", ), operation="converse", correlation_id=correlation_id, ) def client(nexus_client: object | None = None) -> TestClient: settings = Settings.from_values(api_key="test", require_api_key=False) return TestClient( create_app(settings=settings, nexus_client=nexus_client or FakeNexusClient()) ) def test_health() -> None: response = client().get("/health") assert response.status_code == 200 assert response.json() == {"status": "ok"} def test_root_head() -> None: response = client().head("/") assert response.status_code == 200 assert response.content == b"" def test_models() -> None: response = client().get("/v1/models") assert response.status_code == 200 data = response.json()["data"] assert [model["id"] for model in data] == ["claude-opus-4.6"] def test_messages_non_stream() -> None: response = client().post( "/v1/messages", json={ "model": "claude-opus-4.6", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 32, }, ) assert response.status_code == 200 body = response.json() assert body["id"] == "req-route" assert body["content"][0]["text"] == "Hi" def test_messages_normalizes_legacy_system_role() -> None: response = client().post( "/v1/messages", json={ "model": "claude-opus-4.6", "messages": [ {"role": "system", "content": "You are helpful."}, {"role": "user", "content": "Hello"}, ], "max_tokens": 32, }, ) assert response.status_code == 200 assert response.json()["content"][0]["text"] == "Hi" def test_messages_upstream_error_returns_correlation_id() -> None: response = client(AccessDeniedNexusClient()).post( "/v1/messages", json={ "model": "claude-opus-4.6", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 32, }, ) assert response.status_code == 403 error = response.json()["error"] assert error["type"] == "api_error" assert error["correlation_id"] assert f"correlation_id={error['correlation_id']}" in error["message"] def test_client_error_mapping_logs_sanitized_details(caplog) -> None: caplog.set_level(logging.WARNING, logger="nexus_claude_api.nexus_client") mapped = _map_client_error( ClientError( { "Error": { "Code": "AccessDeniedException", "Message": "model access denied", }, "ResponseMetadata": { "HTTPStatusCode": 400, "RequestId": "req-log", }, }, "Converse", ), operation="converse", correlation_id="corr-log", ) assert mapped.status_code == 403 assert mapped.error_type == "api_error" record = caplog.records[-1] assert record.levelno == logging.WARNING assert record.correlation_id == "corr-log" assert record.operation == "converse" assert record.nexus_error_code == "AccessDeniedException" assert record.http_status == 403 assert record.nexus_request_id == "req-log" log_text = caplog.text assert "api_key" not in log_text assert "Authorization" not in log_text assert "aW1hZ2UtYnl0ZXM=" not in log_text def test_messages_stream() -> None: with client().stream( "POST", "/v1/messages", json={ "model": "claude-opus-4.6", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 32, "stream": True, }, ) as response: body = response.read().decode("utf-8") assert response.status_code == 200 assert "event: message_start" in body assert "event: content_block_delta" in body assert "event: message_stop" in body def test_count_tokens() -> None: response = client().post( "/v1/messages/count_tokens", json={ "model": "claude-opus-4.6", "messages": [{"role": "user", "content": "Hello"}], }, ) assert response.status_code == 200 assert response.json()["input_tokens"] > 0