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" & <tags>', }); expect(out).toContain('<title>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('