21ec9cb4c3
- Add auth_user, admin_user, mock_minio_presigned, mock_minio_stat fixtures to conftest.py - Create test_quota.py with 4 xfail stubs (STORE-03, STORE-05, STORE-06, SC2 race) - Append test_migration_0003 to test_alembic.py (full pre-seed + post-migration assertions) - Append 3 classifier xfail stubs (DOC-03, DOC-05, D-15) - Append 6 document xfail stubs (D-05, STORE-04, SEC-04, D-16) - Append 4 topic xfail stubs (DOC-04, D-09, D-17) - Append test_settings_endpoint_removed stub (D-12) - All 19 new test IDs collect cleanly with xfail(strict=False)
247 lines
9.0 KiB
Python
247 lines
9.0 KiB
Python
"""
|
|
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()
|