from __future__ import annotations import json import subprocess from types import SimpleNamespace import pytest from test_deploy_broker import BROKER_ROOT, _load_module @pytest.mark.parametrize("repository", ["jens/quiz-monitor", "jens/another-registered-app"]) def test_verified_registered_build_uses_controller_metadata(repository, monkeypatch): module = _load_module("registered_build_metadata", BROKER_ROOT / "deploy.py") revision = "a" * 40 metadata = {"repository": repository, "revision": revision, "tree": "b" * 40, "id": "protected-build-42", "builtAt": "2026-10-07T00:00:00Z"} monkeypatch.setenv("APPOPS_SOURCE_TREE", "stale-inherited-tree") project = {"compose_project": "existing-project", "verify_oci_revision": True, "environment": {"APPOPS_SOURCE_TREE": "wrong-tree", "APPOPS_BUILD_ID": "wrong-id", "APPOPS_BUILD_DATE": "wrong-date", "USER_SETTING": "preserved$literal"}} actual = module.deployment_environment(project, revision, repository, metadata) assert {key: actual[key] for key in ( "APPOPS_SOURCE_REVISION", "APPOPS_SOURCE_REPOSITORY", "APPOPS_SOURCE_TREE", "APPOPS_BUILD_ID", "APPOPS_BUILD_DATE", "COMPOSE_PROJECT_NAME", "USER_SETTING", )} == {"APPOPS_SOURCE_REVISION": revision, "APPOPS_SOURCE_REPOSITORY": repository, "APPOPS_SOURCE_TREE": metadata["tree"], "APPOPS_BUILD_ID": metadata["id"], "APPOPS_BUILD_DATE": metadata["builtAt"], "COMPOSE_PROJECT_NAME": "existing-project", "USER_SETTING": "preserved$literal"} def test_unverified_target_retains_existing_environment_overrides(): module = _load_module("unverified_build_metadata", BROKER_ROOT / "deploy.py") project = {"compose_project": "existing-project", "environment": { "APPOPS_SOURCE_TREE": "existing-tree", "APPOPS_BUILD_ID": "existing-id", "APPOPS_BUILD_DATE": "existing-date"}} actual = module.deployment_environment(project, "a" * 40, "jens/another-app") assert actual["APPOPS_SOURCE_TREE"] == "existing-tree" assert actual["APPOPS_BUILD_ID"] == "existing-id" assert actual["APPOPS_BUILD_DATE"] == "existing-date" def test_unbound_target_does_not_enter_legacy_build_adoption(monkeypatch): module = _load_module("unbound_build_metadata", BROKER_ROOT / "deploy.py") monkeypatch.setattr(module, "compose_command", lambda *_: pytest.fail("unbound target rendered")) module.prepare_registered_legacy_build({}, "a" * 40, "jens/quiz-monitor", {}) def test_adopted_target_environment_excludes_controller_secrets(monkeypatch): module = _load_module("isolated_legacy_environment", BROKER_ROOT / "deploy.py") monkeypatch.setenv("UNRELATED_CONTROLLER_CREDENTIAL", "private-controller-value") project = {"compose_project": "quiz-monitor", "_registered_legacy_request_id": "6f9619ff-8b86-4011-b42d-00c04fc964ff", "environment": { "OPENRGB_HOST": "preserved-value"}} environment = module.deployment_environment(project, "a" * 40, "jens/quiz-monitor") assert "UNRELATED_CONTROLLER_CREDENTIAL" not in environment assert environment["OPENRGB_HOST"] == "preserved-value" assert environment["APPOPS_SOURCE_REVISION"] == "a" * 40 def test_legacy_build_binds_archive_and_private_runtime_configuration(tmp_path, monkeypatch): module = _load_module("legacy_build_metadata", BROKER_ROOT / "deploy.py") revision = "a" * 40 repository = "jens/quiz-monitor" project = {"_registered_legacy_request_id": "6f9619ff-8b86-4011-b42d-00c04fc964ff", "_repository_path": str(tmp_path / "repository.git")} model = {"services": {"quiz-server": {"build": {"context": str(tmp_path)}}}} contexts = {"quiz-server": str(tmp_path / "verified-archive")} environment = {"OPENRGB_HOST": "preserved-value", "APPOPS_SOURCE_REVISION": revision} observed = {} def archive(repo_path, target_revision, target_project, rendered_model, source_root): observed["archive"] = (repo_path, target_revision, target_project, rendered_model, source_root) return contexts def configuration(repo, target_project, request_id, target_revision, rendered_model, context_map, supplied_environment, **kwargs): observed["configuration"] = (repo, request_id, target_revision, rendered_model, context_map, supplied_environment, kwargs) return {"overlayPath": str(tmp_path / "private-compose.json")} monkeypatch.setattr(module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"])) monkeypatch.setattr(module.subprocess, "run", lambda *_, **__: SimpleNamespace( returncode=0, stdout=json.dumps(model))) monkeypatch.setattr(module, "_load_registered_build_context", lambda: SimpleNamespace( prepare_build_contexts=archive)) monkeypatch.setattr(module, "_load_registered_legacy_adoption", lambda: SimpleNamespace( prepare_build_configuration=configuration)) monkeypatch.setattr(module, "_registered_legacy_receipt_key", lambda: b"k" * 32) module.prepare_registered_legacy_build(project, revision, repository, environment) assert observed["archive"][1:] == (revision, project, model, module.EXACT_SOURCE_ROOT) assert observed["configuration"][:6] == ( repository, project["_registered_legacy_request_id"], revision, model, contexts, environment) assert project["_registered_legacy_compose_override_file"] == str(tmp_path / "private-compose.json") assert project["_registered_legacy_compose_environment"] == environment assert project["_registered_legacy_compose_environment"] is not environment def test_failed_source_render_cannot_prepare_archive_or_runtime(tmp_path, monkeypatch): module = _load_module("failed_render_build_metadata", BROKER_ROOT / "deploy.py") project = {"_registered_legacy_request_id": "6f9619ff-8b86-4011-b42d-00c04fc964ff"} monkeypatch.setattr(module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"])) monkeypatch.setattr(module.subprocess, "run", lambda *_, **__: SimpleNamespace(returncode=1, stdout="")) monkeypatch.setattr(module, "_load_registered_build_context", lambda: pytest.fail("unsafe archive preparation")) with pytest.raises(module.DeployError, match="could not be rendered"): module.prepare_registered_legacy_build(project, "a" * 40, "jens/quiz-monitor", {}) def test_registered_metadata_and_source_ignore_git_replacement_refs(tmp_path, monkeypatch): module = _load_module("registered_original_git_objects", BROKER_ROOT / "deploy.py") monkeypatch.delenv("GIT_NO_REPLACE_OBJECTS", raising=False) def git(*arguments): return subprocess.check_output(["git", "-C", str(tmp_path), *arguments], text=True).strip() git("init", "-b", "main") git("config", "user.name", "Test") git("config", "user.email", "test@example.invalid") tracked = tmp_path / "source.txt" tracked.write_text("protected original\n") git("add", "source.txt") git("commit", "-qm", "protected source") protected, original_tree = git("rev-parse", "HEAD"), git("rev-parse", "HEAD^{tree}") tracked.write_text("substituted source\n") git("commit", "-qam", "replacement source") replacement = git("rev-parse", "HEAD") git("reset", "--hard", protected) git("replace", protected, replacement) assert git("show", f"{protected}:source.txt") == "substituted source" monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", "6f9619ff-8b86-4011-b42d-00c04fc964ff") # Restore the process variable after testing the production binding. monkeypatch.setenv("GIT_NO_REPLACE_OBJECTS", "0") project = {} module.bind_registered_legacy_request(project, "deploy") assert module.validate_commit(tmp_path / ".git", "main", protected) == protected assert module.deployment_metadata(tmp_path / ".git", protected, "jens/quiz-monitor")["tree"] == original_tree assert git("show", f"{protected}:source.txt") == "protected original" @pytest.mark.parametrize("mode,request_id", [("rollback", "6f9619ff-8b86-4011-b42d-00c04fc964ff"), ("deploy", "invalid")]) def test_invalid_registered_binding_cannot_change_git_reads(monkeypatch, mode, request_id): module = _load_module("invalid_registered_git_binding", BROKER_ROOT / "deploy.py") monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", request_id) monkeypatch.delenv("GIT_NO_REPLACE_OBJECTS", raising=False) with pytest.raises(module.DeployError, match="binding is invalid"): module.bind_registered_legacy_request({}, mode) assert "GIT_NO_REPLACE_OBJECTS" not in module.os.environ def test_registered_compose_promotion_keeps_sealed_local_image(tmp_path, monkeypatch): module = _load_module("registered_no_mutable_pull", BROKER_ROOT / "deploy.py") project = {"source_dir": str(tmp_path), "_registered_legacy_request_id": "6f9619ff-8b86-4011-b42d-00c04fc964ff"} commands, events = [], [] monkeypatch.setattr(module, "deployment_environment", lambda *_: {}) monkeypatch.setattr(module, "prepare_registered_legacy_build", lambda *_: None) monkeypatch.setattr(module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"])) monkeypatch.setattr(module, "prepare_compose_migration", lambda *_: []) monkeypatch.setattr(module, "workmesh_transition_enabled", lambda *_: False) monkeypatch.setattr(module, "_registered_legacy_receipt_key", lambda: b"k" * 32) monkeypatch.setattr(module, "_load_registered_legacy_adoption", lambda: SimpleNamespace( prepare_candidate_deployment=lambda *_, **__: events.append("sealed"), verify_candidate_pre_mutation=lambda *_, **__: events.append("checked"), record_candidate_runtime=lambda *_, **__: events.append("observed"))) def run(command, **_): commands.append(command) if "up" in command: assert events == ["sealed", "checked"] monkeypatch.setattr(module, "run", run) module.deploy_project(project, "a" * 40, "jens/quiz-monitor") assert commands[-1] == ["docker", "compose", "up", "--detach", "--no-build", "--pull", "never"] assert events[-1] == "observed"