M1-T04: converge startup chain onto the single app DB
run_all_migrations() now adopts/initializes only the app DB and returns
{'app': ...}. app/main.py drops the location/poo readiness checks
(ensure_location_db_ready / ensure_poo_db_ready) and their imports;
ensure_runtime_dirs only provisions the app DB path; lifespan still
fail-closes on a missing/unmanaged app DB. Delete the retired
location/poo adopt scripts and the alembic_location / alembic_poo
chains. Update tests to single-DB expectations and drop the obsolete
location/poo adoption + readiness tests.
pytest 95 passed; ruff clean (pre-existing only); a fresh app DB
initialized via scripts.run_migrations contains location + poo_records.
This commit is contained in:
+3
-27
@@ -17,18 +17,6 @@ def _make_app_alembic_config(database_url: str) -> Config:
|
||||
return config
|
||||
|
||||
|
||||
def _make_alembic_config(database_url: str) -> Config:
|
||||
config = Config("alembic_location.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
|
||||
def _make_poo_alembic_config(database_url: str) -> Config:
|
||||
config = Config("alembic_poo.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_database_urls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
app_database_path = tmp_path / "app_test.db"
|
||||
@@ -61,18 +49,6 @@ def test_database_urls(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
reset_db_caches()
|
||||
|
||||
|
||||
@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 ready_poo_database(test_database_urls):
|
||||
command.upgrade(_make_poo_alembic_config(test_database_urls["poo_url"]), "head")
|
||||
return test_database_urls
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_database(test_database_urls, monkeypatch: pytest.MonkeyPatch):
|
||||
database_url = test_database_urls["app_url"]
|
||||
@@ -84,7 +60,7 @@ def auth_database(test_database_urls, monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(ready_location_database, ready_poo_database, auth_database):
|
||||
def app(auth_database):
|
||||
yield create_app()
|
||||
|
||||
|
||||
@@ -95,7 +71,7 @@ def client(app):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def location_client(ready_location_database, ready_poo_database, auth_database):
|
||||
def location_client(auth_database):
|
||||
app_url = auth_database["app_url"]
|
||||
engine = create_engine(app_url, connect_args={"check_same_thread": False})
|
||||
fastapi_app = create_app()
|
||||
@@ -105,7 +81,7 @@ def location_client(ready_location_database, ready_poo_database, auth_database):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def poo_client(ready_location_database, ready_poo_database, auth_database):
|
||||
def poo_client(auth_database):
|
||||
app_url = auth_database["app_url"]
|
||||
engine = create_engine(app_url, connect_args={"check_same_thread": False})
|
||||
fastapi_app = create_app()
|
||||
|
||||
+1
-114
@@ -9,7 +9,7 @@ from app.db import reset_db_caches
|
||||
from app.config import get_settings
|
||||
from app.main import create_app
|
||||
from scripts.app_db_adopt import APP_BASELINE_REVISION, adopt_or_initialize_app_db
|
||||
from tests.conftest import _make_alembic_config, _make_app_alembic_config, _make_poo_alembic_config
|
||||
from tests.conftest import _make_app_alembic_config
|
||||
|
||||
|
||||
async def _run_lifespan(app) -> None:
|
||||
@@ -38,16 +38,10 @@ def test_status_endpoint(client: TestClient) -> None:
|
||||
|
||||
def test_app_start_fails_when_app_db_missing(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
missing_app_path = tmp_path / "missing_app.db"
|
||||
poo_database_path = tmp_path / "poo_ready.db"
|
||||
location_database_path = tmp_path / "location_ready.db"
|
||||
command.upgrade(_make_poo_alembic_config(f"sqlite:///{poo_database_path}"), "head")
|
||||
command.upgrade(_make_alembic_config(f"sqlite:///{location_database_path}"), "head")
|
||||
|
||||
monkeypatch.setenv("APP_DATABASE_URL", f"sqlite:///{missing_app_path}")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_USERNAME", "admin")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
@@ -86,10 +80,6 @@ def test_app_start_seeds_missing_config_from_env_without_overwriting_existing_va
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
app_database_url = _prepare_app_db(tmp_path)
|
||||
location_database_path = tmp_path / "location_ready.db"
|
||||
poo_database_path = tmp_path / "poo_ready.db"
|
||||
command.upgrade(_make_alembic_config(f"sqlite:///{location_database_path}"), "head")
|
||||
command.upgrade(_make_poo_alembic_config(f"sqlite:///{poo_database_path}"), "head")
|
||||
|
||||
app_database_path = tmp_path / "app_ready.db"
|
||||
conn = sqlite3.connect(app_database_path)
|
||||
@@ -105,8 +95,6 @@ def test_app_start_seeds_missing_config_from_env_without_overwriting_existing_va
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
monkeypatch.setenv("APP_NAME", "Bootstrap Name")
|
||||
monkeypatch.setenv("HOME_ASSISTANT_BASE_URL", "http://bootstrap-ha.local:8123")
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
@@ -131,10 +119,6 @@ def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
app_database_url = _prepare_app_db(tmp_path)
|
||||
location_database_path = tmp_path / "location_ready.db"
|
||||
poo_database_path = tmp_path / "poo_ready.db"
|
||||
command.upgrade(_make_alembic_config(f"sqlite:///{location_database_path}"), "head")
|
||||
command.upgrade(_make_poo_alembic_config(f"sqlite:///{poo_database_path}"), "head")
|
||||
|
||||
app_database_path = tmp_path / "app_ready.db"
|
||||
conn = sqlite3.connect(app_database_path)
|
||||
@@ -149,8 +133,6 @@ def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_USERNAME", "admin")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
monkeypatch.setenv("APP_HOSTNAME", "new.example.com")
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_database_path}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
@@ -167,98 +149,3 @@ def test_app_start_syncs_app_hostname_from_env_even_when_db_has_old_value(
|
||||
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
|
||||
def test_app_start_fails_when_location_db_missing(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
app_database_url = _prepare_app_db(tmp_path)
|
||||
monkeypatch.setenv("APP_DATABASE_URL", app_database_url)
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_USERNAME", "admin")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
poo_database_path = tmp_path / "poo_ready.db"
|
||||
command.upgrade(_make_poo_alembic_config(f"sqlite:///{poo_database_path}"), "head")
|
||||
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{tmp_path / 'missing.db'}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_database_path}")
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
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()
|
||||
reset_db_caches()
|
||||
|
||||
|
||||
def test_app_start_fails_when_location_db_exists_but_is_not_adopted(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
app_database_url = _prepare_app_db(tmp_path)
|
||||
monkeypatch.setenv("APP_DATABASE_URL", app_database_url)
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_USERNAME", "admin")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
poo_database_path = tmp_path / "poo_ready.db"
|
||||
command.upgrade(_make_poo_alembic_config(f"sqlite:///{poo_database_path}"), "head")
|
||||
|
||||
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:///{poo_database_path}")
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
app = create_app()
|
||||
with pytest.raises(RuntimeError, match="is not yet Alembic-managed"):
|
||||
anyio.run(_run_lifespan, app)
|
||||
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
|
||||
def test_app_start_fails_when_location_db_revision_mismatches(
|
||||
tmp_path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
app_database_url = _prepare_app_db(tmp_path)
|
||||
monkeypatch.setenv("APP_DATABASE_URL", app_database_url)
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_USERNAME", "admin")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
poo_database_path = tmp_path / "poo_ready.db"
|
||||
command.upgrade(_make_poo_alembic_config(f"sqlite:///{poo_database_path}"), "head")
|
||||
|
||||
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:///{poo_database_path}")
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
app = create_app()
|
||||
with pytest.raises(RuntimeError, match="Location DB revision mismatch"):
|
||||
anyio.run(_run_lifespan, app)
|
||||
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
@@ -194,9 +194,6 @@ def test_config_page_update_persists_to_database(
|
||||
|
||||
|
||||
def test_config_page_shows_ticktick_oauth_link_when_ticktick_is_configured(
|
||||
test_database_urls,
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
|
||||
+5
-109
@@ -4,18 +4,12 @@ import sqlite3
|
||||
import anyio
|
||||
import pytest
|
||||
import yaml
|
||||
from alembic import command
|
||||
|
||||
from app.db import reset_db_caches
|
||||
from app.config import get_settings
|
||||
from app.main import create_app
|
||||
from scripts.app_db_adopt import APP_BASELINE_REVISION
|
||||
from scripts.location_db_adopt import EXPECTED_USER_VERSION as LOCATION_USER_VERSION
|
||||
from scripts.location_db_adopt import LOCATION_BASELINE_REVISION
|
||||
from scripts.poo_db_adopt import EXPECTED_USER_VERSION as POO_USER_VERSION
|
||||
from scripts.poo_db_adopt import POO_BASELINE_REVISION
|
||||
from scripts.run_migrations import run_all_migrations
|
||||
from tests.conftest import _make_alembic_config, _make_poo_alembic_config
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
@@ -31,12 +25,8 @@ async def _run_lifespan(app) -> None:
|
||||
|
||||
def _configure_database_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Path | str]:
|
||||
app_path = tmp_path / "app.db"
|
||||
location_path = tmp_path / "location.db"
|
||||
poo_path = tmp_path / "poo.db"
|
||||
|
||||
monkeypatch.setenv("APP_DATABASE_URL", f"sqlite:///{app_path}")
|
||||
monkeypatch.setenv("LOCATION_DATABASE_URL", f"sqlite:///{location_path}")
|
||||
monkeypatch.setenv("POO_DATABASE_URL", f"sqlite:///{poo_path}")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_USERNAME", "admin")
|
||||
monkeypatch.setenv("AUTH_BOOTSTRAP_PASSWORD", "test-password")
|
||||
monkeypatch.setenv("AUTH_COOKIE_SECURE_OVERRIDE", "false")
|
||||
@@ -46,58 +36,9 @@ def _configure_database_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) ->
|
||||
return {
|
||||
"app_path": app_path,
|
||||
"app_url": f"sqlite:///{app_path}",
|
||||
"location_path": location_path,
|
||||
"location_url": f"sqlite:///{location_path}",
|
||||
"poo_path": poo_path,
|
||||
"poo_url": f"sqlite:///{poo_path}",
|
||||
}
|
||||
|
||||
|
||||
def _create_legacy_location_db(database_path: Path) -> None:
|
||||
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(
|
||||
"INSERT INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)",
|
||||
("alice", "2026-04-22T10:00:00Z", 1.23, 4.56, 7.89),
|
||||
)
|
||||
conn.execute(f"PRAGMA user_version = {LOCATION_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def _create_legacy_poo_db(database_path: Path) -> None:
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE poo_records (
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
PRIMARY KEY (timestamp)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO poo_records (timestamp, status, latitude, longitude) VALUES (?, ?, ?, ?)",
|
||||
("2026-04-22T11:00:00Z", "complete", 9.87, 6.54),
|
||||
)
|
||||
conn.execute(f"PRAGMA user_version = {POO_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_compose_uses_migration_job_before_app() -> None:
|
||||
compose = _read_yaml("docker-compose.yml")
|
||||
override = _read_yaml("docker-compose.override.yml")
|
||||
@@ -131,12 +72,8 @@ def test_migration_runner_initializes_and_is_idempotent(
|
||||
first_run = run_all_migrations()
|
||||
second_run = run_all_migrations()
|
||||
|
||||
assert first_run == {"app": "initialized", "location": "initialized", "poo": "initialized"}
|
||||
assert second_run == {
|
||||
"app": "already_managed",
|
||||
"location": "already_managed",
|
||||
"poo": "already_managed",
|
||||
}
|
||||
assert first_run == {"app": "initialized"}
|
||||
assert second_run == {"app": "already_managed"}
|
||||
|
||||
conn = sqlite3.connect(database_urls["app_path"])
|
||||
try:
|
||||
@@ -150,48 +87,9 @@ def test_migration_runner_initializes_and_is_idempotent(
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert {"auth_users", "auth_sessions", "app_config", "alembic_version"} <= tables
|
||||
|
||||
conn = sqlite3.connect(database_urls["location_path"])
|
||||
try:
|
||||
assert conn.execute("SELECT version_num FROM alembic_version").fetchone()[0] == LOCATION_BASELINE_REVISION
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
conn = sqlite3.connect(database_urls["poo_path"])
|
||||
try:
|
||||
assert conn.execute("SELECT version_num FROM alembic_version").fetchone()[0] == POO_BASELINE_REVISION
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
|
||||
|
||||
def test_migration_runner_adopts_legacy_sqlite_without_data_loss(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
database_urls = _configure_database_env(tmp_path, monkeypatch)
|
||||
_create_legacy_location_db(database_urls["location_path"])
|
||||
_create_legacy_poo_db(database_urls["poo_path"])
|
||||
|
||||
results = run_all_migrations()
|
||||
|
||||
assert results == {"app": "initialized", "location": "adopted", "poo": "adopted"}
|
||||
|
||||
conn = sqlite3.connect(database_urls["location_path"])
|
||||
try:
|
||||
assert conn.execute("SELECT version_num FROM alembic_version").fetchone()[0] == LOCATION_BASELINE_REVISION
|
||||
assert conn.execute("SELECT COUNT(*) FROM location").fetchone()[0] == 1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
conn = sqlite3.connect(database_urls["poo_path"])
|
||||
try:
|
||||
assert conn.execute("SELECT version_num FROM alembic_version").fetchone()[0] == POO_BASELINE_REVISION
|
||||
assert conn.execute("SELECT COUNT(*) FROM poo_records").fetchone()[0] == 1
|
||||
finally:
|
||||
conn.close()
|
||||
assert {
|
||||
"auth_users", "auth_sessions", "app_config", "alembic_version", "location", "poo_records"
|
||||
} <= tables
|
||||
|
||||
get_settings.cache_clear()
|
||||
reset_db_caches()
|
||||
@@ -202,8 +100,6 @@ def test_app_startup_still_fails_closed_without_running_adoption(
|
||||
) -> None:
|
||||
database_urls = _configure_database_env(tmp_path, monkeypatch)
|
||||
missing_app_path = database_urls["app_path"]
|
||||
command.upgrade(_make_alembic_config(database_urls["location_url"]), "head")
|
||||
command.upgrade(_make_poo_alembic_config(database_urls["poo_url"]), "head")
|
||||
|
||||
app = create_app()
|
||||
with pytest.raises(RuntimeError, match="Run 'python scripts/app_db_adopt.py' first"):
|
||||
|
||||
@@ -156,8 +156,6 @@ def test_homeassistant_publish_rejects_invalid_ticktick_content(location_client)
|
||||
|
||||
|
||||
def test_homeassistant_publish_poo_get_latest_publishes_latest_status(
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
) -> None:
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -215,8 +213,6 @@ def test_homeassistant_publish_poo_get_latest_publishes_latest_status(
|
||||
|
||||
|
||||
def test_homeassistant_publish_returns_internal_error_for_unknown_poo_action(
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
) -> None:
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import text
|
||||
|
||||
from scripts.location_db_adopt import (
|
||||
EXPECTED_USER_VERSION,
|
||||
LOCATION_BASELINE_REVISION,
|
||||
LocationDatabaseAdoptionError,
|
||||
adopt_or_initialize_location_db,
|
||||
)
|
||||
|
||||
|
||||
def _make_alembic_config(database_url: str) -> Config:
|
||||
config = Config("alembic_location.ini")
|
||||
config.set_main_option("sqlalchemy.url", database_url)
|
||||
return config
|
||||
|
||||
def test_location_record_endpoint_writes_row(location_client) -> None:
|
||||
client, engine = location_client
|
||||
@@ -193,136 +177,3 @@ def test_location_record_endpoint_defaults_invalid_altitude_to_zero(location_cli
|
||||
assert row.altitude == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_location_db_adoption_initializes_new_db(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "new_location.db"
|
||||
result = adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||
|
||||
assert result == "initialized"
|
||||
assert database_path.exists()
|
||||
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
revision = conn.execute("SELECT version_num FROM alembic_version").fetchone()[0]
|
||||
location_table = conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'location'"
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert revision == LOCATION_BASELINE_REVISION
|
||||
assert location_table is not None
|
||||
|
||||
|
||||
def test_location_db_adoption_validates_and_stamps_legacy_db(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "legacy_location.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(f"PRAGMA user_version = {EXPECTED_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
result = adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||
|
||||
assert result == "adopted"
|
||||
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
revision = conn.execute("SELECT version_num FROM alembic_version").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
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="known migration revision"):
|
||||
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)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE location (
|
||||
person TEXT NOT NULL,
|
||||
datetime TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
PRIMARY KEY (person, datetime)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(f"PRAGMA user_version = {EXPECTED_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
with pytest.raises(LocationDatabaseAdoptionError, match="schema does not match"):
|
||||
adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||
|
||||
|
||||
def test_location_db_adoption_fails_closed_on_user_version_mismatch(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "bad_user_version.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 = 999")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
with pytest.raises(LocationDatabaseAdoptionError, match="Expected PRAGMA user_version"):
|
||||
adopt_or_initialize_location_db(f"sqlite:///{database_path}")
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config import Settings, get_settings
|
||||
from app.dependencies import get_app_settings, get_homeassistant_client
|
||||
from scripts.poo_db_adopt import (
|
||||
EXPECTED_USER_VERSION,
|
||||
POO_BASELINE_REVISION,
|
||||
PooDatabaseAdoptionError,
|
||||
adopt_or_initialize_poo_db,
|
||||
)
|
||||
|
||||
|
||||
class _FakeHomeAssistantClient:
|
||||
@@ -153,96 +144,3 @@ def test_poo_latest_endpoint_returns_ok_when_no_record_exists(poo_client_with_ov
|
||||
assert response.text == ""
|
||||
|
||||
|
||||
def test_poo_db_adoption_initializes_new_db(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "new_poo.db"
|
||||
|
||||
result = adopt_or_initialize_poo_db(f"sqlite:///{database_path}")
|
||||
|
||||
assert result == "initialized"
|
||||
assert database_path.exists()
|
||||
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
revision = conn.execute("SELECT version_num FROM alembic_version").fetchone()[0]
|
||||
poo_table = conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'poo_records'"
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert revision == POO_BASELINE_REVISION
|
||||
assert poo_table is not None
|
||||
|
||||
|
||||
def test_poo_db_adoption_validates_and_stamps_legacy_db(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "legacy_poo.db"
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE poo_records (
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
PRIMARY KEY (timestamp)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(f"PRAGMA user_version = {EXPECTED_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
result = adopt_or_initialize_poo_db(f"sqlite:///{database_path}")
|
||||
|
||||
assert result == "adopted"
|
||||
|
||||
conn = sqlite3.connect(database_path)
|
||||
try:
|
||||
revision = conn.execute("SELECT version_num FROM alembic_version").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
assert revision == POO_BASELINE_REVISION
|
||||
|
||||
|
||||
def test_poo_db_adoption_fails_closed_on_schema_mismatch(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "bad_poo_schema.db"
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE poo_records (
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
PRIMARY KEY (timestamp)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(f"PRAGMA user_version = {EXPECTED_USER_VERSION}")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
with pytest.raises(PooDatabaseAdoptionError, match="schema does not match"):
|
||||
adopt_or_initialize_poo_db(f"sqlite:///{database_path}")
|
||||
|
||||
|
||||
def test_poo_db_adoption_fails_closed_on_user_version_mismatch(tmp_path: Path) -> None:
|
||||
database_path = tmp_path / "bad_poo_user_version.db"
|
||||
conn = sqlite3.connect(database_path)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE poo_records (
|
||||
timestamp TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
PRIMARY KEY (timestamp)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute("PRAGMA user_version = 999")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
with pytest.raises(PooDatabaseAdoptionError, match="Expected PRAGMA user_version"):
|
||||
adopt_or_initialize_poo_db(f"sqlite:///{database_path}")
|
||||
|
||||
+1
-16
@@ -209,9 +209,6 @@ def test_create_task_posts_expected_payload(monkeypatch: pytest.MonkeyPatch) ->
|
||||
|
||||
|
||||
def test_homeassistant_publish_creates_ticktick_action_task(
|
||||
test_database_urls,
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -255,9 +252,6 @@ def test_homeassistant_publish_creates_ticktick_action_task(
|
||||
|
||||
|
||||
def test_ticktick_auth_start_redirects_authenticated_user(
|
||||
test_database_urls,
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -291,9 +285,6 @@ def test_ticktick_auth_start_redirects_authenticated_user(
|
||||
|
||||
|
||||
def test_ticktick_auth_callback_persists_token(
|
||||
test_database_urls,
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -318,7 +309,7 @@ def test_ticktick_auth_callback_persists_token(
|
||||
assert response.status_code == 303
|
||||
assert response.headers["location"] == "/config?ticktick_oauth=success"
|
||||
|
||||
conn = sqlite3.connect(test_database_urls["app_path"])
|
||||
conn = sqlite3.connect(auth_database["app_path"])
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT value FROM app_config WHERE key = ?",
|
||||
@@ -332,9 +323,6 @@ def test_ticktick_auth_callback_persists_token(
|
||||
|
||||
|
||||
def test_ticktick_auth_callback_redirects_on_invalid_state(
|
||||
test_database_urls,
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -356,9 +344,6 @@ def test_ticktick_auth_callback_redirects_on_invalid_state(
|
||||
|
||||
|
||||
def test_ticktick_auth_callback_redirects_when_token_exchange_fails(
|
||||
test_database_urls,
|
||||
ready_location_database,
|
||||
ready_poo_database,
|
||||
auth_database,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user