| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- from __future__ import annotations
- import hashlib
- import json
- import os
- import subprocess
- import tarfile
- from pathlib import Path
- import pytest
- ROOT = Path(__file__).resolve().parents[1]
- COMPOSE = ROOT / "deploy/docker/docker-compose.yml"
- OPERATIONS = ROOT / "deployment/compose"
- def _compose_config() -> dict:
- completed = subprocess.run(
- [
- "docker",
- "compose",
- "-f",
- str(COMPOSE),
- "config",
- "--format",
- "json",
- ],
- cwd=ROOT,
- check=True,
- capture_output=True,
- text=True,
- )
- return json.loads(completed.stdout)
- def _tree_hashes(root: Path) -> dict[str, str]:
- hashes: dict[str, str] = {}
- for path in sorted(root.rglob("*")):
- if not path.is_file():
- continue
- relative = path.relative_to(root)
- if "__pycache__" in relative.parts or path.suffix in {".pyc", ".pyo"}:
- continue
- hashes[relative.as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
- return hashes
- def test_release_copy_matches_all_sources_of_truth():
- for source, release in (
- (ROOT / "app", ROOT / "deployment/app"),
- (ROOT / "database", ROOT / "deployment/database"),
- (ROOT / "migrations", ROOT / "deployment/migrations"),
- ):
- assert _tree_hashes(release) == _tree_hashes(source)
- for filename in ("requirements.txt", "gunicorn_config.py", "__init__.py", "alembic.ini"):
- assert (ROOT / f"deployment/{filename}").read_bytes() == (
- ROOT / filename
- ).read_bytes()
- def test_locally_built_services_have_stable_offline_image_names():
- services = _compose_config()["services"]
- expected = {
- "backend": "dataops-platform-backend:local",
- "runner": "dataops-platform-runner:local",
- "frontend": "dataops-platform-frontend:local",
- }
- for service, image in expected.items():
- assert services[service]["image"] == image
- def test_published_ports_can_be_isolated_for_recovery_rehearsal():
- source = COMPOSE.read_text(encoding="utf-8")
- expected_overrides = {
- "DATAOPS_POSTGRES_PORT": "15432",
- "SOURCE_POSTGRES_PORT": "25432",
- "SOURCE_MYSQL_PORT": "23306",
- "NEO4J_HTTP_PORT": "17474",
- "NEO4J_BOLT_PORT": "17687",
- "MINIO_API_PORT": "19000",
- "MINIO_CONSOLE_PORT": "19001",
- "N8N_PORT": "15678",
- "KESTRA_PORT": "18080",
- "RUNNER_PORT": "15600",
- "BACKEND_PORT": "15500",
- "FRONTEND_PORT": "18183",
- }
- for name, default in expected_overrides.items():
- assert f"${{{name}:-{default}}}" in source
- @pytest.mark.parametrize(
- "script_name",
- (
- "preflight.sh",
- "install_offline.sh",
- "package_offline.sh",
- "backup.sh",
- "restore.sh",
- "rollback.sh",
- ),
- )
- def test_wp13_commands_publish_help_without_mutating_state(script_name: str):
- completed = subprocess.run(
- ["bash", str(OPERATIONS / script_name), "--help"],
- cwd=ROOT,
- capture_output=True,
- text=True,
- )
- assert completed.returncode == 0, completed.stderr
- assert "Usage:" in completed.stdout
- def test_offline_package_contains_runtime_payload_and_checksums(tmp_path: Path):
- completed = subprocess.run(
- [
- "bash",
- str(OPERATIONS / "package_offline.sh"),
- "--repo-root",
- str(ROOT),
- "--output-dir",
- str(tmp_path),
- "--version",
- "contract-test",
- "--skip-images",
- ],
- cwd=ROOT,
- capture_output=True,
- text=True,
- )
- assert completed.returncode == 0, completed.stderr
- archive = tmp_path / "dataops-platform-offline-contract-test.tar.gz"
- assert archive.is_file()
- with tarfile.open(archive, "r:gz") as package:
- names = set(package.getnames())
- prefix = "dataops-platform-offline-contract-test/"
- for relative in (
- "deploy/docker/docker-compose.yml",
- "deploy/docker/.env.example",
- "database/create_data_orders_table.sql",
- "migrations/env.py",
- "operations/install_offline.sh",
- "operations/preflight.sh",
- "operations/backup.sh",
- "operations/restore.sh",
- "release-manifest.txt",
- "checksums.sha256",
- ):
- assert prefix + relative in names
- def test_preflight_rejects_modified_bundle_before_docker_access(tmp_path: Path):
- bundle = tmp_path / "bundle"
- bundle.mkdir()
- payload = bundle / "release-manifest.txt"
- payload.write_text("version=contract-test\n", encoding="utf-8")
- digest = hashlib.sha256(payload.read_bytes()).hexdigest()
- (bundle / "checksums.sha256").write_text(
- f"{digest} release-manifest.txt\n",
- encoding="utf-8",
- )
- payload.write_text("version=tampered\n", encoding="utf-8")
- env = os.environ.copy()
- env["PATH"] = "/usr/bin:/bin"
- completed = subprocess.run(
- [
- "bash",
- str(OPERATIONS / "preflight.sh"),
- "--bundle-root",
- str(bundle),
- "--checksums-only",
- ],
- cwd=ROOT,
- env=env,
- capture_output=True,
- text=True,
- )
- assert completed.returncode != 0
- assert "checksum" in (completed.stdout + completed.stderr).lower()
- def test_restore_requires_exact_project_confirmation_before_file_or_docker_access():
- completed = subprocess.run(
- [
- "bash",
- str(OPERATIONS / "restore.sh"),
- "--backup-dir",
- "/path/that/does/not/exist",
- "--project-name",
- "dataops-wp13-restore",
- "--confirm-project",
- "wrong-project",
- ],
- cwd=ROOT,
- capture_output=True,
- text=True,
- )
- assert completed.returncode != 0
- assert "confirmation" in (completed.stdout + completed.stderr).lower()
- def test_rollback_never_attempts_database_downgrade():
- source = (OPERATIONS / "rollback.sh").read_text(encoding="utf-8")
- assert "alembic downgrade" not in source
- assert "restore.sh" in source
|