115 lines
4.6 KiB
JavaScript
115 lines
4.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { verifyPdfArchive } from '../scripts/verify-pdf-archive.mjs';
|
|
|
|
async function withArchiveFixture(run) {
|
|
const root = await mkdtemp(join(tmpdir(), 'incognito-pdf-archive-'));
|
|
try {
|
|
const relativePath = 'public/media/legacy-dokuwiki/study/course/exam.pdf';
|
|
const absolutePath = join(root, relativePath);
|
|
const bytes = Buffer.from('%PDF-1.4\nfixture\n%%EOF\n');
|
|
await mkdir(join(absolutePath, '..'), { recursive: true });
|
|
await writeFile(absolutePath, bytes);
|
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
const inventoryPath = join(root, 'inventory.tsv');
|
|
const row = [
|
|
'study:course:exam.pdf',
|
|
'/media/legacy-dokuwiki/study/course/exam.pdf',
|
|
relativePath,
|
|
'https://msvincognito.nl/wiki/_media/study/course/exam.pdf',
|
|
String(bytes.length),
|
|
sha256,
|
|
];
|
|
await writeFile(inventoryPath, [
|
|
'media_id\tpublic_url\tlocal_path\tsource_url\tbytes\tsha256',
|
|
row.join('\t'),
|
|
'',
|
|
].join('\n'));
|
|
await run({ root, inventoryPath, absolutePath, bytes, row });
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test('verifies a complete byte-identical public PDF archive', async () => {
|
|
await withArchiveFixture(async ({ root, inventoryPath, bytes }) => {
|
|
const result = await verifyPdfArchive({ inventoryPath, projectRoot: root, expectedCount: 1 });
|
|
assert.deepEqual(result, { fileCount: 1, totalBytes: bytes.length, uniqueHashes: 1 });
|
|
});
|
|
});
|
|
|
|
test('rejects a missing inventoried public PDF', async () => {
|
|
await withArchiveFixture(async ({ root, inventoryPath, absolutePath }) => {
|
|
await rm(absolutePath);
|
|
await assert.rejects(
|
|
verifyPdfArchive({ inventoryPath, projectRoot: root, expectedCount: 1 }),
|
|
/missing public PDF/,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('rejects a PDF whose published bytes do not match the inventory', async () => {
|
|
await withArchiveFixture(async ({ root, inventoryPath, absolutePath }) => {
|
|
await writeFile(absolutePath, Buffer.from('%PDF-1.4\nchanged\n%%EOF\n'));
|
|
await assert.rejects(
|
|
verifyPdfArchive({ inventoryPath, projectRoot: root, expectedCount: 1 }),
|
|
/byte size mismatch|SHA-256 mismatch/,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('rejects an inventoried file without a PDF signature', async () => {
|
|
await withArchiveFixture(async ({ root, inventoryPath, absolutePath, row }) => {
|
|
const bytes = Buffer.from('not a PDF');
|
|
await writeFile(absolutePath, bytes);
|
|
row[4] = String(bytes.length);
|
|
row[5] = createHash('sha256').update(bytes).digest('hex');
|
|
await writeFile(inventoryPath, [
|
|
'media_id\tpublic_url\tlocal_path\tsource_url\tbytes\tsha256',
|
|
row.join('\t'),
|
|
'',
|
|
].join('\n'));
|
|
await assert.rejects(
|
|
verifyPdfArchive({ inventoryPath, projectRoot: root, expectedCount: 1 }),
|
|
/invalid PDF signature/,
|
|
);
|
|
});
|
|
});
|
|
|
|
test('the reader-facing archive links every inventoried PDF exactly once', async () => {
|
|
const inventory = (await readFile('docs/pdf-inventory.tsv', 'utf8')).trim().split(/\r?\n/).slice(1);
|
|
const publicUrls = inventory.map((row) => row.split('\t')[1]);
|
|
const archive = await readFile('src/content/docs/previous-exams-and-documents.md', 'utf8');
|
|
const pdfLinks = [...archive.matchAll(/\]\((\/media\/legacy-dokuwiki\/[^)]+\.pdf)\)/g)]
|
|
.map((match) => match[1]);
|
|
|
|
assert.equal(pdfLinks.length, 332);
|
|
assert.deepEqual(new Set(pdfLinks), new Set(publicUrls));
|
|
for (const url of publicUrls) {
|
|
assert.equal(pdfLinks.filter((candidate) => candidate === url).length, 1, `${url} must appear once`);
|
|
}
|
|
});
|
|
|
|
test('the PDF archive is registered in supplemental content and the sidebar', async () => {
|
|
const supplemental = JSON.parse(await readFile('docs/supplemental-content.json', 'utf8'));
|
|
assert.deepEqual(supplemental.find(({ category }) => category === 'pdf-archive'), {
|
|
destination: 'src/content/docs/previous-exams-and-documents.md',
|
|
source: 'docs/pdf-inventory.tsv',
|
|
category: 'pdf-archive',
|
|
});
|
|
|
|
const sidebar = await readFile('src/config/sidebar.mjs', 'utf8');
|
|
assert.match(sidebar, /slug: 'previous-exams-and-documents'/);
|
|
for (const overview of [
|
|
'src/content/docs/bachelor/index.md',
|
|
'src/content/docs/master-ai/index.md',
|
|
'src/content/docs/master-dsdm/index.md',
|
|
]) {
|
|
assert.match(await readFile(overview, 'utf8'), /\[Previous exams and documents\]\(\/previous-exams-and-documents\/\)/);
|
|
}
|
|
});
|