feat: W1 batch — config, protocol, ring-buffer, origin, mock-pty (TDD)
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.
This commit is contained in:
78
test/origin.test.ts
Normal file
78
test/origin.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user