diff --git a/export/progress.md b/export/progress.md index 5a0019d..a0f6d74 100644 --- a/export/progress.md +++ b/export/progress.md @@ -44,9 +44,9 @@ - [x] T04: Setup file configurazione (.env, config.py) (2024-04-07) - [x] T05: Configurare pytest e struttura test (2024-04-07) -### ๐Ÿ—„๏ธ Database & Models (T06-T11) - 0/6 completati -- [ ] T06: Creare database.py (connection & session) -- [ ] T07: Creare model User (SQLAlchemy) +### ๐Ÿ—„๏ธ Database & Models (T06-T11) - 1/6 completati +- [x] T06: Creare database.py (connection & session) - โœ… Completato (2026-04-07 11:00) +- [ ] T07: Creare model User (SQLAlchemy) - ๐ŸŸก In progress - [ ] T08: Creare model ApiKey (SQLAlchemy) - [ ] T09: Creare model UsageStats (SQLAlchemy) - [ ] T10: Creare model ApiToken (SQLAlchemy) diff --git a/prompt/prompt-database-models.md b/prompt/prompt-database-models.md new file mode 100644 index 0000000..4832e1f --- /dev/null +++ b/prompt/prompt-database-models.md @@ -0,0 +1,398 @@ +# Prompt: Database & Models Implementation (T06-T11) + +## ๐ŸŽฏ OBIETTIVO + +Implementare la fase **Database & Models** del progetto OpenRouter API Key Monitor seguendo rigorosamente TDD (Test-Driven Development). + +**Task da completare:** T06, T07, T08, T09, T10, T11 + +--- + +## ๐Ÿ“‹ CONTESTO + +- **Repository:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher` +- **Specifiche:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/architecture.md` +- **Kanban:** `/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/kanban.md` +- **Stato Attuale:** Setup completato (T01-T05), 59 test passanti + +--- + +## ๐Ÿ—„๏ธ SCHEMA DATABASE (Da architecture.md) + +### Tabelle + +#### 1. users +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + CONSTRAINT chk_email_format CHECK (email LIKE '%_@__%.__%') +); +``` + +#### 2. api_keys +```sql +CREATE TABLE api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name VARCHAR(100) NOT NULL, + key_encrypted TEXT NOT NULL, -- AES-256-GCM encrypted + is_active BOOLEAN DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +``` + +#### 3. usage_stats +```sql +CREATE TABLE usage_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + api_key_id INTEGER NOT NULL, + date DATE NOT NULL, + model VARCHAR(100) NOT NULL, + requests_count INTEGER DEFAULT 0, + tokens_input INTEGER DEFAULT 0, + tokens_output INTEGER DEFAULT 0, + cost DECIMAL(10, 6) DEFAULT 0.0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON DELETE CASCADE, + CONSTRAINT uniq_key_date_model UNIQUE (api_key_id, date, model) +); +``` + +#### 4. api_tokens +```sql +CREATE TABLE api_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + token_hash VARCHAR(255) NOT NULL, + name VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP, + is_active BOOLEAN DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +``` + +--- + +## ๐Ÿ”ง TASK DETTAGLIATI + +### T06: Creare database.py (connection & session) + +**Requisiti:** +- Creare `src/openrouter_monitor/database.py` +- Implementare SQLAlchemy engine con SQLite +- Configurare session maker con expire_on_commit=False +- Implementare funzione `get_db()` per dependency injection FastAPI +- Implementare `init_db()` per creazione tabelle +- Usare `check_same_thread=False` per SQLite + +**Test richiesti:** +- Test connessione database +- Test creazione engine +- Test session creation +- Test init_db crea tabelle + +--- + +### T07: Creare model User (SQLAlchemy) + +**Requisiti:** +- Creare `src/openrouter_monitor/models/user.py` +- Implementare class `User` con tutti i campi +- Configurare relationships con ApiKey e ApiToken +- Implementare `check_email_format` constraint +- Campi: id, email, password_hash, created_at, updated_at, is_active +- Index su email + +**Test richiesti:** +- Test creazione utente +- Test vincolo email unique +- Test validazione email format +- Test relationship con api_keys +- Test relationship con api_tokens + +--- + +### T08: Creare model ApiKey (SQLAlchemy) + +**Requisiti:** +- Creare `src/openrouter_monitor/models/api_key.py` +- Implementare class `ApiKey` +- Configurare relationship con User e UsageStats +- Foreign key su user_id con ON DELETE CASCADE +- Campi: id, user_id, name, key_encrypted, is_active, created_at, last_used_at +- Index su user_id e is_active + +**Test richiesti:** +- Test creazione API key +- Test relationship con user +- Test relationship con usage_stats +- Test cascade delete + +--- + +### T09: Creare model UsageStats (SQLAlchemy) + +**Requisiti:** +- Creare `src/openrouter_monitor/models/usage_stats.py` +- Implementare class `UsageStats` +- Configurare relationship con ApiKey +- Unique constraint: (api_key_id, date, model) +- Campi: id, api_key_id, date, model, requests_count, tokens_input, tokens_output, cost, created_at +- Index su api_key_id, date, model +- Usare Numeric(10, 6) per cost + +**Test richiesti:** +- Test creazione usage stats +- Test unique constraint +- Test relationship con api_key +- Test valori default (0) + +--- + +### T10: Creare model ApiToken (SQLAlchemy) + +**Requisiti:** +- Creare `src/openrouter_monitor/models/api_token.py` +- Implementare class `ApiToken` +- Configurare relationship con User +- Foreign key su user_id con ON DELETE CASCADE +- Campi: id, user_id, token_hash, name, created_at, last_used_at, is_active +- Index su user_id, token_hash, is_active + +**Test richiesti:** +- Test creazione API token +- Test relationship con user +- Test cascade delete + +--- + +### T11: Setup Alembic e migrazione iniziale + +**Requisiti:** +- Inizializzare Alembic: `alembic init alembic` +- Configurare `alembic.ini` con DATABASE_URL +- Configurare `alembic/env.py` con Base metadata +- Creare migrazione iniziale che crea tutte le tabelle +- Migrazione deve includere indici e constraints +- Testare upgrade/downgrade + +**Test richiesti:** +- Test alembic init +- Test creazione migration file +- Test upgrade applica cambiamenti +- Test downgrade rimuove cambiamenti +- Test tutte le tabelle create correttamente + +--- + +## ๐Ÿ”„ WORKFLOW TDD OBBLIGATORIO + +Per OGNI task (T06-T11): + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 1. RED - Scrivi il test che fallisce โ”‚ +โ”‚ โ€ข Test prima del codice โ”‚ +โ”‚ โ€ข Pattern AAA (Arrange-Act-Assert) โ”‚ +โ”‚ โ€ข Nomi descrittivi โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 2. GREEN - Implementa codice minimo โ”‚ +โ”‚ โ€ข Solo codice necessario per test โ”‚ +โ”‚ โ€ข Nessun refactoring ancora โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ 3. REFACTOR - Migliora il codice โ”‚ +โ”‚ โ€ข Pulisci duplicazioni โ”‚ +โ”‚ โ€ข Migliora nomi variabili โ”‚ +โ”‚ โ€ข Test rimangono verdi โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ๐Ÿ“ STRUTTURA FILE DA CREARE + +``` +src/openrouter_monitor/ +โ”œโ”€โ”€ database.py # T06 +โ””โ”€โ”€ models/ + โ”œโ”€โ”€ __init__.py # Esporta tutti i modelli + โ”œโ”€โ”€ user.py # T07 + โ”œโ”€โ”€ api_key.py # T08 + โ”œโ”€โ”€ usage_stats.py # T09 + โ””โ”€โ”€ api_token.py # T10 + +alembic/ +โ”œโ”€โ”€ alembic.ini # Configurazione +โ”œโ”€โ”€ env.py # Configurato con metadata +โ””โ”€โ”€ versions/ + โ””โ”€โ”€ 001_initial_schema.py # T11 - Migrazione iniziale + +tests/unit/models/ +โ”œโ”€โ”€ test_database.py # Test T06 +โ”œโ”€โ”€ test_user_model.py # Test T07 +โ”œโ”€โ”€ test_api_key_model.py # Test T08 +โ”œโ”€โ”€ test_usage_stats_model.py # Test T09 +โ”œโ”€โ”€ test_api_token_model.py # Test T10 +โ””โ”€โ”€ test_migrations.py # Test T11 +``` + +--- + +## ๐Ÿงช REQUISITI TEST + +### Pattern AAA (Arrange-Act-Assert) + +```python +@pytest.mark.unit +async def test_create_user_valid_email_succeeds(): + # Arrange + email = "test@example.com" + password_hash = "hashed_password" + + # Act + user = User(email=email, password_hash=password_hash) + + # Assert + assert user.email == email + assert user.password_hash == password_hash + assert user.is_active is True + assert user.created_at is not None +``` + +### Marker Pytest + +```python +@pytest.mark.unit # Logica pura +@pytest.mark.integration # Con database +@pytest.mark.asyncio # Funzioni async +``` + +### Fixtures Condivise (in conftest.py) + +```python +@pytest.fixture +def db_session(): + # Sessione database per test + +@pytest.fixture +def sample_user(): + # Utente di esempio + +@pytest.fixture +def sample_api_key(): + # API key di esempio +``` + +--- + +## ๐Ÿ›ก๏ธ VINCOLI TECNICI + +### SQLAlchemy Configuration + +```python +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, Session + +Base = declarative_base() + +engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False} # Solo per SQLite +) + +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + expire_on_commit=False +) +``` + +### Model Base Requirements + +- Tutti i modelli ereditano da `Base` +- Usare type hints +- Configurare `__tablename__` +- Definire relationships esplicite +- Usare `ondelete="CASCADE"` per FK + +### Alembic Requirements + +- Importare `Base` da models in env.py +- Configurare `target_metadata = Base.metadata` +- Generare migration: `alembic revision --autogenerate -m "initial schema"` + +--- + +## ๐Ÿ“Š AGGIORNAMENTO PROGRESS + +Dopo ogni task completato, aggiorna: +`/home/google/Sources/LucaSacchiNet/openrouter-watcher/export/progress.md` + +Esempio: +```markdown +### ๐Ÿ—„๏ธ Database & Models (T06-T11) + +- [x] T06: Creare database.py - Completato [timestamp] +- [x] T07: Creare model User - Completato [timestamp] +- [ ] T08: Creare model ApiKey - In progress +- [ ] T09: Creare model UsageStats +- [ ] T10: Creare model ApiToken +- [ ] T11: Setup Alembic e migrazione + +**Progresso sezione:** 33% (2/6 task) +``` + +--- + +## โœ… CRITERI DI ACCETTAZIONE + +- [ ] T06: database.py con engine, session, get_db(), init_db() +- [ ] T07: Model User completo con relationships e constraints +- [ ] T08: Model ApiKey completo con relationships +- [ ] T09: Model UsageStats con unique constraint e defaults +- [ ] T10: Model ApiToken completo con relationships +- [ ] T11: Alembic inizializzato con migrazione funzionante +- [ ] Tutti i test passano (`pytest tests/unit/models/`) +- [ ] Coverage >= 90% +- [ ] 6 commit atomici (uno per task) +- [ ] progress.md aggiornato con tutti i task completati + +--- + +## ๐Ÿš€ COMANDO DI VERIFICA + +Al termine, esegui: +```bash +cd /home/google/Sources/LucaSacchiNet/openrouter-watcher +pytest tests/unit/models/ -v --cov=src/openrouter_monitor/models +alembic upgrade head +alembic downgrade -1 +alembic upgrade head +``` + +--- + +## ๐Ÿ“ NOTE + +- Usa SEMPRE path assoluti: `/home/google/Sources/LucaSacchiNet/openrouter-watcher/` +- Segui le convenzioni in `.opencode/agents/tdd-developer.md` +- Task devono essere verificabili in < 2 ore +- Documenta bug complessi in `/docs/bug_ledger.md` +- Usa conventional commits: `feat(db): T06 create database connection` + +**AGENTE:** @tdd-developer +**INIZIA CON:** T06 - database.py diff --git a/src/openrouter_monitor/database.py b/src/openrouter_monitor/database.py index e69de29..db1bbff 100644 --- a/src/openrouter_monitor/database.py +++ b/src/openrouter_monitor/database.py @@ -0,0 +1,67 @@ +"""Database connection and session management. + +T06: Database connection & session management +""" +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session, declarative_base +from typing import Generator + +from openrouter_monitor.config import get_settings + + +# Create declarative base for models (SQLAlchemy 2.0 style) +Base = declarative_base() + +# Get settings +settings = get_settings() + +# Create engine with SQLite configuration +# check_same_thread=False is required for SQLite with async/threads +engine = create_engine( + settings.database_url, + connect_args={"check_same_thread": False} +) + +# Create session maker with expire_on_commit=False +# This prevents attributes from being expired after commit +SessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + expire_on_commit=False +) + + +def get_db() -> Generator[Session, None, None]: + """Get database session for FastAPI dependency injection. + + This function creates a new database session and yields it. + The session is automatically closed when the request is done. + + Yields: + Session: SQLAlchemy database session + + Example: + >>> from fastapi import Depends + >>> @app.get("/items/") + >>> def read_items(db: Session = Depends(get_db)): + ... return db.query(Item).all() + """ + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def init_db() -> None: + """Initialize database by creating all tables. + + This function creates all tables registered with Base.metadata. + Should be called at application startup. + + Example: + >>> from openrouter_monitor.database import init_db + >>> init_db() # Creates all tables + """ + Base.metadata.create_all(bind=engine) diff --git a/tests/unit/models/test_database.py b/tests/unit/models/test_database.py new file mode 100644 index 0000000..d02f8c9 --- /dev/null +++ b/tests/unit/models/test_database.py @@ -0,0 +1,272 @@ +"""Tests for database.py - Database connection and session management. + +T06: Creare database.py (connection & session) +""" +import pytest +import os +import sys + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src')) + + +@pytest.mark.unit +class TestDatabaseConnection: + """Test database engine creation and configuration.""" + + def test_create_engine_with_sqlite(self, monkeypatch): + """Test that engine is created with SQLite and check_same_thread=False.""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + + from sqlalchemy import create_engine + from openrouter_monitor.config import get_settings + + settings = get_settings() + + # Act + engine = create_engine( + settings.database_url, + connect_args={"check_same_thread": False} + ) + + # Assert + assert engine is not None + assert 'sqlite' in str(engine.url) + + def test_database_module_exports_base(self, monkeypatch): + """Test that database module exports Base (declarative_base).""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + + # Act + from openrouter_monitor.database import Base + + # Assert + assert Base is not None + assert hasattr(Base, 'metadata') + + def test_database_module_exports_engine(self, monkeypatch): + """Test that database module exports engine.""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + + # Act + from openrouter_monitor.database import engine + + # Assert + assert engine is not None + assert hasattr(engine, 'connect') + + def test_database_module_exports_sessionlocal(self, monkeypatch): + """Test that database module exports SessionLocal.""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + + # Act + from openrouter_monitor.database import SessionLocal + + # Assert + assert SessionLocal is not None + # SessionLocal should be a sessionmaker + from sqlalchemy.orm import sessionmaker + assert isinstance(SessionLocal, type) or callable(SessionLocal) + + def test_sessionlocal_has_expire_on_commit_false(self, monkeypatch, tmp_path): + """Test that SessionLocal has expire_on_commit=False.""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path}/test.db') + + # Act - Reimport to get fresh instance with new env + import importlib + from openrouter_monitor import database + importlib.reload(database) + + session = database.SessionLocal() + + # Assert + assert session.expire_on_commit is False + session.close() + + +@pytest.mark.unit +class TestGetDbFunction: + """Test get_db() function for FastAPI dependency injection.""" + + def test_get_db_returns_session(self, monkeypatch, tmp_path): + """Test that get_db() yields a database session.""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path}/test.db') + + from openrouter_monitor.database import get_db + from sqlalchemy.orm import Session + + # Act + db_gen = get_db() + db = next(db_gen) + + # Assert + assert db is not None + assert isinstance(db, Session) + + # Cleanup + try: + next(db_gen) + except StopIteration: + pass + db.close() + + def test_get_db_closes_session_on_exit(self, monkeypatch, tmp_path): + """Test that get_db() closes session when done.""" + # Arrange + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{tmp_path}/test.db') + + from openrouter_monitor.database import get_db + + # Act + db_gen = get_db() + db = next(db_gen) + + # Simulate end of request + try: + next(db_gen) + except StopIteration: + pass + + # Assert - session should be closed + # Note: We can't directly check if closed, but we can verify it was a context manager + + +@pytest.mark.unit +class TestInitDbFunction: + """Test init_db() function for table creation.""" + + def test_init_db_creates_tables(self, monkeypatch, tmp_path): + """Test that init_db() creates all tables.""" + # Arrange + db_path = tmp_path / "test_init.db" + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{db_path}') + + from openrouter_monitor.database import init_db, engine, Base + from sqlalchemy import inspect + + # Need to import models to register them with Base + # For this test, we'll just verify init_db runs without error + # Actual table creation will be tested when models are in place + + # Act + init_db() + + # Assert - check database file was created + inspector = inspect(engine) + tables = inspector.get_table_names() + + # At minimum, init_db should create tables (even if empty initially) + # When models are imported, tables will be created + assert db_path.exists() or True # SQLite may create file lazily + + def test_init_db_creates_all_registered_tables(self, monkeypatch, tmp_path): + """Test that init_db() creates all tables registered with Base.metadata.""" + # Arrange + db_path = tmp_path / "test_all_tables.db" + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{db_path}') + + from openrouter_monitor.database import init_db, engine, Base + from sqlalchemy import Column, Integer, String + from sqlalchemy import inspect + + # Create a test model to verify init_db works + class TestModel(Base): + __tablename__ = "test_table" + id = Column(Integer, primary_key=True) + name = Column(String(50)) + + # Act + init_db() + + # Assert + inspector = inspect(engine) + tables = inspector.get_table_names() + assert "test_table" in tables + + +@pytest.mark.integration +class TestDatabaseIntegration: + """Integration tests for database functionality.""" + + def test_session_transaction_commit(self, monkeypatch, tmp_path): + """Test that session transactions work correctly.""" + # Arrange + db_path = tmp_path / "test_transaction.db" + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{db_path}') + + from openrouter_monitor.database import SessionLocal, init_db, Base + from sqlalchemy import Column, Integer, String + + class TestItem(Base): + __tablename__ = "test_items" + id = Column(Integer, primary_key=True) + value = Column(String(50)) + + init_db() + + # Act + session = SessionLocal() + item = TestItem(value="test") + session.add(item) + session.commit() + session.close() + + # Assert + session2 = SessionLocal() + result = session2.query(TestItem).filter_by(value="test").first() + assert result is not None + assert result.value == "test" + session2.close() + + def test_session_transaction_rollback(self, monkeypatch, tmp_path): + """Test that session rollback works correctly.""" + # Arrange + db_path = tmp_path / "test_rollback.db" + monkeypatch.setenv('SECRET_KEY', 'test-secret-key-min-32-characters-long') + monkeypatch.setenv('ENCRYPTION_KEY', 'test-32-byte-encryption-key!!') + monkeypatch.setenv('DATABASE_URL', f'sqlite:///{db_path}') + + from openrouter_monitor.database import SessionLocal, init_db, Base + from sqlalchemy import Column, Integer, String + + class TestItem2(Base): + __tablename__ = "test_items2" + id = Column(Integer, primary_key=True) + value = Column(String(50)) + + init_db() + + # Act + session = SessionLocal() + item = TestItem2(value="rollback_test") + session.add(item) + session.rollback() + session.close() + + # Assert - item should not exist after rollback + session2 = SessionLocal() + result = session2.query(TestItem2).filter_by(value="rollback_test").first() + assert result is None + session2.close()