"""What a backup carries (steward/backup/contents.py) and the file it makes (steward/backup/dump.py): every table is a decision, and the tar reads back.""" import tarfile from datetime import datetime, timezone import pytest from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, Table from steward.backup import contents, dump from steward.backup.restore import all_metadata, dangling_fixes def undecided(tables: set[str]) -> set[str]: return tables - contents.KEPT - contents.EXCLUDED def test_every_table_is_kept_or_left_out(): tables = set(all_metadata().tables) assert "prometheus_targets" in tables and "plugin_metrics" in tables # plugins were loaded assert undecided(tables) == set(), ( "Name these in steward/backup/contents.py as KEPT or EXCLUDED") def test_the_guard_can_fail(): assert undecided({"app_settings", "plugin_metrics", "a_new_table"}) == {"a_new_table"} def test_no_table_is_both_and_none_is_named_for_nothing(): assert contents.KEPT & contents.EXCLUDED == set() stale = (contents.KEPT | contents.EXCLUDED) - set(all_metadata().tables) assert stale == set(), "contents.py names tables that no longer exist" def test_a_set_null_reference_is_cleared_and_the_row_kept(): md = MetaData() Table("ansible_runs", md, Column("id", Integer, primary_key=True)) Table("plans", md, Column("id", Integer, primary_key=True), Column("run_id", Integer, ForeignKey("ansible_runs.id", ondelete="SET NULL"))) assert dangling_fixes(md) == [ 'UPDATE "plans" SET "run_id" = NULL WHERE "run_id" IS NOT NULL AND NOT EXISTS ' '(SELECT 1 FROM "ansible_runs" p WHERE p."id" = "plans"."run_id")' ] def test_logs_history_and_state_are_left_out(): """The 2026-10-15 ruling: a backup is settings and keys, for recovery.""" for table in ("audit_events", "alert_events", "alert_states", "metric_anomalies", "metric_anomaly_spans", "user_invites"): assert table in contents.EXCLUDED, table for table in ("app_settings", "users", "api_tokens", "hosts", "host_agent_registrations", "monitors", "alert_rules"): assert table in contents.KEPT, table def test_a_cascading_reference_removes_the_row(): md = MetaData() Table("plugin_metrics", md, Column("id", Integer, primary_key=True)) Table("notes", md, Column("id", Integer, primary_key=True), Column("metric_id", Integer, ForeignKey("plugin_metrics.id", ondelete="CASCADE")), Column("owner", String, ForeignKey("users.name"))) Table("users", md, Column("name", String, primary_key=True)) assert dangling_fixes(md) == [ 'DELETE FROM "notes" WHERE "metric_id" IS NOT NULL AND NOT EXISTS ' '(SELECT 1 FROM "plugin_metrics" p WHERE p."id" = "notes"."metric_id")' ] def test_dump_leaves_out_each_excluded_tables_rows(tmp_path): args = dump.dump_command(tmp_path / "x.dump") assert "--format=custom" in args assert "--exclude-table-data=plugin_metrics" in args assert "--exclude-table-data=audit_events" in args assert "--exclude-table-data=app_settings" not in args def test_password_goes_in_the_environment_not_the_command_line(): env = dump.pg_env("postgresql+asyncpg://steward:s3cret@db:5433/steward?ssl=true") assert env["PGHOST"] == "db" and env["PGPORT"] == "5433" assert env["PGUSER"] == "steward" and env["PGPASSWORD"] == "s3cret" assert env["PGDATABASE"] == "steward" and env["PGSSLMODE"] == "require" def test_file_name_is_utc(): at = datetime(2026, 10, 12, 21, 5, 9, tzinfo=timezone.utc) assert dump.file_name(at) == "steward-20261012T210509Z.tar" def test_a_packed_backup_reads_back(tmp_path): at = datetime(2026, 10, 12, 21, 5, 9, tzinfo=timezone.utc) src = tmp_path / "in.dump" src.write_bytes(b"PGDMP" + b"x" * 3000) info = dump.manifest(at, ["0004_app_settings", "prometheus_001_targets"]) out = tmp_path / dump.file_name(at) dump.pack(out, src, "the-key", info) assert out.stat().st_mode & 0o777 == 0o600 assert not out.with_name(out.name + ".part").exists() into = tmp_path / "out" into.mkdir() got, dumped, key = dump.read_backup(out, into) assert key == "the-key" assert dumped.read_bytes() == src.read_bytes() assert got["format"] == dump.FORMAT and got["contains_secret_key"] is True assert got["created_at"] == "2026-10-12T21:05:09+00:00" assert "plugin_metrics" in got["rows_left_out"] def test_read_backup_refuses_what_isnt_one(tmp_path): plain = tmp_path / "plain.tar" with tarfile.open(plain, "w") as tar: tar.add(__file__, arcname="notes.txt") with pytest.raises(dump.BackupError, match="lacks manifest.json, secret.key, steward.dump"): dump.read_backup(plain, tmp_path) (tmp_path / "junk.tar").write_bytes(b"not a tar") with pytest.raises(dump.BackupError, match="isn't a Steward backup"): dump.read_backup(tmp_path / "junk.tar", tmp_path) def test_read_backup_refuses_another_format(tmp_path): src = tmp_path / "in.dump" src.write_bytes(b"PGDMP") info = dump.manifest(datetime.now(timezone.utc), []) | {"format": 99} out = tmp_path / "b.tar" dump.pack(out, src, "k", info) with pytest.raises(dump.BackupError, match="format 99"): dump.read_backup(out, tmp_path)