Files
InstallerRobot/agent/tests/test_apt_components.py
2026-07-20 16:08:36 +07:00

457 lines
17 KiB
Python

from __future__ import annotations
import subprocess
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from app.core.command_runner import CommandError
from app.core.installer import APT_NONINTERACTIVE_ENV, AptInstaller
from app.core.manifest_validator import ManifestValidator
from app.core.service_manager import ServiceManager
from app.core.task_runner import InstalledComponentVerificationError, TaskRunner
def apt_manifest(component: dict | None = None) -> dict:
return {
"schemaVersion": "1.0",
"appId": "postgresql",
"appName": "PostgreSQL",
"version": "16",
"components": [
component
or {
"componentId": "postgresql",
"type": "apt",
"packageName": "postgresql",
"version": "16",
}
],
}
class FakeCommandRunner:
def __init__(self) -> None:
self.calls: list[tuple[list[str], int | None, dict[str, str] | None]] = []
def run(
self,
command: list[str],
timeout: int | None = None,
env: dict[str, str] | None = None,
log_output: bool = True,
) -> subprocess.CompletedProcess[str]:
self.calls.append((command, timeout, env))
return subprocess.CompletedProcess(command, 0, stdout="accepting connections\n", stderr="")
class AptManifestTests(unittest.TestCase):
def test_postgresql_is_accepted_when_allowlisted(self) -> None:
validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"])
with patch("app.core.manifest_validator.settings", validator_settings):
manifest = ManifestValidator().validate(apt_manifest())
self.assertEqual(manifest["components"][0]["type"], "apt")
self.assertEqual(manifest["components"][0]["packageName"], "postgresql")
def test_non_allowlisted_package_is_rejected(self) -> None:
validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"])
with patch("app.core.manifest_validator.settings", validator_settings):
with self.assertRaisesRegex(ValueError, "APT package is not allowed"):
ManifestValidator().validate(
apt_manifest(
{
"componentId": "curl",
"type": "apt",
"packageName": "curl",
}
)
)
def test_manifest_cannot_supply_a_shell_command(self) -> None:
validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"])
with patch("app.core.manifest_validator.settings", validator_settings):
with self.assertRaises(ValueError):
ManifestValidator().validate(
apt_manifest(
{
"componentId": "postgresql",
"type": "apt",
"packageName": "postgresql",
"command": "rm -rf /",
}
)
)
class AptInstallerTests(unittest.TestCase):
def test_apt_commands_are_fixed_argument_lists(self) -> None:
runner = FakeCommandRunner()
installer = AptInstaller(runner)
installer.update_package_index()
installer.install_package("postgresql")
installer.wait_for_postgresql(attempts=1, delay_seconds=0)
self.assertTrue(installer.is_postgresql_ready())
self.assertEqual(runner.calls[0][0], ["apt-get", "update"])
self.assertEqual(
runner.calls[1][0],
[
"apt-get",
"-o",
"Dpkg::Use-Pty=0",
"-o",
"Dpkg::Options::=--force-confdef",
"-o",
"Dpkg::Options::=--force-confold",
"install",
"--yes",
"postgresql",
],
)
self.assertEqual(runner.calls[1][2], APT_NONINTERACTIVE_ENV)
self.assertEqual(runner.calls[2][0], ["pg_isready", "--timeout=5"])
self.assertEqual(runner.calls[3][0], ["pg_isready", "--timeout=5"])
def test_discovers_concrete_service_units_without_shell_commands(self) -> None:
class PackageFileRunner(FakeCommandRunner):
def run(
self,
command: list[str],
timeout: int | None = None,
env: dict[str, str] | None = None,
log_output: bool = True,
) -> subprocess.CompletedProcess[str]:
self.calls.append((command, timeout, env))
return subprocess.CompletedProcess(
command,
0,
stdout=(
"/usr/lib/systemd/system/example.service\n"
"/lib/systemd/system/example-worker.service\n"
"/lib/systemd/system/example@.service\n"
"/usr/share/doc/example/README\n"
),
stderr="",
)
runner = PackageFileRunner()
services = AptInstaller(runner).discover_service_units("example")
self.assertEqual(services, ["example-worker.service", "example.service"])
self.assertEqual(runner.calls[0][0], ["dpkg-query", "-L", "example"])
class ServiceManagerTests(unittest.TestCase):
def test_reports_full_active_service_state(self) -> None:
class StatusRunner:
def run(self, command: list[str], timeout: int | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(
command,
0,
stdout=(
"LoadState=loaded\n"
"ActiveState=active\n"
"SubState=running\n"
"UnitFileState=enabled\n"
),
stderr="",
)
status = ServiceManager(StatusRunner()).get_service_status("example.service")
self.assertTrue(status["active"])
self.assertTrue(status["enabled"])
self.assertTrue(status["healthy"])
self.assertEqual(status["status"], "active")
self.assertEqual(status["subState"], "running")
def test_preserves_inactive_state_from_nonzero_systemctl_result(self) -> None:
class InactiveRunner:
def run(self, command: list[str], timeout: int | None = None) -> subprocess.CompletedProcess[str]:
stdout = (
"LoadState=loaded\n"
"ActiveState=inactive\n"
"SubState=dead\n"
"UnitFileState=disabled\n"
)
raise CommandError(command, 3, stdout, "")
status = ServiceManager(InactiveRunner()).get_service_status("example.service")
self.assertFalse(status["active"])
self.assertFalse(status["healthy"])
self.assertEqual(status["activeState"], "inactive")
self.assertEqual(status["unitFileState"], "disabled")
class FakeRepository:
def __init__(self) -> None:
self.component_updates: list[dict] = []
self.logs: list[tuple[str, str]] = []
self.installed_component: dict | None = None
def update_task_component(self, task_id: str, component_id: str, **fields: object) -> None:
self.component_updates.append(dict(fields))
def add_log(self, task_id: str, level: str, message: str) -> None:
self.logs.append((level, message))
def upsert_installed_component(self, app_id: str, component: dict) -> None:
self.installed_component = component
class FakeRunRepository:
def __init__(self) -> None:
self.task = {"current_component_id": "postgresql"}
self.task_updates: list[dict] = []
self.logs: list[tuple[str, str]] = []
self.installed_app_updates: list[dict] = []
def update_task(self, task_id: str, **fields: object) -> None:
self.task_updates.append(dict(fields))
def update_task_component(self, task_id: str, component_id: str, **fields: object) -> None:
return None
def add_log(self, task_id: str, level: str, message: str) -> None:
self.logs.append((level, message))
def get_task(self, task_id: str) -> dict:
return self.task
def export_manifest_hash(self, manifest: dict) -> str:
return "manifest"
def upsert_installed_app(
self,
app_id: str,
app_name: str,
version: str,
manifest_hash: str,
open_url: str | None,
status: str = "installed",
) -> None:
self.installed_app_updates.append(
{"app_id": app_id, "version": version, "status": status}
)
class FakeAptInstaller:
actions: list[str] = []
def __init__(self, command_runner: object) -> None:
self.command_runner = command_runner
def update_package_index(self) -> None:
self.actions.append("update")
def install_package(self, package_name: str) -> None:
self.actions.append(f"install:{package_name}")
def get_package_version(self, package_name: str) -> str:
self.actions.append(f"version:{package_name}")
return "16+257build1"
def wait_for_postgresql(self) -> None:
self.actions.append("pg_isready")
def discover_service_units(self, package_name: str) -> list[str]:
self.actions.append(f"discover:{package_name}")
return []
class FakeServiceManager:
actions: list[str] = []
def __init__(self, command_runner: object) -> None:
self.command_runner = command_runner
def enable_service(self, service_name: str) -> None:
self.actions.append(f"enable:{service_name}")
def start_service(self, service_name: str) -> None:
self.actions.append(f"start:{service_name}")
def assert_service_active(self, service_name: str) -> None:
self.actions.append(f"active:{service_name}")
def get_service_status(self, service_name: str) -> dict[str, object]:
self.actions.append(f"status:{service_name}")
return {
"serviceName": service_name,
"loadState": "loaded",
"activeState": "active",
"subState": "running",
"unitFileState": "enabled",
"active": True,
"enabled": True,
"healthy": True,
"status": "active",
"checkedAt": "2026-07-20T00:00:00Z",
"errorMessage": None,
}
class AptTaskRunnerTests(unittest.TestCase):
def test_only_post_install_verification_failure_marks_app_attention(self) -> None:
manifest = apt_manifest()
repository = FakeRunRepository()
runner = TaskRunner(repository)
with (
patch.object(runner, "_require_root_if_available"),
patch.object(runner, "_resolve_manifest", return_value=manifest),
patch.object(
runner,
"_install_manifest",
side_effect=InstalledComponentVerificationError("service unhealthy"),
),
):
runner.run_install("task-attention", SimpleNamespace(), "install")
self.assertEqual(
repository.installed_app_updates,
[{"app_id": "postgresql", "version": "16", "status": "attention"}],
)
self.assertEqual(repository.task_updates[-1]["status"], "failed")
def test_early_install_failure_does_not_overwrite_installed_app_version(self) -> None:
manifest = apt_manifest()
repository = FakeRunRepository()
runner = TaskRunner(repository)
with (
patch.object(runner, "_require_root_if_available"),
patch.object(runner, "_resolve_manifest", return_value=manifest),
patch.object(runner, "_install_manifest", side_effect=RuntimeError("apt update failed")),
):
runner.run_install("task-early-failure", SimpleNamespace(), "update")
self.assertEqual(repository.installed_app_updates, [])
self.assertEqual(repository.task_updates[-1]["status"], "failed")
def test_postgresql_install_verifies_service_and_readiness(self) -> None:
repository = FakeRepository()
FakeAptInstaller.actions = []
FakeServiceManager.actions = []
with (
patch("app.core.task_runner.CommandRunner", return_value=object()),
patch("app.core.task_runner.AptInstaller", FakeAptInstaller),
patch("app.core.task_runner.ServiceManager", FakeServiceManager),
):
TaskRunner(repository)._install_apt_component(
"task-1",
"postgresql-app",
{
"componentId": "postgresql",
"type": "apt",
"packageName": "postgresql",
"version": "16",
},
)
self.assertEqual(
FakeAptInstaller.actions,
[
"update",
"install:postgresql",
"version:postgresql",
"discover:postgresql",
"pg_isready",
],
)
self.assertEqual(
FakeServiceManager.actions,
[
"enable:postgresql.service",
"start:postgresql.service",
"status:postgresql.service",
],
)
self.assertEqual(repository.installed_component["version"], "16+257build1")
self.assertEqual(repository.installed_component["serviceName"], "postgresql.service")
self.assertEqual(repository.installed_component["serviceCheckStatus"], "healthy")
self.assertEqual(repository.installed_component["serviceChecks"][0]["readinessType"], "postgresql")
self.assertEqual(repository.installed_component["serviceChecks"][0]["readinessStatus"], "ready")
self.assertEqual(repository.component_updates[-1]["service_check_status"], "healthy")
self.assertEqual(repository.component_updates[-1]["current_step"], "service checks completed")
def test_generic_apt_service_is_checked_but_not_started_automatically(self) -> None:
class GenericAptInstaller(FakeAptInstaller):
def discover_service_units(self, package_name: str) -> list[str]:
self.actions.append(f"discover:{package_name}")
return ["example.service"]
class InactiveServiceManager(FakeServiceManager):
def get_service_status(self, service_name: str) -> dict[str, object]:
self.actions.append(f"status:{service_name}")
return {
"serviceName": service_name,
"loadState": "loaded",
"activeState": "inactive",
"subState": "dead",
"unitFileState": "disabled",
"active": False,
"enabled": False,
"healthy": False,
"status": "inactive",
"checkedAt": "2026-07-20T00:00:00Z",
"errorMessage": None,
}
repository = FakeRepository()
FakeAptInstaller.actions = []
FakeServiceManager.actions = []
with (
patch("app.core.task_runner.CommandRunner", return_value=object()),
patch("app.core.task_runner.AptInstaller", GenericAptInstaller),
patch("app.core.task_runner.ServiceManager", InactiveServiceManager),
):
TaskRunner(repository)._install_apt_component(
"task-2",
"example-app",
{
"componentId": "example",
"type": "apt",
"packageName": "example",
"version": "1",
},
)
self.assertNotIn("start:example.service", FakeServiceManager.actions)
self.assertNotIn("enable:example.service", FakeServiceManager.actions)
self.assertIn("status:example.service", FakeServiceManager.actions)
self.assertEqual(repository.installed_component["serviceCheckStatus"], "unhealthy")
def test_apt_package_without_service_is_marked_not_applicable(self) -> None:
repository = FakeRepository()
FakeAptInstaller.actions = []
FakeServiceManager.actions = []
with (
patch("app.core.task_runner.CommandRunner", return_value=object()),
patch("app.core.task_runner.AptInstaller", FakeAptInstaller),
patch("app.core.task_runner.ServiceManager", FakeServiceManager),
):
TaskRunner(repository)._install_apt_component(
"task-3",
"utility-app",
{
"componentId": "utility",
"type": "apt",
"packageName": "utility",
"version": "1",
},
)
self.assertEqual(repository.installed_component["serviceCheckStatus"], "not-applicable")
self.assertEqual(repository.installed_component["serviceChecks"], [])
if __name__ == "__main__":
unittest.main()