70 lines
2.8 KiB
TypeScript
70 lines
2.8 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
parsePortfolioContent,
|
|
parsePublicAssetManifest,
|
|
parseSiteConfig,
|
|
validateProductionInputs,
|
|
} from '@/content/schemas';
|
|
|
|
const fixture = (name: string): unknown =>
|
|
JSON.parse(readFileSync(resolve(process.cwd(), 'tests/fixtures', name), 'utf8'));
|
|
|
|
const validContent = fixture('portfolio-content.valid.json');
|
|
const validManifest = fixture('asset-manifest.valid.json');
|
|
const validSite = fixture('site-config.preview.json');
|
|
type MutableContentFixture = {
|
|
navigation: Array<{ id: string }>;
|
|
projects: {
|
|
selected: Array<{ visibility: string; url?: string; assetIds: string[]; title: string }>;
|
|
};
|
|
};
|
|
|
|
const cloneContent = (): MutableContentFixture => structuredClone(validContent) as MutableContentFixture;
|
|
|
|
describe('content schemas', () => {
|
|
it('accepts the complete synthetic public input boundary', () => {
|
|
expect(parsePortfolioContent(validContent).identity.name).toBe('Luca Pîslaru');
|
|
expect(parsePublicAssetManifest(validManifest).assets).toHaveLength(5);
|
|
expect(parseSiteConfig(validSite).environment).toBe('preview');
|
|
});
|
|
|
|
it('rejects a public link on a private project', () => {
|
|
const input = cloneContent();
|
|
input.projects.selected[0]!.visibility = 'private';
|
|
input.projects.selected[0]!.url = 'https://example.test/private';
|
|
expect(() => parsePortfolioContent(input)).toThrow(/private project.*url/i);
|
|
});
|
|
|
|
it('rejects unknown site configuration keys', () => {
|
|
expect(() => parseSiteConfig({
|
|
environment: 'preview',
|
|
canonicalUrl: 'https://portfolio.example.test',
|
|
buildId: 'preview-1',
|
|
contactEmail: 'forbidden@example.test',
|
|
})).toThrow(/unrecognized/i);
|
|
});
|
|
|
|
it('rejects an asset reference absent from the manifest', () => {
|
|
const content = cloneContent();
|
|
content.projects.selected[0]!.assetIds = ['missing-asset'];
|
|
expect(() => validateProductionInputs(content, validManifest, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/unmanifested asset/i);
|
|
});
|
|
|
|
it('rejects duplicate manifest asset IDs and unsafe generated filenames', () => {
|
|
const manifest = structuredClone(validManifest) as { assets: Array<{ id: string; fileName: string }> };
|
|
manifest.assets[1]!.id = manifest.assets[0]!.id;
|
|
manifest.assets[2]!.fileName = '../original.webp';
|
|
expect(() => parsePublicAssetManifest(manifest)).toThrow(/duplicate asset id|generated safe filename/i);
|
|
});
|
|
|
|
it('requires canonical navigation and generic private titles', () => {
|
|
const content = cloneContent();
|
|
content.navigation[7]!.id = 'profile';
|
|
content.projects.selected[0]!.title = 'Private medical tracker';
|
|
expect(() => parsePortfolioContent(content)).toThrow(/navigation|private project title/i);
|
|
});
|
|
});
|