fix: enforce approved media publication closure
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
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,
|
||||
materializeEmptyAssetSet,
|
||||
verifyApprovedAssetSet,
|
||||
} from '../../scripts/materialize-empty-asset-set.mjs';
|
||||
|
||||
const temporaryRoots = [];
|
||||
@@ -14,35 +15,115 @@ const createScratchRepository = async () => {
|
||||
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('empty asset materialization', () => {
|
||||
describe('approved asset publication closure', () => {
|
||||
it('creates an empty prepared-asset directory in the supplied repository root', async () => {
|
||||
const root = await createScratchRepository();
|
||||
await materializeEmptyAssetSet(root);
|
||||
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('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(materializeEmptyAssetSet(root)).rejects.toThrow(/non-symlink directory/i);
|
||||
await expect(verifyApprovedAssetSet(root)).rejects.toThrow(/non-symlink directory/i);
|
||||
});
|
||||
|
||||
it('rejects unexpected prepared-asset members in its supplied repository root', async () => {
|
||||
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(materializeEmptyAssetSet(root)).rejects.toThrow(/must be empty/i);
|
||||
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 () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parsePortfolioContent, parsePublicAssetManifest, parseSiteConfig } from '@/content/schemas';
|
||||
import { validateProductionInputs } from '@/content/schemas';
|
||||
|
||||
const repositoryRoot = process.cwd();
|
||||
const readJson = async (relativePath: string): Promise<unknown> =>
|
||||
@@ -8,9 +8,12 @@ const readJson = async (relativePath: string): Promise<unknown> =>
|
||||
|
||||
describe('approved public portfolio projection', () => {
|
||||
it('contains approved identity and balanced evidence without prohibited surfaces', async () => {
|
||||
const content = parsePortfolioContent(await readJson('content/public/portfolio-content.json'));
|
||||
const manifest = parsePublicAssetManifest(await readJson('content/public/asset-manifest.json'));
|
||||
const site = parseSiteConfig(await readJson('config/site.preview.json'));
|
||||
const { content, manifest, site } = validateProductionInputs(
|
||||
await readJson('content/public/portfolio-content.json'),
|
||||
await readJson('content/public/asset-manifest.json'),
|
||||
await readJson('config/site.preview.json'),
|
||||
new URL('file:///prepared-assets/'),
|
||||
);
|
||||
|
||||
expect(content.identity).toEqual(expect.objectContaining({
|
||||
name: 'Luca Pîslaru',
|
||||
@@ -25,8 +28,6 @@ describe('approved public portfolio projection', () => {
|
||||
.toBe('Private self-hosted medication tracker');
|
||||
expect(content.projects.additional.find((project) => project.id === 'private-game-prototype')?.title)
|
||||
.toBe('Private game prototype');
|
||||
expect([...content.projects.selected, ...content.projects.additional]
|
||||
.every((project) => project.assetIds.length === 0)).toBe(true);
|
||||
expect(content.toolkit.flatMap((group) => group.technologies).map((technology) => technology.name))
|
||||
.toEqual(expect.arrayContaining(['Linux', 'Docker', 'Python', 'Vue.js', 'Java']));
|
||||
expect(content.contact).toEqual(expect.objectContaining({ email: 'luca.pislaru@gmail.com' }));
|
||||
@@ -35,7 +36,14 @@ describe('approved public portfolio projection', () => {
|
||||
'https://gitlab.com/luca_pisl',
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
]);
|
||||
expect(manifest.assets).toEqual([]);
|
||||
const publishedReferences = [
|
||||
...content.projects.selected.flatMap((project) => project.assetIds),
|
||||
...content.projects.additional.flatMap((project) => project.assetIds),
|
||||
...content.toolkit.flatMap((group) => group.technologies.flatMap((technology) =>
|
||||
technology.assetId === undefined ? [] : [technology.assetId])),
|
||||
...(content.metadata.socialPreviewAssetId === undefined ? [] : [content.metadata.socialPreviewAssetId]),
|
||||
];
|
||||
expect([...publishedReferences].sort()).toEqual(manifest.assets.map((asset) => asset.id).sort());
|
||||
expect(site).toEqual({
|
||||
environment: 'preview',
|
||||
canonicalUrl: 'https://portfolio.localhost/',
|
||||
|
||||
Reference in New Issue
Block a user