/** * Unit tests for the pure static-file resolver (D1). Covers index resolution, MIME mapping, query * stripping, and the STRICT path-traversal guard (raw `..`, percent-encoded `%2e%2e`, and NUL byte * all reject — even when the escaped target file genuinely exists on disk). */ import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { serveStatic } from '../src/servers/static-web.js' const INDEX_HTML = 'idx' const PAIR_HTML = 'pair' const INDEX_JS = 'export const x = 1\n' const XTERM_CSS = 'body{margin:0}' const SECRET = 'TOP SECRET — outside root' let base: string // temp parent dir (holds the secret sibling) let root: string // the static root passed to serveStatic beforeAll(() => { base = mkdtempSync(join(tmpdir(), 'relay-static-')) root = join(base, 'public') mkdirSync(join(root, 'build'), { recursive: true }) writeFileSync(join(root, 'index.html'), INDEX_HTML) writeFileSync(join(root, 'pair.html'), PAIR_HTML) writeFileSync(join(root, 'build', 'index.js'), INDEX_JS) writeFileSync(join(root, 'build', 'xterm.css'), XTERM_CSS) // A real file OUTSIDE root — the traversal target the guard must never reach. writeFileSync(join(base, 'secret.txt'), SECRET) }) afterAll(() => { rmSync(base, { recursive: true, force: true }) }) describe('serveStatic — index resolution', () => { it('serves index.html at "/"', () => { const res = serveStatic(root, '/') expect(res).not.toBeNull() expect(res?.status).toBe(200) expect(res?.headers['content-type']).toBe('text/html; charset=utf-8') expect(res?.body.toString('utf8')).toBe(INDEX_HTML) expect(res?.headers['content-length']).toBe(String(Buffer.byteLength(INDEX_HTML))) }) it('serves an explicit .html page', () => { const res = serveStatic(root, '/pair.html') expect(res?.status).toBe(200) expect(res?.headers['content-type']).toBe('text/html; charset=utf-8') expect(res?.body.toString('utf8')).toBe(PAIR_HTML) }) it('strips the query string before resolving', () => { const res = serveStatic(root, '/index.html?join=abc123') expect(res?.status).toBe(200) expect(res?.body.toString('utf8')).toBe(INDEX_HTML) }) it('strips the hash fragment before resolving', () => { const res = serveStatic(root, '/#section') expect(res?.status).toBe(200) expect(res?.body.toString('utf8')).toBe(INDEX_HTML) }) }) describe('serveStatic — MIME types', () => { it('maps /build/*.js to text/javascript', () => { const res = serveStatic(root, '/build/index.js') expect(res?.status).toBe(200) expect(res?.headers['content-type']).toBe('text/javascript; charset=utf-8') expect(res?.body.toString('utf8')).toBe(INDEX_JS) }) it('maps .css to text/css', () => { const res = serveStatic(root, '/build/xterm.css') expect(res?.status).toBe(200) expect(res?.headers['content-type']).toBe('text/css; charset=utf-8') }) }) describe('serveStatic — traversal guard (returns null)', () => { it('blocks a raw ".." escape even though the target exists', () => { // Sanity: the target file really is readable outside root, so a null result proves the guard. expect(serveStatic(root, '/../secret.txt')).toBeNull() }) it('blocks a deep ".." escape', () => { expect(serveStatic(root, '/build/../../secret.txt')).toBeNull() }) it('blocks percent-encoded ".." (%2e%2e)', () => { expect(serveStatic(root, '/%2e%2e/secret.txt')).toBeNull() }) it('rejects a NUL byte in the path', () => { expect(serveStatic(root, '/index.html%00.js')).toBeNull() }) }) describe('serveStatic — misses (returns null)', () => { it('returns null for a nonexistent file', () => { expect(serveStatic(root, '/nope.html')).toBeNull() }) it('returns null for a directory (no listing)', () => { expect(serveStatic(root, '/build')).toBeNull() }) })