feat: publish av-tools 0.1.0
This commit is contained in:
251
tests/unit/release-package.test.mjs
Normal file
251
tests/unit/release-package.test.mjs
Normal file
@@ -0,0 +1,251 @@
|
||||
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.1.0.zip');
|
||||
expect(line).toBe(`${digest} av-tools-0.1.0.zip\n`);
|
||||
expect(parseChecksumLine(line)).toEqual({
|
||||
archiveName: 'av-tools-0.1.0.zip',
|
||||
digest,
|
||||
});
|
||||
});
|
||||
|
||||
it('writes and verifies a checksum sidecar', async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const archive = path.join(directory, 'av-tools-0.1.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.1.0.zip'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects checksum mismatches', async () => {
|
||||
const directory = await temporaryDirectory();
|
||||
const archive = path.join(directory, 'av-tools-0.1.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; }
|
||||
# vendor/ffmpeg/0.12.10
|
||||
`;
|
||||
|
||||
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('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
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user