sec-kn-4c-migration-hook-restore.test.mjs
183 lines 7.9 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago
1 /**
2 * SEC-KN-4c — seven-tier coverage for the T4 migration-hook identity restore.
3 *
4 * Frozen spec: docs/SEC-KN-4C-MIGRATION-HOOK-RESTORE-FREEZE.md (4C-R1–R9)
5 *
6 * After the one-time V7→`created_by` upgrade (SEC-KN-4 T1) ran on the live hub
7 * canister, the actor upgrade hook must be identity on `StableStorage` so repeat
8 * deploys succeed (no Compatibility error M0216). These tests pin that restore:
9 * the hook shape (4C-R1), removal of the one-shot TODO marker (4C-R2), retention
10 * of historical map helpers (4C-R3), the documented post-T1 invariant (4C-R4),
11 * verify-script contracts (4C-R5), and the seven-tier matrix (4C-R7).
12 */
13
14 import { test, describe } from 'node:test';
15 import assert from 'node:assert/strict';
16 import fs from 'node:fs';
17 import path from 'node:path';
18 import { performance } from 'node:perf_hooks';
19 import { fileURLToPath } from 'node:url';
20 import { execSync } from 'node:child_process';
21
22 const __dirname = path.dirname(fileURLToPath(import.meta.url));
23 const ROOT = path.resolve(__dirname, '..');
24 const MIGRATION_MO = path.join(ROOT, 'hub/icp/src/hub/Migration.mo');
25 const MAIN_MO = path.join(ROOT, 'hub/icp/src/hub/main.mo');
26 const ICP_DIR = path.join(ROOT, 'hub/icp');
27
28 const IDENTITY_HOOK_RE =
29 /public func migration\(old : \{ var storage : StableStorage \}\) : \{ var storage : StableStorage \}/;
30
31 function migrationSource() {
32 return fs.readFileSync(MIGRATION_MO, 'utf8');
33 }
34
35 /**
36 * Source of the public actor hook only (it is the final declaration in the
37 * module), so body-shape assertions cannot be satisfied by historical helpers.
38 */
39 function publicMigrationHookSource(src) {
40 const start = src.indexOf('public func migration(');
41 assert.notEqual(start, -1, 'public migration hook not found in Migration.mo');
42 return src.slice(start);
43 }
44
45 // ---------------------------------------------------------------------------
46 // Tier 1 — unit (4C-R1, 4C-R2, 4C-R3, 4C-R4)
47 // ---------------------------------------------------------------------------
48 describe('SEC-KN-4c unit — identity hook source contracts', () => {
49 test('4C-R1: actor hook is identity on StableStorage; no V7 domain, no map in hook', () => {
50 const src = migrationSource();
51 assert.match(src, IDENTITY_HOOK_RE);
52 const hook = publicMigrationHookSource(src);
53 assert.doesNotMatch(hook, /StableStorageV7/);
54 assert.doesNotMatch(hook, /_proposalV7ToCurrent/);
55 });
56
57 test('4C-R2: TODO(SEC-KN-4c) marker removed; locatable SEC-KN-4c comment remains', () => {
58 const src = migrationSource();
59 assert.doesNotMatch(src, /TODO\(SEC-KN-4c\)/);
60 assert.match(src, /SEC-KN-4c/);
61 });
62
63 test('4C-R3: historical helpers and V5/V6/V7 type pins retained', () => {
64 const src = migrationSource();
65 assert.match(src, /func _proposalV7ToCurrent\(p : ProposalRecordV7\) : ProposalRecord/);
66 assert.match(
67 src,
68 /func _proposalBeforeEnrichToCurrent\(p : ProposalRecordBeforeEnrich\) : ProposalRecordV7/,
69 );
70 assert.match(src, /func _proposalV4ToV5\(p : ProposalRecordV4\) : ProposalRecordV7/);
71 assert.match(src, /public type StableStorageV5/);
72 assert.match(src, /public type StableStorageV6/);
73 assert.match(src, /public type StableStorageV7/);
74 });
75
76 test('4C-R4: header documents the post-T1 identity invariant', () => {
77 const src = migrationSource();
78 // One-time V7→created_by upgrade (T1) then identity — both facts must be stated.
79 assert.match(src, /one.*V7.*`?created_by`?.*deploy|V7→`created_by` upgrade/s);
80 assert.match(src, /identity/i);
81 assert.match(src, /SEC-KN-4 T1/);
82 });
83 });
84
85 // ---------------------------------------------------------------------------
86 // Tier 2 — integration (Motoko compile check)
87 // ---------------------------------------------------------------------------
88 function dfxAvailable() {
89 try {
90 execSync('command -v dfx', { stdio: 'pipe', shell: '/bin/bash' });
91 return true;
92 } catch {
93 return false;
94 }
95 }
96
97 describe('SEC-KN-4c integration — Motoko compile check', () => {
98 // CI runners do not install dfx; the compile check is enforced locally and in
99 // build-verification (freeze 4C-R7 integration tier). Skipping when the
100 // toolchain is absent is explicit, never silent.
101 test(
102 'dfx build --check hub exits 0 (scrubbed env)',
103 { skip: dfxAvailable() ? false : 'dfx not installed on this runner — enforced locally/BV' },
104 () => {
105 execSync('dfx build --check hub', {
106 cwd: ICP_DIR,
107 stdio: 'pipe',
108 env: {
109 PATH: process.env.PATH,
110 HOME: process.env.HOME,
111 NO_COLOR: '1',
112 TERM: 'dumb',
113 },
114 });
115 },
116 );
117 });
118
119 // ---------------------------------------------------------------------------
120 // Tier 3 — e2e (actor wiring; no new HTTP surface)
121 // ---------------------------------------------------------------------------
122 describe('SEC-KN-4c e2e — actor still installs through Migration.migration', () => {
123 test('main.mo declares (with migration = Migration.migration)', () => {
124 const main = fs.readFileSync(MAIN_MO, 'utf8');
125 assert.match(main, /\(with migration = Migration\.migration\)/);
126 });
127 });
128
129 // ---------------------------------------------------------------------------
130 // Tier 4 — stress (hook is O(1); no per-row work on upgrade)
131 // ---------------------------------------------------------------------------
132 describe('SEC-KN-4c stress — identity hook is O(1)', () => {
133 test('public hook body has no Array.map over proposalEntries', () => {
134 const hook = publicMigrationHookSource(migrationSource());
135 assert.doesNotMatch(hook, /Array\.map/);
136 assert.doesNotMatch(hook, /proposalEntries/);
137 });
138 });
139
140 // ---------------------------------------------------------------------------
141 // Tier 5 — data-integrity (historical map unchanged; hook does not call it)
142 // ---------------------------------------------------------------------------
143 describe('SEC-KN-4c data-integrity — historical V7 map preserved, disconnected', () => {
144 test('_proposalV7ToCurrent still sets created_by = "" and is not called by the hook', () => {
145 const src = migrationSource();
146 const helperStart = src.indexOf('func _proposalV7ToCurrent');
147 assert.notEqual(helperStart, -1);
148 const helper = src.slice(helperStart, src.indexOf('};', helperStart) + 2);
149 assert.match(helper, /created_by = ""/);
150 const hook = publicMigrationHookSource(src);
151 assert.doesNotMatch(hook, /_proposalV7ToCurrent/);
152 });
153 });
154
155 // ---------------------------------------------------------------------------
156 // Tier 6 — performance (verify script wall clock)
157 // ---------------------------------------------------------------------------
158 describe('SEC-KN-4c performance — verify script completes quickly', () => {
159 test('canister:verify-migration exits 0 in under 2s', () => {
160 const start = performance.now();
161 execSync('npm run canister:verify-migration', { cwd: ROOT, stdio: 'pipe' });
162 const elapsed = performance.now() - start;
163 assert.ok(elapsed < 2000, `verify script took ${elapsed.toFixed(0)}ms (limit 2000ms)`);
164 });
165 });
166
167 // ---------------------------------------------------------------------------
168 // Tier 7 — security (no secrets; no authorship-spoof surface reintroduced)
169 // ---------------------------------------------------------------------------
170 describe('SEC-KN-4c security — no new secrets or authorship paths', () => {
171 test('Migration.mo introduces no header/body authorship or secret material', () => {
172 const src = migrationSource();
173 assert.doesNotMatch(src, /X-User-Id/);
174 // Secret fields are pass-through storage names only; no literal secret values.
175 assert.doesNotMatch(src, /(operator_export_secret|gateway_auth_secret)\s*=\s*"[^"]+"/);
176 });
177
178 test('main.mo authorship stays on createdByFromRequest (SEC-KN-4 R contract intact)', () => {
179 const main = fs.readFileSync(MAIN_MO, 'utf8');
180 assert.match(main, /func createdByFromRequest\(req : HttpRequest\) : Text/);
181 assert.doesNotMatch(main, /created_by = userId\(req\)/);
182 });
183 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 10 days ago