Harden location db startup validation
This commit is contained in:
+41
-2
@@ -1,12 +1,51 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.config import get_settings
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
def _make_alembic_config(database_url: str) -> Config:
|
||||
config = Config("alembic.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_app()
|
||||
def test_database_urls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
location_database_path = tmp_path / "location_test.db"
|
||||
poo_database_path = tmp_path / "poo_placeholder.db"
|
||||
location_database_url = f"sqlite:///{location_database_path}"
|
||||
poo_database_url = f"sqlite:///{poo_database_path}"
|
||||
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", location_database_url)
|
||||
monkeypatch.setenv("POO_DATABASE_URL", poo_database_url)
|
||||
get_settings.cache_clear()
|
||||
|
||||
try:
|
||||
yield {
|
||||
"location_path": location_database_path,
|
||||
"location_url": location_database_url,
|
||||
"poo_path": poo_database_path,
|
||||
"poo_url": poo_database_url,
|
||||
}
|
||||
finally:
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ready_location_database(test_database_urls):
|
||||
command.upgrade(_make_alembic_config(test_database_urls["location_url"]), "head")
|
||||
return test_database_urls
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(ready_location_database):
|
||||
yield create_app()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import sqlite3
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
from alembic import command
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.config import get_settings
|
||||
from app.main import create_app
|
||||
from tests.conftest import _make_alembic_config
|
||||
|
||||
|
||||
async def _run_lifespan(app) -> None:
|
||||
async with app.router.lifespan_context(app):
|
||||
return None
|
||||
|
||||
|
||||
def test_app_starts(client: TestClient) -> None:
|
||||
response = client.get("/")
|
||||
@@ -11,3 +25,70 @@ def test_status_endpoint(client: TestClient) -> None:
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
|
||||
def test_app_start_fails_when_location_db_missing(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{tmp_path / 'missing.db'}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{tmp_path / 'poo_placeholder.db'}")
|
||||
get_settings.cache_clear()
|
||||
|
||||
app = create_app()
|
||||
with pytest.raises(RuntimeError, match="Run 'python scripts/location_db_adopt.py' first"):
|
||||
anyio.run(_run_lifespan, app)
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_app_start_fails_when_location_db_exists_but_is_not_adopted(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
database_path = tmp_path / "legacy_only.db"
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE location (
|
||||
person TEXT NOT NULL,
|
||||
datetime TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
altitude REAL,
|
||||
PRIMARY KEY (person, datetime)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("PRAGMA user_version = 2")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{database_path}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{tmp_path / 'poo_placeholder.db'}")
|
||||
get_settings.cache_clear()
|
||||
|
||||
app = create_app()
|
||||
with pytest.raises(RuntimeError, match="is not yet Alembic-managed"):
|
||||
anyio.run(_run_lifespan, app)
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_app_start_fails_when_location_db_revision_mismatches(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
database_path = tmp_path / "wrong_revision.db"
|
||||
command.upgrade(_make_alembic_config(f"sqlite:///{database_path}"), "head")
|
||||
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute("UPDATE alembic_version SET version_num = 'wrong_revision'")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{database_path}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{tmp_path / 'poo_placeholder.db'}")
|
||||
get_settings.cache_clear()
|
||||
|
||||
app = create_app()
|
||||
with pytest.raises(RuntimeError, match="Location DB revision mismatch"):
|
||||
anyio.run(_run_lifespan, app)
|
||||
|
||||
get_settings.cache_clear()
|
||||
|
||||
+44
-8
@@ -25,11 +25,8 @@ def _make_alembic_config(database_url: str) -> Config:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def location_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
database_path = tmp_path / "location_test.db"
|
||||
database_url = f"sqlite:///{database_path}"
|
||||
|
||||
command.upgrade(_make_alembic_config(database_url), "head")
|
||||
def location_client(ready_location_database, monkeypatch: pytest.MonkeyPatch):
|
||||
database_url = ready_location_database["location_url"]
|
||||
|
||||
engine = create_engine(database_url, connect_args={"check_same_thread": False})
|
||||
session_local = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
@@ -122,9 +119,11 @@ def test_location_record_endpoint_keeps_legacy_lenient_number_parsing(location_c
|
||||
|
||||
|
||||
def test_legacy_style_location_db_can_be_stamped_and_adopted(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
test_database_urls, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
database_path = tmp_path / "legacy_location.db"
|
||||
database_path = test_database_urls["location_path"]
|
||||
database_url = test_database_urls["location_url"]
|
||||
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -142,7 +141,6 @@ def test_legacy_style_location_db_can_be_stamped_and_adopted(
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
database_url = f"sqlite:///{database_path}"
|
||||
command.stamp(_make_alembic_config(database_url), LOCATION_BASELINE_REVISION)
|
||||
|
||||
engine = create_engine(database_url, connect_args={"check_same_thread": False})
|
||||
@@ -228,6 +226,44 @@ def test_location_db_adoption_validates_and_stamps_legacy_db(tmp_path: Path) ->
|
||||
assert revision == LOCATION_BASELINE_REVISION
|
||||
|
||||
|
||||
def test_location_db_adoption_accepts_already_managed_matching_revision(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
database_path = tmp_path / "managed_location.db"
|
||||
command.upgrade(_make_alembic_config(f"sqlite:///{database_path}"), "head")
|
||||
|
||||
result = adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||
|
||||
assert result == "already_managed"
|
||||
|
||||
|
||||
def test_location_db_adoption_fails_closed_on_alembic_revision_mismatch(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
database_path = tmp_path / "wrong_revision.db"
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE location (
|
||||
person TEXT NOT NULL,
|
||||
datetime TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
altitude REAL,
|
||||
PRIMARY KEY (person, datetime)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
||||
conn.execute("INSERT INTO alembic_version (version_num) VALUES ('wrong_revision')")
|
||||
conn.execute(f"PRAGMA user_version = {EXPECTED_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
with pytest.raises(LocationDatabaseAdoptionError, match="revision does not match"):
|
||||
adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||
|
||||
|
||||
def test_location_db_adoption_fails_closed_on_schema_mismatch(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "bad_schema.db"
|
||||
conn = sqlite3.connect(database_path)
|
||||
|
||||
Reference in New Issue
Block a user