import json import unittest from baudolo.generation import FILES_DIR, MANIFEST_FILE, MANIFEST_SCHEMA, SQL_DIR from .helpers import ( POSTGRES_DATA_DIR, POSTGRES_IMAGE, backup_path, cleanup_docker, create_minimal_compose_dir, ensure_empty_dir, latest_version_dir, require_docker, run, unique, wait_for_postgres, write_databases_csv, ) class TestE2EOnlySqlFallbackToFiles(unittest.TestCase): @classmethod def setUpClass(cls) -> None: require_docker() cls.prefix = unique("baudolo-e2e-only-sql-fallback") cls.backups_dir = f"/tmp/{cls.prefix}/Backups" ensure_empty_dir(cls.backups_dir) cls.compose_dir = create_minimal_compose_dir(f"/tmp/{cls.prefix}") cls.repo_name = cls.prefix cls.pg_container = f"{cls.prefix}-pg" cls.pg_volume = f"{cls.prefix}-pg-vol" cls.restore_volume = f"{cls.prefix}-restore-vol" cls.containers = [cls.pg_container] cls.volumes = [cls.pg_volume, cls.restore_volume] run(["docker", "volume", "create", cls.pg_volume]) run( [ "docker", "run", "-d", "--name", cls.pg_container, "-e", "POSTGRES_PASSWORD=pgpw", "-e", "POSTGRES_DB=appdb", "-e", "POSTGRES_USER=postgres", "-v", f"{cls.pg_volume}:{POSTGRES_DATA_DIR}", POSTGRES_IMAGE, ] ) wait_for_postgres(cls.pg_container, user="postgres", timeout_s=90) # Add a deterministic marker file into the volume cls.marker = "only-sql-fallback-marker" run( [ "docker", "exec", cls.pg_container, "sh", "-lc", f"echo '{cls.marker}' > {POSTGRES_DATA_DIR}/marker.txt", ] ) # databases.csv WITHOUT matching entry for this instance -> should skip dump cls.databases_csv = f"/tmp/{cls.prefix}/databases.csv" write_databases_csv(cls.databases_csv, []) # empty except header # Run baudolo with --only-sql and a DB container present: # Expected: WARNING + FALLBACK to file backup (files/ must exist) cmd = [ "baudolo", "--compose-dir", cls.compose_dir, "--hard-restart-projects", "mailu", "--repo-name", cls.repo_name, "--databases-csv", cls.databases_csv, "--backups-dir", cls.backups_dir, "--database-containers", cls.pg_container, "--images-no-stop-required", POSTGRES_IMAGE, "--only-sql", ] cp = run(cmd, capture=True, check=True) cls.stdout = cp.stdout or "" cls.hash, cls.version = latest_version_dir(cls.backups_dir, cls.repo_name) # Restore files into a fresh volume to prove file backup happened run(["docker", "volume", "create", cls.restore_volume]) run( [ "baudolo-restore", "files", cls.restore_volume, cls.hash, cls.version, "--backups-dir", cls.backups_dir, "--repo-name", cls.repo_name, "--source-volume", cls.pg_volume, ] ) @classmethod def tearDownClass(cls) -> None: cleanup_docker(containers=cls.containers, volumes=cls.volumes) def test_warns_about_missing_dump_in_dump_only_mode(self) -> None: self.assertIn( "WARNING: only-sql requested but no DB dump was produced", self.stdout, f"Expected warning in baudolo output. STDOUT:\n{self.stdout}", ) def test_files_backup_exists_due_to_fallback(self) -> None: p = ( backup_path( self.backups_dir, self.repo_name, self.version, self.pg_volume, ) / "files" ) self.assertTrue(p.is_dir(), f"Expected files backup dir at: {p}") def test_sql_dump_not_present(self) -> None: # There should be no sql dumps because databases.csv had no matching entry. sql_dir = ( backup_path( self.backups_dir, self.repo_name, self.version, self.pg_volume, ) / "sql" ) # Could exist (dir created) in some edge cases, but should contain no *.sql dumps. if sql_dir.exists(): dumps = list(sql_dir.glob("*.sql")) self.assertEqual( len(dumps), 0, f"Did not expect SQL dump files, found: {dumps}", ) def manifest(self) -> dict: generation = backup_path( self.backups_dir, self.repo_name, self.version, self.pg_volume ).parent return json.loads((generation / MANIFEST_FILE).read_text(encoding="utf-8")) def test_the_manifest_records_the_volume_as_a_database_left_undumped(self) -> None: """The fallback is invisible in the tree: files/ looks like any copy.""" self.assertEqual( self.manifest()["volumes"][self.pg_volume], {"database": True, "dumped": False, "engine": "postgres"}, ) def test_the_manifest_layout_names_where_the_payload_really_landed(self) -> None: layout = self.manifest()["layout"] volume_dir = backup_path( self.backups_dir, self.repo_name, self.version, self.pg_volume ) self.assertTrue((volume_dir / layout["files_dir"]).is_dir()) self.assertEqual(layout["files_dir"], FILES_DIR) self.assertEqual(layout["sql_dir"], SQL_DIR) def test_the_manifest_states_a_schema_a_reader_can_check(self) -> None: self.assertEqual(self.manifest()["schema"], MANIFEST_SCHEMA) def test_restored_files_contain_marker(self) -> None: p = run( [ "docker", "run", "--rm", "-v", f"{self.restore_volume}:/data", "alpine:3.20", "sh", "-lc", "cat /data/marker.txt", ] ) self.assertEqual((p.stdout or "").strip(), self.marker)