Files
openrouter-watcher/tests/unit/schemas/test_api_key_schemas.py
Luca Sacchi Ricciardi 2e4c1bb1e5 feat(schemas): T23 add Pydantic API key schemas
- Add ApiKeyCreate schema with OpenRouter key format validation
- Add ApiKeyUpdate schema for partial updates
- Add ApiKeyResponse schema (excludes key value for security)
- Add ApiKeyListResponse schema for pagination
- Export schemas from __init__.py
- 100% coverage on new module (23 tests)

Refs: T23
2026-04-07 14:28:03 +02:00

305 lines
10 KiB
Python

"""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