from __future__ import annotations import hashlib import http.client import importlib.util import io import json import subprocess import sys import threading from contextlib import redirect_stdout from pathlib import Path from types import ModuleType import pytest REPO_ROOT = Path(__file__).resolve().parents[2] BROKER_DIR = REPO_ROOT / "ops" / "gitea-deploy-broker" def _load_module(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) assert spec and spec.loader module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module @pytest.fixture def storage_effects(): _load_module("repopulse_adoption_store", BROKER_DIR / "repopulse_adoption_store.py") return _load_module( "repopulse_adoption_storage_effects_test", BROKER_DIR / "repopulse_adoption_storage_effects.py", ) def _run_git(*args: str, cwd: Path | None = None) -> str: result = subprocess.run( ["git", *args], cwd=cwd, check=True, capture_output=True, text=True, ) return result.stdout.strip() def _bare_repo_with_helper(tmp_path: Path, helper: bytes) -> tuple[Path, str]: worktree = tmp_path / "worktree" bare_repo = tmp_path / "repositories" / "jens" / "repopulse.git" worktree.mkdir() bare_repo.parent.mkdir(parents=True) _run_git("init", "--bare", str(bare_repo)) _run_git("init", "--initial-branch=main", str(worktree)) _run_git("config", "user.name", "Storage effects test", cwd=worktree) _run_git("config", "user.email", "storage-effects@example.invalid", cwd=worktree) helper_path = worktree / "scripts" / "repopulse_db_migration.py" helper_path.parent.mkdir() helper_path.write_bytes(helper) _run_git("add", "scripts/repopulse_db_migration.py", cwd=worktree) _run_git("commit", "-m", "Add reviewed migration helper", cwd=worktree) _run_git("remote", "add", "origin", str(bare_repo), cwd=worktree) _run_git("push", "origin", "main", cwd=worktree) return bare_repo, _run_git("rev-parse", "HEAD", cwd=worktree) def test_maintenance_http_error_is_sanitized(storage_effects): with pytest.raises(storage_effects.StorageEffectsError) as error: storage_effects._maintenance_is_drained( { "before": { "status": 200, "body": { "enabled": False, "queue_depth": 0, "active_jobs": 0, }, }, "enter": {"status": [], "body": None}, "status": {"status": 200, "body": {"enabled": True}}, } ) assert error.value.code == "maintenance_response_invalid" def test_maintenance_queue_precheck_avoids_freezing_queued_work( storage_effects, monkeypatch ): token = "test-maintenance-token-value-0123456789" calls: list[tuple[str, str]] = [] class Response: status = 200 def __init__(self, payload): self.payload = json.dumps(payload).encode("utf-8") def read(self, _limit): return self.payload class Connection: def __init__(self, _host, *args, **kwargs): assert args == (8000,) assert kwargs["timeout"] == 5 def request(self, method, path, **_kwargs): calls.append((method, path)) def getresponse(self): if len(calls) == 1: return Response( { "enabled": False, "queue_depth": 2, "active_jobs": 0, "active_drained": True, "drained": False, } ) return Response( { "enabled": True, "queue_depth": 2, "active_jobs": 0, "active_drained": True, "drained": False, } ) def close(self): return None monkeypatch.setattr(http.client, "HTTPConnection", Connection) monkeypatch.setattr(sys, "stdin", io.StringIO(token)) output = io.StringIO() with redirect_stdout(output): exec( # noqa: S102 - exercise the exact fixed in-container exchange script storage_effects._REMOTE_MAINTENANCE_EXCHANGE, {"__name__": "__main__"} ) response = json.loads(output.getvalue()) assert calls == [("GET", storage_effects.MAINTENANCE_STATUS_PATH)] assert response["enter"] is None assert token not in output.getvalue() def test_legacy_main_revision_mismatch_stops_before_loading_helper( storage_effects, tmp_path, monkeypatch ): helper_bytes = b"# reviewed test helper\n" bare_repo, actual_revision = _bare_repo_with_helper(tmp_path, helper_bytes) source_root = tmp_path / "sources" / "repopulse" helper_source = source_root / "scripts" / "repopulse_db_migration.py" helper_source.parent.mkdir(parents=True) helper_source.write_bytes(helper_bytes) expected_revision = "0" * 40 assert actual_revision != expected_revision core = type("GitCore", (), {})() core.repository_path = lambda _repository, _project: bare_repo core.output = lambda command: subprocess.run( command, check=True, capture_output=True, text=True, ).stdout.strip() core.validate_commit = lambda repo, branch, revision: storage_effects._fixed_core( None ).validate_commit(repo, branch, revision) monkeypatch.setattr(storage_effects, "EXACT_SOURCE_ROOT", source_root.parent) monkeypatch.setattr(storage_effects._store, "_ROOT_UID", source_root.stat().st_uid) def controller_directory(path: Path, *, root_owned: bool = False): storage_effects._store._assert_no_symlink_components(path) info = path.lstat() assert info.st_mode return info monkeypatch.setattr(storage_effects, "_validate_directory", controller_directory) monkeypatch.setattr( storage_effects, "_validate_git_repository_directory", lambda path: path.lstat(), ) monkeypatch.setattr( storage_effects._store, "_safe_directory", lambda path, **_kwargs: ( storage_effects._store._assert_no_symlink_components(path) or path.lstat() ), ) with pytest.raises(storage_effects.StorageEffectsError) as error: storage_effects._load_reviewed_migration_helper( core, {"source_dir": str(source_root)}, expected_revision, ) assert error.value.code == "main_revision_mismatch" def test_oversized_helper_is_rejected_before_git_returns_blob( storage_effects, tmp_path, monkeypatch ): helper_bytes = b"#" * (storage_effects._HELPER_MAX_BYTES + 1) bare_repo, revision = _bare_repo_with_helper(tmp_path, helper_bytes) real_run = subprocess.run commands: list[list[str]] = [] def recording_run(args, *positional, **keyword): commands.append(list(args)) return real_run(args, *positional, **keyword) monkeypatch.setattr(storage_effects.subprocess, "run", recording_run) with pytest.raises(storage_effects.StorageEffectsError) as error: storage_effects._read_git_blob(bare_repo, revision) assert error.value.code == "reviewed_migration_helper_invalid" assert len(commands) == 1 assert commands[0][2:4] == ["cat-file", "-s"] def test_legacy_maintenance_404_blocks_stop(storage_effects, tmp_path, monkeypatch): revision = "a" * 40 container_id = "b" * 64 image_id = "sha256:" + "c" * 64 image_ref = "ghcr.io/jens/repopulse:legacy" token = "fixed-maintenance-token-value-0123456789" intent_id = "rpa-0123456789abcdef0123" binding = "d" * 64 container_id_sha256 = hashlib.sha256(container_id.encode("ascii")).hexdigest() image_sha256 = hashlib.sha256(image_ref.encode("utf-8")).hexdigest() class Store: def _lock_identity(self): return ("project-lock", "global-lock") def _transaction_owner(self, _identity): return threading.get_ident() def load_intent(self): return { "intentId": intent_id, "intentBindingSha256": binding, "status": "in_progress", "phase": "target_gates_ready", } class Core: def __init__(self): self.commands: list[list[str]] = [] def inspect_container(self, _name): return { "Id": container_id, "Name": "/repopulse", "Image": image_id, "Config": { "Image": image_ref, "Env": [f"REPOPULSE_MAINTENANCE_TOKEN={token}"], "Labels": { "com.docker.compose.project": "repopulse", "org.opencontainers.image.revision": revision, }, }, "State": { "Status": "running", "Running": True, "Health": {"Status": "healthy"}, }, "Mounts": [ { "Type": "volume", "Name": "repopulse_data", "Destination": "/data", "Source": str(storage_effects.LEGACY_VOLUME_MOUNTPOINT), "RW": True, } ], } def inspect_image(self, _image_id): return { "Id": image_id, "Config": { "Labels": { "org.opencontainers.image.revision": revision, "org.opencontainers.image.source": "jens/repopulse", } }, } def run(self, command): self.commands.append(list(command)) core = Core() effects = storage_effects.FixedRepoPulseStorageEffects( store=Store(), deployment_core=core ) monkeypatch.setattr( effects, "_project_and_helper", lambda _intent, _revision: ({}, ModuleType("unused"), "e" * 64), ) monkeypatch.setattr( storage_effects, "_private_environment_facts", lambda: (token, True, True) ) monkeypatch.setattr( storage_effects, "_private_record_path", lambda _name, _intent: tmp_path / "unused-receipt.json", ) monkeypatch.setattr(storage_effects, "_read_private_record", lambda *_args: None) monkeypatch.setattr( storage_effects, "_assert_legacy_volume_retained", lambda _core: storage_effects.LEGACY_VOLUME_MOUNTPOINT, ) monkeypatch.setattr( storage_effects, "_maintenance_exchange", lambda *_args: { "before": {"status": 404, "body": None}, "enter": None, "status": None, }, ) with pytest.raises(storage_effects.StorageEffectsError) as error: effects.ensure_quiesced( intent_id=intent_id, intent_binding_sha256=binding, expected_main_revision=revision, expected_container_id_sha256=container_id_sha256, expected_container_image_sha256=image_sha256, ) assert error.value.code == "maintenance_endpoint_unavailable" assert core.commands == []