- 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
305 lines
10 KiB
Python
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
|