588 lines
20 KiB
Python
588 lines
20 KiB
Python
"""
|
||
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")
|