""" Wave 0 integration tests for Plan 03 (Alembic migration). Both tests are marked xfail(strict=False) because the Alembic migration and db.models are implemented in Plan 03. The xfail markers will be removed once Plan 03 lands and alembic upgrade head successfully creates all expected tables. Requirements covered: STORE-01 — Platform migrated to PostgreSQL; full v1 schema applied D-02 — groups table stub included in the Phase 1 migration D-03 — documents.user_id is nullable in Phase 1 """ from __future__ import annotations import pytest EXPECTED_TABLES = { "users", "quotas", "refresh_tokens", "folders", "documents", "topics", "document_topics", "shares", "audit_log", "cloud_connections", "groups", } # --------------------------------------------------------------------------- # Test 1: alembic upgrade head creates all 11 expected tables # --------------------------------------------------------------------------- @pytest.mark.xfail(strict=False, reason="implemented in plan 03") def test_migration_creates_all_tables(tmp_path, monkeypatch): """After alembic upgrade head, the DB contains all 11 v1 tables (D-01, D-02).""" try: import alembic.command from alembic.config import Config except ImportError as exc: pytest.skip(f"alembic not installed: {exc}") db_path = tmp_path / "test_migrate.db" db_url = f"sqlite+aiosqlite:///{db_path}" monkeypatch.setenv("DATABASE_MIGRATE_URL", db_url) # Run alembic upgrade head using the Python API alembic_cfg = Config("alembic.ini") alembic_cfg.set_main_option("sqlalchemy.url", db_url) try: alembic.command.upgrade(alembic_cfg, "head") except Exception as exc: pytest.skip(f"alembic upgrade failed — plan 03 not yet complete: {exc}") # Connect synchronously (sqlite3 is available in stdlib) and check tables import sqlite3 conn = sqlite3.connect(str(db_path)) try: cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table'") actual_tables = {row[0] for row in cursor.fetchall()} finally: conn.close() missing = EXPECTED_TABLES - actual_tables assert not missing, ( f"Migration is missing tables: {sorted(missing)}. " f"Tables found: {sorted(actual_tables)}" ) # --------------------------------------------------------------------------- # Test 2: documents.user_id is nullable (D-03) # --------------------------------------------------------------------------- @pytest.mark.xfail(strict=False, reason="implemented in plan 03") def test_documents_user_id_nullable(tmp_path, monkeypatch): """documents.user_id must be nullable in Phase 1 (D-03 — no auth yet).""" try: import alembic.command from alembic.config import Config except ImportError as exc: pytest.skip(f"alembic not installed: {exc}") db_path = tmp_path / "test_nullable.db" db_url = f"sqlite+aiosqlite:///{db_path}" monkeypatch.setenv("DATABASE_MIGRATE_URL", db_url) alembic_cfg = Config("alembic.ini") alembic_cfg.set_main_option("sqlalchemy.url", db_url) try: alembic.command.upgrade(alembic_cfg, "head") except Exception as exc: pytest.skip(f"alembic upgrade failed — plan 03 not yet complete: {exc}") # PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk import sqlite3 conn = sqlite3.connect(str(db_path)) try: cursor = conn.execute("PRAGMA table_info(documents)") columns = {row[1]: {"notnull": row[3]} for row in cursor.fetchall()} finally: conn.close() assert "user_id" in columns, ( "documents table has no user_id column — migration may not have applied" ) assert columns["user_id"]["notnull"] == 0, ( "documents.user_id is NOT NULL but D-03 requires it to be nullable in Phase 1" ) # --------------------------------------------------------------------------- # Test 3: migration 0003 — null-user cleanup + NOT NULL + quota reconciliation # --------------------------------------------------------------------------- @pytest.mark.xfail(strict=False, reason="implemented in plan 03-01 migration step") def test_migration_0003(tmp_path, monkeypatch): """After alembic upgrade head applying 0003: - The documents row with user_id=None is deleted (D-01, D-02). - The documents row with a real user_id is preserved. - PRAGMA table_info shows documents.user_id notnull=1. - All topics rows are deleted (D-10). - ix_topics_user_id exists in sqlite_master. - quotas.used_bytes for the populated user equals SUM(size_bytes). """ try: import alembic.command from alembic.config import Config except ImportError as exc: pytest.skip(f"alembic not installed: {exc}") import sqlite3 import uuid db_path = tmp_path / "test_0003.db" db_url = f"sqlite+aiosqlite:///{db_path}" monkeypatch.setenv("DATABASE_MIGRATE_URL", db_url) alembic_cfg = Config("alembic.ini") alembic_cfg.set_main_option("sqlalchemy.url", db_url) # Apply migrations up to 0002 so the schema is in the pre-0003 state try: alembic.command.upgrade(alembic_cfg, "0002") except Exception as exc: pytest.skip(f"alembic upgrade to 0002 failed: {exc}") # Pre-seed test data using raw sqlite3 (synchronous) user_id = str(uuid.uuid4()) doc_id_null = str(uuid.uuid4()) doc_id_user = str(uuid.uuid4()) topic_id = str(uuid.uuid4()) conn = sqlite3.connect(str(db_path)) try: # Insert a user so we can seed a quota and a user-owned document conn.execute( "INSERT INTO users (id, handle, email, password_hash, role, is_active, " "password_must_change, default_storage_backend) " "VALUES (?, ?, ?, ?, ?, 1, 0, 'minio')", (user_id, "testuser", "test@example.com", "hash", "user"), ) conn.execute( "INSERT INTO quotas (user_id, limit_bytes, used_bytes) VALUES (?, ?, ?)", (user_id, 104857600, 0), ) # Null-user document (to be deleted) conn.execute( "INSERT INTO documents (id, user_id, filename, object_key, content_type, " "size_bytes, storage_backend, status) VALUES (?, NULL, ?, ?, ?, ?, 'minio', 'uploaded')", (doc_id_null, "null_doc.txt", "null/key.txt", "text/plain", 1000), ) # User-owned document (to be preserved) conn.execute( "INSERT INTO documents (id, user_id, filename, object_key, content_type, " "size_bytes, storage_backend, status) VALUES (?, ?, ?, ?, ?, ?, 'minio', 'uploaded')", (doc_id_user, user_id, "user_doc.txt", "user/key.txt", "text/plain", 2048), ) # A topic row (all topics deleted in 0003) conn.execute( "INSERT INTO topics (id, user_id, name, description, color) " "VALUES (?, NULL, ?, '', ?)", (topic_id, "Finance", "#6366f1"), ) conn.commit() finally: conn.close() # Apply migration 0003 (no MinIO env set — MinIO step is skipped safely) try: alembic.command.upgrade(alembic_cfg, "0003") except Exception as exc: pytest.fail(f"alembic upgrade to 0003 failed: {exc}") conn = sqlite3.connect(str(db_path)) try: # 1. Null-user document must be gone cursor = conn.execute( "SELECT id FROM documents WHERE id = ?", (doc_id_null,) ) assert cursor.fetchone() is None, "Null-user document was not deleted by migration 0003" # 2. User-owned document must be preserved cursor = conn.execute( "SELECT id FROM documents WHERE id = ?", (doc_id_user,) ) assert cursor.fetchone() is not None, "User-owned document was incorrectly deleted" # 3. documents.user_id must now be NOT NULL cursor = conn.execute("PRAGMA table_info(documents)") columns = {row[1]: {"notnull": row[3]} for row in cursor.fetchall()} assert columns["user_id"]["notnull"] == 1, ( "documents.user_id is still nullable after migration 0003" ) # 4. All topics rows must be deleted cursor = conn.execute("SELECT COUNT(*) FROM topics") count = cursor.fetchone()[0] assert count == 0, f"Expected 0 topics after migration 0003, found {count}" # 5. ix_topics_user_id must exist cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='index' AND name='ix_topics_user_id'" ) assert cursor.fetchone() is not None, "ix_topics_user_id index not created by migration 0003" # 6. quotas.used_bytes must equal SUM(size_bytes) for the user cursor = conn.execute( "SELECT used_bytes FROM quotas WHERE user_id = ?", (user_id,) ) row = cursor.fetchone() assert row is not None, "Quota row not found for test user" assert row[0] == 2048, ( f"quotas.used_bytes should be 2048 (SUM of user docs) but is {row[0]}" ) finally: conn.close()