from __future__ import annotations import copy import hashlib import json import os import subprocess import tempfile import unittest from pathlib import Path from scripts import plan_deploy_seed as seed_plan from scripts import serviceification_d3_engine as deploy REPO_ROOT = Path(__file__).resolve().parents[1] DEPLOY_SEED_CATALOG = ( REPO_ROOT / "deployments" / "offline" / "runtime" / "seeds" / "deploy-seed-catalog.json" ) class ServiceificationD3AppShellTests(unittest.TestCase): @staticmethod def write_l2_app_shell(root: Path) -> Path: config = root / "configs" / "app-shell.json" config.parent.mkdir(parents=True) config.write_text( json.dumps( { "id": "crm_pack_app", "frontendAppId": "crm", "isDefault": True, "navigationItems": [{"id": "home"}, {"id": "account"}], }, separators=(",", ":"), ) + "\n", encoding="utf-8", ) (root / "stack.lock.json").write_text( json.dumps({"app_shell_config": "configs/app-shell.json"}) + "\n", encoding="utf-8", ) return config @staticmethod def commit_l2_checkout(root: Path) -> str: subprocess.run(("git", "init", "--quiet", str(root)), check=True) subprocess.run(("git", "-C", str(root), "add", "."), check=True) subprocess.run( ( "git", "-C", str(root), "-c", "user.name=D3 App Shell Test", "-c", "user.email=d3-app-shell@example.invalid", "commit", "--quiet", "-m", "fixture", ), check=True, ) return subprocess.check_output( ("git", "-C", str(root), "rev-parse", "HEAD"), text=True, ).strip() def test_full_d3_profiles_share_one_app_shell_callsite(self) -> None: catalog = json.loads(DEPLOY_SEED_CATALOG.read_text(encoding="utf-8")) expected_invocations = [ "local.app_shell.d3_post_pack_install", "test.app_shell.d3_runtime_apply", "staging.app_shell.d3_runtime_apply", "production.app_shell.d3_runtime_apply", ] expected_phases = { "local": "d3_post_pack_install", "test": "d3_runtime_apply", "staging": "d3_runtime_apply", "production": "d3_runtime_apply", } for profile, invocation_id in zip(expected_phases, expected_invocations): with self.subTest(profile=profile): invocation = next( item for item in catalog["profiles"][profile]["invocations"] if item["invocation_id"] == invocation_id ) self.assertEqual("app_shell", invocation["module_id"]) self.assertEqual(expected_phases[profile], invocation["phase"]) callsite = next( item for item in catalog["expected_callsites"] if item["callsite_id"] == "d3.app_shell" ) self.assertEqual( "scripts/serviceification_d3_engine.py", callsite["path"], ) self.assertEqual(expected_invocations, callsite["invocation_ids"]) self.assertEqual(1, callsite["expected_count"]) self.assertFalse( any( item["callsite_id"] in {"d3_local.app_shell", "staging.app_shell"} for item in catalog["expected_callsites"] ), "retired or duplicate app-shell callsites must not satisfy D3 closure", ) def test_shared_callsite_requires_distinct_profiles(self) -> None: catalog = json.loads(DEPLOY_SEED_CATALOG.read_text(encoding="utf-8")) duplicate_invocation_id = "staging.app_shell.duplicate_runtime_apply" catalog["profiles"]["staging"]["invocations"].insert( 1, { "invocation_id": duplicate_invocation_id, "module_id": "app_shell", "phase": "d3_runtime_apply", "order": 11, "condition": {"type": "always"}, "selection": {}, }, ) callsite = next( item for item in catalog["expected_callsites"] if item["callsite_id"] == "d3.app_shell" ) callsite["invocation_ids"].append(duplicate_invocation_id) with self.assertRaisesRegex( seed_plan.SeedPlanError, "distinct environment profiles", ): seed_plan.validate_catalog( copy.deepcopy(catalog), seed_plan.DirectorySource(REPO_ROOT), ) def test_receipt_match_requires_complete_config_bound_shape(self) -> None: with tempfile.TemporaryDirectory() as directory: l2_root = Path(directory) / "exact-l2" config = self.write_l2_app_shell(l2_root) l2_commit = self.commit_l2_checkout(l2_root) receipt = { "schema": deploy.D3_APP_SHELL_RECEIPT_SCHEMA, "status": "passed", "l2Commit": l2_commit, "configSha256": hashlib.sha256(config.read_bytes()).hexdigest(), "appId": "crm_pack_app", "frontendAppId": "crm", "isDefault": True, "navigationItemIds": ["home", "account"], "navigationRevision": 1, "activeProfileCount": 2, "defaultProfileCount": 2, "mode": "apply-and-assert", } self.assertTrue( deploy.runtime_app_shell_receipt_matches( receipt, l2_root=l2_root, l2_commit=l2_commit, ) ) invalid_receipts = [] self.assertFalse(deploy.runtime_app_shell_receipt_matches( receipt, l2_root=l2_root, l2_commit=l2_commit, navigation_required=True, )) self.assertTrue(deploy.runtime_app_shell_receipt_matches( {**receipt, "navigationInitialization": {"revision": 1}}, l2_root=l2_root, l2_commit=l2_commit, navigation_required=True, )) for field in ("activeProfileCount", "defaultProfileCount"): incomplete = dict(receipt) incomplete.pop(field) invalid_receipts.append(incomplete) invalid_receipts.extend( ( {**receipt, "appId": "another-app"}, {**receipt, "navigationItemIds": ["home", ""]}, {**receipt, "activeProfileCount": True}, {**receipt, "defaultProfileCount": 1}, {**receipt, "unexpected": "field"}, ) ) for invalid in invalid_receipts: with self.subTest(receipt=invalid): self.assertFalse( deploy.runtime_app_shell_receipt_matches( invalid, l2_root=l2_root, l2_commit=l2_commit, ) ) def test_app_shell_config_rejects_authority_escape(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) l2_root = root / "exact-l2" l2_root.mkdir() (root / "outside.json").write_text("{}\n", encoding="utf-8") (l2_root / "stack.lock.json").write_text( json.dumps({"app_shell_config": "../outside.json"}) + "\n", encoding="utf-8", ) with self.assertRaisesRegex( deploy.DeployError, "canonical relative path", ): deploy.offline.runtime_app_shell_config(l2_root) if __name__ == "__main__": unittest.main()