Files
Unraid-AppOps-Gateway-Public/backend/tests/test_deployment_target_registry.py
T

240 lines
9.2 KiB
Python

from types import SimpleNamespace
from backend.app.services.deployment_target_registry import DeploymentTargetRegistry
class FakeResponse:
def __init__(self, payload):
self.payload = payload
def json(self):
return self.payload
class FakePortainer:
def __init__(self, exit_code=0):
self.exit_code = exit_code
self.calls = []
def get_container_status(self, name):
assert name == "gitea-deploy-control"
return {
"status": "ok",
"container": {
"id": "controller-id",
"name": "gitea-deploy-control",
"state": "running",
},
}
def _config(self):
return SimpleNamespace(endpoint_id="3")
def _request(self, method, path, **kwargs):
self.calls.append((method, path, kwargs))
if path.endswith("/containers/controller-id/exec"):
return FakeResponse({"Id": "exec-1"})
if path.endswith("/exec/exec-1/start"):
return FakeResponse({})
if path.endswith("/exec/exec-1/json"):
return FakeResponse({"Running": False, "ExitCode": self.exit_code})
raise AssertionError(path)
def test_rejects_non_jens_repository_without_portainer_access():
fake = FakePortainer()
result = DeploymentTargetRegistry(fake).register_single_container(
"other/CertScope", "a" * 40
)
assert result["status"] == "invalid"
assert fake.calls == []
def test_rejects_non_sha_without_portainer_access():
fake = FakePortainer()
result = DeploymentTargetRegistry(fake).register_single_container(
"Jens/CertScope", "main"
)
assert result["status"] == "invalid"
assert fake.calls == []
def test_derives_bounded_single_container_topology_and_uses_fixed_exec():
fake = FakePortainer()
revision = "b" * 40
result = DeploymentTargetRegistry(fake).register_single_container(
"Jens/CertScope", revision
)
assert result == {
"status": "registered",
"repository": "jens/certscope",
"revision": revision,
"branch": "main",
"slug": "certscope",
"compose_file": "docker-compose.yml",
"compose_project": "certscope",
"container": "certscope",
"containers": ["certscope"],
}
create = fake.calls[0]
assert create[0] == "POST"
assert create[1].endswith("/containers/controller-id/exec")
command = create[2]["json"]["Cmd"]
assert command[:2] == ["python3", "-c"]
assert command[3:6] == ["jens/certscope", revision, "main"]
assert '"compose_files":["docker-compose.yml"]' in command[6]
assert '"source_dir":"/mnt/cache/appdata/gitea-deploy-runners/sources/certscope"' in command[6]
assert '"containers":["certscope"]' in command[6]
script = command[2]
assert '/mnt/cache/appdata/gitea-deploy-runners/controller/projects.json' in script
assert '/srv/example/appdata/gitea-deploy-runners/deploy.lock' not in script
assert '/mnt/cache/appdata/gitea-deploy-runners/deploy.lock' in script
assert 'os.path.samefile(path, host_path)' in script
assert 'refs/heads/{branch}' in script
assert create[2]["json"]["AttachStdout"] is False
assert create[2]["json"]["AttachStderr"] is False
def test_supports_master_branch_and_bounded_multi_container_topology():
fake = FakePortainer()
revision = "e" * 40
result = DeploymentTargetRegistry(fake).register_compose_target(
"Jens/Mediarr",
revision,
branch="master",
compose_file="compose.yaml",
compose_project="mediarr",
containers=["mediarr-api-1", "mediarr-web-1"],
)
assert result["status"] == "registered"
assert result["branch"] == "master"
assert result["compose_file"] == "compose.yaml"
assert result["container"] is None
assert result["containers"] == ["mediarr-api-1", "mediarr-web-1"]
command = fake.calls[0][2]["json"]["Cmd"]
assert command[3:6] == ["jens/mediarr", revision, "master"]
def test_rejects_branch_traversal_and_unsafe_compose_path_before_portainer_access():
fake = FakePortainer()
result = DeploymentTargetRegistry(fake).register_compose_target(
"Jens/Mediarr",
"e" * 40,
branch="../main",
compose_file="../compose.yml",
containers=["mediarr"],
)
assert result["status"] == "invalid"
assert fake.calls == []
def test_reports_existing_conflicting_target_without_exposing_exec_output():
fake = FakePortainer(exit_code=23)
result = DeploymentTargetRegistry(fake).register_single_container(
"jens/eventlens", "c" * 40
)
assert result == {
"status": "conflict",
"reason": "repository already has a different deployment target",
}
def test_reports_bounded_controller_preflight_and_mutation_reasons_without_exec_output():
expected = {
20: "deployment projects registry is unavailable or unsafe",
21: "deployment projects registry is not a regular file",
22: "deployment projects registry does not contain an object",
24: "mounted bare repository is unavailable",
25: "exact revision cannot be resolved in the mounted bare repository",
26: "default branch cannot be resolved in the mounted bare repository",
27: "exact revision is not an ancestor of the default branch in the mounted bare repository",
28: "configured Compose file is absent at the exact revision",
29: "deployment registry lock file cannot be opened",
30: "deployment registry lock cannot be acquired",
31: "deployment projects registry cannot be read as JSON",
32: "deployment projects registry backup cannot be created",
33: "deployment projects registry replacement file cannot be durably written",
34: "deployment projects registry replacement cannot be installed atomically",
35: "deployment projects registry directory durability sync failed after replacement",
36: "deployment projects registry host alias is unavailable or does not match the controller registry",
37: "controller project branch does not match the requested default branch",
38: "controller project Compose contract is invalid",
}
for exit_code, reason in expected.items():
fake = FakePortainer(exit_code=exit_code)
result = DeploymentTargetRegistry(fake).register_single_container(
"jens/eventlens", "d" * 40
)
assert result == {"status": "validation_failed", "reason": reason}
def test_existing_target_may_preserve_only_bounded_root_owned_runtime_inputs():
fake = FakePortainer()
revision = "f" * 40
result = DeploymentTargetRegistry(fake).register_compose_target(
"Jens/Mediarr",
revision,
branch="master",
compose_file="docker-compose.unraid.yml",
compose_project="mediarr",
containers=["mediarr"],
verify_oci_revision=True,
)
assert result["status"] == "registered"
script = fake.calls[0][2]["json"]["Cmd"][2]
assert '"env_source"' in script
assert '"environment"' in script
assert '"gitea_repository_id"' in script
assert '"gitea_webhook_secret_file"' in script
assert '"validation"' in script
assert "extra_keys.issubset(allowed_extra_keys)" in script
assert 'env_source.startswith("/srv/example/appdata/")' in script
assert "not isinstance(environment, dict) or len(environment) > 64" in script
assert 'expected_secret_file = repository.replace("/", "__")' in script
assert '"Managed validation / full (push)"' in script
assert '"timeout_seconds": 3600 if repository == "jens/mediarr" else 1800' in script
def test_existing_target_compatibility_still_rejects_unknown_or_behavioral_metadata():
fake = FakePortainer()
DeploymentTargetRegistry(fake).register_single_container(
"Jens/EventLens", "a" * 40
)
script = fake.calls[0][2]["json"]["Cmd"][2]
line = next(
value for value in script.splitlines()
if "allowed_extra_keys =" in value
)
assert '"deploy_script"' not in line
assert '"manual"' not in line
assert '"live_probe"' not in line
def test_runnerless_validation_compatibility_is_fixed_push_only():
fake = FakePortainer()
DeploymentTargetRegistry(fake).register_single_container(
"Jens/EventLens", "b" * 40
)
script = fake.calls[0][2]["json"]["Cmd"][2]
assert '"validation_gate_version": 1' in script
assert '"required": True' in script
assert '"contexts": ["Managed validation / full (push)"]' in script
assert '"timeout_seconds": 3600 if repository == "jens/mediarr" else 1800' in script
assert '"poll_interval_seconds": 5' in script
assert '"max_poll_interval_seconds": 30' in script
assert "if validation != expected_validation" in script
def test_runnerless_compatibility_requires_the_complete_metadata_triplet():
fake = FakePortainer()
DeploymentTargetRegistry(fake).register_single_container(
"Jens/EventLens", "c" * 40
)
script = fake.calls[0][2]["json"]["Cmd"][2]
assert 'runnerless_keys = {' in script
assert '"gitea_repository_id"' in script
assert '"gitea_webhook_secret_file"' in script
assert '"validation"' in script
assert "present_runnerless_keys and present_runnerless_keys != runnerless_keys" in script