Files
gh-christianlouis-docuelevate/tests/test_db_migrate.py
2026-05-31 01:16:23 +02:00

103 lines
4.4 KiB
Python

"""Tests for the SQLite-to-SQL database migration helper."""
from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy import create_engine, inspect, text
from sqlalchemy.pool import StaticPool
from app.utils.db_migrate import _make_engine, _ordered_tables, migrate_data, preview_migration
@pytest.mark.unit
class TestMakeEngine:
def test_sqlite_engine_is_created(self):
engine = _make_engine("sqlite:///:memory:")
assert engine is not None
engine.dispose()
def test_non_sqlite_engine_does_not_use_sqlite_connect_args(self):
engine = _make_engine("postgresql+psycopg://user:pass@localhost:5432/docuelevate")
assert engine is not None
engine.dispose()
@pytest.mark.unit
class TestOrderedTables:
def test_known_tables_are_ordered_before_unknown_tables(self):
inspector = MagicMock()
inspector.get_table_names.return_value = ["z_table", "files", "documents", "sqlite_sequence"]
assert _ordered_tables(inspector) == ["documents", "files", "z_table"]
def test_unsafe_table_names_are_skipped(self):
inspector = MagicMock()
inspector.get_table_names.return_value = ["files", "bad-table"]
assert _ordered_tables(inspector) == ["files"]
@pytest.mark.unit
class TestPreviewMigration:
def test_preview_returns_row_counts(self, tmp_path):
db_path = tmp_path / "source.db"
engine = create_engine(f"sqlite:///{db_path}")
with engine.begin() as conn:
conn.execute(text("CREATE TABLE files (id INTEGER PRIMARY KEY, local_filename VARCHAR NOT NULL)"))
conn.execute(text("INSERT INTO files (local_filename) VALUES ('a.pdf'), ('b.pdf')"))
result = preview_migration(f"sqlite:///{db_path}")
assert result["success"] is True
assert result["total_rows"] == 2
assert result["tables"] == [{"name": "files", "row_count": 2}]
@pytest.mark.unit
class TestMigrateData:
def test_migrates_reflected_schema_and_rows(self, tmp_path):
source_path = tmp_path / "source.db"
target_path = tmp_path / "target.db"
source = create_engine(f"sqlite:///{source_path}")
with source.begin() as conn:
conn.execute(text("CREATE TABLE files (id INTEGER PRIMARY KEY, local_filename VARCHAR NOT NULL)"))
conn.execute(text("CREATE TABLE future_table (id INTEGER PRIMARY KEY, value VARCHAR)"))
conn.execute(text("INSERT INTO files (id, local_filename) VALUES (7, 'stable.pdf')"))
conn.execute(text("INSERT INTO future_table (id, value) VALUES (1, 'kept')"))
result = migrate_data(f"sqlite:///{source_path}", f"sqlite:///{target_path}")
assert result["success"] is True
assert result["rows_copied"] == 2
target = create_engine(f"sqlite:///{target_path}")
inspector = inspect(target)
assert "files" in inspector.get_table_names()
assert "future_table" in inspector.get_table_names()
with target.connect() as conn:
assert conn.execute(text("SELECT local_filename FROM files WHERE id = 7")).scalar_one() == "stable.pdf"
assert conn.execute(text("SELECT value FROM future_table WHERE id = 1")).scalar_one() == "kept"
def test_migration_reports_global_errors(self):
with patch("app.utils.db_migrate._make_engine", side_effect=RuntimeError("boom")):
result = migrate_data("sqlite:///:memory:", "sqlite:///:memory:")
assert result["success"] is False
assert "boom" in result["errors"][0]
def test_migration_progress_callback_is_called(self):
source = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool)
target = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool)
with source.begin() as conn:
conn.execute(text("CREATE TABLE files (id INTEGER PRIMARY KEY, local_filename VARCHAR NOT NULL)"))
conn.execute(text("INSERT INTO files (id, local_filename) VALUES (1, 'a.pdf'), (2, 'b.pdf')"))
callback = MagicMock()
with patch("app.utils.db_migrate._make_engine") as make_engine:
make_engine.side_effect = [source, target]
result = migrate_data("sqlite:///:memory:", "sqlite:///:memory:", batch_size=1, progress_callback=callback)
assert result["success"] is True
assert callback.call_count == 2