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

171 lines
7.8 KiB
TypeScript

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(
'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.',
);
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/',
email: 'mailto:luca.pislaru@gmail.com',
homeLocation: {
'@type': 'Place',
address: {
'@type': 'PostalAddress',
addressLocality: 'Bucharest',
addressCountry: 'RO',
},
},
sameAs: [
'https://github.com/LucaPisl',
'https://gitlab.com/luca_pisl',
'https://linkedin.com/in/luca-pisl',
],
},
});
expect(JSON.stringify(metadata.jsonLd)).not.toMatch(
/streetAddress|postalCode|telephone|interactionStatistic|"image"/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('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=()',
);
});
});