import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Hono } from 'hono';
import { afterEach, describe, expect, it, vi } from 'vitest';
// seo.ts imports logger (which pulls in env validation at import time) — mock
// it so this unit test is hermetic.
vi.mock('@server/lib/logger', () => ({
logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() },
}));
const { createSeoShell, escapeHtml, injectSeo } = await import('@server/lib/seo');
// Mirrors the shape of the built dist/client/index.html: the tags injectSeo
// rewrites must already exist in the shell for the replacement to land.
const SHELL = `
My SaaS
`;
describe('escapeHtml', () => {
it('escapes HTML metacharacters', () => {
expect(escapeHtml(`'&'`)).toBe(
'<a href="x">'&'</a>'
);
});
});
describe('injectSeo', () => {
it('replaces title and description metas', () => {
const out = injectSeo(SHELL, {
title: 'Getting Started | My SaaS',
description: 'Set up your project in five minutes.',
});
expect(out).toContain('Getting Started | My SaaS');
expect(out).toContain(
''
);
expect(out).toContain('');
expect(out).toContain(
''
);
expect(out).not.toContain('Default description');
});
it('escapes metadata values so they cannot break out of attributes', () => {
const out = injectSeo(SHELL, {
title: 'A "quoted" ',
description: 'desc with "quotes" & ',
});
expect(out).toContain('A "quoted" <title>');
expect(out).toContain('content="desc with "quotes" & <tags>"');
expect(out).not.toContain('content="desc with "');
});
it('adds canonical link and rewrites og:url', () => {
const out = injectSeo(SHELL, {
title: 't',
description: 'd',
canonical: 'https://example.com/docs/getting-started',
});
expect(out).toContain(
''
);
expect(out).toContain(
''
);
});
it('injects JSON-LD with < escaped so data cannot close the script tag', () => {
const out = injectSeo(SHELL, {
title: 't',
description: 'd',
jsonLd: { '@type': 'TechArticle', headline: '' },
});
expect(out).toContain('