"""Tests for API Key Pydantic schemas. T23: Test Pydantic schemas for API key management. """ import pytest from datetime import datetime, timezone from pydantic import ValidationError class TestApiKeyCreate: """Tests for ApiKeyCreate schema.""" def test_valid_api_key_create(self): """Test valid API key creation with OpenRouter format.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate data = ApiKeyCreate( name="My Production Key", key="sk-or-v1-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz" ) assert data.name == "My Production Key" assert data.key == "sk-or-v1-abc123def456ghi789jkl012mno345pqr678stu901vwx234yz" def test_name_min_length(self): """Test that name must be at least 1 character.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate with pytest.raises(ValidationError, match="name"): ApiKeyCreate( name="", key="sk-or-v1-abc123" ) def test_name_max_length(self): """Test that name cannot exceed 100 characters.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate with pytest.raises(ValidationError, match="name"): ApiKeyCreate( name="x" * 101, key="sk-or-v1-abc123" ) def test_name_exactly_max_length(self): """Test that name can be exactly 100 characters.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate name = "x" * 100 data = ApiKeyCreate( name=name, key="sk-or-v1-abc123" ) assert data.name == name assert len(data.name) == 100 def test_valid_openrouter_key_format(self): """Test valid OpenRouter API key format (sk-or-v1- prefix).""" from openrouter_monitor.schemas.api_key import ApiKeyCreate # Various valid OpenRouter key formats valid_keys = [ "sk-or-v1-abc123", "sk-or-v1-abc123def456", "sk-or-v1-" + "x" * 100, ] for key in valid_keys: data = ApiKeyCreate(name="Test", key=key) assert data.key == key def test_invalid_key_format_missing_prefix(self): """Test that key without OpenRouter prefix raises ValidationError.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate with pytest.raises(ValidationError, match="key"): ApiKeyCreate( name="Test Key", key="invalid-key-format" ) def test_invalid_key_format_wrong_prefix(self): """Test that key with wrong prefix raises ValidationError.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate with pytest.raises(ValidationError, match="key"): ApiKeyCreate( name="Test Key", key="sk-abc123" # Missing -or-v1- ) def test_empty_key(self): """Test that empty key raises ValidationError.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate with pytest.raises(ValidationError, match="key"): ApiKeyCreate( name="Test Key", key="" ) def test_whitespace_only_key(self): """Test that whitespace-only key raises ValidationError.""" from openrouter_monitor.schemas.api_key import ApiKeyCreate with pytest.raises(ValidationError, match="key"): ApiKeyCreate( name="Test Key", key=" " ) class TestApiKeyUpdate: """Tests for ApiKeyUpdate schema.""" def test_valid_update_name_only(self): """Test valid update with name only.""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate data = ApiKeyUpdate(name="Updated Name") assert data.name == "Updated Name" assert data.is_active is None def test_valid_update_is_active_only(self): """Test valid update with is_active only.""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate data = ApiKeyUpdate(is_active=False) assert data.name is None assert data.is_active is False def test_valid_update_both_fields(self): """Test valid update with both fields.""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate data = ApiKeyUpdate(name="New Name", is_active=True) assert data.name == "New Name" assert data.is_active is True def test_empty_update_allowed(self): """Test that empty update is allowed (no fields provided).""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate data = ApiKeyUpdate() assert data.name is None assert data.is_active is None def test_update_name_too_long(self): """Test that name longer than 100 chars raises ValidationError.""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate with pytest.raises(ValidationError, match="name"): ApiKeyUpdate(name="x" * 101) def test_update_name_min_length(self): """Test that empty name raises ValidationError.""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate with pytest.raises(ValidationError, match="name"): ApiKeyUpdate(name="") def test_update_name_valid_length(self): """Test that valid name length is accepted.""" from openrouter_monitor.schemas.api_key import ApiKeyUpdate data = ApiKeyUpdate(name="Valid Name") assert data.name == "Valid Name" class TestApiKeyResponse: """Tests for ApiKeyResponse schema.""" def test_valid_response(self): """Test valid API key response.""" from openrouter_monitor.schemas.api_key import ApiKeyResponse created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) last_used_at = datetime(2024, 1, 2, 15, 30, 0, tzinfo=timezone.utc) data = ApiKeyResponse( id=1, name="Production Key", is_active=True, created_at=created_at, last_used_at=last_used_at ) assert data.id == 1 assert data.name == "Production Key" assert data.is_active is True assert data.created_at == created_at assert data.last_used_at == last_used_at def test_response_optional_last_used_at(self): """Test that last_used_at is optional (key never used).""" from openrouter_monitor.schemas.api_key import ApiKeyResponse data = ApiKeyResponse( id=1, name="New Key", is_active=True, created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc), last_used_at=None ) assert data.last_used_at is None def test_response_from_orm(self): """Test that ApiKeyResponse can be created from ORM model.""" from openrouter_monitor.schemas.api_key import ApiKeyResponse # Mock ORM object class MockApiKey: id = 1 name = "Test Key" is_active = True created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) last_used_at = None key = ApiKeyResponse.model_validate(MockApiKey()) assert key.id == 1 assert key.name == "Test Key" assert key.is_active is True def test_response_no_key_field(self): """Test that API key value is NOT included in response.""" from openrouter_monitor.schemas.api_key import ApiKeyResponse # Verify that 'key' field doesn't exist in the model fields = ApiKeyResponse.model_fields.keys() assert 'key' not in fields assert 'key_encrypted' not in fields class TestApiKeyListResponse: """Tests for ApiKeyListResponse schema.""" def test_valid_list_response(self): """Test valid list response with multiple keys.""" from openrouter_monitor.schemas.api_key import ApiKeyListResponse, ApiKeyResponse created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) items = [ ApiKeyResponse( id=1, name="Key 1", is_active=True, created_at=created_at, last_used_at=None ), ApiKeyResponse( id=2, name="Key 2", is_active=False, created_at=created_at, last_used_at=created_at ) ] data = ApiKeyListResponse(items=items, total=2) assert len(data.items) == 2 assert data.total == 2 assert data.items[0].name == "Key 1" assert data.items[1].name == "Key 2" def test_empty_list_response(self): """Test valid list response with no keys.""" from openrouter_monitor.schemas.api_key import ApiKeyListResponse data = ApiKeyListResponse(items=[], total=0) assert data.items == [] assert data.total == 0 def test_pagination_response(self): """Test list response simulating pagination.""" from openrouter_monitor.schemas.api_key import ApiKeyListResponse, ApiKeyResponse created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) # Simulate page 1 of 2, 10 items per page items = [ ApiKeyResponse( id=i, name=f"Key {i}", is_active=True, created_at=created_at, last_used_at=None ) for i in range(1, 11) ] data = ApiKeyListResponse(items=items, total=25) # 25 total, showing first 10 assert len(data.items) == 10 assert data.total == 25