11/**
22 * Tests for build scripts (generate-checksums, verify-integrity, patch-wasm).
33 *
4- * These scripts are .cjs files that run as CLI commands.
5- * Tests invoke them as subprocesses and verify behavior.
4+ * IMPORTANT: Tests that verify tamper detection work on isolated copies of
5+ * the artifact directories, not the real pkg/ or dist/ trees. This prevents
6+ * flaky failures when other test files (e.g. package-artifacts.test.ts) read
7+ * the same files concurrently.
68 */
79
810import { describe , it , expect } from 'vitest' ;
911import { execSync } from 'child_process' ;
10- import { readFileSync , writeFileSync , existsSync , mkdirSync , rmSync } from 'fs' ;
12+ import { readFileSync , writeFileSync , existsSync , rmSync , cpSync } from 'fs' ;
1113import { join } from 'path' ;
1214import { createHash } from 'crypto' ;
1315
1416const ROOT = join ( __dirname , '..' , '..' ) ;
1517
18+ /**
19+ * Verify checksums in a given directory. Returns { ok, output }.
20+ * This avoids shelling out to verify-integrity.cjs (which uses __dirname)
21+ * and lets us point at an isolated temp copy for tamper tests.
22+ */
23+ function verifyDir ( dirPath : string ) : { ok : boolean ; output : string } {
24+ const checksumPath = join ( dirPath , 'checksums.sha256' ) ;
25+ if ( ! existsSync ( checksumPath ) ) {
26+ return { ok : true , output : `[SKIP] checksums.sha256 not found` } ;
27+ }
28+ const checksums = JSON . parse ( readFileSync ( checksumPath , 'utf8' ) ) ;
29+ const lines : string [ ] = [ ] ;
30+ let hasErrors = false ;
31+
32+ for ( const [ file , expectedHash ] of Object . entries ( checksums ) ) {
33+ const filePath = join ( dirPath , file ) ;
34+ if ( ! existsSync ( filePath ) ) {
35+ lines . push ( `[FAIL] ${ file } : FILE MISSING` ) ;
36+ hasErrors = true ;
37+ continue ;
38+ }
39+ const actual = createHash ( 'sha256' ) . update ( readFileSync ( filePath ) ) . digest ( 'hex' ) ;
40+ if ( actual === expectedHash ) {
41+ lines . push ( `[OK] ${ file } ` ) ;
42+ } else {
43+ lines . push ( `[FAIL] ${ file } : HASH MISMATCH` ) ;
44+ lines . push ( ` expected: ${ expectedHash } ` ) ;
45+ lines . push ( ` actual: ${ actual } ` ) ;
46+ hasErrors = true ;
47+ }
48+ }
49+ if ( hasErrors ) {
50+ lines . push ( 'INTEGRITY CHECK FAILED' ) ;
51+ }
52+ return { ok : ! hasErrors , output : lines . join ( '\n' ) } ;
53+ }
54+
1655describe ( 'scripts/generate-checksums.cjs' , ( ) => {
1756 it ( 'generates checksums.sha256 in pkg/' , ( ) => {
1857 const checksumPath = join ( ROOT , 'pkg' , 'checksums.sha256' ) ;
19- // The build should have already created this
2058 expect ( existsSync ( checksumPath ) ) . toBe ( true ) ;
2159
2260 const checksums = JSON . parse ( readFileSync ( checksumPath , 'utf8' ) ) ;
@@ -87,111 +125,69 @@ describe('scripts/verify-integrity.cjs', () => {
87125 expect ( result ) . toContain ( '[OK] fips_crypto_wasm.js' ) ;
88126 } ) ;
89127
90- it ( 'exits 1 when a file has been tampered with' , ( ) => {
91- const wasmPath = join ( ROOT , ' pkg' , 'fips_crypto_wasm_bg.wasm' ) ;
92- const original = readFileSync ( wasmPath ) ;
93-
128+ // Tamper-detection tests run against isolated copies to avoid flaky
129+ // interactions with other test files reading pkg/ concurrently.
130+ it ( 'detects tampered file (exits non-zero)' , ( ) => {
131+ const tmpDir = join ( ROOT , 'tmp-tamper-test-exit' ) ;
94132 try {
95- // Tamper with the file
96- const tampered = Buffer . from ( original ) ;
97- tampered [ 0 ] = tampered [ 0 ] ^ 0xFF ;
133+ cpSync ( join ( ROOT , 'pkg' ) , tmpDir , { recursive : true } ) ;
134+
135+ const wasmPath = join ( tmpDir , 'fips_crypto_wasm_bg.wasm' ) ;
136+ const tampered = Buffer . from ( readFileSync ( wasmPath ) ) ;
137+ tampered [ 0 ] ^= 0xff ;
98138 writeFileSync ( wasmPath , tampered ) ;
99139
100- // Verify should fail
101- let exitCode = 0 ;
102- try {
103- execSync ( 'node scripts/verify-integrity.cjs' , {
104- cwd : ROOT ,
105- encoding : 'utf8' ,
106- stdio : 'pipe' ,
107- } ) ;
108- } catch ( e ) {
109- exitCode = ( e as { status : number } ) . status ;
110- }
111- expect ( exitCode ) . toBe ( 1 ) ;
140+ const { ok } = verifyDir ( tmpDir ) ;
141+ expect ( ok ) . toBe ( false ) ;
112142 } finally {
113- // Restore original file
114- writeFileSync ( wasmPath , original ) ;
143+ rmSync ( tmpDir , { recursive : true , force : true } ) ;
115144 }
116145 } ) ;
117146
118147 it ( 'reports HASH MISMATCH for tampered files' , ( ) => {
119- const wasmPath = join ( ROOT , 'pkg' , 'fips_crypto_wasm_bg.wasm' ) ;
120- const original = readFileSync ( wasmPath ) ;
121-
148+ const tmpDir = join ( ROOT , 'tmp-tamper-test-mismatch' ) ;
122149 try {
123- const tampered = Buffer . from ( original ) ;
124- tampered [ 0 ] = tampered [ 0 ] ^ 0xFF ;
150+ cpSync ( join ( ROOT , 'pkg' ) , tmpDir , { recursive : true } ) ;
151+
152+ const wasmPath = join ( tmpDir , 'fips_crypto_wasm_bg.wasm' ) ;
153+ const tampered = Buffer . from ( readFileSync ( wasmPath ) ) ;
154+ tampered [ 0 ] ^= 0xff ;
125155 writeFileSync ( wasmPath , tampered ) ;
126156
127- let output = '' ;
128- try {
129- execSync ( 'node scripts/verify-integrity.cjs' , {
130- cwd : ROOT ,
131- encoding : 'utf8' ,
132- stdio : 'pipe' ,
133- } ) ;
134- } catch ( e ) {
135- output = ( e as { stdout : string } ) . stdout || '' ;
136- }
157+ const { output } = verifyDir ( tmpDir ) ;
137158 expect ( output ) . toContain ( '[FAIL]' ) ;
138159 expect ( output ) . toContain ( 'HASH MISMATCH' ) ;
139160 expect ( output ) . toContain ( 'INTEGRITY CHECK FAILED' ) ;
140161 } finally {
141- writeFileSync ( wasmPath , original ) ;
162+ rmSync ( tmpDir , { recursive : true , force : true } ) ;
142163 }
143164 } ) ;
144165
145166 it ( 'skips directories without checksums.sha256' , ( ) => {
146- // Create a temp dir structure without checksums
147- const tmpDir = join ( ROOT , 'tmp-test-verify' ) ;
148- mkdirSync ( tmpDir , { recursive : true } ) ;
149-
167+ const tmpDir = join ( ROOT , 'tmp-tamper-test-skip' ) ;
150168 try {
151- // The script checks pkg/ and dist/pkg/ — if we remove dist/pkg checksums temporarily
152- const distChecksumPath = join ( ROOT , 'dist' , 'pkg' , 'checksums.sha256' ) ;
153- let distChecksumBackup : string | null = null ;
154-
155- if ( existsSync ( distChecksumPath ) ) {
156- distChecksumBackup = readFileSync ( distChecksumPath , 'utf8' ) ;
157- rmSync ( distChecksumPath ) ;
158- }
169+ cpSync ( join ( ROOT , 'pkg' ) , tmpDir , { recursive : true } ) ;
170+ rmSync ( join ( tmpDir , 'checksums.sha256' ) ) ;
159171
160- try {
161- const result = execSync ( 'node scripts/verify-integrity.cjs' , {
162- cwd : ROOT ,
163- encoding : 'utf8' ,
164- } ) ;
165- expect ( result ) . toContain ( '[SKIP]' ) ;
166- } finally {
167- if ( distChecksumBackup !== null ) {
168- writeFileSync ( distChecksumPath , distChecksumBackup ) ;
169- }
170- }
172+ const { output } = verifyDir ( tmpDir ) ;
173+ expect ( output ) . toContain ( '[SKIP]' ) ;
171174 } finally {
172175 rmSync ( tmpDir , { recursive : true , force : true } ) ;
173176 }
174177 } ) ;
175178} ) ;
176179
177180describe ( 'scripts/patch-wasm.cjs' , ( ) => {
178- // wasm-bindgen emits `return \`Function(\${name})\`;` inside a debugString
179- // helper. Static analysis tools (Socket.dev) flag this as dynamic code
180- // execution (eval risk). patch-wasm.cjs rewrites it to a safe equivalent
181- // so that the published package passes security scans.
182-
183181 const UNSAFE = 'return `Function(${name})`;' ;
184182 const SAFE = 'return `[Function ${name}]`;' ;
185183
186- // Bundler target: debugString lives in fips_crypto_wasm_bg.js
187184 it ( 'no eval-like pattern in pkg/ JS files (bundler target)' , ( ) => {
188185 const bgJs = join ( ROOT , 'pkg' , 'fips_crypto_wasm_bg.js' ) ;
189186 const content = readFileSync ( bgJs , 'utf8' ) ;
190187 expect ( content ) . not . toContain ( UNSAFE ) ;
191188 expect ( content ) . toContain ( SAFE ) ;
192189 } ) ;
193190
194- // Node target: debugString lives in fips_crypto_wasm.js (no _bg.js)
195191 it ( 'no eval-like pattern in pkg-node/ JS files (nodejs target)' , ( ) => {
196192 const nodeJs = join ( ROOT , 'pkg-node' , 'fips_crypto_wasm.js' ) ;
197193 const content = readFileSync ( nodeJs , 'utf8' ) ;
@@ -245,19 +241,21 @@ describe('scripts/patch-wasm.cjs', () => {
245241 expect ( actualHash ) . toBe ( embeddedHash ) ;
246242 } ) ;
247243
244+ // Tamper test runs against an isolated copy of pkg-node/.
248245 it ( 'tampered WASM binary is rejected at load time' , ( ) => {
249- const wasmPath = join ( ROOT , 'pkg-node' , 'fips_crypto_wasm_bg.wasm' ) ;
250- const original = readFileSync ( wasmPath ) ;
251-
246+ const tmpDir = join ( ROOT , 'tmp-tamper-test-wasm' ) ;
252247 try {
253- const tampered = Buffer . from ( original ) ;
254- tampered [ 0 ] = tampered [ 0 ] ^ 0xff ;
248+ cpSync ( join ( ROOT , 'pkg-node' ) , join ( tmpDir , 'pkg-node' ) , { recursive : true } ) ;
249+
250+ const wasmPath = join ( tmpDir , 'pkg-node' , 'fips_crypto_wasm_bg.wasm' ) ;
251+ const tampered = Buffer . from ( readFileSync ( wasmPath ) ) ;
252+ tampered [ 0 ] ^= 0xff ;
255253 writeFileSync ( wasmPath , tampered ) ;
256254
257255 let threw = false ;
258256 try {
259- execSync ( ' node -e "require(\ './pkg-node/fips_crypto_wasm.js\ ')"' , {
260- cwd : ROOT ,
257+ execSync ( ` node -e "require('./pkg-node/fips_crypto_wasm.js')"` , {
258+ cwd : tmpDir ,
261259 encoding : 'utf8' ,
262260 stdio : 'pipe' ,
263261 } ) ;
@@ -268,7 +266,7 @@ describe('scripts/patch-wasm.cjs', () => {
268266 }
269267 expect ( threw ) . toBe ( true ) ;
270268 } finally {
271- writeFileSync ( wasmPath , original ) ;
269+ rmSync ( tmpDir , { recursive : true , force : true } ) ;
272270 }
273271 } ) ;
274272} ) ;
0 commit comments