content: add approved public portfolio projection
This commit is contained in:
2
tests/fixtures/portfolio-content.valid.json
vendored
2
tests/fixtures/portfolio-content.valid.json
vendored
@@ -47,7 +47,7 @@
|
||||
"selected": [
|
||||
{
|
||||
"id": "private-build-one",
|
||||
"title": "Private project",
|
||||
"title": "Private self-hosted medication tracker",
|
||||
"visibility": "private",
|
||||
"maturity": "active",
|
||||
"summary": "A private multi-device application.",
|
||||
|
||||
64
tests/integration/public-content.test.ts
Normal file
64
tests/integration/public-content.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { execFile as execFileCallback } from 'node:child_process';
|
||||
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { parsePortfolioContent, parsePublicAssetManifest, parseSiteConfig } from '@/content/schemas';
|
||||
|
||||
const execFile = promisify(execFileCallback);
|
||||
const repositoryRoot = process.cwd();
|
||||
const readJson = async (relativePath: string): Promise<unknown> =>
|
||||
JSON.parse(await readFile(`${repositoryRoot}/${relativePath}`, 'utf8')) as unknown;
|
||||
const preparedAssetsPath = `${repositoryRoot}/inputs/prepared-assets`;
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(preparedAssetsPath, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
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'));
|
||||
|
||||
expect(content.identity).toEqual(expect.objectContaining({
|
||||
name: 'Luca Pîslaru',
|
||||
role: 'System Administrator & Full-Stack Developer',
|
||||
}));
|
||||
expect(content.systemsOperations.title).toBe('Professional systems operations');
|
||||
expect(content.infrastructure.title).toBe('Secure self-hosted platform and lifecycle');
|
||||
expect(content.projects.selected).toHaveLength(3);
|
||||
expect(content.projects.selected.filter((project) => project.visibility === 'private')
|
||||
.every((project) => project.url === undefined)).toBe(true);
|
||||
expect(content.projects.selected.find((project) => project.visibility === 'private')?.title)
|
||||
.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' }));
|
||||
expect(content.contact.social.map((link) => link.url)).toEqual([
|
||||
'https://github.com/LucaPisl',
|
||||
'https://gitlab.com/luca_pisl',
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
]);
|
||||
expect(manifest.assets).toEqual([]);
|
||||
expect(site).toEqual({
|
||||
environment: 'preview',
|
||||
canonicalUrl: 'https://portfolio.localhost/',
|
||||
buildId: 'preview-semantic-v1',
|
||||
});
|
||||
expect(JSON.stringify(content)).not.toMatch(/Arch Linux|CV download|heatmap|expert|percent/i);
|
||||
});
|
||||
|
||||
it('materializes an empty prepared-asset directory and rejects unexpected members', async () => {
|
||||
await execFile(process.execPath, ['scripts/materialize-empty-asset-set.mjs'], { cwd: repositoryRoot });
|
||||
expect(await readdir(preparedAssetsPath)).toEqual([]);
|
||||
|
||||
await mkdir(preparedAssetsPath, { recursive: true });
|
||||
await writeFile(`${preparedAssetsPath}/unexpected.webp`, 'not an asset');
|
||||
await expect(execFile(process.execPath, ['scripts/materialize-empty-asset-set.mjs'], { cwd: repositoryRoot }))
|
||||
.rejects.toThrow(/must be empty/i);
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,10 @@ type MutableContentFixture = {
|
||||
contact: { social: Array<{ service: string; url: string }> };
|
||||
projects: {
|
||||
selected: Array<{ visibility: string; url?: string; assetIds: string[]; title: string }>;
|
||||
additional: Array<{ assetIds: string[] }>;
|
||||
};
|
||||
toolkit: Array<{ technologies: Array<{ assetId?: string }> }>;
|
||||
metadata: { socialPreviewAssetId?: string };
|
||||
};
|
||||
|
||||
const cloneContent = (): MutableContentFixture => structuredClone(validContent) as MutableContentFixture;
|
||||
@@ -32,6 +35,45 @@ describe('content schemas', () => {
|
||||
expect(parseSiteConfig(validSite).environment).toBe('preview');
|
||||
});
|
||||
|
||||
it('accepts an empty manifest before Phase 3 assets are prepared', () => {
|
||||
expect(parsePublicAssetManifest({ assets: [] })).toEqual({ assets: [] });
|
||||
});
|
||||
|
||||
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.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 = [];
|
||||
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';
|
||||
|
||||
Reference in New Issue
Block a user