Refs #169. Pin and verify pristine distribution assets, upgrade to 1.15.2, and add tested maintainer tooling plus desktop and touch browser checks.
108 lines
5.6 KiB
JavaScript
108 lines
5.6 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import { syncViewer } from '../../tools/vendor/vendor-viewerjs.mjs';
|
|
import { updateViewer } from '../../tools/vendor/update-viewerjs.mjs';
|
|
|
|
function fixture(t) {
|
|
const root = mkdtempSync(join(tmpdir(), 'pm-vendor-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
const tooling = join(root, 'tools/vendor');
|
|
const dependency = join(tooling, 'node_modules/viewerjs');
|
|
mkdirSync(join(dependency, 'dist'), { recursive: true });
|
|
writeFileSync(join(tooling, 'package.json'), JSON.stringify({ devDependencies: { viewerjs: '1.15.2' } }));
|
|
writeFileSync(join(tooling, 'package-lock.json'), JSON.stringify({ packages: {
|
|
'node_modules/viewerjs': { version: '1.15.2', resolved: 'https://registry.npmjs.org/viewerjs/-/viewerjs-1.15.2.tgz', integrity: 'sha512-fixture' },
|
|
} }));
|
|
writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: 'viewerjs', version: '1.15.2', license: 'MIT' }));
|
|
for (const name of ['viewer.min.js', 'viewer.min.css']) {
|
|
writeFileSync(join(dependency, 'dist', name), `/*! Viewer.js v1.15.2 */\n${name}`);
|
|
}
|
|
writeFileSync(join(dependency, 'LICENSE'), 'The MIT License (MIT)\nCopyright Chen Fengyuan\n');
|
|
return { root, tooling, dependency, destination: join(root, 'web/lib/viewerjs') };
|
|
}
|
|
|
|
test('sync copies pristine assets and license and records locked provenance deterministically', t => {
|
|
const f = fixture(t);
|
|
syncViewer({ root: f.root });
|
|
const first = readFileSync(join(f.destination, 'provenance.json'), 'utf8');
|
|
const record = JSON.parse(first);
|
|
assert.equal(record.version, '1.15.2');
|
|
assert.equal(record.integrity, 'sha512-fixture');
|
|
for (const [target, source] of Object.entries({ 'viewer.min.js': 'dist/viewer.min.js', 'viewer.min.css': 'dist/viewer.min.css', LICENSE: 'LICENSE' })) {
|
|
const original = readFileSync(join(f.dependency, source));
|
|
assert.deepEqual(readFileSync(join(f.destination, target)), original);
|
|
assert.equal(record.files[target].sha256, createHash('sha256').update(original).digest('hex'));
|
|
}
|
|
syncViewer({ root: f.root });
|
|
assert.equal(readFileSync(join(f.destination, 'provenance.json'), 'utf8'), first);
|
|
syncViewer({ root: f.root, check: true });
|
|
});
|
|
|
|
test('check rejects drift without overwriting local changes', t => {
|
|
const f = fixture(t);
|
|
syncViewer({ root: f.root });
|
|
writeFileSync(join(f.destination, 'viewer.min.js'), 'local edit');
|
|
assert.throws(() => syncViewer({ root: f.root, check: true }), /viewer.min.js/);
|
|
assert.equal(readFileSync(join(f.destination, 'viewer.min.js'), 'utf8'), 'local edit');
|
|
rmSync(join(f.destination, 'LICENSE'));
|
|
assert.throws(() => syncViewer({ root: f.root, check: true }), /LICENSE/);
|
|
});
|
|
|
|
test('sync refuses mismatched installed versions and unpinned dependencies', t => {
|
|
const f = fixture(t);
|
|
writeFileSync(join(f.dependency, 'package.json'), JSON.stringify({ name: 'viewerjs', version: '1.11.6', license: 'MIT' }));
|
|
assert.throws(() => syncViewer({ root: f.root }), /version/i);
|
|
writeFileSync(join(f.tooling, 'package.json'), JSON.stringify({ devDependencies: { viewerjs: '^1.15.2' } }));
|
|
assert.throws(() => syncViewer({ root: f.root }), /exact|pin/i);
|
|
});
|
|
|
|
test('sync validates every upstream asset before writing anything', t => {
|
|
const f = fixture(t);
|
|
writeFileSync(join(f.dependency, 'dist/viewer.min.css'), '/*! Viewer.js v1.11.6 */');
|
|
assert.throws(() => syncViewer({ root: f.root }), /version|banner/i);
|
|
assert.throws(() => readFileSync(join(f.destination, 'viewer.min.js')), /ENOENT/);
|
|
});
|
|
|
|
test('updater resolves the stable release and installs it exactly with lifecycle scripts disabled', t => {
|
|
const f = fixture(t);
|
|
const calls = [];
|
|
let synced = false;
|
|
const run = (args, options) => {
|
|
calls.push({ args, options });
|
|
return args[0] === 'view' ? '"1.15.3"' : '';
|
|
};
|
|
assert.equal(updateViewer({ root: f.root, run, sync: () => { synced = true; } }), '1.15.3');
|
|
assert.deepEqual(calls[1].args, ['install', '--save-dev', '--save-exact', '--ignore-scripts', 'viewerjs@1.15.3']);
|
|
assert.equal(calls[1].options.cwd, f.tooling);
|
|
assert.equal(synced, true);
|
|
});
|
|
|
|
test('updater is a no-op at the current release and rejects invalid version arguments', t => {
|
|
const f = fixture(t);
|
|
const run = () => { throw new Error('must not install'); };
|
|
assert.equal(updateViewer({ root: f.root, version: '1.15.2', run }), null);
|
|
for (const version of ['latest', '1.15.3-beta.1', '1.15.3;anything', '--global']) {
|
|
assert.throws(() => updateViewer({ root: f.root, version, run }), /version/i);
|
|
}
|
|
});
|
|
|
|
test('failed installation never copies vendor assets', t => {
|
|
const f = fixture(t);
|
|
let synced = false;
|
|
assert.throws(() => updateViewer({ root: f.root, version: '1.15.3', run: () => { throw new Error('offline'); }, sync: () => { synced = true; } }), /offline/);
|
|
assert.equal(synced, false);
|
|
});
|
|
|
|
test('updater accepts a single registry result array and rejects ambiguous metadata', t => {
|
|
const f = fixture(t);
|
|
const calls = [];
|
|
const run = args => { calls.push(args); return '["1.15.2"]'; };
|
|
assert.equal(updateViewer({ root: f.root, run }), null);
|
|
assert.equal(calls.length, 1);
|
|
assert.throws(() => updateViewer({ root: f.root, run: () => '["1.15.2", "1.15.3"]' }), /version/i);
|
|
});
|