4b74092879
* auto-claude: subtask-1-1 - Investigate Claude Code CLI token storage format * auto-claude: subtask-1-2 - Trace existing token flow from frontend to backend Documented complete token flow analysis in INVESTIGATION.md: - Frontend (pty-manager.ts): Passes token from environment without decryption - Backend token retrieval (auth.py): * get_auth_token(): Returns token as-is with enc: prefix intact * require_auth_token(): Passes encrypted token through * ensure_claude_code_oauth_token(): Sets encrypted token in environment - SDK client creation (client.py, simple_client.py): * Both set encrypted token in CLAUDE_CODE_OAUTH_TOKEN env var * SDK receives encrypted token → API 401 error - Identified optimal decryption insertion point: get_auth_token() * Single location ensures all downstream functions get decrypted tokens * Backward compatible with plaintext tokens * Consistent across env vars and keychain sources * auto-claude: subtask-2-1 - Add token format detection utility (detect enc: prefix) * auto-claude: subtask-2-2 - Implement cross-platform token decryption function * auto-claude: subtask-2-3 - Integrate decryption into get_auth_token() and require_auth_token() * auto-claude: subtask-2-4 - Add comprehensive error handling for decryption fa * auto-claude: subtask-3-1 - Add pre-SDK-init token validation in create_client * auto-claude: subtask-3-2 - Add same validation to simple_client.py * fix: Address QA issues - add required unit tests and improve documentation (qa-requested) Fixes: - Added 5 unit tests to tests/test_auth.py for token decryption functionality - Created tests/test_client.py with client token validation test - Updated decrypt_token() docstring to document encrypted token limitation - Improved error messages in platform-specific decryption functions - Fixed is_encrypted_token() to handle None input gracefully - Modified get_auth_token() to return encrypted token when decryption fails Verified: - All 44 auth tests pass (39 existing + 5 new) - Client validation test passes - No regressions in existing functionality QA Fix Session: 1 * fix: Address PR review issues - DRY validation, platform abstraction, security PR Review Issues Fixed: HIGH: - Extract duplicated token validation into shared validate_token_not_encrypted() function in auth.py (removes 10-line duplication in client.py/simple_client.py) - Replace all platform.system() calls with core.platform imports (is_macos(), is_windows(), is_linux()) to follow platform abstraction guidelines MEDIUM: - Remove token data from error message in decrypt_token() to prevent credential exposure in logs - Add log.warning() when token decryption fails to improve runtime visibility - Add explicit assertion in test_get_auth_token_decrypts_encrypted_env_token - Add test_create_simple_client_rejects_encrypted_tokens test - Update INVESTIGATION.md to use function names instead of line numbers LOW: - Remove unused imports (os, Path) from test_client.py - Use find_executable() from platform module in _decrypt_token_macos() - Error messages now consistent between client.py and simple_client.py All 46 tests pass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Remove unused shutil import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Address follow-up review findings - Remove encrypted data length from error message (security hardening) - Fix misleading SDK version message in NotImplementedError handler - Add language specifiers to markdown code blocks in INVESTIGATION.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Address PR review findings for auth token handling - Make decryption failure handling consistent between env vars and keychain (both now return encrypted token for specific error messaging) - Remove unused claude_path variable in _decrypt_token_macos() - Add clarifying comment for mixed base64 encoding acceptance - Add direct unit tests for validate_token_not_encrypted() - Add assertion that decrypt_token() was called in existing test - Add positive test cases for valid token flow in test_client.py - Add happy-path test for decrypt_token success (mocked) Fixes: NEWREV-001, NEWREV-002, NEWREV-003, NEWREV-004, NEWREV-005, NEWREV-006, NEW-004 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Test User <test@example.com>
111 lines
4.8 KiB
Python
111 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for Client Creation and Token Validation
|
|
===============================================
|
|
|
|
Tests the client.py and simple_client.py module functionality including:
|
|
- Token validation before SDK initialization
|
|
- Encrypted token rejection
|
|
- Client creation with valid tokens
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
class TestClientTokenValidation:
|
|
"""Tests for client token validation."""
|
|
|
|
def test_create_client_rejects_encrypted_tokens(self, tmp_path, monkeypatch):
|
|
"""Verify create_client() rejects encrypted tokens."""
|
|
from core.client import create_client
|
|
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "enc:test123456789012")
|
|
# Mock keychain to ensure encrypted token is the only source
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda: None)
|
|
|
|
with pytest.raises(ValueError, match="encrypted format"):
|
|
create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
def test_create_simple_client_rejects_encrypted_tokens(self, monkeypatch):
|
|
"""Verify create_simple_client() rejects encrypted tokens."""
|
|
from core.simple_client import create_simple_client
|
|
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "enc:test123456789012")
|
|
# Mock keychain to ensure encrypted token is the only source
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda: None)
|
|
|
|
with pytest.raises(ValueError, match="encrypted format"):
|
|
create_simple_client(agent_type="merge_resolver")
|
|
|
|
def test_create_client_accepts_valid_plaintext_token(self, tmp_path, monkeypatch):
|
|
"""Verify create_client() accepts valid plaintext tokens and creates SDK client."""
|
|
valid_token = "sk-ant-oat01-valid-plaintext-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda: None)
|
|
|
|
# Mock the SDK client to avoid actual initialization
|
|
mock_sdk_client = MagicMock()
|
|
with patch("core.client.ClaudeSDKClient", return_value=mock_sdk_client):
|
|
from core.client import create_client
|
|
|
|
client = create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
def test_create_simple_client_accepts_valid_plaintext_token(self, monkeypatch):
|
|
"""Verify create_simple_client() accepts valid plaintext tokens and creates SDK client."""
|
|
valid_token = "sk-ant-oat01-valid-plaintext-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda: None)
|
|
|
|
# Mock the SDK client to avoid actual initialization
|
|
mock_sdk_client = MagicMock()
|
|
with patch(
|
|
"core.simple_client.ClaudeSDKClient", return_value=mock_sdk_client
|
|
):
|
|
from core.simple_client import create_simple_client
|
|
|
|
client = create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify SDK client was created
|
|
assert client is mock_sdk_client
|
|
|
|
def test_create_client_validates_token_before_sdk_init(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
"""Verify create_client() validates token format before SDK initialization."""
|
|
valid_token = "sk-ant-oat01-valid-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda: None)
|
|
|
|
# Mock validate_token_not_encrypted to verify it's called
|
|
with patch(
|
|
"core.client.validate_token_not_encrypted"
|
|
) as mock_validate, patch("core.client.ClaudeSDKClient"):
|
|
from core.client import create_client
|
|
|
|
create_client(tmp_path, tmp_path, "claude-sonnet-4", "coder")
|
|
|
|
# Verify validation was called with the token
|
|
mock_validate.assert_called_once_with(valid_token)
|
|
|
|
def test_create_simple_client_validates_token_before_sdk_init(self, monkeypatch):
|
|
"""Verify create_simple_client() validates token format before SDK initialization."""
|
|
valid_token = "sk-ant-oat01-valid-token"
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", valid_token)
|
|
monkeypatch.setattr("core.auth.get_token_from_keychain", lambda: None)
|
|
|
|
# Mock validate_token_not_encrypted to verify it's called
|
|
with patch(
|
|
"core.simple_client.validate_token_not_encrypted"
|
|
) as mock_validate, patch("core.simple_client.ClaudeSDKClient"):
|
|
from core.simple_client import create_simple_client
|
|
|
|
create_simple_client(agent_type="merge_resolver")
|
|
|
|
# Verify validation was called with the token
|
|
mock_validate.assert_called_once_with(valid_token)
|