"""Integration: checking a backup against Postgres (#5823). pg_restore isn't in this lane (the publish smoke step checks a real backup with the image), so the backup's rows are a snapshot of this database taken through the check's own reader; what is real is everything read in place, the comparison and the report. Requires STEWARD_DATABASE_URL.""" from __future__ import annotations import asyncio import os import uuid from datetime import datetime, timezone import pytest pytestmark = pytest.mark.integration @pytest.fixture def app(): if not os.environ.get("STEWARD_DATABASE_URL"): pytest.skip("needs Postgres") from steward.app import create_app return create_app(testing=False) def test_a_check_finds_a_changed_setting_a_deleted_monitor_and_a_new_host(app, monkeypatch, tmp_path): from sqlalchemy import delete from steward.backup import check, contents, dump, job from steward.core.settings import set_setting from steward.models.hosts import Host from steward.models.monitors import Monitor tag = uuid.uuid4().hex[:8] setting, monitor_name, host_name = f"test.check_{tag}", f"check-mon-{tag}", f"check-host-{tag}" url = app.config["DATABASE_URL"] async def _go(): async with app.db_sessionmaker() as s: async with s.begin(): await set_setting(s, setting, "before") mon = Monitor(name=monitor_name, type="tcp", target="10.9.9.9", config_json="{}") s.add(mon) # The "backup": this database as it stands, read the way a check reads. snapshot, *_ = await check._here(url, sorted(contents.KEPT)) theirs = {t: (side.columns, side.rows) for t, side in snapshot.items()} async def backup_rows(_dump): return theirs monkeypatch.setattr(check, "_backup_rows", backup_rows) path = tmp_path / "steward-20261015T030000Z.tar" (tmp_path / "steward.dump").write_bytes(b"PGDMP") heads = await job.migration_heads(url) dump.pack(path, tmp_path / "steward.dump", app.config["SECRET_KEY"], dump.manifest(datetime.now(timezone.utc), heads)) same = await job.check_file(app, path) async with app.db_sessionmaker() as s: async with s.begin(): await set_setting(s, setting, "after") await s.execute(delete(Monitor).where(Monitor.name == monitor_name)) s.add(Host(name=host_name, address="10.9.9.10")) changed = await job.check_file(app, path) return same, changed same, changed = asyncio.run(_go()) assert same.ok and same.changes == 0, same.summary() assert same.schema == "same" and same.key_same and same.rows > 0 by_table = {t.table: t for a in changed.areas for t in a.tables} [diff] = [d for d in by_table["app_settings"].differ if d.label == setting] assert (diff.backup_value, diff.here_value) == ('"before"', '"after"') assert monitor_name in by_table["monitors"].only_backup assert host_name in by_table["hosts"].only_here assert changed.changes == 3, changed.summary() def test_a_file_that_isnt_a_backup_fails_the_check(app, tmp_path): from steward.backup import job from steward.backup.dump import BackupError bad = tmp_path / "steward-20261015T030000Z.tar" bad.write_bytes(b"not a tar") with pytest.raises(BackupError, match="isn't a Steward backup"): asyncio.run(job.check_file(app, bad))