mirror of
https://github.com/kevinveenbirkenbach/docker-volume-backup.git
synced 2026-08-20 21:22:54 +00:00
The package carried no ruff configuration at all, so it ran on the defaults (E4+E7+E9+F) while infinito-nexus-core, its only consumer, holds itself to a far wider selection. Measured against that selection this tree had 224 findings. It now has none. The selector list is core's verbatim so both repositories answer to one bar. target-version stays py39 rather than core's py311, because requires-python still declares >=3.9 and pyupgrade would otherwise propose syntax the declared minimum cannot run. Every ignore carries its reason: S603/S607 in particular, since running docker and dump binaries from PATH in list form is this tool's whole job and is already injection-safe. Two conversions are judgement rather than mechanics. os.path.join(dir, '') was the rsync idiom for a trailing separator, which Path drops, so it becomes an explicit os.sep. os.path.abspath stays where Path.resolve() would follow symlinks and let a symlinked volume test as inside the snapshot subject. BREAKING CHANGE: VersionMismatch is renamed VersionMismatchError. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import pandas as pd
|
|
|
|
from baudolo.backup import db as db_mod
|
|
|
|
|
|
def _df(rows):
|
|
return pd.DataFrame(rows, columns=["instance", "database", "username", "password"])
|
|
|
|
|
|
def _capture_dumps(*, db_type, rows, container, dump_tool="mariadb-dump"):
|
|
"""Every (argv, env) the dump path would have run."""
|
|
captured = []
|
|
|
|
def _capture(command, out_file, *, env=None):
|
|
captured.append((list(command), env))
|
|
|
|
with (
|
|
tempfile.TemporaryDirectory() as td,
|
|
patch.object(db_mod, "execute_to_file", side_effect=_capture),
|
|
):
|
|
db_mod.backup_database(
|
|
container=container,
|
|
volume_dir=td,
|
|
db_type=db_type,
|
|
dump_tool=dump_tool,
|
|
databases_df=_df(rows),
|
|
database_containers=[container],
|
|
)
|
|
return captured
|
|
|
|
|
|
class TestMariaDBDumpUsesTCP(unittest.TestCase):
|
|
# Regression guard for 'Access denied for user <user>@localhost' when only
|
|
# '<user>'@'%' is granted: the in-container mariadb-dump MUST force TCP so
|
|
# the connection is auth-matched against '%' instead of socket->localhost.
|
|
|
|
def test_mariadb_dump_forces_tcp_loopback(self):
|
|
captured = _capture_dumps(
|
|
db_type="mariadb",
|
|
rows=[("mariadb", "appdb", "appuser", "s3cret")],
|
|
container="mariadb",
|
|
)
|
|
self.assertEqual(len(captured), 1, f"expected one dump, got: {captured}")
|
|
|
|
argv, env = captured[0]
|
|
self.assertEqual(argv[:3], ["docker", "exec", "mariadb"])
|
|
self.assertIn("--protocol=tcp", argv)
|
|
self.assertEqual(argv[argv.index("-h") + 1], "127.0.0.1")
|
|
self.assertEqual(argv[argv.index("-u") + 1], "appuser")
|
|
self.assertIn("-ps3cret", argv)
|
|
self.assertEqual(argv[-1], "appdb")
|
|
self.assertIsNone(env)
|
|
|
|
def test_the_probed_client_is_the_one_invoked(self):
|
|
captured = _capture_dumps(
|
|
db_type="mariadb",
|
|
rows=[("mariadb", "appdb", "appuser", "s3cret")],
|
|
container="mariadb",
|
|
dump_tool="mysqldump",
|
|
)
|
|
argv, _env = captured[0]
|
|
self.assertIn("mysqldump", argv)
|
|
self.assertNotIn("mariadb-dump", argv)
|
|
|
|
def test_postgres_dump_unaffected(self):
|
|
captured = _capture_dumps(
|
|
db_type="postgres",
|
|
rows=[("pg", "appdb", "appuser", "s3cret")],
|
|
container="pg",
|
|
)
|
|
argv, _env = captured[0]
|
|
self.assertIn("pg_dump", argv)
|
|
self.assertNotIn("--protocol=tcp", argv)
|
|
|
|
def test_the_password_travels_in_the_environment_not_the_argv(self):
|
|
"""A process listing shows argv; PGPASSWORD must not be in it."""
|
|
captured = _capture_dumps(
|
|
db_type="postgres",
|
|
rows=[("pg", "appdb", "appuser", "s3cret")],
|
|
container="pg",
|
|
)
|
|
argv, env = captured[0]
|
|
self.assertEqual(env, {"PGPASSWORD": "s3cret"})
|
|
self.assertNotIn("s3cret", argv)
|
|
|
|
|
|
class TestNoShellReachesTheDump(unittest.TestCase):
|
|
def test_a_hostile_database_name_never_reaches_a_command(self):
|
|
"""validate_database refuses it, so no argv is built at all."""
|
|
with self.assertRaises(ValueError):
|
|
_capture_dumps(
|
|
db_type="postgres",
|
|
rows=[("pg", "app;rm -rf /", "appuser", "s3cret")],
|
|
container="pg",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|