157 lines
6.8 KiB
Python
157 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
BUILDER = SCRIPT_DIR / "build-engine-data-product-publish-grant-artifact.mjs"
|
|
EXPECTED_ENTRIES = [
|
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
|
"nodedc-source/server/dataProductPublishGrant",
|
|
"nodedc-source/server/engineAgents/store.js",
|
|
"nodedc-source/server/routes/engineAgentGateway.js",
|
|
"nodedc-source/server/routes/n8n.js",
|
|
"nodedc-source/services/backend/data-product-publish-grant/docker-compose.immutable-runtime.yml",
|
|
]
|
|
PATCH_ID = "engine-data-product-publish-grant-20990101-001"
|
|
SUCCESSFUL_CREDENTIAL_SINK_INDEX_SHA256 = (
|
|
"b2b790b02839570d967a2ca68b00e2724485a99389ac9b3a589a1b22302a36b8"
|
|
)
|
|
|
|
|
|
class EnginePublishGrantArtifactTest(unittest.TestCase):
|
|
def build(self, artifact_dir, patch_id):
|
|
environment = os.environ.copy()
|
|
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
|
result = subprocess.run(
|
|
["node", str(BUILDER), patch_id],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=environment,
|
|
)
|
|
return json.loads(result.stdout)
|
|
|
|
def test_artifact_is_narrow_secret_free_and_deterministic(self):
|
|
with tempfile.TemporaryDirectory(prefix="nodedc-engine-publish-artifact-") as directory:
|
|
root = Path(directory)
|
|
first_dir = root / "first"
|
|
second_dir = root / "second"
|
|
first_dir.mkdir()
|
|
second_dir.mkdir()
|
|
first = self.build(first_dir, PATCH_ID)
|
|
artifact = Path(first["artifact"])
|
|
first_bytes = artifact.read_bytes()
|
|
second = self.build(second_dir, PATCH_ID)
|
|
second_bytes = Path(second["artifact"]).read_bytes()
|
|
|
|
self.assertEqual(first["entries"], EXPECTED_ENTRIES)
|
|
self.assertEqual(first["sha256"], hashlib.sha256(first_bytes).hexdigest())
|
|
self.assertEqual(first["sha256"], second["sha256"])
|
|
self.assertEqual(first_bytes, second_bytes)
|
|
|
|
with tarfile.open(artifact, "r:gz") as archive:
|
|
names = {member.name for member in archive.getmembers()}
|
|
files = archive.extractfile("files.txt").read().decode("utf-8").splitlines()
|
|
manifest = archive.extractfile("manifest.env").read().decode("utf-8")
|
|
n8n_source = archive.extractfile(
|
|
"payload/nodedc-source/server/routes/n8n.js"
|
|
).read().decode("utf-8")
|
|
gateway_source = archive.extractfile(
|
|
"payload/nodedc-source/server/routes/engineAgentGateway.js"
|
|
).read().decode("utf-8")
|
|
runtime_override = archive.extractfile(
|
|
"payload/nodedc-source/services/backend/data-product-publish-grant/"
|
|
"docker-compose.immutable-runtime.yml"
|
|
).read().decode("utf-8")
|
|
|
|
self.assertEqual(files, EXPECTED_ENTRIES)
|
|
self.assertEqual(
|
|
manifest,
|
|
f"id={PATCH_ID}\ncomponent=engine\ntype=app-overlay\n",
|
|
)
|
|
forbidden_roots = (
|
|
"payload/docker-compose.yml",
|
|
"payload/nodedc-source/server/index.js",
|
|
"payload/nodedc-source/server/credentialPolicies/ndcPrivateNode.js",
|
|
"payload/nodedc-source/server/credentialSink/",
|
|
"payload/nodedc-source/server/routes/engineCredentialSink.js",
|
|
"payload/nodedc-source/server/middleware/demoAccess.js",
|
|
"payload/nodedc-source/server/routes/ndcAgentMcp.js",
|
|
"payload/nodedc-source/server/tests/",
|
|
"payload/nodedc-source/dist/",
|
|
"payload/nodedc-source/server/data/",
|
|
)
|
|
self.assertFalse(any(
|
|
name == root.rstrip("/") or name.startswith(root)
|
|
for name in names
|
|
for root in forbidden_roots
|
|
))
|
|
self.assertNotIn("from '../credentialSink/", n8n_source)
|
|
self.assertIn("engineCredentialSinkN8nAdapter", n8n_source)
|
|
self.assertIn("engineDataProductPublishGrantN8nAdapter", n8n_source)
|
|
for tool in (
|
|
"engine_plan_data_product_publish_grant",
|
|
"engine_apply_data_product_publish_grant",
|
|
"engine_accept_data_product_publish_grant",
|
|
"engine_rollback_data_product_publish_grant",
|
|
):
|
|
self.assertIn(tool, gateway_source)
|
|
self.assertIn(" nodedc-backend:", runtime_override)
|
|
self.assertNotIn(" n8n:", runtime_override)
|
|
self.assertIn(
|
|
"source: /volume2/nodedc-demo/nodedc-control-plane/publish-grants",
|
|
runtime_override,
|
|
)
|
|
self.assertNotIn(
|
|
"source: /volume2/nodedc-demo/nodedc-control-plane\n",
|
|
runtime_override,
|
|
)
|
|
self.assertIn("read_only: true", runtime_override)
|
|
self.assertEqual(runtime_override.count("create_host_path: false"), 2)
|
|
|
|
def test_builder_requires_a_fresh_never_issued_patch_id(self):
|
|
with tempfile.TemporaryDirectory(prefix="nodedc-engine-publish-id-") as directory:
|
|
artifact_dir = Path(directory)
|
|
environment = os.environ.copy()
|
|
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
|
cases = (
|
|
([], "fresh-patch-id"),
|
|
(["engine-data-product-publish-grant-20260716-001"], "patch_id_already_issued"),
|
|
(["engine-data-product-publish-grant-20260717-001"], "patch_id_already_issued"),
|
|
(["engine-data-product-publish-grant-20260717-002"], "patch_id_already_issued"),
|
|
(["engine-publish-grant-unit-001"], "fresh-patch-id"),
|
|
)
|
|
for arguments, expected in cases:
|
|
with self.subTest(arguments=arguments):
|
|
result = subprocess.run(
|
|
["node", str(BUILDER), *arguments],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
env=environment,
|
|
)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn(expected, result.stderr)
|
|
|
|
self.build(artifact_dir, PATCH_ID)
|
|
duplicate = subprocess.run(
|
|
["node", str(BUILDER), PATCH_ID],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
env=environment,
|
|
)
|
|
self.assertNotEqual(duplicate.returncode, 0)
|
|
self.assertIn("artifact_already_exists", duplicate.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|