#!/usr/bin/env python3 import importlib.machinery import importlib.util import hashlib import inspect import json import os import subprocess import tarfile import tempfile import unittest from pathlib import Path from unittest import mock SCRIPT_DIR = Path(__file__).resolve().parent PLATFORM_ROOT = SCRIPT_DIR.parent.parent ENGINE_ROOT = PLATFORM_ROOT.parent / "NODEDC_ENGINE_INFRA" RUNNER_PATH = Path( os.environ.get("NODEDC_DEPLOY_RUNNER_UNDER_TEST", SCRIPT_DIR / "nodedc-deploy") ).resolve() BUILDER_PATH = SCRIPT_DIR / "build-engine-n8n-private-extension-artifact.mjs" STAGE_ARTIFACT = ( PLATFORM_ROOT / "infra/deploy-artifacts" / "nodedc-n8n-private-extension-n8n-nodes-ndc-provider-rotating-access-20260718-006.tgz" ) PREDECESSOR_ARTIFACT = ( PLATFORM_ROOT / "infra/deploy-artifacts" / "nodedc-engine-n8n-private-extension-20260717-005.tgz" ) TRANSITION_ID = "20260718-008" SEALED_RELEASE_ID = "0.1.4-59dc9f7882721d6a" SEALED_PACKAGE_SHA256 = "59dc9f7882721d6a5e2ae84ffa6aa8cd9bc5432f5904d24e70e363573ed2757f" PREDECESSOR_RELEASE_ID = "0.1.3-3354149b5245e39a" BUILDER_ENGINE_PATHS = ( "nodedc-source/server/assets/n8n/schema/v2.3.2/nodes.catalog.json", "nodedc-source/server/assets/n8n/schema/v2.3.2/credentials.catalog.json", "nodedc-source/server/assets/n8n/schema/v2.3.2/meta.json", "nodedc-source/server/assets/n8n/icons/ndc.svg", "nodedc-source/server/assets/n8n/icons/ndc.dark.svg", "nodedc-source/services/n8n/private-extensions/ndc-activation.json", "nodedc-source/services/n8n/private-extensions/docker-compose.ndc-private-extension.yml", ) EXPECTED_NODES = [ "n8n-nodes-ndc.ndcDataProductPublish", "n8n-nodes-ndc.ndcDataProductRead", "n8n-nodes-ndc.ndcFoundryBinding", ] EXPECTED_CREDENTIALS = [ "ndcDataProductWriterApi", "ndcDataProductReaderApi", "ndcFoundryBindingApi", "ndcProviderRotatingAccessApi", ] PREDECESSOR_CREDENTIALS = EXPECTED_CREDENTIALS[:3] PRIVATE_EXTENSION_CANON_FUNCTION_SHA256 = { "expected_engine_n8n_compose_override": "8ee93f3e7c407f5557c711119236449a440d73a7fcac1bbfa50a09e6a13c1cff", "engine_n8n_package_loader_probe_script": "2b3b3c96fad6e5e48ebea74426b21bc7eea6b6fe0e786b537da3366d18aedd9b", "engine_n8n_private_loader_catalog": "a2ae389b4ef215900cf967b863d01056bd2a8eec55554566d0f6005e4e0bcbac", "validate_engine_n8n_base_compose_source": "adacc5b20e52684cbc427362ddba5a6d2e13ef2091a89859e2b8f7bbf2e20458", "preflight_engine_n8n_transition": "25bf33b3be0916a9dd82f064a710060eaf6bd7d87b3614ee0065af07ddbb446e", "accept_engine_n8n_runtime": "bc49275adb2d5ab4fd98abd07b9059a197d023e03b40145b6fc910b70b2f738b", } def load_runner(): loader = importlib.machinery.SourceFileLoader("nodedc_engine_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 def snapshot_engine_builder_paths(): snapshot = {} for relative in BUILDER_ENGINE_PATHS: path = ENGINE_ROOT / relative try: path_stat = path.lstat() except FileNotFoundError: snapshot[relative] = None continue if path.is_symlink(): content = ("symlink", os.readlink(path)) elif path.is_file(): content = ("file", path.read_bytes()) else: content = ("other", None) snapshot[relative] = ( path_stat.st_mode, path_stat.st_uid, path_stat.st_gid, path_stat.st_ino, path_stat.st_mtime_ns, content, ) return snapshot RUNNER = load_runner() class EngineN8nPrivateExtensionTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.temporary = tempfile.TemporaryDirectory(prefix="nodedc-engine-n8n-policy-") cls.root = Path(cls.temporary.name) cls.results = [] cls.engine_snapshot_before = snapshot_engine_builder_paths() for index in range(2): output = cls.root / f"build-{index}" output.mkdir() env = os.environ.copy() env.pop("NODEDC_N8N_TRANSITION_ID", None) env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(output) env["NODEDC_N8N_EXTENSION_STAGE_ARTIFACT"] = str(STAGE_ARTIFACT) env["NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT"] = str(PREDECESSOR_ARTIFACT) command = ["node", str(BUILDER_PATH)] if index == 0: command.append(TRANSITION_ID) else: env["NODEDC_N8N_TRANSITION_ID"] = TRANSITION_ID result = subprocess.run( command, cwd=PLATFORM_ROOT, env=env, check=True, capture_output=True, text=True, ) cls.results.append(json.loads(result.stdout)) cls.engine_snapshot_after = snapshot_engine_builder_paths() @classmethod def tearDownClass(cls): cls.temporary.cleanup() def artifact(self, build_index, kind): return Path(self.results[build_index][kind]["artifact"]) def test_engine_artifacts_are_byte_reproducible(self): for kind in ("activation", "rollback"): first = self.artifact(0, kind).read_bytes() second = self.artifact(1, kind).read_bytes() self.assertEqual(first, second) self.assertEqual(self.results[0][kind]["sha256"], self.results[1][kind]["sha256"]) self.assertEqual(first[4:8], b"\0\0\0\0") self.assertEqual(first[3] & 0x08, 0) def test_state_aware_upgrade_runner_functions_are_frozen(self): actual = { name: hashlib.sha256(inspect.getsource(getattr(RUNNER, name)).encode("utf-8")).hexdigest() for name in PRIVATE_EXTENSION_CANON_FUNCTION_SHA256 } self.assertEqual(actual, PRIVATE_EXTENSION_CANON_FUNCTION_SHA256) def test_explicit_transition_id_does_not_mutate_sealed_release_identity(self): expected_ids = { "activation": f"engine-n8n-private-extension-{TRANSITION_ID}", "rollback": f"engine-n8n-private-extension-rollback-{TRANSITION_ID}", } for result in self.results: self.assertEqual(result["transitionId"], TRANSITION_ID) self.assertEqual(result["releaseId"], SEALED_RELEASE_ID) self.assertEqual(result["packageSha256"], SEALED_PACKAGE_SHA256) for kind, expected_id in expected_ids.items(): self.assertEqual(result[kind]["id"], expected_id) self.assertEqual(Path(result[kind]["artifact"]).name, f"nodedc-{expected_id}.tgz") def test_builder_does_not_mutate_engine_source(self): self.assertEqual(self.engine_snapshot_after, self.engine_snapshot_before) def test_transition_id_rejects_missing_invalid_and_previously_issued_values(self): cases = ( ([], "transition_id_required"), (["20260718-007"], "transition_id_already_issued"), (["engine-n8n-private-extension-20260718-008"], "transition_id_invalid"), (["20260230-004"], "transition_id_invalid"), (["20260717-000"], "transition_id_invalid"), ([TRANSITION_ID, "unexpected-second-id"], "transition_id_argument_count_invalid"), ) for arguments, expected_error in cases: with self.subTest(arguments=arguments): env = os.environ.copy() env.pop("NODEDC_N8N_TRANSITION_ID", None) result = subprocess.run( ["node", str(BUILDER_PATH), *arguments], cwd=PLATFORM_ROOT, env=env, check=False, capture_output=True, text=True, ) self.assertNotEqual(result.returncode, 0) self.assertIn(expected_error, result.stderr) def test_existing_transition_artifact_is_never_overwritten(self): env = os.environ.copy() env.pop("NODEDC_N8N_TRANSITION_ID", None) env["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(self.artifact(0, "activation").parent) env["NODEDC_N8N_EXTENSION_STAGE_ARTIFACT"] = str(STAGE_ARTIFACT) env["NODEDC_N8N_EXTENSION_PREDECESSOR_ARTIFACT"] = str(PREDECESSOR_ARTIFACT) result = subprocess.run( ["node", str(BUILDER_PATH), TRANSITION_ID], cwd=PLATFORM_ROOT, env=env, check=False, capture_output=True, text=True, ) self.assertNotEqual(result.returncode, 0) self.assertIn("transition_artifact_already_exists", result.stderr) def test_activation_and_rollback_pass_strict_runner_policy(self): expected = {"activation": ("activate", 7), "rollback": ("activate", 7)} for kind, (action, entry_count) in expected.items(): with self.subTest(kind=kind), tempfile.TemporaryDirectory() as directory: manifest, entries, payload = RUNNER.load_artifact( self.artifact(0, kind), Path(directory), ) descriptor = RUNNER.read_engine_n8n_transition_descriptor( payload / RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL ) self.assertEqual(manifest["component"], "engine") self.assertEqual(descriptor["action"], action) self.assertEqual(len(entries), entry_count) self.assertEqual(RUNNER.component_services("engine", entries), ("n8n",)) self.assertEqual(RUNNER.component_builds("engine", entries), ()) self.assertFalse(RUNNER.component_publish_dist("engine", entries)) def test_activation_catalog_is_exact_three_without_tool_variants(self): with tempfile.TemporaryDirectory() as directory: _manifest, _entries, payload = RUNNER.load_artifact( self.artifact(0, "activation"), Path(directory), ) nodes = json.loads((payload / RUNNER.ENGINE_N8N_NODES_CATALOG_REL).read_text()) credentials = json.loads((payload / RUNNER.ENGINE_N8N_CREDENTIALS_CATALOG_REL).read_text()) private_nodes = [item for item in nodes if item.get("name", "").startswith("n8n-nodes-ndc.")] private_credentials = [item for item in credentials if item.get("name") in EXPECTED_CREDENTIALS] self.assertEqual([item["name"] for item in private_nodes], EXPECTED_NODES) self.assertEqual([item["name"] for item in private_credentials], EXPECTED_CREDENTIALS) self.assertTrue(all("usableAsTool" not in item for item in private_nodes)) self.assertEqual((len(nodes), len(credentials)), (437, 389)) def test_rollback_catalog_restores_verified_predecessor_release(self): with tempfile.TemporaryDirectory() as directory: _manifest, _entries, payload = RUNNER.load_artifact( self.artifact(0, "rollback"), Path(directory), ) nodes = json.loads((payload / RUNNER.ENGINE_N8N_NODES_CATALOG_REL).read_text()) credentials = json.loads((payload / RUNNER.ENGINE_N8N_CREDENTIALS_CATALOG_REL).read_text()) descriptor = RUNNER.read_engine_n8n_transition_descriptor( payload / RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL ) self.assertEqual(descriptor["releaseId"], PREDECESSOR_RELEASE_ID) self.assertEqual(descriptor["expectedCurrent"], SEALED_RELEASE_ID) self.assertEqual( [item.get("name") for item in nodes if item.get("name", "").startswith("n8n-nodes-ndc.")], EXPECTED_NODES, ) self.assertEqual( [item.get("name") for item in credentials if item.get("name") in PREDECESSOR_CREDENTIALS], PREDECESSOR_CREDENTIALS, ) self.assertEqual((len(nodes), len(credentials)), (437, 388)) def test_compose_override_is_runner_derived_and_offline(self): with tempfile.TemporaryDirectory() as directory: _manifest, _entries, payload = RUNNER.load_artifact( self.artifact(0, "activation"), Path(directory), ) descriptor = RUNNER.read_engine_n8n_transition_descriptor( payload / RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL ) override = (payload / RUNNER.ENGINE_N8N_COMPOSE_OVERRIDE_REL).read_text() self.assertEqual(override, RUNNER.expected_engine_n8n_compose_override(descriptor)) self.assertEqual( hashlib.sha256(override.encode("utf-8")).hexdigest(), "d6d1249e93b5bed5e697715b6b773c7efa4ac234f12eab57d868407e6bb5390e", ) self.assertIn("pull_policy: never", override) self.assertIn("N8N_USER_FOLDER: /home/node", override) self.assertNotIn("NODE_PATH", override) self.assertIn(":/home/node/.n8n/nodes/node_modules/n8n-nodes-ndc:ro", override) self.assertNotIn("N8N_CUSTOM_EXTENSIONS", override) self.assertNotIn("build:", override) def test_upgrade_override_is_accepted_as_installed_canon(self): original_root = RUNNER.COMPONENTS["engine"]["payload_root"] original_compose = RUNNER.COMPONENTS["engine"]["compose_root"] try: with tempfile.TemporaryDirectory() as directory: work = Path(directory) extracted = work / "artifact" extracted.mkdir() _manifest, _entries, payload = RUNNER.load_artifact( self.artifact(0, "activation"), extracted, ) engine_root = work / "engine" engine_root.mkdir() (engine_root / "docker-compose.yml").write_text("services:\n", encoding="utf-8") for relative in ( RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL, RUNNER.ENGINE_N8N_COMPOSE_OVERRIDE_REL, ): target = engine_root / relative target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes((payload / relative).read_bytes()) RUNNER.COMPONENTS["engine"]["payload_root"] = engine_root RUNNER.COMPONENTS["engine"]["compose_root"] = engine_root compose_files = RUNNER.component_compose_files("engine") self.assertEqual(compose_files, ( engine_root / "docker-compose.yml", engine_root / RUNNER.ENGINE_N8N_COMPOSE_OVERRIDE_REL, )) finally: RUNNER.COMPONENTS["engine"]["payload_root"] = original_root RUNNER.COMPONENTS["engine"]["compose_root"] = original_compose def test_loader_probe_pins_n8n_node_path(self): container = { "Mounts": [{ "Destination": RUNNER.ENGINE_N8N_RUNTIME_PACKAGE_PATH, "RW": False, }], } completed = subprocess.CompletedProcess( args=[], returncode=0, stdout=json.dumps({ "packageName": "n8n-nodes-ndc", "nodes": ["ndcDataProductPublish", "ndcDataProductRead", "ndcFoundryBinding"], "credentials": EXPECTED_CREDENTIALS, }), stderr="", ) with ( mock.patch.object(RUNNER, "inspect_container", return_value=container), mock.patch.object(RUNNER.subprocess, "run", return_value=completed) as run, ): catalog = RUNNER.engine_n8n_private_loader_catalog("n8n-container") command = run.call_args.args[0] self.assertEqual(command[:-1], [ str(RUNNER.DOCKER), "exec", "n8n-container", "node", "-e", ]) probe_script = command[-1] self.assertIn( f"process.env.NODE_PATH='{RUNNER.ENGINE_N8N_NODE_MODULES_PATH}'", probe_script, ) self.assertIn("Module._initPaths()", probe_script) self.assertIn("PackageDirectoryLoader", probe_script) self.assertEqual(catalog, { "node_types": EXPECTED_NODES, "credential_types": EXPECTED_CREDENTIALS, }) def test_loader_probe_failure_emits_only_allowlisted_diagnostic(self): container = { "Mounts": [{ "Destination": RUNNER.ENGINE_N8N_RUNTIME_PACKAGE_PATH, "RW": False, }], } cases = ( ("MODULE_NOT_FOUND", "MODULE_NOT_FOUND"), ("Bearer secret-that-must-not-be-logged", "PROBE_ERROR"), ) for probe_stderr, expected_category in cases: with ( self.subTest(stderr=probe_stderr), mock.patch.object(RUNNER, "inspect_container", return_value=container), mock.patch.object( RUNNER.subprocess, "run", return_value=subprocess.CompletedProcess( args=[], returncode=1, stdout="", stderr=probe_stderr, ), ), ): with self.assertRaises(RUNNER.DeployError) as raised: RUNNER.engine_n8n_private_loader_catalog("n8n-container") message = str(raised.exception) self.assertIn(f"category={expected_category}", message) self.assertNotIn("secret-that-must-not-be-logged", message) def test_active_runtime_acceptance_does_not_require_node_path(self): with tempfile.TemporaryDirectory() as directory: _manifest, _entries, payload = RUNNER.load_artifact( self.artifact(0, "activation"), Path(directory), ) descriptor = RUNNER.read_engine_n8n_transition_descriptor( payload / RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL ) container = { "State": {"Status": "running", "StartedAt": "2026-07-16T00:00:00Z"}, "Image": "sha256:base-image", "Config": { "Env": [ "N8N_USER_FOLDER=/home/node", "N8N_COMMUNITY_PACKAGES_ENABLED=true", "N8N_COMMUNITY_PACKAGES_PREVENT_LOADING=false", "N8N_REINSTALL_MISSING_PACKAGES=false", ], "Labels": { "nodedc.n8n-private-extension.release": descriptor["releaseId"], "nodedc.n8n-private-extension.package-sha256": descriptor["packageSha256"], }, }, "Mounts": [{ "Destination": RUNNER.ENGINE_N8N_RUNTIME_PACKAGE_PATH, "Source": f"/volume2/nodedc-demo/{descriptor['sealedReleaseRelativePath']}", "RW": False, }], "RestartCount": 0, } log_result = subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="") with ( mock.patch.object( RUNNER, "inspect_engine_n8n_base_image", return_value={"id": "sha256:base-image"}, ), mock.patch.object(RUNNER, "engine_n8n_container_ids", return_value=("n8n-container",)), mock.patch.object(RUNNER, "inspect_container", return_value=container), mock.patch.object(RUNNER, "wait_engine_n8n_readiness"), mock.patch.object(RUNNER, "engine_n8n_version", return_value=RUNNER.ENGINE_N8N_VERSION), mock.patch.object(RUNNER.subprocess, "run", return_value=log_result), mock.patch.object( RUNNER, "engine_n8n_private_loader_catalog", return_value={ "node_types": EXPECTED_NODES, "credential_types": EXPECTED_CREDENTIALS, }, ), mock.patch.object(RUNNER, "validate_engine_n8n_catalog_payload"), mock.patch.object(RUNNER.time, "sleep"), ): accepted = RUNNER.accept_engine_n8n_runtime(descriptor) self.assertEqual(accepted["node_types"], EXPECTED_NODES) def test_private_extension_transition_does_not_enter_publish_grant_domain(self): entries = (RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL,) descriptor = {"action": "rollback-inactive"} self.assertFalse(RUNNER.touches_engine_data_product_publish_grant(entries)) self.assertEqual(RUNNER.component_services("engine", entries), ("n8n",)) with ( mock.patch.object(RUNNER, "current_engine_n8n_transition_descriptor", return_value=descriptor), mock.patch.object(RUNNER, "ensure_engine_edp_managed_provisioner_keypair") as keypair, mock.patch.object(RUNNER, "ensure_engine_publish_grant_private_state") as private_state, ): RUNNER.prepare_component_runtime("engine", entries) keypair.assert_not_called() private_state.assert_not_called() def test_generic_engine_compose_artifact_keeps_legacy_service_selection(self): self.assertEqual( RUNNER.component_services("engine", ("docker-compose.yml",)), RUNNER.COMPONENTS["engine"]["services"], ) self.assertEqual( RUNNER.component_services( "engine", ("docker-compose.yml", "nodedc-source/server/dataProductPublishGrant/store.js"), ), RUNNER.COMPONENTS["engine"]["services"], ) self.assertFalse(RUNNER.is_engine_data_product_publish_grant_slice( "engine", ("docker-compose.yml", "nodedc-source/server/dataProductPublishGrant/store.js"), )) def test_unknown_descriptor_key_is_rejected(self): with tempfile.TemporaryDirectory() as directory: work = Path(directory) RUNNER.safe_extract(self.artifact(0, "activation"), work) descriptor_path = work / "payload" / RUNNER.ENGINE_N8N_TRANSITION_DESCRIPTOR_REL descriptor = json.loads(descriptor_path.read_text()) descriptor["unexpected"] = True descriptor_path.write_text(json.dumps(descriptor)) with self.assertRaisesRegex(RUNNER.DeployError, "keys mismatch"): RUNNER.validate_engine_n8n_transition( work / "payload", RUNNER.parse_files_list(work / "files.txt"), ) def test_core_catalog_substitution_is_rejected_even_when_counts_match(self): cases = ( ("activation", RUNNER.ENGINE_N8N_NODES_CATALOG_REL, "node"), ("activation", RUNNER.ENGINE_N8N_CREDENTIALS_CATALOG_REL, "credential"), ("rollback", RUNNER.ENGINE_N8N_NODES_CATALOG_REL, "node"), ("rollback", RUNNER.ENGINE_N8N_CREDENTIALS_CATALOG_REL, "credential"), ) for kind, relative_path, label in cases: with self.subTest(kind=kind, catalog=label), tempfile.TemporaryDirectory() as directory: work = Path(directory) RUNNER.safe_extract(self.artifact(0, kind), work) catalog_path = work / "payload" / relative_path catalog = json.loads(catalog_path.read_text()) if label == "node": candidate = next( item for item in catalog if not item.get("name", "").startswith("n8n-nodes-ndc.") ) candidate["name"] = "n8n-nodes-unreviewed.hiddenNode" else: candidate = next( item for item in catalog if item.get("name") not in EXPECTED_CREDENTIALS ) candidate["name"] = "unreviewedCredential" catalog_path.write_text(json.dumps(catalog, indent=2) + "\n") with self.assertRaisesRegex(RUNNER.DeployError, "catalog sha256 mismatch"): RUNNER.validate_engine_n8n_transition( work / "payload", RUNNER.parse_files_list(work / "files.txt"), ) def test_old_engine_artifact_is_rejected_by_descriptor_gate(self): old = PLATFORM_ROOT / "infra/deploy-artifacts/nodedc-engine-n8n-private-extension-20260715-002.tgz" if not old.is_file(): self.skipTest("rejected historical artifact not present") with tempfile.TemporaryDirectory() as directory: with self.assertRaisesRegex(RUNNER.DeployError, "canonical transition descriptor"): RUNNER.load_artifact(old, Path(directory)) def test_engine_source_keeps_base_compose_and_separate_override(self): original_root = RUNNER.COMPONENTS["engine"]["payload_root"] original_compose = RUNNER.COMPONENTS["engine"]["compose_root"] try: RUNNER.COMPONENTS["engine"]["payload_root"] = ENGINE_ROOT RUNNER.COMPONENTS["engine"]["compose_root"] = ENGINE_ROOT RUNNER.validate_engine_n8n_base_compose_source() finally: RUNNER.COMPONENTS["engine"]["payload_root"] = original_root RUNNER.COMPONENTS["engine"]["compose_root"] = original_compose def test_additional_n8n_runtime_service_is_rejected(self): original_root = RUNNER.COMPONENTS["engine"]["payload_root"] original_compose = RUNNER.COMPONENTS["engine"]["compose_root"] try: with tempfile.TemporaryDirectory() as directory: engine_root = Path(directory) compose = (ENGINE_ROOT / "docker-compose.yml").read_text() compose += ( "\n n8n-worker-2:\n" " image: docker.n8n.io/n8nio/n8n:${N8N_IMAGE_TAG:-2.3.2}\n" ) (engine_root / "docker-compose.yml").write_text(compose) RUNNER.COMPONENTS["engine"]["payload_root"] = engine_root RUNNER.COMPONENTS["engine"]["compose_root"] = engine_root with self.assertRaisesRegex(RUNNER.DeployError, "exact service topology mismatch"): RUNNER.validate_engine_n8n_base_compose_source() finally: RUNNER.COMPONENTS["engine"]["payload_root"] = original_root RUNNER.COMPONENTS["engine"]["compose_root"] = original_compose def test_sealed_release_exact_set_includes_implicit_directories(self): release_relative = Path( "payload/releases/n8n-nodes-ndc/0.1.4-59dc9f7882721d6a" ) with tempfile.TemporaryDirectory() as directory: work = Path(directory) outer = work / "outer" RUNNER.safe_extract(STAGE_ARTIFACT, outer) staged_release = outer / release_relative sealed_release = work / "sealed" sealed_release.mkdir() for filename in ("package.tgz", "release.json", "rollback.json"): (sealed_release / filename).write_bytes( (staged_release / filename).read_bytes() ) expected_paths = { "package", "package.tgz", "release.json", "rollback.json", } with tarfile.open(staged_release / "package.tgz", "r:gz") as archive: for member in archive: RUNNER.add_engine_n8n_sealed_member_paths( expected_paths, member.name, ) target = sealed_release.joinpath(*Path(member.name).parts) if member.isdir(): target.mkdir(parents=True, exist_ok=True) continue target.parent.mkdir(parents=True, exist_ok=True) source = archive.extractfile(member) self.assertIsNotNone(source) target.write_bytes(source.read()) actual_paths = { path.relative_to(sealed_release).as_posix() for path in sealed_release.rglob("*") } self.assertEqual(actual_paths, expected_paths) self.assertIn("package/dist/nodes", expected_paths) self.assertIn("package/dist/credentials", expected_paths) def test_tar_members_have_no_appledouble_or_special_types(self): for kind in ("activation", "rollback"): with tarfile.open(self.artifact(0, kind), "r:gz") as archive: for member in archive: self.assertFalse(Path(member.name).name.startswith("._")) self.assertTrue(member.isfile() or member.isdir()) if __name__ == "__main__": unittest.main(verbosity=2)