from __future__ import annotations import ast import importlib.util import json import os import subprocess import sys from pathlib import Path from types import SimpleNamespace import pytest ROOT = Path(__file__).resolve().parents[2] def load_paths(): spec = importlib.util.spec_from_file_location( "controller_config_paths_test", ROOT / "ops/gitea-deploy-broker/controller_paths.py" ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def test_production_code_uses_fixed_protected_mutable_config(): paths = load_paths() code = Path("/opt/gitea-deploy") assert paths.controller_config_root(code) == Path("/controller-hub-releases/controller-config") assert paths.controller_readonly_config_root(code) == Path("/controller-config") assert paths.controller_config_root(code) != code def test_private_install_stage_keeps_its_captured_config(tmp_path): paths = load_paths() stage = tmp_path / "controller" assert paths.controller_config_root(stage) == stage assert paths.controller_readonly_config_root(stage) == stage def test_config_location_has_no_environment_override(monkeypatch): paths = load_paths() monkeypatch.setenv("ITWORX_CONTROLLER_CONFIG_ROOT", "/untrusted") monkeypatch.setenv("CONTROLLER_CONFIG_ROOT", "/untrusted") assert paths.controller_config_root(Path("/opt/gitea-deploy")) == Path( "/controller-hub-releases/controller-config" ) def test_repopulse_store_keeps_readonly_and_mutable_config_aliases(): paths = load_paths() monkeypatch_path = ROOT / "ops/gitea-deploy-broker" import sys if str(monkeypatch_path) not in sys.path: sys.path.insert(0, str(monkeypatch_path)) spec = importlib.util.spec_from_file_location( "repopulse_config_alias_test", monkeypatch_path / "repopulse_adoption_store.py" ) store = importlib.util.module_from_spec(spec) spec.loader.exec_module(store) code = Path("/opt/gitea-deploy") assert store.CONTROLLER_READONLY_ROOT == paths.controller_readonly_config_root(code) assert store.CONTROLLER_RW_ROOT == paths.controller_config_root(code) assert store.CONTROLLER_READONLY_ROOT != store.CONTROLLER_RW_ROOT def test_registry_updates_use_mutable_config_and_preserve_code(monkeypatch, tmp_path): monkeypatch.syspath_prepend(str(ROOT / "ops/gitea-deploy-broker")) import controller_paths config = tmp_path / "config" config.mkdir(mode=0o755) registry = config / "projects.json" registry.write_text("{}\n", encoding="utf-8") registry.chmod(0o600) code = ROOT / "ops/gitea-deploy-broker" original_code = (code / "status_server.py").read_bytes() monkeypatch.setattr(controller_paths, "controller_config_root", lambda _root: config) spec = importlib.util.spec_from_file_location("mutable_config_controller_test", code / "status_server.py") controller = importlib.util.module_from_spec(spec) spec.loader.exec_module(controller) assert controller.PROJECTS_PATH == registry assert controller.WEBHOOK_SECRET_ROOT == config / "webhook-secrets" assert controller.APPOPS_DEPLOYMENT_SECRET_ROOT == config / "appops-deployment-secrets" assert controller.GITEA_VALIDATION_TOKEN_FILE == config / "gitea-validation-token" assert controller.PRIVATE_TLS_CONFIG_PATH == config / "private-tls.json" assert controller.CONTROLLER_RELEASE_PATH == code / "controller-release.json" controller._write_projects({"jens/example": {"slug": "example"}}) assert controller._load_projects() == {"jens/example": {"slug": "example"}} assert registry.stat().st_mode & 0o777 == 0o600 assert (code / "status_server.py").read_bytes() == original_code def test_status_reads_hub_state_through_the_fixed_pinned_adapter(monkeypatch, tmp_path): monkeypatch.syspath_prepend(str(ROOT / "ops/gitea-deploy-broker")) import mcp_hub_release_store code = ROOT / "ops/gitea-deploy-broker" spec = importlib.util.spec_from_file_location("hub_state_controller_test", code / "status_server.py") controller = importlib.util.module_from_spec(spec) spec.loader.exec_module(controller) state = tmp_path / "state" state.mkdir() (state / "itworx-mcp-hub.json").write_text('{"live_revision":"untrusted-path"}') (state / "ordinary-project.json").write_text('{"live_revision":"ordinary"}') monkeypatch.setattr(controller, "STATE_ROOT", state) monkeypatch.setattr( mcp_hub_release_store, "read_hub_state_bytes", lambda: b'{"live_revision":"pinned"}' ) assert controller._deployment_state("itworx-mcp-hub") == {"live_revision": "pinned"} assert controller._deployment_state("ordinary-project") == {"live_revision": "ordinary"} def unsafe(): raise mcp_hub_release_store.ReleaseStoreError("state path changed") monkeypatch.setattr(mcp_hub_release_store, "read_hub_state_bytes", unsafe) assert controller._deployment_state("itworx-mcp-hub") == {} monkeypatch.setattr(mcp_hub_release_store, "read_hub_state_bytes", lambda: b'[]') assert controller._deployment_state("itworx-mcp-hub") == {} monkeypatch.setattr(mcp_hub_release_store, "read_hub_state_bytes", lambda: b'\xff') assert controller._deployment_state("itworx-mcp-hub") == {} @pytest.mark.skipif(os.name == "nt", reason="the registration script uses POSIX flock") @pytest.mark.parametrize("matching_aliases", [True, False]) def test_embedded_registry_writer_uses_protected_aliases_and_preserves_code( monkeypatch, tmp_path, matching_aliases ): from backend.app.services.deployment_target_registry import _REGISTER_SCRIPT registry = tmp_path / "protected" / "projects.json" registry.parent.mkdir(mode=0o755) registry.write_text("{}\n") registry.chmod(0o600) readonly = registry if matching_aliases else tmp_path / "different-registry.json" if not matching_aliases: readonly.write_text("{}\n") readonly.chmod(0o600) code = tmp_path / "old-code" code.mkdir() old_registry = code / "projects.json" old_registry.write_text('{"old":"must stay unchanged"}\n') original = old_registry.read_bytes() old_host_registry = tmp_path / "old-host-registry.json" old_host_registry.write_text("{}\n") repositories = tmp_path / "repositories" bare = repositories / "jens" / "example.git" bare.mkdir(parents=True) paths = { "/controller-config/projects.json": readonly, "/controller-hub-releases/controller-config/projects.json": registry, "/opt/gitea-deploy/projects.json": old_registry, "/mnt/cache/appdata/gitea-deploy-runners/controller/projects.json": old_host_registry, "/mnt/cache/appdata/gitea-deploy-runners/deploy.lock": tmp_path / "deploy.lock", "/gitea-repositories": repositories, } def fixture_path(value): return paths.get(str(value), Path(value)) revision = "a" * 40 monkeypatch.setattr( subprocess, "run", lambda *_args, **_kwargs: SimpleNamespace(returncode=0, stdout=revision + "\n") ) project = {"branch": "main", "slug": "example", "compose_files": ["docker-compose.yml"]} monkeypatch.setattr(sys, "argv", ["script", "jens/example", revision, "main", json.dumps(project)]) tree = ast.parse(_REGISTER_SCRIPT) # Redirect only filesystem paths into disposable fixtures. All production # validation, locking, backup, atomic replace and readback code executes. tree.body = [node for node in tree.body if not (isinstance(node, ast.ImportFrom) and node.module == "pathlib")] if matching_aliases: exec(compile(tree, "registry-writer", "exec"), {"Path": fixture_path}) assert json.loads(registry.read_text()) == {"jens/example": project} assert registry.stat().st_mode & 0o777 == 0o600 assert list(registry.parent.glob("projects.json.appops-backup-*")) else: with pytest.raises(SystemExit) as error: exec(compile(tree, "registry-writer", "exec"), {"Path": fixture_path}) assert error.value.code == 36 assert registry.read_text() == "{}\n" assert old_registry.read_bytes() == original assert old_host_registry.read_text() == "{}\n"