| """Tests that API and SSE logging avoid raw sensitive payloads by default.""" |
|
|
| from unittest.mock import MagicMock, patch |
|
|
| import pytest |
| from fastapi import HTTPException |
| from fastapi.responses import JSONResponse |
|
|
| from free_claude_code.api import request_errors |
| from free_claude_code.api.handlers import MessagesHandler, TokenCountHandler |
| from free_claude_code.application import execution |
| from free_claude_code.config.settings import Settings |
| from free_claude_code.core.anthropic import AnthropicStreamLedger |
| from free_claude_code.core.anthropic.models import Message, MessagesRequest |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_create_message_skips_full_payload_debug_log_by_default(): |
| settings = Settings() |
| assert settings.log_raw_api_payloads is False |
| mock_provider = MagicMock() |
|
|
| async def fake_stream(*_a, **_kw): |
| yield "event: ping\ndata: {}\n\n" |
|
|
| mock_provider.stream_response = fake_stream |
| service = MessagesHandler(settings, provider_resolver=lambda _: mock_provider) |
|
|
| request = MessagesRequest( |
| model="claude-3-haiku-20240307", |
| max_tokens=10, |
| messages=[Message(role="user", content="secret-user-text")], |
| ) |
|
|
| with patch.object(execution.logger, "debug") as mock_debug: |
| await service.create(request) |
|
|
| full_payload_calls = [ |
| c |
| for c in mock_debug.call_args_list |
| if c.args and str(c.args[0]) == "FULL_PAYLOAD [{}]: {}" |
| ] |
| assert not full_payload_calls |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_create_message_logs_full_payload_when_opt_in(): |
| settings = Settings() |
| settings.log_raw_api_payloads = True |
| mock_provider = MagicMock() |
|
|
| async def fake_stream(*_a, **_kw): |
| yield "event: ping\ndata: {}\n\n" |
|
|
| mock_provider.stream_response = fake_stream |
| service = MessagesHandler(settings, provider_resolver=lambda _: mock_provider) |
| request = MessagesRequest( |
| model="claude-3-haiku-20240307", |
| max_tokens=10, |
| messages=[Message(role="user", content="visible")], |
| ) |
|
|
| with patch.object(execution.logger, "debug") as mock_debug: |
| await service.create(request) |
|
|
| keys = [c.args[0] for c in mock_debug.call_args_list if c.args] |
| assert any(k == "FULL_PAYLOAD [{}]: {}" for k in keys) |
|
|
|
|
| def test_stream_ledger_default_debug_has_no_serialized_json_content(): |
| with patch( |
| "free_claude_code.core.anthropic.streaming.emitter.logger.debug" |
| ) as mock_debug: |
| ledger = AnthropicStreamLedger("msg_x", "m", 1, log_raw_events=False) |
| ledger.message_start() |
|
|
| assert mock_debug.call_count == 0 |
|
|
|
|
| def test_stream_ledger_raw_logging_includes_event_body_when_enabled(): |
| with patch( |
| "free_claude_code.core.anthropic.streaming.emitter.logger.debug" |
| ) as mock_debug: |
| ledger = AnthropicStreamLedger("msg_x", "m", 1, log_raw_events=True) |
| ledger.message_start() |
|
|
| assert mock_debug.call_count == 1 |
| message = str(mock_debug.call_args) |
| assert "message_start" in message |
| assert "role" in message |
|
|
|
|
| def _flatten_log_calls(mock_log) -> str: |
| parts: list[str] = [] |
| for call in mock_log.call_args_list: |
| parts.extend(str(arg) for arg in call.args) |
| parts.append(repr(call.kwargs)) |
| return " ".join(parts) |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_create_message_unexpected_error_default_logs_exclude_exception_text(): |
| settings = Settings() |
| assert settings.log_api_error_tracebacks is False |
| secret = "upstream-secret-token-abc" |
|
|
| mock_provider = MagicMock() |
|
|
| def stream_boom(*_a, **_kw): |
| raise RuntimeError(secret) |
|
|
| mock_provider.stream_response = stream_boom |
| service = MessagesHandler(settings, provider_resolver=lambda _: mock_provider) |
| request = MessagesRequest( |
| model="claude-3-haiku-20240307", |
| max_tokens=10, |
| messages=[Message(role="user", content="hi")], |
| ) |
|
|
| with patch.object(request_errors.logger, "error") as log_err: |
| response = await service.create(request) |
|
|
| blob = _flatten_log_calls(log_err) |
| assert secret not in blob |
| assert "RuntimeError" in blob |
| assert isinstance(response, JSONResponse) |
| assert response.status_code == 500 |
| assert response.headers["x-should-retry"] == "false" |
| assert response.body |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_create_message_unexpected_error_terminal_json_ignores_status_code(): |
| """Non-provider stream failures must not leak arbitrary HTTP status attributes.""" |
|
|
| class WeirdError(Exception): |
| status_code = 418 |
|
|
| settings = Settings() |
| mock_provider = MagicMock() |
|
|
| def stream_boom(*_a, **_kw): |
| raise WeirdError("no") |
|
|
| mock_provider.stream_response = stream_boom |
| service = MessagesHandler(settings, provider_resolver=lambda _: mock_provider) |
| request = MessagesRequest( |
| model="claude-3-haiku-20240307", |
| max_tokens=10, |
| messages=[Message(role="user", content="hi")], |
| ) |
|
|
| response = await service.create(request) |
|
|
| assert isinstance(response, JSONResponse) |
| assert response.status_code == 500 |
| assert response.headers["x-should-retry"] == "false" |
| payload = bytes(response.body).decode("utf-8") |
| assert '"type":"api_error"' in payload |
| assert '"message":"no"' in payload |
|
|
|
|
| def test_parse_cli_event_error_logs_metadata_by_default(): |
| """CLI parser must not log raw error text unless LOG_RAW_CLI_DIAGNOSTICS is on.""" |
| from free_claude_code.messaging.event_parser import parse_cli_event |
|
|
| secret = "user-secret-parser-leak-xyz" |
| with patch("free_claude_code.messaging.event_parser.logger.info") as log_info: |
| parse_cli_event( |
| {"type": "error", "error": {"message": secret}}, log_raw_cli=False |
| ) |
| flat = " ".join(str(c) for c in log_info.call_args_list) |
| assert secret not in flat |
| assert "message_chars" in flat |
|
|
|
|
| def test_parse_cli_event_error_logs_text_when_log_raw_cli_enabled(): |
| from free_claude_code.messaging.event_parser import parse_cli_event |
|
|
| secret = "visible-cli-parser-msg" |
| with patch("free_claude_code.messaging.event_parser.logger.info") as log_info: |
| parse_cli_event( |
| {"type": "error", "error": {"message": secret}}, log_raw_cli=True |
| ) |
| flat = " ".join(str(c) for c in log_info.call_args_list) |
| assert secret in flat |
|
|
|
|
| def test_count_tokens_unexpected_error_default_logs_exclude_exception_text(): |
| settings = Settings() |
| assert settings.log_api_error_tracebacks is False |
| secret = "count-tokens-leak-xyz" |
|
|
| def boom(*_a, **_kw): |
| raise ValueError(secret) |
|
|
| service = TokenCountHandler( |
| settings, |
| token_counter=boom, |
| ) |
| from free_claude_code.core.anthropic.models import TokenCountRequest |
|
|
| req = TokenCountRequest( |
| model="claude-3-haiku-20240307", |
| messages=[Message(role="user", content="x")], |
| ) |
|
|
| with ( |
| patch.object(request_errors.logger, "error") as log_err, |
| pytest.raises(HTTPException), |
| ): |
| service.count(req) |
|
|
| blob = _flatten_log_calls(log_err) |
| assert secret not in blob |
| assert "ValueError" in blob |
|
|