import { mkdtemp, mkdir, readFile, rm, symlink, writeFile, } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { assertLicenseReady, collectDirectoryEntries, createDeterministicZip, parseReleaseArguments, safeArchivePath, } from '../../scripts/package-release.mjs'; import { checksumLine, parseChecksumLine, sha256, verifyChecksum, writeChecksum, } from '../../scripts/checksum-release.mjs'; import { inspectPortalHeaders, parsePortalArguments, } from '../../scripts/portal-assembly-smoke.mjs'; const temporaryDirectories = []; async function temporaryDirectory() { const directory = await mkdtemp( path.join(tmpdir(), 'av-tools-release-test-') ); temporaryDirectories.push(directory); return directory; } afterEach(async () => { await Promise.all( temporaryDirectories .splice(0) .map((directory) => rm(directory, { force: true, recursive: true })) ); }); describe('release path safety', () => { it.each([ '', '/index.html', '../index.html', 'assets/../index.html', 'assets//main.js', 'C:/index.html', 'assets\\main.js', 'assets/\0main.js', ])('rejects unsafe archive path %j', (unsafePath) => { expect(() => safeArchivePath(unsafePath)).toThrow(); }); it('accepts a canonical nested path', () => { expect(safeArchivePath('vendor/ffmpeg/core.wasm')).toBe( 'vendor/ffmpeg/core.wasm' ); }); it('rejects symlinks while collecting a directory', async () => { const directory = await temporaryDirectory(); await writeFile(path.join(directory, 'target.txt'), 'target'); await symlink('target.txt', path.join(directory, 'alias.txt')); await expect(collectDirectoryEntries(directory)).rejects.toThrow( /symlink/i ); }); }); describe('deterministic release ZIP', () => { const entries = [ { name: 'z.txt', data: Buffer.from('last') }, { name: 'a.txt', data: Buffer.from('first') }, ]; it('is byte-identical regardless of input order', () => { const first = createDeterministicZip(entries); const second = createDeterministicZip([...entries].reverse()); expect(first.equals(second)).toBe(true); expect(sha256(first)).toMatch(/^[a-f0-9]{64}$/); }); it('stores sorted UTF-8 entries with the normalized epoch and file mode', () => { const archive = createDeterministicZip(entries); expect(archive.readUInt32LE(0)).toBe(0x04034b50); expect(archive.readUInt16LE(10)).toBe(0); expect(archive.readUInt16LE(12)).toBe(33); expect(archive.subarray(30, 35).toString('utf8')).toBe('a.txt'); const centralOffset = archive.indexOf( Buffer.from([0x50, 0x4b, 0x01, 0x02]) ); expect(centralOffset).toBeGreaterThan(0); expect(archive.readUInt16LE(centralOffset + 4)).toBe(0x0314); expect(archive.readUInt32LE(centralOffset + 38) >>> 16).toBe(0o100644); }); it('rejects duplicate paths and local absolute paths in text', () => { expect(() => createDeterministicZip([ { name: 'same.txt', data: Buffer.from('one') }, { name: 'same.txt', data: Buffer.from('two') }, ]) ).toThrow(/duplicate/i); expect(() => createDeterministicZip([ { name: 'bundle.js', data: Buffer.from('source: "/home/user/file"') }, ]) ).toThrow(/local absolute path/i); }); }); describe('licence release gate', () => { it('blocks a normal release without an application licence', async () => { const directory = await temporaryDirectory(); await expect(assertLicenseReady(directory)).rejects.toThrow( /RELEASE BLOCKED/ ); }); it('allows only the explicit development-smoke escape hatch', async () => { const directory = await temporaryDirectory(); await expect(assertLicenseReady(directory, true)).resolves.toBeNull(); expect( parseReleaseArguments(['--allow-unlicensed-development-smoke']) ).toEqual({ allowUnlicensedDevelopmentSmoke: true, force: false, }); expect(() => parseReleaseArguments(['--publish'])).toThrow(/unknown/i); }); it('recognizes a complete GPL version 3 root application licence', async () => { const directory = await temporaryDirectory(); const licence = path.join(directory, 'LICENSE'); await writeFile( licence, [ 'GNU GENERAL PUBLIC LICENSE', 'Version 3, 29 June 2007', 'END OF TERMS AND CONDITIONS', ].join('\n') ); await expect(assertLicenseReady(directory)).resolves.toBe(licence); }); it('rejects an arbitrary file named LICENSE', async () => { const directory = await temporaryDirectory(); await writeFile(path.join(directory, 'LICENSE'), 'example only'); await expect(assertLicenseReady(directory)).rejects.toThrow( /complete GNU GPL version 3/i ); }); }); describe('release checksum', () => { it('formats and parses the portable sidecar form', () => { const digest = 'a'.repeat(64); const line = checksumLine(digest, 'av-tools-0.3.0.zip'); expect(line).toBe(`${digest} av-tools-0.3.0.zip\n`); expect(parseChecksumLine(line)).toEqual({ archiveName: 'av-tools-0.3.0.zip', digest, }); }); it('writes and verifies a checksum sidecar', async () => { const directory = await temporaryDirectory(); const archive = path.join(directory, 'av-tools-0.3.0.zip'); await writeFile(archive, 'deterministic bytes'); const result = await writeChecksum(archive); expect(await verifyChecksum(archive, result.sidecarPath)).toBe( result.digest ); expect(await readFile(result.sidecarPath, 'utf8')).toContain( ' av-tools-0.3.0.zip' ); }); it('rejects checksum mismatches', async () => { const directory = await temporaryDirectory(); const archive = path.join(directory, 'av-tools-0.3.0.zip'); const sidecar = `${archive}.sha256`; await writeFile(archive, 'changed'); await mkdir(path.dirname(sidecar), { recursive: true }); await writeFile( sidecar, checksumLine('0'.repeat(64), path.basename(archive)) ); await expect(verifyChecksum(archive, sidecar)).rejects.toThrow(/mismatch/i); }); }); describe('portal assembly smoke validation', () => { const isolatedHeaders = ` add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Embedder-Policy "require-corp" always; add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:; media-src 'self' blob:;" always; types { application/wasm wasm; } "~^/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/(?:st|mt)/ffmpeg-core\\.(?:js|wasm)$" "public, max-age=31536000, immutable"; `; it('accepts the required isolation, CSP and MIME profile', () => { expect(inspectPortalHeaders(isolatedHeaders)).toEqual({ errors: [], warnings: [], }); }); it('detects the current media-preview CSP gap', () => { const inspection = inspectPortalHeaders( isolatedHeaders.replace("media-src 'self' blob:;", '') ); expect(inspection.errors).toContain( "CSP media-src must contain 'self' and blob:" ); }); it('does not mistake a plain-semver cache rule for reviewed-build coverage', () => { const inspection = inspectPortalHeaders( isolatedHeaders.replace('0.12.10-reviewed-stack5m.1', '0.12.10') ); expect(inspection.warnings).toContainEqual( expect.stringMatching(/reviewed FFmpeg build/u) ); }); it('parses only explicit smoke overrides and paths', () => { const parsed = parsePortalArguments( [ '--artifact', 'release/example.zip', '--allow-unlicensed-development-smoke', '--allow-known-header-gap', '--keep-temp', ], '/work/av-tools' ); expect(parsed).toEqual({ allowKnownHeaderGap: true, allowUnlicensedDevelopmentSmoke: true, artifact: '/work/av-tools/release/example.zip', keepTemp: true, portalRoot: '/work/references/toolbox-portal', }); expect(() => parsePortalArguments(['--publish'], '/work/av-tools')).toThrow( /unknown/i ); }); });