176 lines
8.3 KiB
JavaScript
176 lines
8.3 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { lstat, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import {
|
|
assertPreparedAssetDirectoryIdentity,
|
|
verifyApprovedAssetSet,
|
|
} from '../../scripts/materialize-empty-asset-set.mjs';
|
|
|
|
const temporaryRoots = [];
|
|
|
|
const createScratchRepository = async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'portfolio-empty-assets-'));
|
|
temporaryRoots.push(root);
|
|
await mkdir(join(root, 'content/public'), { recursive: true });
|
|
await writeFile(join(root, 'content/public/asset-manifest.json'), '{"assets":[]}');
|
|
await writeFile(join(root, 'content/public/portfolio-content.json'), JSON.stringify({
|
|
projects: { selected: [], additional: [] },
|
|
toolkit: [],
|
|
metadata: {},
|
|
}));
|
|
return root;
|
|
};
|
|
|
|
const assetBytes = Buffer.from('approved public asset');
|
|
const assetHash = createHash('sha256').update(assetBytes).digest('hex');
|
|
const writeApprovedSet = async (root) => {
|
|
await writeFile(join(root, 'content/public/asset-manifest.json'), JSON.stringify({ assets: [{
|
|
id: 'approved-image',
|
|
fileName: 'approved-image.webp',
|
|
sha256: assetHash,
|
|
}] }));
|
|
await writeFile(join(root, 'content/public/portfolio-content.json'), JSON.stringify({
|
|
projects: { selected: [{ assetIds: ['approved-image'] }], additional: [] },
|
|
toolkit: [],
|
|
metadata: {},
|
|
}));
|
|
await mkdir(join(root, 'inputs/prepared-assets'), { recursive: true });
|
|
await writeFile(join(root, 'inputs/prepared-assets/approved-image.webp'), assetBytes);
|
|
};
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe('approved asset publication closure', () => {
|
|
it('creates an empty prepared-asset directory in the supplied repository root', async () => {
|
|
const root = await createScratchRepository();
|
|
await verifyApprovedAssetSet(root);
|
|
expect((await lstat(join(root, 'inputs/prepared-assets'))).isDirectory()).toBe(true);
|
|
});
|
|
|
|
it('accepts an approved manifest, public reference, and prepared file with exact closure', async () => {
|
|
const root = await createScratchRepository();
|
|
await writeApprovedSet(root);
|
|
await expect(verifyApprovedAssetSet(root)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('includes every soundtrack codec variant in the approved publication closure', async () => {
|
|
const root = await createScratchRepository();
|
|
const assets = [
|
|
['soundtrack-intro-opus', 'nightdrive-intro.opus'],
|
|
['soundtrack-intro-mp3', 'nightdrive-intro.mp3'],
|
|
['soundtrack-main-opus', 'nightdrive-main.opus'],
|
|
['soundtrack-main-mp3', 'nightdrive-main.mp3'],
|
|
['soundtrack-afterglow-opus', 'nightdrive-afterglow.opus'],
|
|
['soundtrack-afterglow-mp3', 'nightdrive-afterglow.mp3'],
|
|
['soundtrack-drive-lift-opus', 'nightdrive-drive-lift.opus'],
|
|
['soundtrack-drive-lift-mp3', 'nightdrive-drive-lift.mp3'],
|
|
].map(([id, fileName], index) => {
|
|
const bytes = Buffer.from(`approved soundtrack asset ${index}`);
|
|
return { id, fileName, bytes, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
});
|
|
await writeFile(join(root, 'content/public/asset-manifest.json'), JSON.stringify({
|
|
assets: assets.map(({ id, fileName, sha256 }) => ({ id, fileName, sha256 })),
|
|
}));
|
|
await writeFile(join(root, 'content/public/portfolio-content.json'), JSON.stringify({
|
|
projects: { selected: [], additional: [] },
|
|
toolkit: [],
|
|
metadata: {},
|
|
soundtrack: {
|
|
intro: { opusAssetId: 'soundtrack-intro-opus', mp3AssetId: 'soundtrack-intro-mp3' },
|
|
main: { opusAssetId: 'soundtrack-main-opus', mp3AssetId: 'soundtrack-main-mp3' },
|
|
afterglow: { opusAssetId: 'soundtrack-afterglow-opus', mp3AssetId: 'soundtrack-afterglow-mp3' },
|
|
driveLift: { opusAssetId: 'soundtrack-drive-lift-opus', mp3AssetId: 'soundtrack-drive-lift-mp3' },
|
|
},
|
|
}));
|
|
await mkdir(join(root, 'inputs/prepared-assets'), { recursive: true });
|
|
await Promise.all(assets.map(({ fileName, bytes }) =>
|
|
writeFile(join(root, 'inputs/prepared-assets', fileName), bytes)));
|
|
|
|
await expect(verifyApprovedAssetSet(root)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('rejects a symbolic-link prepared-asset directory', async () => {
|
|
const root = await createScratchRepository();
|
|
const target = join(root, 'outside');
|
|
await mkdir(target);
|
|
await mkdir(join(root, 'inputs'), { recursive: true });
|
|
await symlink(target, join(root, 'inputs/prepared-assets'));
|
|
await expect(verifyApprovedAssetSet(root)).rejects.toThrow(/non-symlink directory/i);
|
|
});
|
|
|
|
it('rejects extra prepared files', async () => {
|
|
const root = await createScratchRepository();
|
|
const preparedAssetsPath = join(root, 'inputs/prepared-assets');
|
|
await mkdir(preparedAssetsPath, { recursive: true });
|
|
await writeFile(join(preparedAssetsPath, 'unexpected.webp'), 'not an asset');
|
|
await expect(verifyApprovedAssetSet(root)).rejects.toThrow(/extra prepared asset/i);
|
|
});
|
|
|
|
it('rejects missing and path-mismatched prepared files', async () => {
|
|
const root = await createScratchRepository();
|
|
await writeApprovedSet(root);
|
|
await rm(join(root, 'inputs/prepared-assets/approved-image.webp'));
|
|
await writeFile(join(root, 'inputs/prepared-assets/wrong-image.webp'), assetBytes);
|
|
await expect(verifyApprovedAssetSet(root)).rejects.toThrow(/extra prepared asset|missing prepared asset/i);
|
|
});
|
|
|
|
it('rejects unknown, unreferenced, and duplicate publication IDs', async () => {
|
|
const unknown = await createScratchRepository();
|
|
await writeApprovedSet(unknown);
|
|
await writeFile(join(unknown, 'content/public/portfolio-content.json'), JSON.stringify({
|
|
projects: { selected: [{ assetIds: ['unknown-image'] }], additional: [] }, toolkit: [], metadata: {},
|
|
}));
|
|
await expect(verifyApprovedAssetSet(unknown)).rejects.toThrow(/unknown public asset reference/i);
|
|
|
|
const unreferenced = await createScratchRepository();
|
|
await writeApprovedSet(unreferenced);
|
|
await writeFile(join(unreferenced, 'content/public/portfolio-content.json'), JSON.stringify({
|
|
projects: { selected: [], additional: [] }, toolkit: [], metadata: {},
|
|
}));
|
|
await expect(verifyApprovedAssetSet(unreferenced)).rejects.toThrow(/unreferenced manifest asset/i);
|
|
|
|
const duplicate = await createScratchRepository();
|
|
await writeApprovedSet(duplicate);
|
|
await writeFile(join(duplicate, 'content/public/portfolio-content.json'), JSON.stringify({
|
|
projects: { selected: [{ assetIds: ['approved-image', 'approved-image'] }], additional: [] },
|
|
toolkit: [], metadata: {},
|
|
}));
|
|
await expect(verifyApprovedAssetSet(duplicate)).rejects.toThrow(/duplicate public asset reference/i);
|
|
});
|
|
|
|
it('rejects duplicate manifest IDs', async () => {
|
|
const root = await createScratchRepository();
|
|
await writeApprovedSet(root);
|
|
const duplicateAsset = { id: 'approved-image', fileName: 'approved-image.webp', sha256: assetHash };
|
|
await writeFile(join(root, 'content/public/asset-manifest.json'), JSON.stringify({
|
|
assets: [duplicateAsset, { ...duplicateAsset, fileName: 'second-image.webp' }],
|
|
}));
|
|
await expect(verifyApprovedAssetSet(root)).rejects.toThrow(/duplicate manifest asset id/i);
|
|
});
|
|
|
|
it('rejects duplicate manifest filenames', async () => {
|
|
const root = await createScratchRepository();
|
|
await writeApprovedSet(root);
|
|
const duplicateAsset = { id: 'approved-image', fileName: 'approved-image.webp', sha256: assetHash };
|
|
await writeFile(join(root, 'content/public/asset-manifest.json'), JSON.stringify({
|
|
assets: [duplicateAsset, { ...duplicateAsset, id: 'second-image' }],
|
|
}));
|
|
await expect(verifyApprovedAssetSet(root)).rejects.toThrow(/duplicate manifest filename/i);
|
|
});
|
|
|
|
it('rejects a prepared-asset directory replaced after its identity was captured', async () => {
|
|
const root = await createScratchRepository();
|
|
const preparedAssetsPath = join(root, 'inputs/prepared-assets');
|
|
await mkdir(preparedAssetsPath, { recursive: true });
|
|
const original = await lstat(preparedAssetsPath);
|
|
await rm(preparedAssetsPath, { recursive: true });
|
|
await mkdir(preparedAssetsPath);
|
|
await expect(assertPreparedAssetDirectoryIdentity(preparedAssetsPath, original))
|
|
.rejects.toThrow(/identity changed/i);
|
|
});
|
|
});
|