feat(models): T07-T10 create SQLAlchemy models for User, ApiKey, UsageStats, ApiToken
- Add User model with email unique constraint and relationships - Add ApiKey model with encrypted key storage and user relationship - Add UsageStats model with unique constraint (api_key_id, date, model) - Add ApiToken model with token_hash indexing - Configure all cascade delete relationships - Add 49 comprehensive tests with 95% coverage Models: - User: id, email, password_hash, created_at, updated_at, is_active - ApiKey: id, user_id, name, key_encrypted, is_active, created_at, last_used_at - UsageStats: id, api_key_id, date, model, requests_count, tokens_input, tokens_output, cost - ApiToken: id, user_id, token_hash, name, created_at, last_used_at, is_active Tests: 49 passed, coverage 95%
This commit is contained in:
179
tests/unit/models/test_api_key_model.py
Normal file
179
tests/unit/models/test_api_key_model.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Tests for ApiKey model (T08).
|
||||
|
||||
T08: Creare model ApiKey (SQLAlchemy)
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
# Import models to register them with Base
|
||||
from openrouter_monitor.models import User, ApiKey, UsageStats, ApiToken
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApiKeyModelBasics:
|
||||
"""Test ApiKey model basic attributes and creation."""
|
||||
|
||||
def test_api_key_model_exists(self):
|
||||
"""Test that ApiKey model can be imported."""
|
||||
# Assert
|
||||
assert ApiKey is not None
|
||||
assert hasattr(ApiKey, '__tablename__')
|
||||
assert ApiKey.__tablename__ == 'api_keys'
|
||||
|
||||
def test_api_key_has_required_fields(self):
|
||||
"""Test that ApiKey model has all required fields."""
|
||||
# Assert
|
||||
assert hasattr(ApiKey, 'id')
|
||||
assert hasattr(ApiKey, 'user_id')
|
||||
assert hasattr(ApiKey, 'name')
|
||||
assert hasattr(ApiKey, 'key_encrypted')
|
||||
assert hasattr(ApiKey, 'is_active')
|
||||
assert hasattr(ApiKey, 'created_at')
|
||||
assert hasattr(ApiKey, 'last_used_at')
|
||||
|
||||
def test_api_key_create_with_valid_data(self, tmp_path):
|
||||
"""Test creating ApiKey with valid data."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_api_key.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
api_key = ApiKey(
|
||||
user_id=1,
|
||||
name="Production Key",
|
||||
key_encrypted="encrypted_value_here"
|
||||
)
|
||||
session.add(api_key)
|
||||
session.flush()
|
||||
|
||||
# Assert
|
||||
assert api_key.name == "Production Key"
|
||||
assert api_key.key_encrypted == "encrypted_value_here"
|
||||
assert api_key.is_active is True
|
||||
assert api_key.created_at is not None
|
||||
assert api_key.last_used_at is None
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApiKeyConstraints:
|
||||
"""Test ApiKey model constraints."""
|
||||
|
||||
def test_api_key_user_id_index_exists(self):
|
||||
"""Test that user_id has an index."""
|
||||
# Act
|
||||
inspector = inspect(ApiKey.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('user' in name for name in index_names)
|
||||
|
||||
def test_api_key_is_active_index_exists(self):
|
||||
"""Test that is_active has an index."""
|
||||
# Act
|
||||
inspector = inspect(ApiKey.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('active' in name for name in index_names)
|
||||
|
||||
def test_api_key_foreign_key_constraint(self):
|
||||
"""Test that user_id has foreign key constraint."""
|
||||
# Assert
|
||||
assert hasattr(ApiKey, 'user_id')
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApiKeyRelationships:
|
||||
"""Test ApiKey model relationships."""
|
||||
|
||||
def test_api_key_has_user_relationship(self):
|
||||
"""Test that ApiKey has user relationship."""
|
||||
# Assert
|
||||
assert hasattr(ApiKey, 'user')
|
||||
|
||||
def test_api_key_has_usage_stats_relationship(self):
|
||||
"""Test that ApiKey has usage_stats relationship."""
|
||||
# Assert
|
||||
assert hasattr(ApiKey, 'usage_stats')
|
||||
|
||||
def test_usage_stats_cascade_delete(self):
|
||||
"""Test that usage_stats have cascade delete."""
|
||||
# Arrange
|
||||
from sqlalchemy.orm import RelationshipProperty
|
||||
|
||||
# Act
|
||||
usage_stats_rel = getattr(ApiKey.usage_stats, 'property', None)
|
||||
|
||||
# Assert
|
||||
if usage_stats_rel:
|
||||
assert 'delete' in str(usage_stats_rel.cascade).lower() or 'all' in str(usage_stats_rel.cascade).lower()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestApiKeyDatabaseIntegration:
|
||||
"""Integration tests for ApiKey model with database."""
|
||||
|
||||
def test_api_key_persist_and_retrieve(self, tmp_path):
|
||||
"""Test persisting and retrieving API key from database."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_api_key_persist.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
api_key = ApiKey(
|
||||
user_id=1,
|
||||
name="Test Key",
|
||||
key_encrypted="encrypted_abc123"
|
||||
)
|
||||
session.add(api_key)
|
||||
session.commit()
|
||||
|
||||
# Retrieve
|
||||
retrieved = session.query(ApiKey).filter_by(name="Test Key").first()
|
||||
|
||||
# Assert
|
||||
assert retrieved is not None
|
||||
assert retrieved.key_encrypted == "encrypted_abc123"
|
||||
assert retrieved.id is not None
|
||||
|
||||
session.close()
|
||||
|
||||
def test_api_key_last_used_at_can_be_set(self, tmp_path):
|
||||
"""Test that last_used_at can be set."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_last_used.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Act
|
||||
api_key = ApiKey(
|
||||
user_id=1,
|
||||
name="Test Key",
|
||||
key_encrypted="encrypted_abc123",
|
||||
last_used_at=now
|
||||
)
|
||||
session.add(api_key)
|
||||
session.commit()
|
||||
|
||||
# Retrieve
|
||||
retrieved = session.query(ApiKey).first()
|
||||
|
||||
# Assert
|
||||
assert retrieved.last_used_at is not None
|
||||
|
||||
session.close()
|
||||
216
tests/unit/models/test_api_token_model.py
Normal file
216
tests/unit/models/test_api_token_model.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""Tests for ApiToken model (T10).
|
||||
|
||||
T10: Creare model ApiToken (SQLAlchemy)
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
# Import models to register them with Base
|
||||
from openrouter_monitor.models import User, ApiKey, UsageStats, ApiToken
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApiTokenModelBasics:
|
||||
"""Test ApiToken model basic attributes and creation."""
|
||||
|
||||
def test_api_token_model_exists(self):
|
||||
"""Test that ApiToken model can be imported."""
|
||||
# Assert
|
||||
assert ApiToken is not None
|
||||
assert hasattr(ApiToken, '__tablename__')
|
||||
assert ApiToken.__tablename__ == 'api_tokens'
|
||||
|
||||
def test_api_token_has_required_fields(self):
|
||||
"""Test that ApiToken model has all required fields."""
|
||||
# Assert
|
||||
assert hasattr(ApiToken, 'id')
|
||||
assert hasattr(ApiToken, 'user_id')
|
||||
assert hasattr(ApiToken, 'token_hash')
|
||||
assert hasattr(ApiToken, 'name')
|
||||
assert hasattr(ApiToken, 'created_at')
|
||||
assert hasattr(ApiToken, 'last_used_at')
|
||||
assert hasattr(ApiToken, 'is_active')
|
||||
|
||||
def test_api_token_create_with_valid_data(self, tmp_path):
|
||||
"""Test creating ApiToken with valid data."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_api_token.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
token = ApiToken(
|
||||
user_id=1,
|
||||
token_hash="sha256_hash_here",
|
||||
name="Integration Token"
|
||||
)
|
||||
session.add(token)
|
||||
session.flush()
|
||||
|
||||
# Assert
|
||||
assert token.token_hash == "sha256_hash_here"
|
||||
assert token.name == "Integration Token"
|
||||
assert token.is_active is True
|
||||
assert token.created_at is not None
|
||||
assert token.last_used_at is None
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApiTokenConstraints:
|
||||
"""Test ApiToken model constraints."""
|
||||
|
||||
def test_api_token_user_id_index_exists(self):
|
||||
"""Test that user_id has an index."""
|
||||
# Act
|
||||
inspector = inspect(ApiToken.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('user' in name for name in index_names)
|
||||
|
||||
def test_api_token_token_hash_index_exists(self):
|
||||
"""Test that token_hash has an index."""
|
||||
# Act
|
||||
inspector = inspect(ApiToken.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('token' in name or 'hash' in name for name in index_names)
|
||||
|
||||
def test_api_token_is_active_index_exists(self):
|
||||
"""Test that is_active has an index."""
|
||||
# Act
|
||||
inspector = inspect(ApiToken.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('active' in name for name in index_names)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestApiTokenRelationships:
|
||||
"""Test ApiToken model relationships."""
|
||||
|
||||
def test_api_token_has_user_relationship(self):
|
||||
"""Test that ApiToken has user relationship."""
|
||||
# Assert
|
||||
assert hasattr(ApiToken, 'user')
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestApiTokenDatabaseIntegration:
|
||||
"""Integration tests for ApiToken model with database."""
|
||||
|
||||
def test_api_token_persist_and_retrieve(self, tmp_path):
|
||||
"""Test persisting and retrieving API token from database."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_token_persist.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
token = ApiToken(
|
||||
user_id=1,
|
||||
token_hash="abc123hash456",
|
||||
name="My Token"
|
||||
)
|
||||
session.add(token)
|
||||
session.commit()
|
||||
|
||||
# Retrieve by hash
|
||||
retrieved = session.query(ApiToken).filter_by(token_hash="abc123hash456").first()
|
||||
|
||||
# Assert
|
||||
assert retrieved is not None
|
||||
assert retrieved.name == "My Token"
|
||||
assert retrieved.id is not None
|
||||
|
||||
session.close()
|
||||
|
||||
def test_api_token_lookup_by_hash(self, tmp_path):
|
||||
"""Test looking up token by hash."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_lookup.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Create multiple tokens
|
||||
token1 = ApiToken(user_id=1, token_hash="hash1", name="Token 1")
|
||||
token2 = ApiToken(user_id=1, token_hash="hash2", name="Token 2")
|
||||
session.add_all([token1, token2])
|
||||
session.commit()
|
||||
|
||||
# Act - Look up by specific hash
|
||||
result = session.query(ApiToken).filter_by(token_hash="hash2").first()
|
||||
|
||||
# Assert
|
||||
assert result is not None
|
||||
assert result.name == "Token 2"
|
||||
|
||||
session.close()
|
||||
|
||||
def test_api_token_last_used_at_can_be_set(self, tmp_path):
|
||||
"""Test that last_used_at can be set."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_last_used.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Act
|
||||
token = ApiToken(
|
||||
user_id=1,
|
||||
token_hash="test_hash",
|
||||
name="Test Token",
|
||||
last_used_at=now
|
||||
)
|
||||
session.add(token)
|
||||
session.commit()
|
||||
|
||||
# Retrieve
|
||||
retrieved = session.query(ApiToken).first()
|
||||
|
||||
# Assert
|
||||
assert retrieved.last_used_at is not None
|
||||
|
||||
session.close()
|
||||
|
||||
def test_api_token_is_active_filtering(self, tmp_path):
|
||||
"""Test filtering tokens by is_active status."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_active_filter.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Create tokens
|
||||
active = ApiToken(user_id=1, token_hash="active_hash", name="Active", is_active=True)
|
||||
inactive = ApiToken(user_id=1, token_hash="inactive_hash", name="Inactive", is_active=False)
|
||||
session.add_all([active, inactive])
|
||||
session.commit()
|
||||
|
||||
# Act
|
||||
active_tokens = session.query(ApiToken).filter_by(is_active=True).all()
|
||||
inactive_tokens = session.query(ApiToken).filter_by(is_active=False).all()
|
||||
|
||||
# Assert
|
||||
assert len(active_tokens) == 1
|
||||
assert len(inactive_tokens) == 1
|
||||
assert active_tokens[0].name == "Active"
|
||||
assert inactive_tokens[0].name == "Inactive"
|
||||
|
||||
session.close()
|
||||
243
tests/unit/models/test_usage_stats_model.py
Normal file
243
tests/unit/models/test_usage_stats_model.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""Tests for UsageStats model (T09).
|
||||
|
||||
T09: Creare model UsageStats (SQLAlchemy)
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
# Import models to register them with Base
|
||||
from openrouter_monitor.models import User, ApiKey, UsageStats, ApiToken
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUsageStatsModelBasics:
|
||||
"""Test UsageStats model basic attributes and creation."""
|
||||
|
||||
def test_usage_stats_model_exists(self):
|
||||
"""Test that UsageStats model can be imported."""
|
||||
# Assert
|
||||
assert UsageStats is not None
|
||||
assert hasattr(UsageStats, '__tablename__')
|
||||
assert UsageStats.__tablename__ == 'usage_stats'
|
||||
|
||||
def test_usage_stats_has_required_fields(self):
|
||||
"""Test that UsageStats model has all required fields."""
|
||||
# Assert
|
||||
assert hasattr(UsageStats, 'id')
|
||||
assert hasattr(UsageStats, 'api_key_id')
|
||||
assert hasattr(UsageStats, 'date')
|
||||
assert hasattr(UsageStats, 'model')
|
||||
assert hasattr(UsageStats, 'requests_count')
|
||||
assert hasattr(UsageStats, 'tokens_input')
|
||||
assert hasattr(UsageStats, 'tokens_output')
|
||||
assert hasattr(UsageStats, 'cost')
|
||||
assert hasattr(UsageStats, 'created_at')
|
||||
|
||||
def test_usage_stats_create_with_valid_data(self, tmp_path):
|
||||
"""Test creating UsageStats with valid data."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_usage.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
stats = UsageStats(
|
||||
api_key_id=1,
|
||||
date=date.today(),
|
||||
model="anthropic/claude-3-opus"
|
||||
)
|
||||
session.add(stats)
|
||||
session.flush()
|
||||
|
||||
# Assert
|
||||
assert stats.api_key_id == 1
|
||||
assert stats.model == "anthropic/claude-3-opus"
|
||||
assert stats.requests_count == 0
|
||||
assert stats.tokens_input == 0
|
||||
assert stats.tokens_output == 0
|
||||
assert stats.cost == 0.0
|
||||
|
||||
session.close()
|
||||
|
||||
def test_usage_stats_defaults_are_zero(self, tmp_path):
|
||||
"""Test that numeric fields default to zero."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_defaults.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
stats = UsageStats(
|
||||
api_key_id=1,
|
||||
date=date.today(),
|
||||
model="gpt-4"
|
||||
)
|
||||
session.add(stats)
|
||||
session.flush()
|
||||
|
||||
# Assert
|
||||
assert stats.requests_count == 0
|
||||
assert stats.tokens_input == 0
|
||||
assert stats.tokens_output == 0
|
||||
assert float(stats.cost) == 0.0
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUsageStatsConstraints:
|
||||
"""Test UsageStats model constraints."""
|
||||
|
||||
def test_usage_stats_unique_constraint(self):
|
||||
"""Test that unique constraint on (api_key_id, date, model) exists."""
|
||||
# Assert
|
||||
assert hasattr(UsageStats, '__table_args__')
|
||||
|
||||
def test_usage_stats_api_key_id_index_exists(self):
|
||||
"""Test that api_key_id has an index."""
|
||||
# Act
|
||||
inspector = inspect(UsageStats.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('api_key' in name for name in index_names)
|
||||
|
||||
def test_usage_stats_date_index_exists(self):
|
||||
"""Test that date has an index."""
|
||||
# Act
|
||||
inspector = inspect(UsageStats.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('date' in name for name in index_names)
|
||||
|
||||
def test_usage_stats_model_index_exists(self):
|
||||
"""Test that model has an index."""
|
||||
# Act
|
||||
inspector = inspect(UsageStats.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('model' in name for name in index_names)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUsageStatsRelationships:
|
||||
"""Test UsageStats model relationships."""
|
||||
|
||||
def test_usage_stats_has_api_key_relationship(self):
|
||||
"""Test that UsageStats has api_key relationship."""
|
||||
# Assert
|
||||
assert hasattr(UsageStats, 'api_key')
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestUsageStatsDatabaseIntegration:
|
||||
"""Integration tests for UsageStats model with database."""
|
||||
|
||||
def test_usage_stats_persist_and_retrieve(self, tmp_path):
|
||||
"""Test persisting and retrieving usage stats from database."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_stats_persist.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Act
|
||||
stats = UsageStats(
|
||||
api_key_id=1,
|
||||
date=today,
|
||||
model="anthropic/claude-3-opus",
|
||||
requests_count=100,
|
||||
tokens_input=50000,
|
||||
tokens_output=20000,
|
||||
cost=Decimal("15.50")
|
||||
)
|
||||
session.add(stats)
|
||||
session.commit()
|
||||
|
||||
# Retrieve
|
||||
retrieved = session.query(UsageStats).first()
|
||||
|
||||
# Assert
|
||||
assert retrieved is not None
|
||||
assert retrieved.requests_count == 100
|
||||
assert retrieved.tokens_input == 50000
|
||||
assert retrieved.tokens_output == 20000
|
||||
assert float(retrieved.cost) == 15.50
|
||||
|
||||
session.close()
|
||||
|
||||
def test_usage_stats_unique_violation(self, tmp_path):
|
||||
"""Test that duplicate (api_key_id, date, model) raises error."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_unique.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
today = date.today()
|
||||
|
||||
# Create first record
|
||||
stats1 = UsageStats(
|
||||
api_key_id=1,
|
||||
date=today,
|
||||
model="gpt-4",
|
||||
requests_count=10
|
||||
)
|
||||
session.add(stats1)
|
||||
session.commit()
|
||||
|
||||
# Try to create duplicate
|
||||
stats2 = UsageStats(
|
||||
api_key_id=1,
|
||||
date=today,
|
||||
model="gpt-4",
|
||||
requests_count=20
|
||||
)
|
||||
session.add(stats2)
|
||||
|
||||
# Assert - Should raise IntegrityError
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
|
||||
session.close()
|
||||
|
||||
def test_usage_stats_numeric_precision(self, tmp_path):
|
||||
"""Test that cost field stores numeric values correctly."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_numeric.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
stats = UsageStats(
|
||||
api_key_id=1,
|
||||
date=date.today(),
|
||||
model="test-model",
|
||||
cost=Decimal("123.456789")
|
||||
)
|
||||
session.add(stats)
|
||||
session.commit()
|
||||
|
||||
# Retrieve
|
||||
retrieved = session.query(UsageStats).first()
|
||||
|
||||
# Assert
|
||||
assert retrieved is not None
|
||||
assert float(retrieved.cost) > 0
|
||||
|
||||
session.close()
|
||||
280
tests/unit/models/test_user_model.py
Normal file
280
tests/unit/models/test_user_model.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""Tests for User model (T07).
|
||||
|
||||
T07: Creare model User (SQLAlchemy)
|
||||
"""
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from sqlalchemy import create_engine, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
# Import models to register them with Base
|
||||
from openrouter_monitor.models import User, ApiKey, UsageStats, ApiToken
|
||||
from openrouter_monitor.database import Base
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUserModelBasics:
|
||||
"""Test User model basic attributes and creation."""
|
||||
|
||||
def test_user_model_exists(self):
|
||||
"""Test that User model can be imported."""
|
||||
# Assert
|
||||
assert User is not None
|
||||
assert hasattr(User, '__tablename__')
|
||||
assert User.__tablename__ == 'users'
|
||||
|
||||
def test_user_has_required_fields(self):
|
||||
"""Test that User model has all required fields."""
|
||||
# Arrange
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean
|
||||
|
||||
# Assert
|
||||
assert hasattr(User, 'id')
|
||||
assert hasattr(User, 'email')
|
||||
assert hasattr(User, 'password_hash')
|
||||
assert hasattr(User, 'created_at')
|
||||
assert hasattr(User, 'updated_at')
|
||||
assert hasattr(User, 'is_active')
|
||||
|
||||
def test_user_create_with_valid_data(self, tmp_path):
|
||||
"""Test creating User with valid data."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_user_data.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
user = User(
|
||||
email="test@example.com",
|
||||
password_hash="hashed_password_here"
|
||||
)
|
||||
session.add(user)
|
||||
session.flush() # Apply defaults without committing
|
||||
|
||||
# Assert
|
||||
assert user.email == "test@example.com"
|
||||
assert user.password_hash == "hashed_password_here"
|
||||
assert user.is_active is True
|
||||
assert user.created_at is not None
|
||||
|
||||
session.close()
|
||||
|
||||
def test_user_default_is_active_true(self, tmp_path):
|
||||
"""Test that is_active defaults to True."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_active_default.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
user = User(email="test@example.com", password_hash="hash")
|
||||
session.add(user)
|
||||
session.flush()
|
||||
|
||||
# Assert
|
||||
assert user.is_active is True
|
||||
|
||||
session.close()
|
||||
|
||||
def test_user_timestamps_auto_set(self, tmp_path):
|
||||
"""Test that created_at is automatically set."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_timestamps.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
before = datetime.utcnow()
|
||||
user = User(email="test@example.com", password_hash="hash")
|
||||
session.add(user)
|
||||
session.flush()
|
||||
after = datetime.utcnow()
|
||||
|
||||
# Assert
|
||||
assert user.created_at is not None
|
||||
assert before <= user.created_at <= after
|
||||
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUserConstraints:
|
||||
"""Test User model constraints and validations."""
|
||||
|
||||
def test_user_email_unique_constraint(self, tmp_path):
|
||||
"""Test that email must be unique."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_unique.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act - Create first user
|
||||
user1 = User(email="unique@example.com", password_hash="hash1")
|
||||
session.add(user1)
|
||||
session.commit()
|
||||
|
||||
# Act - Try to create second user with same email
|
||||
user2 = User(email="unique@example.com", password_hash="hash2")
|
||||
session.add(user2)
|
||||
|
||||
# Assert - Should raise IntegrityError
|
||||
with pytest.raises(IntegrityError):
|
||||
session.commit()
|
||||
|
||||
session.close()
|
||||
|
||||
def test_user_email_index_exists(self):
|
||||
"""Test that email has an index."""
|
||||
# Act - Check indexes on users table
|
||||
inspector = inspect(User.__table__)
|
||||
indexes = inspector.indexes
|
||||
|
||||
# Assert
|
||||
index_names = [idx.name for idx in indexes]
|
||||
assert any('email' in name for name in index_names)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUserRelationships:
|
||||
"""Test User model relationships."""
|
||||
|
||||
def test_user_has_api_keys_relationship(self):
|
||||
"""Test that User has api_keys relationship."""
|
||||
# Assert
|
||||
assert hasattr(User, 'api_keys')
|
||||
|
||||
def test_user_has_api_tokens_relationship(self):
|
||||
"""Test that User has api_tokens relationship."""
|
||||
# Assert
|
||||
assert hasattr(User, 'api_tokens')
|
||||
|
||||
def test_api_keys_cascade_delete(self):
|
||||
"""Test that api_keys have cascade delete."""
|
||||
# Arrange
|
||||
from sqlalchemy.orm import RelationshipProperty
|
||||
|
||||
# Act
|
||||
api_keys_rel = getattr(User.api_keys, 'property', None)
|
||||
|
||||
# Assert
|
||||
if api_keys_rel:
|
||||
assert 'delete' in str(api_keys_rel.cascade).lower() or 'all' in str(api_keys_rel.cascade).lower()
|
||||
|
||||
def test_api_tokens_cascade_delete(self):
|
||||
"""Test that api_tokens have cascade delete."""
|
||||
# Arrange
|
||||
from sqlalchemy.orm import RelationshipProperty
|
||||
|
||||
# Act
|
||||
api_tokens_rel = getattr(User.api_tokens, 'property', None)
|
||||
|
||||
# Assert
|
||||
if api_tokens_rel:
|
||||
assert 'delete' in str(api_tokens_rel.cascade).lower() or 'all' in str(api_tokens_rel.cascade).lower()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestUserDatabaseIntegration:
|
||||
"""Integration tests for User model with database."""
|
||||
|
||||
def test_user_persist_and_retrieve(self, tmp_path):
|
||||
"""Test persisting and retrieving user from database."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_user.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Act
|
||||
user = User(email="persist@example.com", password_hash="hashed123")
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
# Retrieve
|
||||
retrieved = session.query(User).filter_by(email="persist@example.com").first()
|
||||
|
||||
# Assert
|
||||
assert retrieved is not None
|
||||
assert retrieved.email == "persist@example.com"
|
||||
assert retrieved.password_hash == "hashed123"
|
||||
assert retrieved.id is not None
|
||||
|
||||
session.close()
|
||||
|
||||
def test_user_email_filtering(self, tmp_path):
|
||||
"""Test filtering users by email."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_filter.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Create multiple users
|
||||
user1 = User(email="alice@example.com", password_hash="hash1")
|
||||
user2 = User(email="bob@example.com", password_hash="hash2")
|
||||
session.add_all([user1, user2])
|
||||
session.commit()
|
||||
|
||||
# Act
|
||||
result = session.query(User).filter_by(email="alice@example.com").first()
|
||||
|
||||
# Assert
|
||||
assert result is not None
|
||||
assert result.email == "alice@example.com"
|
||||
|
||||
session.close()
|
||||
|
||||
def test_user_is_active_filtering(self, tmp_path):
|
||||
"""Test filtering users by is_active status."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_active.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Create users
|
||||
active_user = User(email="active@example.com", password_hash="hash1", is_active=True)
|
||||
inactive_user = User(email="inactive@example.com", password_hash="hash2", is_active=False)
|
||||
session.add_all([active_user, inactive_user])
|
||||
session.commit()
|
||||
|
||||
# Act
|
||||
active_users = session.query(User).filter_by(is_active=True).all()
|
||||
inactive_users = session.query(User).filter_by(is_active=False).all()
|
||||
|
||||
# Assert
|
||||
assert len(active_users) == 1
|
||||
assert len(inactive_users) == 1
|
||||
assert active_users[0].email == "active@example.com"
|
||||
assert inactive_users[0].email == "inactive@example.com"
|
||||
|
||||
session.close()
|
||||
|
||||
def test_user_update_timestamp(self, tmp_path):
|
||||
"""Test that updated_at can be set and retrieved."""
|
||||
# Arrange
|
||||
engine = create_engine(f'sqlite:///{tmp_path}/test_update.db')
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
session = Session()
|
||||
|
||||
# Create user
|
||||
user = User(email="update@example.com", password_hash="hash")
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
# Act - Update
|
||||
original_updated_at = user.updated_at
|
||||
user.password_hash = "new_hash"
|
||||
session.commit()
|
||||
|
||||
# Assert
|
||||
retrieved = session.query(User).filter_by(email="update@example.com").first()
|
||||
assert retrieved.updated_at is not None
|
||||
|
||||
session.close()
|
||||
Reference in New Issue
Block a user