Files
Portfolio/tests/unit/metadata.test.ts

194 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
import { createPageMetadata, createStaticHostArtifacts } from '@/content/metadata';
import { parsePortfolioContent, parsePublicAssetManifest } from '@/content/schemas';
import { serializeJsonLd } from '@/security/serialize-json-ld';
import type { SiteConfig } from '@/content/types';
const fixture = (path: string): unknown =>
JSON.parse(readFileSync(resolve(process.cwd(), path), 'utf8')) as unknown;
const content = parsePortfolioContent(fixture('content/public/portfolio-content.json'));
const manifest = parsePublicAssetManifest(fixture('content/public/asset-manifest.json'));
const previewSite: SiteConfig = {
environment: 'preview',
canonicalUrl: 'https://portfolio.localhost/',
buildId: 'preview-test',
};
const productionSite: SiteConfig = {
environment: 'production',
canonicalUrl: 'https://portfolio.example.test/',
buildId: 'production-test',
};
describe('safe metadata', () => {
it('escapes every HTML-significant and JavaScript line-separator code point', () => {
const serialized = serializeJsonLd({ value: '</script><tag>&\u2028\u2029' });
expect(serialized).toBe('{"value":"\\u003c/script\\u003e\\u003ctag\\u003e\\u0026\\u2028\\u2029"}');
expect(serialized.toLowerCase()).not.toContain('</script');
});
it('derives canonical identity and verified profiles from public content only', () => {
const metadata = createPageMetadata(content, manifest, productionSite);
expect(metadata.title).toBe('Luca Pîslaru — System Administrator & Full-Stack Developer');
expect(metadata.description).toBe(
'I’m Luca Pîslaru, a System Administrator & Full-Stack Developer based in Bucharest. Explore my work in Linux infrastructure and end-to-end application development.',
);
expect(metadata.canonicalUrl).toBe('https://portfolio.example.test/');
const previewAsset = manifest.assets.find(({ id }) => id === content.metadata.socialPreviewAssetId)!;
expect(metadata.openGraph.image).toBe(`https://portfolio.example.test/assets/${previewAsset.fileName}`);
expect(metadata.twitter).toEqual({
card: 'summary_large_image',
title: metadata.title,
description: metadata.description,
image: `https://portfolio.example.test/assets/${previewAsset.fileName}`,
});
expect(metadata.jsonLd).toEqual({
'@context': 'https://schema.org',
'@type': 'ProfilePage',
name: 'Luca Pîslaru',
description: metadata.description,
url: 'https://portfolio.example.test/',
mainEntity: {
'@type': 'Person',
name: 'Luca Pîslaru',
jobTitle: 'System Administrator & Full-Stack Developer',
description: metadata.description,
url: 'https://portfolio.example.test/',
homeLocation: {
'@type': 'Place',
address: {
'@type': 'PostalAddress',
addressLocality: 'Bucharest',
addressCountry: 'RO',
},
},
sameAs: [
'https://linkedin.com/in/luca-pisl',
'https://github.com/LucaPisl',
'https://gitlab.com/luca_pisl',
],
},
});
expect(JSON.stringify(metadata.jsonLd)).not.toMatch(
/streetAddress|postalCode|telephone|interactionStatistic|"image"|"email"/iu,
);
const expectedHash = createHash('sha256').update(metadata.serializedJsonLd).digest('base64');
expect(metadata.jsonLdCspHash).toBe(`sha256-${expectedHash}`);
});
it('uses a summary card without an image when no social preview is approved', () => {
const withoutImage = structuredClone(content);
delete withoutImage.metadata.socialPreviewAssetId;
const metadata = createPageMetadata(withoutImage, manifest, productionSite);
expect(metadata.openGraph).not.toHaveProperty('image');
expect(metadata.twitter).toEqual({
card: 'summary',
title: 'Luca Pîslaru — System Administrator & Full-Stack Developer',
description: content.metadata.description,
});
});
it.each([
{ name: 'audio', mediaType: 'audio/mpeg' as const, fileName: 'project-social-preview.mp3', width: null, height: null, durationMs: 1000 },
{ name: 'video', mediaType: 'video/mp4' as const, fileName: 'project-social-preview.mp4', width: 1200, height: 630, durationMs: 1000 },
{ name: 'vector image', mediaType: 'image/svg+xml' as const, fileName: 'project-social-preview.svg', width: 1200, height: 630, durationMs: null },
{ name: 'wrong-size raster', mediaType: 'image/webp' as const, fileName: 'project-social-preview.webp', width: 1199, height: 630, durationMs: null },
])('falls back to a summary card when the configured preview is $name', ({ mediaType, fileName, width, height, durationMs }) => {
const unsafeManifest = structuredClone(manifest);
const previewAsset = unsafeManifest.assets.find(({ id }) => id === content.metadata.socialPreviewAssetId)!;
previewAsset.mediaType = mediaType;
previewAsset.fileName = fileName;
previewAsset.width = width;
previewAsset.height = height;
previewAsset.durationMs = durationMs;
const metadata = createPageMetadata(content, unsafeManifest, productionSite);
expect(metadata.openGraph).not.toHaveProperty('image');
expect(metadata.twitter).toEqual({
card: 'summary',
title: 'Luca Pîslaru — System Administrator & Full-Stack Developer',
description: content.metadata.description,
});
});
it('omits sameAs when no verified social profiles exist', () => {
const withoutSocial = structuredClone(content);
withoutSocial.contact.social = [];
const metadata = createPageMetadata(withoutSocial, manifest, productionSite);
expect(metadata.jsonLd.mainEntity).not.toHaveProperty('sameAs');
});
it('rejects metadata title copy that diverges from the canonical identity and role', () => {
const inconsistent = structuredClone(content);
inconsistent.metadata.title = 'Luca Pîslaru — Developer';
expect(() => createPageMetadata(inconsistent, manifest, productionSite)).toThrow(/canonical identity.*role/i);
});
});
describe('environment-aware crawl and response artifacts', () => {
it('keeps preview private, noindex, sitemap-free, and without HSTS', () => {
const artifacts = createStaticHostArtifacts(createPageMetadata(content, manifest, previewSite), previewSite);
expect(artifacts.robots).toBe('User-agent: *\nDisallow: /\n');
expect(artifacts.sitemap).toBeUndefined();
expect(artifacts.headers).toContain('Cache-Control: private, no-store');
expect(artifacts.headers).toContain('X-Robots-Tag: noindex, nofollow, noarchive');
expect(artifacts.headers).not.toContain('Strict-Transport-Security');
expect(artifacts.headers).not.toContain('Sitemap:');
expect(artifacts.redirects).not.toContain('portfolio.example.test');
});
it('generates production-only sitemap, allow policy, HSTS, and no preview directives', () => {
const metadata = createPageMetadata(content, manifest, productionSite);
const artifacts = createStaticHostArtifacts(metadata, productionSite);
expect(artifacts.robots).toBe(
'User-agent: *\nAllow: /\nSitemap: https://portfolio.example.test/sitemap.xml\n',
);
expect(artifacts.sitemap).toContain('<loc>https://portfolio.example.test/</loc>');
expect(artifacts.headers).toContain('Strict-Transport-Security: max-age=31536000');
expect(artifacts.headers).not.toContain('X-Robots-Tag');
expect(artifacts.headers).not.toContain('private, no-store');
expect(artifacts.headers).toBe([
'/assets/*',
' Cache-Control: public, max-age=31536000, immutable',
'',
'/*',
` Content-Security-Policy: default-src 'self'; script-src 'self' '${metadata.jsonLdCspHash}'; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests`,
' X-Content-Type-Options: nosniff',
' Referrer-Policy: no-referrer',
' Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()',
' X-Frame-Options: DENY',
' Cache-Control: public, max-age=0, must-revalidate',
' Strict-Transport-Security: max-age=31536000',
'',
].join('\n'));
});
it('emits the least-privilege browser security baseline', () => {
const artifacts = createStaticHostArtifacts(createPageMetadata(content, manifest, previewSite), previewSite);
expect(artifacts.headers).toContain("default-src 'self'");
expect(artifacts.headers).toContain("script-src 'self' 'sha256-");
expect(artifacts.headers).toContain("connect-src 'self'");
expect(artifacts.headers).toContain("frame-ancestors 'none'");
expect(artifacts.headers).toContain('X-Frame-Options: DENY');
expect(artifacts.headers).toContain('X-Content-Type-Options: nosniff');
expect(artifacts.headers).toContain('Referrer-Policy: no-referrer');
expect(artifacts.headers).toContain(
'Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()',
);
});
});