#!/usr/bin/env python3 import importlib.machinery import importlib.util import tempfile import unittest from pathlib import Path from unittest import mock SCRIPT_DIR = Path(__file__).resolve().parent RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy" def load_runner(): loader = importlib.machinery.SourceFileLoader( "nodedc_device_plane_deploy_under_test", str(RUNNER_PATH), ) spec = importlib.util.spec_from_loader(loader.name, loader) module = importlib.util.module_from_spec(spec) loader.exec_module(module) return module RUNNER = load_runner() class DevicePlaneRegistryTest(unittest.TestCase): def test_registry_has_exact_roots_project_and_stateless_services(self): component = RUNNER.COMPONENTS["device-plane"] root = Path("/volume1/docker/nodedc-device-plane") self.assertEqual(component["payload_root"], root) self.assertEqual(component["compose_root"], root) self.assertEqual(component["compose_project"], "nodedc-device-plane") self.assertEqual( component["compose_files"], (root / "docker-compose.device-plane.yml",), ) self.assertTrue(component["bootstrap_root"]) self.assertTrue(component["compose_no_deps"]) self.assertEqual( component["services"], ("device-control-core", "device-gateway"), ) self.assertNotIn("device-postgres", component["services"]) self.assertIsNone(component.get("compose_env_file")) def test_files_select_only_the_affected_stateless_services(self): self.assertEqual( RUNNER.component_services( "device-plane", ("services/device-control-core/src/server.mjs",), ), ("device-control-core",), ) self.assertEqual( RUNNER.component_services( "device-plane", ("services/device-gateway/src/server.mjs",), ), ("device-gateway",), ) self.assertEqual( RUNNER.component_services( "device-plane", ("packages/arusnavi-b2-adapter",), ), ("device-control-core", "device-gateway"), ) services = RUNNER.component_services( "device-plane", ("docker-compose.device-plane.yml",), ) self.assertEqual( services, ("device-control-core", "device-gateway"), ) self.assertNotIn("device-postgres", services) def test_build_selection_is_exact_and_database_free(self): builds = RUNNER.component_builds( "device-plane", ("services/device-gateway",), ) self.assertEqual(len(builds), 1) self.assertEqual(builds[0][0], RUNNER.DEVICE_PLANE_ROOT) self.assertIn( "services/device-gateway/Dockerfile", builds[0][1], ) self.assertIn(RUNNER.DEVICE_PLANE_GATEWAY_IMAGE, builds[0][1]) all_builds = RUNNER.component_builds( "device-plane", ("package-lock.json",), ) self.assertEqual(len(all_builds), 2) self.assertNotIn("device-postgres", " ".join( argument for _root, arguments in all_builds for argument in arguments )) def test_payload_allowlist_rejects_runtime_secrets_and_broad_paths(self): for allowed in ( ".dockerignore", "docker-compose.device-plane.yml", "packages/device-protocol-contract/src/index.mjs", "services/device-control-core/Dockerfile", "services/device-gateway/src/runtime.mjs", ): self.assertTrue( RUNNER.allowed_payload_path("device-plane", allowed), ) for rejected in ( ".env", "secrets/postgres-password", "runtime/postgres/data", "services/unknown/server.mjs", "docker-compose.yml", "services/device-control-core/start.sh", "services/device-control-core/test/app.test.mjs", ): with self.assertRaises(RUNNER.DeployError): RUNNER.allowed_payload_path("device-plane", rejected) def test_healthchecks_are_service_scoped_and_fail_closed(self): core_checks = RUNNER.component_healthchecks( "device-plane", ("services/device-control-core",), ("device-control-core",), ) self.assertEqual(len(core_checks), 1) self.assertEqual( core_checks[0]["expected_json"]["commandTransport"], "disabled", ) self.assertEqual( core_checks[0]["expected_json"]["discoveryIngest"], "disabled", ) gateway_checks = RUNNER.component_healthchecks( "device-plane", ("services/device-gateway",), ("device-gateway",), ) self.assertEqual(len(gateway_checks), 1) self.assertEqual( gateway_checks[0]["expected_json"]["publicIngress"], "disabled", ) self.assertEqual( gateway_checks[0]["expected_json"]["tcpListener"], "disabled", ) def test_runtime_secrets_are_runner_owned_and_not_manifest_selected(self): with mock.patch.object( RUNNER, "ensure_platform_runtime_secret", ) as ensure: RUNNER.prepare_component_runtime( "device-plane", ("services/device-control-core",), ) self.assertEqual( [call.args[0] for call in ensure.call_args_list], [ RUNNER.DEVICE_PLANE_POSTGRES_PASSWORD_FILE, RUNNER.DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE, RUNNER.DEVICE_PLANE_IDENTIFIER_PEPPER_FILE, ], ) def test_compose_runtime_uses_no_deps_and_never_selects_database(self): with mock.patch.object(RUNNER.subprocess, "run") as run: RUNNER.run_compose( "device-plane", ("device-control-core", "device-gateway"), ("docker-compose.device-plane.yml",), ) command = run.call_args_list[0].args[0] self.assertIn("--no-deps", command) self.assertNotIn("device-postgres", command) self.assertEqual( command[-2:], ["device-control-core", "device-gateway"], ) def test_initial_candidate_rollback_removes_only_stateless_services(self): entries = ( "docker-compose.device-plane.yml", "services/device-control-core", "services/device-gateway", ) with tempfile.TemporaryDirectory( prefix="nodedc-device-plane-rollback-", ) as directory: root = Path(directory) / "live" backup = Path(directory) / "backup" root.mkdir() backup.mkdir() (backup / "existing-files.txt").write_text("", encoding="utf-8") (backup / "missing-files.txt").write_text( "\n".join(entries) + "\n", encoding="utf-8", ) with ( mock.patch.object( RUNNER, "stop_and_remove_compose_services", ) as stop, mock.patch.object( RUNNER, "restore_platform_overlay", return_value=3, ) as restore, ): result = RUNNER.rollback_device_plane_apply( root, backup, entries, "test-stamp", True, ("device-control-core", "device-gateway"), ) stop.assert_called_once_with( "device-plane", ("device-control-core", "device-gateway"), ) restore.assert_called_once() self.assertEqual(result, "source-restored-runtime-unchanged:3") if __name__ == "__main__": unittest.main(verbosity=2)