273 lines
13 KiB
TypeScript
273 lines
13 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 = {
|
|
identity: {
|
|
location: { city: string; country: string; countryCode: string };
|
|
availability: string;
|
|
};
|
|
navigation: Array<{ id: string }>;
|
|
profile: { introduction: string };
|
|
contact: { introduction: string; social: Array<{ service: string; url: string }> };
|
|
infrastructure: { featuredOperation: { actions: string[] } };
|
|
projects: {
|
|
selected: Array<{
|
|
visibility: string;
|
|
url?: string;
|
|
assetIds: string[];
|
|
title: string;
|
|
media?: Array<{ assetId: string; alt: string; kind: 'image' }>;
|
|
mediaLayout?: 'single' | 'portrait-strip' | 'chart-pair';
|
|
}>;
|
|
additional: Array<{ assetIds: string[] }>;
|
|
};
|
|
toolkit: Array<{ technologies: Array<{ assetId?: string }> }>;
|
|
metadata: { socialPreviewAssetId?: string };
|
|
soundtrack?: {
|
|
intro: { opusAssetId: string; mp3AssetId: string };
|
|
main: { opusAssetId: string; mp3AssetId: string };
|
|
afterglow: { opusAssetId: string; mp3AssetId: string };
|
|
driveLift: { opusAssetId: string; mp3AssetId: 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('requires public availability, city-level location, featured operations evidence, and contact copy', () => {
|
|
const content = cloneContent();
|
|
expect(content.identity.location).toEqual({ city: 'Bucharest', country: 'Romania', countryCode: 'RO' });
|
|
expect(content.identity.availability).toMatch(/Bucharest.*remote/iu);
|
|
expect(content.infrastructure.featuredOperation.actions.length).toBeGreaterThanOrEqual(3);
|
|
expect(content.contact.introduction).toMatch(/System Administrator.*Full-Stack Developer/iu);
|
|
|
|
delete (content.identity as Partial<typeof content.identity>).availability;
|
|
expect(() => parsePortfolioContent(content)).toThrow();
|
|
});
|
|
|
|
it('accepts only the approved project media layouts', () => {
|
|
const content = cloneContent();
|
|
content.projects.selected[0]!.mediaLayout = 'portrait-strip';
|
|
expect(() => parsePortfolioContent(content)).not.toThrow();
|
|
content.projects.selected[0]!.mediaLayout = 'carousel' as never;
|
|
expect(() => parsePortfolioContent(content)).toThrow(/mediaLayout|unrecognized/iu);
|
|
});
|
|
|
|
it('accepts an empty manifest before Phase 3 assets are prepared', () => {
|
|
expect(parsePublicAssetManifest({ assets: [] })).toEqual({ assets: [] });
|
|
});
|
|
|
|
it('accepts an optional soundtrack whose audio variants have exact manifest closure', () => {
|
|
const content = cloneContent();
|
|
content.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' },
|
|
};
|
|
const audioMetadata = {
|
|
width: null,
|
|
height: null,
|
|
rightsHolder: 'Luca Pîslaru',
|
|
license: 'All rights reserved',
|
|
attribution: 'Original soundtrack by Luca Pîslaru',
|
|
};
|
|
const soundtrackAssets = [
|
|
{ id: 'soundtrack-intro-opus', fileName: 'nightdrive-intro.opus', sha256: 'a'.repeat(64), mediaType: 'audio/ogg', durationMs: 56_064, ...audioMetadata },
|
|
{ id: 'soundtrack-intro-mp3', fileName: 'nightdrive-intro.mp3', sha256: 'b'.repeat(64), mediaType: 'audio/mpeg', durationMs: 56_064, ...audioMetadata },
|
|
{ id: 'soundtrack-main-opus', fileName: 'nightdrive-main.opus', sha256: 'c'.repeat(64), mediaType: 'audio/ogg', durationMs: 178_605, ...audioMetadata },
|
|
{ id: 'soundtrack-main-mp3', fileName: 'nightdrive-main.mp3', sha256: 'd'.repeat(64), mediaType: 'audio/mpeg', durationMs: 178_605, ...audioMetadata },
|
|
{ id: 'soundtrack-afterglow-opus', fileName: 'nightdrive-afterglow.opus', sha256: 'e'.repeat(64), mediaType: 'audio/ogg', durationMs: 56_064, ...audioMetadata },
|
|
{ id: 'soundtrack-afterglow-mp3', fileName: 'nightdrive-afterglow.mp3', sha256: 'f'.repeat(64), mediaType: 'audio/mpeg', durationMs: 56_064, ...audioMetadata },
|
|
{ id: 'soundtrack-drive-lift-opus', fileName: 'nightdrive-drive-lift.opus', sha256: '1'.repeat(64), mediaType: 'audio/ogg', durationMs: 178_605, ...audioMetadata },
|
|
{ id: 'soundtrack-drive-lift-mp3', fileName: 'nightdrive-drive-lift.mp3', sha256: '2'.repeat(64), mediaType: 'audio/mpeg', durationMs: 178_605, ...audioMetadata },
|
|
];
|
|
const manifest = {
|
|
assets: [...(validManifest as { assets: unknown[] }).assets, ...soundtrackAssets],
|
|
};
|
|
|
|
expect(validateProductionInputs(content, manifest, validSite, new URL('file:///prepared/')).content.soundtrack)
|
|
.toEqual(content.soundtrack);
|
|
|
|
content.soundtrack.main.mp3AssetId = 'missing-soundtrack';
|
|
expect(() => validateProductionInputs(content, manifest, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/unmanifested asset reference: missing-soundtrack/i);
|
|
});
|
|
|
|
it('requires audio assets to have null dimensions and a positive duration', () => {
|
|
const audio = {
|
|
id: 'soundtrack-main-opus',
|
|
fileName: 'nightdrive-main.opus',
|
|
sha256: 'a'.repeat(64),
|
|
mediaType: 'audio/ogg',
|
|
width: null,
|
|
height: null,
|
|
durationMs: 178_605,
|
|
rightsHolder: 'Luca Pîslaru',
|
|
license: 'All rights reserved',
|
|
attribution: 'Original soundtrack by Luca Pîslaru',
|
|
};
|
|
|
|
expect(() => parsePublicAssetManifest({ assets: [audio] })).not.toThrow();
|
|
expect(() => parsePublicAssetManifest({ assets: [{ ...audio, width: 1 }] })).toThrow(/audio assets require null dimensions/i);
|
|
expect(() => parsePublicAssetManifest({ assets: [{ ...audio, durationMs: null }] })).toThrow(/audio assets require.*duration/i);
|
|
});
|
|
|
|
it('accepts the approved generic private project titles', () => {
|
|
for (const title of [
|
|
'Private self-hosted medication tracker',
|
|
'Private healthcare platform',
|
|
'Private game prototype',
|
|
]) {
|
|
const content = cloneContent();
|
|
content.projects.selected[0]!.title = title;
|
|
expect(() => parsePortfolioContent(content)).not.toThrow();
|
|
}
|
|
});
|
|
|
|
it('permits omitted Phase 3 asset references while retaining supplied-reference checks', () => {
|
|
const content = cloneContent();
|
|
content.projects.selected[0]!.title = 'Private self-hosted medication tracker';
|
|
for (const project of [...content.projects.selected, ...content.projects.additional]) project.assetIds = [];
|
|
delete content.projects.selected[0]!.media;
|
|
delete content.toolkit[0]!.technologies[0]!.assetId;
|
|
delete content.metadata.socialPreviewAssetId;
|
|
expect(() => validateProductionInputs(content, { assets: [] }, validSite, new URL('file:///prepared/'))).not.toThrow();
|
|
});
|
|
|
|
it('rejects supplied toolkit and metadata asset references absent from the manifest', () => {
|
|
const content = cloneContent();
|
|
content.projects.selected[0]!.title = 'Private self-hosted medication tracker';
|
|
for (const project of [...content.projects.selected, ...content.projects.additional]) project.assetIds = [];
|
|
delete content.projects.selected[0]!.media;
|
|
content.toolkit[0]!.technologies[0]!.assetId = 'missing-toolkit-asset';
|
|
expect(() => validateProductionInputs(content, { assets: [] }, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/unmanifested asset reference: missing-toolkit-asset/i);
|
|
|
|
delete content.toolkit[0]!.technologies[0]!.assetId;
|
|
content.metadata.socialPreviewAssetId = 'missing-social-preview';
|
|
expect(() => validateProductionInputs(content, { assets: [] }, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/unmanifested asset reference: missing-social-preview/i);
|
|
});
|
|
|
|
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('accepts only the fixed preview canonical URL and no production hostname', () => {
|
|
expect(() => parseSiteConfig({
|
|
environment: 'preview', canonicalUrl: 'https://portfolio.example.test/', buildId: 'preview-1',
|
|
})).toThrow(/portfolio\.localhost/i);
|
|
expect(() => parseSiteConfig({
|
|
environment: 'preview', canonicalUrl: 'https://portfolio.localhost:444/', buildId: 'preview-1',
|
|
})).toThrow(/portfolio\.localhost/i);
|
|
expect(() => parseSiteConfig({
|
|
environment: 'production', canonicalUrl: 'https://portfolio.localhost/', buildId: 'production-1',
|
|
})).toThrow(/production hostname is not approved/i);
|
|
});
|
|
|
|
it('rejects preview URL normalization aliases instead of accepting their normalized value', () => {
|
|
for (const canonicalUrl of [
|
|
'https://portfolio.localhost/.',
|
|
'https://portfolio.localhost/%2e',
|
|
'https://portfolio.localhost/?',
|
|
]) {
|
|
expect(() => parseSiteConfig({ environment: 'preview', canonicalUrl, buildId: 'preview-1' }))
|
|
.toThrow(/exactly https:\/\/portfolio\.localhost\//i);
|
|
}
|
|
});
|
|
|
|
it('rejects controls, non-NFC URLs, and ports at all public URL boundaries', () => {
|
|
expect(() => parseSiteConfig({
|
|
environment: 'preview', canonicalUrl: 'https://portfolio.localhost/\n', buildId: 'preview-1',
|
|
})).toThrow(/url/i);
|
|
expect(() => parseSiteConfig({
|
|
environment: 'preview', canonicalUrl: 'https://portfolio.localhost:443/', buildId: 'preview-1',
|
|
})).toThrow(/url/i);
|
|
|
|
const content = cloneContent();
|
|
content.contact.social[0]!.url = 'https://github.com:443/example';
|
|
expect(() => parsePortfolioContent(content)).toThrow(/url/i);
|
|
|
|
const project = cloneContent();
|
|
project.projects.selected[1]!.url = 'https://example.test/cafe\u0301';
|
|
expect(() => parsePortfolioContent(project)).toThrow(/url/i);
|
|
});
|
|
|
|
it('rejects visible copy containing an unpaired UTF-16 surrogate', () => {
|
|
const content = cloneContent();
|
|
content.profile.introduction = 'Broken \ud800 copy';
|
|
expect(() => parsePortfolioContent(content)).toThrow(/visible copy/i);
|
|
|
|
const wellFormedContent = cloneContent();
|
|
wellFormedContent.profile.introduction = 'Well-formed 🚗 copy';
|
|
expect(() => parsePortfolioContent(wellFormedContent)).not.toThrow();
|
|
});
|
|
|
|
it('rejects an asset reference absent from the manifest', () => {
|
|
const content = cloneContent();
|
|
content.projects.selected[0]!.assetIds = ['missing-asset'];
|
|
content.projects.selected[0]!.media![0]!.assetId = 'missing-asset';
|
|
expect(() => validateProductionInputs(content, validManifest, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/unmanifested asset/i);
|
|
});
|
|
|
|
it('requires duplicate-free set equality between content references and manifest assets', () => {
|
|
const duplicateReference = cloneContent();
|
|
duplicateReference.projects.selected[1]!.assetIds = ['selected-build-one'];
|
|
expect(() => validateProductionInputs(duplicateReference, validManifest, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/duplicate public asset reference/i);
|
|
|
|
const unreferencedManifestAsset = cloneContent();
|
|
unreferencedManifestAsset.projects.selected[0]!.assetIds = [];
|
|
delete unreferencedManifestAsset.projects.selected[0]!.media;
|
|
expect(() => validateProductionInputs(unreferencedManifestAsset, validManifest, validSite, new URL('file:///prepared/')))
|
|
.toThrow(/unreferenced manifest 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);
|
|
});
|
|
});
|