Files
Jyotisha/tests/test_rehearse_production_migrations.py
T
jesse-uxandGrok (agent loop) 09906cdccc fix(release): keep owners and grants in production recovery points
Co-Authored-By: Grok (agent loop)
2026-10-14 21:01:52 +08:00

241 lines
7.8 KiB
Python

"""Safety checks for the disposable migration rehearsal. No production database."""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "deploy" / "rehearse-production-migrations.sh"
def resolve_bash() -> str:
found = shutil.which("bash")
git_bash = Path(r"D:\Program Files\Git\bin\bash.exe")
if os.name == "nt" and git_bash.exists():
lowered = (found or "").lower()
if not found or "system32" in lowered or "windowsapps" in lowered:
return str(git_bash)
if found:
return found
if git_bash.exists():
return str(git_bash)
return "bash"
BASH = resolve_bash()
def run(env: dict[str, str]) -> subprocess.CompletedProcess[str]:
full = os.environ.copy()
full.update(env)
return subprocess.run(
[BASH, str(SCRIPT)],
cwd=ROOT,
env=full,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
def test_refuses_a_non_production_path() -> None:
completed = run({"DEPLOY_PATH": "/tmp/not-production"})
assert completed.returncode == 1
assert "unexpected production path" in completed.stderr
def test_refuses_the_production_database_name() -> None:
completed = run(
{
"JYOTISHA_REHEARSAL_LOCAL": "1",
"JYOTISHA_REHEARSAL_DATABASE_URL": "postgresql://postgres:secret-value@127.0.0.1:1/jyotisha",
}
)
assert completed.returncode == 1
assert "refusing to touch the production database" in completed.stderr
assert "secret-value" not in completed.stdout
assert "secret-value" not in completed.stderr
def test_plan_keeps_the_stop_file_out_of_the_first_batch() -> None:
completed = run(
{
"JYOTISHA_REHEARSAL_LOCAL": "1",
"JYOTISHA_REHEARSAL_PLAN": "1",
"JYOTISHA_REHEARSAL_DATABASE_URL": "postgresql://postgres:secret-value@127.0.0.1:1/rehearse_plan",
}
)
assert completed.returncode == 0, completed.stderr
text = completed.stdout
assert "rehearsal_plan=1" in text
assert "database_name=rehearse_plan" in text
assert "stop_before=20260925030000_chart_subject_trust_and_limit.sql" in text
assert "stop_in_before=no" in text
values = dict(line.split("=", 1) for line in text.splitlines() if "=" in line)
assert int(values["batch_before"]) < int(values["batch_all"])
assert "secret-value" not in completed.stdout
assert "secret-value" not in completed.stderr
def test_local_url_with_query_is_refused() -> None:
completed = run(
{
"JYOTISHA_REHEARSAL_LOCAL": "1",
"JYOTISHA_REHEARSAL_DATABASE_URL": "postgresql://postgres:secret-value@127.0.0.1:1/rehearse_plan?database=jyotisha",
}
)
assert completed.returncode == 1
assert "query parameters" in completed.stderr
assert "secret-value" not in completed.stdout
assert "secret-value" not in completed.stderr
def bash_path(path: Path) -> str:
if os.name != "nt":
return str(path)
completed = subprocess.run(
[BASH, "-lc", 'cygpath -u "$1"', "cygpath", str(path)],
check=True,
capture_output=True,
text=True,
)
return completed.stdout.strip()
def test_host_mode_does_not_call_host_node_and_failed_cleanup_is_nonzero(tmp_path: Path) -> None:
bin_dir = tmp_path / "bin"
host = tmp_path / "host"
(host / ".state").mkdir(parents=True)
(host / "backups").mkdir()
dump = host / "backups" / "production-pre-migration-20261014T000000Z.dump.enc"
dump.write_bytes(b"not-a-real-dump")
(host / ".env.production.database").write_text(
"\n".join(
[
"POSTGRES_DB=jyotisha",
"POSTGRES_USER=postgres",
"POSTGRES_PASSWORD=secret-value",
"PRODUCTION_BACKUP_ENCRYPTION_KEY=not-a-real-key",
"",
]
),
encoding="utf-8",
newline="\n",
)
docker_log = tmp_path / "docker.log"
node_log = tmp_path / "node.log"
bin_dir.mkdir()
(bin_dir / "docker").write_text(
"\n".join(
[
"#!/bin/bash",
'printf "%s\\n" "$*" >> "${DOCKER_LOG:?}"',
'case "$*" in',
" *exec\\ -i*|*-i\\ *) cat >/dev/null || true ;;",
"esac",
'joined="$*"',
'case "$joined" in',
' *ps\\ *) echo container1; exit 0 ;;',
"esac",
'case "$joined" in',
" *'{{.Image}}'*) echo sha256:0000000000000000000000000000000000000000000000000000000000000000; exit 0 ;;",
"esac",
'case "$joined" in',
" *inspect*) echo jyotisha-net; exit 0 ;;",
"esac",
'case "$joined" in',
" *DROP\\ DATABASE*) exit 1 ;;",
"esac",
'case "$joined" in',
" *has_table_privilege*)",
" printf '%s\\n' 'authenticated public.profiles true' 'authenticated public.chat_sessions true' 'service_role public.chart_profiles true'",
" exit 0",
" ;;",
"esac",
'case "$joined" in',
" *count*) echo 0; exit 0 ;;",
"esac",
"exit 0",
"",
]
),
encoding="utf-8",
newline="\n",
)
(bin_dir / "node").write_text(
"\n".join(
[
"#!/bin/bash",
'printf "%s\\n" "$*" >> "${NODE_LOG:?}"',
"exit 9",
"",
]
),
encoding="utf-8",
newline="\n",
)
(bin_dir / "openssl").write_text("#!/bin/bash\nexit 0\n", encoding="utf-8", newline="\n")
unix_bin = bash_path(bin_dir)
python_bin = bash_path(Path(sys.executable))
(bin_dir / "python3").write_text(
f"#!/bin/bash\nexec '{python_bin}' \"$@\"\n",
encoding="utf-8",
newline="\n",
)
subprocess.run(
[BASH, "-lc", f'chmod +x "{unix_bin}/docker" "{unix_bin}/node" "{unix_bin}/openssl" "{unix_bin}/python3"'],
check=True,
)
full = os.environ.copy()
full.update(
{
"JYOTISHA_REHEARSAL_FAKE_HOST": "1",
"DEPLOY_PATH": bash_path(host),
"RECOVERY_DUMP": bash_path(dump),
"DOCKER_LOG": bash_path(docker_log),
"NODE_LOG": bash_path(node_log),
}
)
completed = subprocess.run(
[
BASH,
"-lc",
'export PATH="$1:$PATH"; exec bash "$2"',
"bash",
unix_bin,
bash_path(SCRIPT),
],
cwd=ROOT,
env=full,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
assert completed.returncode != 0, completed.stdout + completed.stderr
assert "演练库未删除,请手动 DROP DATABASE rehearse_" in completed.stderr
assert "secret-value" not in completed.stdout
assert "secret-value" not in completed.stderr
assert not node_log.exists()
docker_text = docker_log.read_text(encoding="utf-8")
assert "--entrypoint node" in docker_text
assert "DROP DATABASE" in docker_text
def test_restore_keeps_backup_owners_and_compares_grants() -> None:
text = SCRIPT.read_text(encoding="utf-8")
assert "--no-owner" not in text
assert "ALTER TYPE" not in text
assert "typtype" not in text
assert "revoked_grants" in text
assert "chart_profiles_service_role_select_missing" in text
assert "role%3Dschema_owner" in text
assert "grant connect, create on database" in text