feat: prerender crawlable portfolio artifacts
This commit is contained in:
123
tests/unit/metadata.test.ts
Normal file
123
tests/unit/metadata.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
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 System Administrator & Full-Stack Developer building and operating reliable systems and software.',
|
||||
);
|
||||
expect(metadata.canonicalUrl).toBe('https://portfolio.example.test/');
|
||||
expect(metadata.openGraph.image).toBeUndefined();
|
||||
expect(metadata.jsonLd).toEqual({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ProfilePage',
|
||||
name: 'Luca Pîslaru — System Administrator & Full-Stack Developer',
|
||||
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',
|
||||
sameAs: [
|
||||
'https://github.com/LucaPisl',
|
||||
'https://gitlab.com/luca_pisl',
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
],
|
||||
},
|
||||
});
|
||||
const expectedHash = createHash('sha256').update(metadata.serializedJsonLd).digest('base64');
|
||||
expect(metadata.jsonLdCspHash).toBe(`sha256-${expectedHash}`);
|
||||
});
|
||||
|
||||
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 artifacts = createStaticHostArtifacts(createPageMetadata(content, manifest, productionSite), 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');
|
||||
});
|
||||
|
||||
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 'none'");
|
||||
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=()',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user