TradingAgents/tests/api/test_api_key_service.py

588 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Test suite for API Key Service (Issue #3).
This module tests the API key generation, hashing, and verification service:
1. Generate secure random API keys
2. Hash API keys using bcrypt
3. Verify API keys against hashes
4. Key format validation
5. Security best practices
Tests follow TDD - written before implementation.
"""
import pytest
import re
from typing import Optional
pytestmark = pytest.mark.unit
# ============================================================================
# Unit Tests: API Key Generation
# ============================================================================
class TestApiKeyGeneration:
"""Test API key generation functionality."""
def test_generate_api_key_returns_string(self):
"""Test that generate_api_key returns a string."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
api_key = generate_api_key()
# Assert
assert isinstance(api_key, str)
assert len(api_key) > 0
except ImportError:
pytest.skip("API key service not implemented yet")
def test_generate_api_key_format(self):
"""Test that generated API key has correct format."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
api_key = generate_api_key()
# Assert: Should be prefixed with "ta_" (TradingAgents)
assert api_key.startswith("ta_")
# Should contain only alphanumeric characters after prefix
key_part = api_key[3:] # Remove "ta_" prefix
assert re.match(r'^[A-Za-z0-9]+$', key_part)
except ImportError:
pytest.skip("API key service not implemented yet")
def test_generate_api_key_length(self):
"""Test that generated API key has sufficient length for security."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
api_key = generate_api_key()
# Assert: Should be at least 32 characters (including prefix)
assert len(api_key) >= 32
# Key part (without prefix) should be at least 29 chars
key_part = api_key[3:]
assert len(key_part) >= 29
except ImportError:
pytest.skip("API key service not implemented yet")
def test_generate_api_key_uniqueness(self):
"""Test that each generated API key is unique."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
keys = [generate_api_key() for _ in range(100)]
# Assert: All keys should be unique
assert len(keys) == len(set(keys))
except ImportError:
pytest.skip("API key service not implemented yet")
def test_generate_api_key_randomness(self):
"""Test that API keys have sufficient randomness."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
keys = [generate_api_key() for _ in range(10)]
# Assert: Keys should not have common patterns
# Check that no two keys share same first 10 chars after prefix
prefixes = [key[3:13] for key in keys]
assert len(prefixes) == len(set(prefixes))
except ImportError:
pytest.skip("API key service not implemented yet")
def test_generate_api_key_custom_length(self):
"""Test generating API key with custom length."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
# Try generating key with custom length (if supported)
api_key = generate_api_key(length=40)
# Assert: Should respect custom length
assert len(api_key) >= 40 or len(api_key) >= 32 # May have min length
except (ImportError, TypeError):
# TypeError is ok if length parameter not implemented
pytest.skip("Custom length not implemented yet")
# ============================================================================
# Unit Tests: API Key Hashing
# ============================================================================
class TestApiKeyHashing:
"""Test API key hashing functionality."""
def test_hash_api_key_returns_string(self):
"""Test that hash_api_key returns a string."""
# Arrange
try:
from tradingagents.api.services.api_key_service import hash_api_key
api_key = "ta_test1234567890abcdefghijklmnop"
# Act
hashed = hash_api_key(api_key)
# Assert
assert isinstance(hashed, str)
assert len(hashed) > 0
except ImportError:
pytest.skip("API key service not implemented yet")
def test_hash_api_key_bcrypt_format(self):
"""Test that hashed API key uses bcrypt format."""
# Arrange
try:
from tradingagents.api.services.api_key_service import hash_api_key
api_key = "ta_test1234567890abcdefghijklmnop"
# Act
hashed = hash_api_key(api_key)
# Assert: Should be bcrypt format ($2b$rounds$salt+hash)
assert hashed.startswith("$2b$") or hashed.startswith("$2a$")
assert len(hashed) == 60 # Standard bcrypt hash length
except ImportError:
pytest.skip("API key service not implemented yet")
def test_hash_api_key_different_for_same_input(self):
"""Test that hashing same key twice produces different hashes (salt)."""
# Arrange
try:
from tradingagents.api.services.api_key_service import hash_api_key
api_key = "ta_test1234567890abcdefghijklmnop"
# Act
hash1 = hash_api_key(api_key)
hash2 = hash_api_key(api_key)
# Assert: Should be different due to different salts
assert hash1 != hash2
assert hash1.startswith("$2b$")
assert hash2.startswith("$2b$")
except ImportError:
pytest.skip("API key service not implemented yet")
def test_hash_api_key_different_keys_different_hashes(self):
"""Test that different keys produce different hashes."""
# Arrange
try:
from tradingagents.api.services.api_key_service import hash_api_key
key1 = "ta_key1234567890abcdefghijklmnop"
key2 = "ta_key9876543210zyxwvutsrqponmlk"
# Act
hash1 = hash_api_key(key1)
hash2 = hash_api_key(key2)
# Assert
assert hash1 != hash2
except ImportError:
pytest.skip("API key service not implemented yet")
def test_hash_api_key_empty_string(self):
"""Test hashing empty string."""
# Arrange
try:
from tradingagents.api.services.api_key_service import hash_api_key
# Act & Assert: Should handle gracefully or raise ValueError
try:
hashed = hash_api_key("")
# If it doesn't raise, should still return valid hash
assert isinstance(hashed, str)
except ValueError:
# Acceptable to raise ValueError for empty key
pass
except ImportError:
pytest.skip("API key service not implemented yet")
def test_hash_api_key_special_characters(self):
"""Test hashing API key with special characters."""
# Arrange
try:
from tradingagents.api.services.api_key_service import hash_api_key
api_key = "ta_key!@#$%^&*()_+-=[]{}|;:,.<>?"
# Act
hashed = hash_api_key(api_key)
# Assert: Should handle special characters
assert isinstance(hashed, str)
assert hashed.startswith("$2b$") or hashed.startswith("$2a$")
except ImportError:
pytest.skip("API key service not implemented yet")
# ============================================================================
# Unit Tests: API Key Verification
# ============================================================================
class TestApiKeyVerification:
"""Test API key verification functionality."""
def test_verify_api_key_correct_key(self):
"""Test verifying API key with correct hash."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
api_key = "ta_correct1234567890abcdefghijk"
hashed = hash_api_key(api_key)
# Act
is_valid = verify_api_key(api_key, hashed)
# Assert
assert is_valid is True
except ImportError:
pytest.skip("API key service not implemented yet")
def test_verify_api_key_incorrect_key(self):
"""Test verifying API key with wrong hash."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
correct_key = "ta_correct1234567890abcdefghijk"
wrong_key = "ta_wrongkey1234567890abcdefghij"
hashed = hash_api_key(correct_key)
# Act
is_valid = verify_api_key(wrong_key, hashed)
# Assert
assert is_valid is False
except ImportError:
pytest.skip("API key service not implemented yet")
def test_verify_api_key_empty_key(self):
"""Test verifying empty API key."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
api_key = "ta_test1234567890abcdefghijklmn"
hashed = hash_api_key(api_key)
# Act
is_valid = verify_api_key("", hashed)
# Assert
assert is_valid is False
except ImportError:
pytest.skip("API key service not implemented yet")
def test_verify_api_key_none_key(self):
"""Test verifying None API key."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
api_key = "ta_test1234567890abcdefghijklmn"
hashed = hash_api_key(api_key)
# Act & Assert: Should return False or raise TypeError
try:
is_valid = verify_api_key(None, hashed)
assert is_valid is False
except TypeError:
# Acceptable to raise TypeError for None
pass
except ImportError:
pytest.skip("API key service not implemented yet")
def test_verify_api_key_invalid_hash(self):
"""Test verifying API key against invalid hash."""
# Arrange
try:
from tradingagents.api.services.api_key_service import verify_api_key
api_key = "ta_test1234567890abcdefghijklmn"
invalid_hash = "not-a-valid-bcrypt-hash"
# Act & Assert: Should return False or raise ValueError
try:
is_valid = verify_api_key(api_key, invalid_hash)
assert is_valid is False
except ValueError:
# Acceptable to raise ValueError for invalid hash
pass
except ImportError:
pytest.skip("API key service not implemented yet")
def test_verify_api_key_case_sensitive(self):
"""Test that API key verification is case-sensitive."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
api_key = "ta_TestKey1234567890ABCDEFGHIJK"
hashed = hash_api_key(api_key)
# Act
is_valid_correct = verify_api_key(api_key, hashed)
is_valid_wrong_case = verify_api_key(api_key.lower(), hashed)
# Assert
assert is_valid_correct is True
assert is_valid_wrong_case is False
except ImportError:
pytest.skip("API key service not implemented yet")
def test_verify_api_key_similar_keys(self):
"""Test that similar keys don't validate."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
api_key = "ta_key1234567890abcdefghijklmnop"
similar_key = "ta_key1234567890abcdefghijklmnox" # Last char different
hashed = hash_api_key(api_key)
# Act
is_valid = verify_api_key(similar_key, hashed)
# Assert
assert is_valid is False
except ImportError:
pytest.skip("API key service not implemented yet")
# ============================================================================
# Integration Tests: Full API Key Lifecycle
# ============================================================================
class TestApiKeyLifecycle:
"""Test complete API key generation, hashing, and verification workflow."""
def test_full_api_key_lifecycle(self):
"""Test complete lifecycle: generate -> hash -> verify."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import (
generate_api_key,
hash_api_key,
verify_api_key,
)
# Generate new API key
api_key = generate_api_key()
# Hash the API key
hashed = hash_api_key(api_key)
# Verify with correct key
is_valid = verify_api_key(api_key, hashed)
# Assert
assert isinstance(api_key, str)
assert api_key.startswith("ta_")
assert isinstance(hashed, str)
assert hashed.startswith("$2b$") or hashed.startswith("$2a$")
assert is_valid is True
except ImportError:
pytest.skip("API key service not implemented yet")
def test_multiple_keys_independent(self):
"""Test that multiple API keys can coexist independently."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import (
generate_api_key,
hash_api_key,
verify_api_key,
)
# Generate multiple keys
key1 = generate_api_key()
key2 = generate_api_key()
key3 = generate_api_key()
# Hash each key
hash1 = hash_api_key(key1)
hash2 = hash_api_key(key2)
hash3 = hash_api_key(key3)
# Assert: Each key only verifies against its own hash
assert verify_api_key(key1, hash1) is True
assert verify_api_key(key1, hash2) is False
assert verify_api_key(key1, hash3) is False
assert verify_api_key(key2, hash1) is False
assert verify_api_key(key2, hash2) is True
assert verify_api_key(key2, hash3) is False
assert verify_api_key(key3, hash1) is False
assert verify_api_key(key3, hash2) is False
assert verify_api_key(key3, hash3) is True
except ImportError:
pytest.skip("API key service not implemented yet")
def test_regenerate_key_invalidates_old_hash(self):
"""Test that regenerating a key invalidates the old hash."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import (
generate_api_key,
hash_api_key,
verify_api_key,
)
# Generate and hash first key
old_key = generate_api_key()
old_hash = hash_api_key(old_key)
# Generate new key (simulate regeneration)
new_key = generate_api_key()
# Assert: Old hash should not work with new key
assert verify_api_key(old_key, old_hash) is True
assert verify_api_key(new_key, old_hash) is False
except ImportError:
pytest.skip("API key service not implemented yet")
# ============================================================================
# Edge Cases: API Key Service
# ============================================================================
class TestApiKeyEdgeCases:
"""Test edge cases in API key service."""
def test_hash_very_long_key(self):
"""Test hashing very long API key."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
# Create very long key (bcrypt has 72 byte limit)
long_key = "ta_" + "a" * 200
# Act
hashed = hash_api_key(long_key)
is_valid = verify_api_key(long_key, hashed)
# Assert: Should handle gracefully (may truncate to 72 bytes)
assert isinstance(hashed, str)
# Bcrypt will only use first ~72 bytes, so verification should work
assert is_valid is True
except ImportError:
pytest.skip("API key service not implemented yet")
def test_hash_unicode_characters(self):
"""Test hashing API key with Unicode characters."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
hash_api_key,
verify_api_key,
)
# Key with Unicode characters
unicode_key = "ta_测试key_🔑_αβγ"
# Act
hashed = hash_api_key(unicode_key)
is_valid = verify_api_key(unicode_key, hashed)
# Assert
assert isinstance(hashed, str)
assert is_valid is True
except ImportError:
pytest.skip("API key service not implemented yet")
def test_timing_attack_resistance(self):
"""Test that verification takes similar time for valid/invalid keys."""
# Arrange
try:
from tradingagents.api.services.api_key_service import (
generate_api_key,
hash_api_key,
verify_api_key,
)
import time
api_key = generate_api_key()
hashed = hash_api_key(api_key)
wrong_key = generate_api_key()
# Act: Measure time for correct and incorrect verification
times_correct = []
times_incorrect = []
for _ in range(10):
start = time.perf_counter()
verify_api_key(api_key, hashed)
times_correct.append(time.perf_counter() - start)
start = time.perf_counter()
verify_api_key(wrong_key, hashed)
times_incorrect.append(time.perf_counter() - start)
# Assert: Times should be similar (within same order of magnitude)
# This is a basic check - bcrypt is inherently resistant to timing attacks
avg_correct = sum(times_correct) / len(times_correct)
avg_incorrect = sum(times_incorrect) / len(times_incorrect)
# Both should take similar time (bcrypt always does full comparison)
assert avg_correct > 0
assert avg_incorrect > 0
except ImportError:
pytest.skip("API key service not implemented yet")
def test_concurrent_key_generation(self):
"""Test generating API keys concurrently."""
# Arrange & Act
try:
from tradingagents.api.services.api_key_service import generate_api_key
import concurrent.futures
# Generate keys concurrently
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(generate_api_key) for _ in range(100)]
keys = [f.result() for f in futures]
# Assert: All keys should be unique
assert len(keys) == len(set(keys))
assert all(k.startswith("ta_") for k in keys)
except ImportError:
pytest.skip("API key service not implemented yet")