import assert from 'node:assert/strict'; import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import test from 'node:test'; import { checkInternalLinks } from '../scripts/lib/internal-links.mjs'; async function withBuiltSite(run) { const root = await mkdtemp(join(tmpdir(), 'incognito-links-')); try { await run(root); } finally { await rm(root, { recursive: true, force: true }); } } test('reports a missing built internal route', async () => { await withBuiltSite(async (root) => { await writeFile(join(root, 'index.html'), 'Missing'); const result = await checkInternalLinks({ distRoot: root, base: '/' }); assert.deepEqual(result.broken.map(({ href }) => href), ['/missing/']); }); }); test('accepts an existing directory index route', async () => { await withBuiltSite(async (root) => { await mkdir(join(root, 'about'), { recursive: true }); await writeFile(join(root, 'index.html'), 'About'); await writeFile(join(root, 'about/index.html'), '

About

'); const result = await checkInternalLinks({ distRoot: root, base: '/' }); assert.equal(result.broken.length, 0); }); }); test('accepts an existing exact-file link', async () => { await withBuiltSite(async (root) => { await writeFile(join(root, 'index.html'), 'Manual'); await writeFile(join(root, 'manual.pdf'), 'manual'); const result = await checkInternalLinks({ distRoot: root, base: '/' }); assert.equal(result.broken.length, 0); }); }); test('maps the root route to the built root index', async () => { await withBuiltSite(async (root) => { await writeFile(join(root, 'index.html'), 'Home'); const result = await checkInternalLinks({ distRoot: root, base: '/' }); assert.equal(result.broken.length, 0); }); }); test('removes the configured base prefix before resolving built routes', async () => { await withBuiltSite(async (root) => { await mkdir(join(root, 'about'), { recursive: true }); await writeFile(join(root, 'index.html'), [ 'About', 'Missing base prefix', ].join('')); await writeFile(join(root, 'about/index.html'), '

About

'); const result = await checkInternalLinks({ distRoot: root, base: '/wiki/' }); assert.deepEqual(result.broken.map(({ href }) => href), ['/about/']); assert.equal(result.broken[0].resolvedPath, '/about/'); }); }); test('resolves relative routes from the source HTML route', async () => { await withBuiltSite(async (root) => { await mkdir(join(root, 'guide', 'install'), { recursive: true }); await mkdir(join(root, 'guide', 'about'), { recursive: true }); await writeFile(join(root, 'guide/install/index.html'), 'About'); await writeFile(join(root, 'guide/about/index.html'), '

About

'); const result = await checkInternalLinks({ distRoot: root, base: '/' }); assert.equal(result.broken.length, 0); }); }); test('ignores fragments, remote protocols, and Astro-generated assets', async () => { await withBuiltSite(async (root) => { await writeFile(join(root, 'index.html'), [ 'Section', 'Email', 'Call', 'Remote', '', ].join('')); const result = await checkInternalLinks({ distRoot: root, base: '/' }); assert.equal(result.broken.length, 0); }); }); test('ignores links inside generated redirect documents', async () => { await withBuiltSite(async (root) => { await writeFile(join(root, 'index.html'), 'Redirect'); const result = await checkInternalLinks({ distRoot: root, base: '/wiki/' }); assert.equal(result.broken.length, 0); }); }); for (const { name, href } of [ { name: 'encoded slash', href: '/..%2Fmanual.pdf' }, { name: 'encoded backslash', href: '/..%5Cmanual.pdf' }, ]) { test(`rejects ${name} traversal even when the escaped target exists`, async () => { const siteRoot = await mkdtemp(join(tmpdir(), 'incognito-links-escape-')); const distRoot = join(siteRoot, 'dist'); try { await mkdir(distRoot); await writeFile(join(siteRoot, 'manual.pdf'), 'outside dist'); await writeFile(join(distRoot, 'index.html'), `Escaped`); const result = await checkInternalLinks({ distRoot, base: '/' }); assert.deepEqual(result.broken.map((broken) => broken.href), [href]); } finally { await rm(siteRoot, { recursive: true, force: true }); } }); }