test(v2): define runtime and metadata contracts
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import inspect, text
|
||||
|
||||
EXPECTED_TABLES = {
|
||||
"alembic_version",
|
||||
"api_tokens",
|
||||
"audit_events",
|
||||
"backups",
|
||||
"executions",
|
||||
"idempotency_records",
|
||||
"jobs",
|
||||
"notification_deliveries",
|
||||
"notification_subscriptions",
|
||||
"repositories",
|
||||
"restores",
|
||||
"schedules",
|
||||
"secrets",
|
||||
"sources",
|
||||
"users",
|
||||
}
|
||||
|
||||
|
||||
def settings_for(tmp_path: Path) -> Any:
|
||||
config_module = importlib.import_module("backup_tool.config")
|
||||
key = tmp_path / "master.key"
|
||||
key.write_bytes(os.urandom(32))
|
||||
key.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
||||
roots = []
|
||||
for name in ("repositories", "sources", "restores"):
|
||||
root = tmp_path / name
|
||||
root.mkdir()
|
||||
roots.append(root)
|
||||
return config_module.Settings(
|
||||
database_url=f"sqlite+aiosqlite:///{(tmp_path / 'metadata.db').as_posix()}",
|
||||
data_dir=tmp_path,
|
||||
repository_roots=[roots[0]],
|
||||
local_source_roots=[roots[1]],
|
||||
restore_roots=[roots[2]],
|
||||
master_key_file=key,
|
||||
)
|
||||
|
||||
|
||||
def alembic_config(database_url: str) -> Config:
|
||||
config = Config("backend/alembic.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_round_trip_and_sqlite_pragmas(tmp_path: Path) -> None:
|
||||
db_module = importlib.import_module("backup_tool.db.engine")
|
||||
settings = settings_for(tmp_path)
|
||||
migration = alembic_config(settings.database_url)
|
||||
|
||||
command.upgrade(migration, "head")
|
||||
engine = db_module.create_engine(settings)
|
||||
try:
|
||||
await db_module.assert_schema_current(engine, migration)
|
||||
async with engine.connect() as connection:
|
||||
tables = await connection.run_sync(
|
||||
lambda sync_connection: set(inspect(sync_connection).get_table_names())
|
||||
)
|
||||
foreign_keys = await connection.scalar(text("PRAGMA foreign_keys"))
|
||||
journal_mode = await connection.scalar(text("PRAGMA journal_mode"))
|
||||
busy_timeout = await connection.scalar(text("PRAGMA busy_timeout"))
|
||||
assert tables == EXPECTED_TABLES
|
||||
assert foreign_keys == 1
|
||||
assert str(journal_mode).lower() == "wal"
|
||||
assert busy_timeout == settings.sqlite_busy_timeout_ms
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
command.downgrade(migration, "base")
|
||||
command.upgrade(migration, "head")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_startup_rejects_unmigrated_database(tmp_path: Path) -> None:
|
||||
db_module = importlib.import_module("backup_tool.db.engine")
|
||||
settings = settings_for(tmp_path)
|
||||
migration = alembic_config(settings.database_url)
|
||||
engine = db_module.create_engine(settings)
|
||||
try:
|
||||
with pytest.raises(db_module.SchemaNotCurrentError):
|
||||
await db_module.assert_schema_current(engine, migration)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def test_runtime_never_uses_create_all() -> None:
|
||||
offenders = []
|
||||
for root in (Path("backend/src"), Path("backend/alembic")):
|
||||
if not root.exists():
|
||||
continue
|
||||
for path in root.rglob("*.py"):
|
||||
if "create_all" in path.read_text(encoding="utf-8"):
|
||||
offenders.append(path.as_posix())
|
||||
assert offenders == []
|
||||
Reference in New Issue
Block a user