"""Tests for API token generation service - T15. Tests for generating and verifying API tokens with SHA-256 hashing. """ import hashlib import secrets import pytest pytestmark = [pytest.mark.unit, pytest.mark.security] class TestGenerateAPIToken: """Test suite for generate_api_token function.""" def test_generate_api_token_returns_tuple(self): """Test that generate_api_token returns a tuple.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act result = generate_api_token() # Assert assert isinstance(result, tuple) assert len(result) == 2 def test_generate_api_token_returns_plaintext_and_hash(self): """Test that generate_api_token returns (plaintext, hash).""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act plaintext, token_hash = generate_api_token() # Assert assert isinstance(plaintext, str) assert isinstance(token_hash, str) assert len(plaintext) > 0 assert len(token_hash) > 0 def test_generate_api_token_starts_with_prefix(self): """Test that plaintext token starts with 'or_api_'.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act plaintext, _ = generate_api_token() # Assert assert plaintext.startswith("or_api_") def test_generate_api_token_generates_different_tokens(self): """Test that each call generates a different token.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act plaintext1, hash1 = generate_api_token() plaintext2, hash2 = generate_api_token() # Assert assert plaintext1 != plaintext2 assert hash1 != hash2 def test_generate_api_token_hash_is_sha256(self): """Test that hash is SHA-256 of the plaintext.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act plaintext, token_hash = generate_api_token() # Calculate expected hash expected_hash = hashlib.sha256(plaintext.encode()).hexdigest() # Assert assert token_hash == expected_hash assert len(token_hash) == 64 # SHA-256 hex is 64 chars def test_generate_api_token_plaintext_sufficient_length(self): """Test that plaintext token has sufficient length.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act plaintext, _ = generate_api_token() # Assert - or_api_ prefix + 48 chars of token_urlsafe assert len(plaintext) > 50 # prefix (7) + 48 chars = at least 55 class TestHashToken: """Test suite for hash_token function.""" def test_hash_token_returns_string(self): """Test that hash_token returns a string.""" # Arrange from src.openrouter_monitor.services.token import hash_token token = "test-token" # Act result = hash_token(token) # Assert assert isinstance(result, str) assert len(result) == 64 # SHA-256 hex def test_hash_token_is_sha256(self): """Test that hash_token produces SHA-256 hash.""" # Arrange from src.openrouter_monitor.services.token import hash_token token = "or_api_test_token" # Act result = hash_token(token) # Assert expected = hashlib.sha256(token.encode()).hexdigest() assert result == expected def test_hash_token_consistent(self): """Test that hash_token produces consistent results.""" # Arrange from src.openrouter_monitor.services.token import hash_token token = "consistent-token" # Act hash1 = hash_token(token) hash2 = hash_token(token) # Assert assert hash1 == hash2 class TestVerifyAPIToken: """Test suite for verify_api_token function.""" def test_verify_api_token_valid_returns_true(self): """Test that verify_api_token returns True for valid token.""" # Arrange from src.openrouter_monitor.services.token import ( generate_api_token, verify_api_token, ) plaintext, token_hash = generate_api_token() # Act result = verify_api_token(plaintext, token_hash) # Assert assert result is True def test_verify_api_token_invalid_returns_false(self): """Test that verify_api_token returns False for invalid token.""" # Arrange from src.openrouter_monitor.services.token import ( generate_api_token, verify_api_token, ) _, token_hash = generate_api_token() wrong_plaintext = "wrong-token" # Act result = verify_api_token(wrong_plaintext, token_hash) # Assert assert result is False def test_verify_api_token_wrong_hash_returns_false(self): """Test that verify_api_token returns False with wrong hash.""" # Arrange from src.openrouter_monitor.services.token import ( generate_api_token, verify_api_token, ) plaintext, _ = generate_api_token() wrong_hash = hashlib.sha256("different".encode()).hexdigest() # Act result = verify_api_token(plaintext, wrong_hash) # Assert assert result is False def test_verify_api_token_uses_timing_safe_comparison(self): """Test that verify_api_token uses timing-safe comparison.""" # Arrange from src.openrouter_monitor.services.token import ( generate_api_token, verify_api_token, ) plaintext, token_hash = generate_api_token() # Act - Should not raise any error and work correctly result = verify_api_token(plaintext, token_hash) # Assert assert result is True def test_verify_api_token_empty_strings(self): """Test verify_api_token with empty strings.""" # Arrange from src.openrouter_monitor.services.token import verify_api_token # Act result = verify_api_token("", "") # Assert assert result is False class TestTokenFormat: """Test suite for token format validation.""" def test_token_contains_only_urlsafe_characters(self): """Test that token contains only URL-safe characters.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act plaintext, _ = generate_api_token() token_part = plaintext.replace("or_api_", "") # Assert - URL-safe base64 chars: A-Z, a-z, 0-9, -, _ urlsafe_chars = set( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" ) assert all(c in urlsafe_chars for c in token_part) def test_hash_is_hexadecimal(self): """Test that hash is valid hexadecimal.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act _, token_hash = generate_api_token() # Assert try: int(token_hash, 16) except ValueError: pytest.fail("Hash is not valid hexadecimal") def test_generated_tokens_are_unique(self): """Test that generating many tokens produces unique values.""" # Arrange from src.openrouter_monitor.services.token import generate_api_token # Act tokens = [generate_api_token()[0] for _ in range(100)] # Assert assert len(set(tokens)) == 100 # All unique class TestTokenTypeValidation: """Test suite for type validation in token functions.""" def test_hash_token_non_string_raises_type_error(self): """Test that hash_token with non-string raises TypeError.""" # Arrange from src.openrouter_monitor.services.token import hash_token # Act & Assert with pytest.raises(TypeError, match="plaintext must be a string"): hash_token(12345) def test_verify_api_token_non_string_plaintext_raises_type_error(self): """Test that verify_api_token with non-string plaintext raises TypeError.""" # Arrange from src.openrouter_monitor.services.token import verify_api_token # Act & Assert with pytest.raises(TypeError, match="plaintext must be a string"): verify_api_token(12345, "valid_hash") def test_verify_api_token_non_string_hash_raises_type_error(self): """Test that verify_api_token with non-string hash raises TypeError.""" # Arrange from src.openrouter_monitor.services.token import verify_api_token # Act & Assert with pytest.raises(TypeError, match="token_hash must be a string"): verify_api_token("valid_plaintext", 12345)