Parallel module-builder subagents, disjoint file ownership, shared tree: - T4 src/config.ts — loadConfig+M1 origins (32 tests) - T5 src/protocol.ts — parse/serialize+SESSION_ID_RE+M7 (60 tests) - T6 src/session/ring-buffer.ts — byte-accurate, no UTF-8/ANSI split+M2 (15 tests) - T7 src/http/origin.ts — host+port match, undefined rejected (12 tests) - T3 test/helpers/mock-pty.ts — IPty double w/ emitData/emitExit (9 tests) Verified by orchestrator: tsc --noEmit clean; full suite 128 tests pass.
79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
/**
|
|
* test/origin.test.ts — TDD for src/http/origin.ts (T7)
|
|
*
|
|
* Covers ARCHITECTURE §3.3 and TECH_DOC §7 (CSWSH defence):
|
|
* - whitelist hit → allow
|
|
* - wrong hostname → deny
|
|
* - wrong port → deny
|
|
* - undefined Origin (non-browser client) → deny
|
|
* - evil.com → deny
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import { isOriginAllowed } from '../src/http/origin.js'
|
|
|
|
const ALLOWED: readonly string[] = [
|
|
'http://localhost:3000',
|
|
'http://192.168.1.42:3000',
|
|
'http://my-host.local:3000',
|
|
]
|
|
|
|
describe('isOriginAllowed', () => {
|
|
// ── whitelist hits ────────────────────────────────────────────────
|
|
it('allows an exact localhost origin', () => {
|
|
expect(isOriginAllowed('http://localhost:3000', ALLOWED)).toBe(true)
|
|
})
|
|
|
|
it('allows an exact LAN IP origin', () => {
|
|
expect(isOriginAllowed('http://192.168.1.42:3000', ALLOWED)).toBe(true)
|
|
})
|
|
|
|
it('allows an exact hostname.local origin', () => {
|
|
expect(isOriginAllowed('http://my-host.local:3000', ALLOWED)).toBe(true)
|
|
})
|
|
|
|
// ── hostname mismatch ─────────────────────────────────────────────
|
|
it('rejects when the hostname does not match (evil.com)', () => {
|
|
expect(isOriginAllowed('http://evil.com:3000', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
it('rejects a subdomain of an allowed hostname', () => {
|
|
expect(isOriginAllowed('http://sub.localhost:3000', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
it('rejects a hostname that merely contains an allowed hostname', () => {
|
|
// "evillocalhost:3000" should NOT match "localhost:3000"
|
|
expect(isOriginAllowed('http://evillocalhost:3000', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
// ── port mismatch ─────────────────────────────────────────────────
|
|
it('rejects when the port does not match (correct host, wrong port)', () => {
|
|
expect(isOriginAllowed('http://localhost:9999', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
it('rejects when port is absent but allowed origin has a port', () => {
|
|
// Browser sends Origin without port only when it is the default (80/443).
|
|
// Our server runs on 3000, so no-port origins must not sneak in.
|
|
expect(isOriginAllowed('http://localhost', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
// ── undefined Origin (non-browser clients: curl, scripts) ─────────
|
|
it('rejects undefined Origin by default', () => {
|
|
expect(isOriginAllowed(undefined, ALLOWED)).toBe(false)
|
|
})
|
|
|
|
// ── empty / malformed origins ─────────────────────────────────────
|
|
it('rejects an empty string origin', () => {
|
|
expect(isOriginAllowed('', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
it('rejects a completely different scheme (https vs http in allowed)', () => {
|
|
// Allowed list uses http://; https:// should not silently match.
|
|
expect(isOriginAllowed('https://localhost:3000', ALLOWED)).toBe(false)
|
|
})
|
|
|
|
it('rejects when allowed list is empty', () => {
|
|
expect(isOriginAllowed('http://localhost:3000', [])).toBe(false)
|
|
})
|
|
})
|