Files
Aperant/tests/test_client.py
T
Andy 4b74092879 Fix API 401 - Token Decryption Before SDK Initialization (#1283)
* 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>
2026-01-18 14:13:43 +01:00

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)