Files
shd.build/src/lib/server/build-social-card.test.ts
T
2026-08-02 12:00:30 -04:00

106 lines
3.6 KiB
TypeScript

import sharp from 'sharp';
import { describe, expect, it } from 'vitest';
import {
BUILD_SOCIAL_CARD_HEIGHT,
BUILD_SOCIAL_CARD_WIDTH,
buildBuildSocialCardSvg,
renderBuildSocialCardPng,
type BuildSocialCardInput,
} from './build-social-card';
const input: BuildSocialCardInput = {
title: 'Vanguard Counter-Offensive',
author: 'Agent Kelso',
primaryWeapon: 'Police M4',
secondaryWeapon: 'Super 90',
firepower: 1280,
toughness: 940,
engineering: 615,
supportingAttributes: [
{ label: 'Critical Hit Chance', value: '18.5%' },
{ label: 'Headshot Damage', value: '42%' },
{ label: 'Armor', value: '12,400' },
],
visibility: 'public',
directUrl: 'shd.build/builds/vanguard-counter-offensive',
};
describe('build social card', () => {
it('escapes XML injection and keeps inputs out of markup and URLs', () => {
const attack = `</text><script href="https://attacker.invalid">alert('x') & more</script>`;
const svg = buildBuildSocialCardSvg({
...input,
title: attack,
author: `A\u0000gent <&> "one" 'two'`,
directUrl: `javascript:alert(1)\n${attack}`,
supportingAttributes: [{ label: attack, value: attack }],
});
expect(svg).not.toContain('<script');
expect(svg).not.toContain('href="https://attacker.invalid"');
expect(svg).not.toContain('\u0000');
expect(svg).toContain(
'&lt;/text&gt;&lt;script href=&quot;https://attacker.invalid&quot;&gt;',
);
expect(svg).toContain(
'A gent &lt;&amp;&gt; &quot;one&quot; &apos;two&apos;',
);
expect(svg).not.toMatch(/<[^>]+\s(?:href|xlink:href)\s*=/gu);
});
it('wraps and truncates long Unicode text without splitting graphemes', () => {
const family = '👨‍👩‍👧‍👦';
const svg = buildBuildSocialCardSvg({
...input,
title: `${family.repeat(31)} ${family.repeat(31)} ${family.repeat(31)}`,
author: `Zoë ${'e\u0301'.repeat(60)}`,
directUrl: `shd.build/builds/${'路'.repeat(130)}`,
});
const titleBlock =
svg.match(/<text class="title">(.*?)<\/text>/u)?.[1] ?? '';
expect(titleBlock.match(/<tspan/gu)).toHaveLength(2);
expect(titleBlock).toContain('…');
expect(titleBlock).not.toMatch(
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/gu,
);
expect(svg).toContain('é');
expect(svg).not.toContain('é');
expect(svg).toContain(`${'路'.repeat(20)}`);
expect(svg).toContain('…');
});
it('builds deterministic SVG with bounded supporting facts', () => {
const withExtraFacts = {
...input,
supportingAttributes: [
...input.supportingAttributes!,
{ label: 'Must not render', value: '999' },
],
};
const first = buildBuildSocialCardSvg(withExtraFacts);
const second = buildBuildSocialCardSvg(withExtraFacts);
expect(first).toBe(second);
expect(first).toContain('width="1200" height="630"');
expect(first).toContain('SHD</text><text');
expect(first).toContain('.build');
expect(first).toContain('font-family:"DejaVu Sans Condensed",sans-serif');
expect(first).toContain('SUPPORTING ATTRIBUTES / 03');
expect(first).not.toContain('Must not render');
});
it('renders a valid 1200x630 PNG through Sharp', async () => {
const png = await renderBuildSocialCardPng(input);
const metadata = await sharp(png).metadata();
expect(png.subarray(0, 8)).toEqual(
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
);
expect(metadata.format).toBe('png');
expect(metadata.width).toBe(BUILD_SOCIAL_CARD_WIDTH);
expect(metadata.height).toBe(BUILD_SOCIAL_CARD_HEIGHT);
expect(png.byteLength).toBeLessThan(1_000_000);
});
});