/** * B7 · unit tests for `buildUpgradeRequest` — the browser upgrade → P1 `UpgradeRequest` builder. * Focus: the DPoP proof transport (FUNCTIONAL BLOCKER). A browser cannot set the `dpop` request * header, so the proof rides the `term.dpop.` subprotocol entry; the builder must read it there * while keeping the header path working (header WINS if both). Also pins `activeSessionCount` plumbing. */ import { describe, it, expect } from 'vitest' import type { IncomingMessage } from 'node:http' import { encodeBase64UrlString, APP_SUBPROTOCOL } from 'relay-contracts' import { buildUpgradeRequest } from '../src/servers/browser-server.js' import { DPOP_SUBPROTOCOL_PREFIX } from '../src/servers/dpop-subprotocol.js' const PROOF = 'eyJhbGciOiJFZERTQSJ9.eyJodHUiOiJodHRwczovL2FsaWNlL3dzIn0.c2ln' function dpopEntry(proofJws: string): string { return DPOP_SUBPROTOCOL_PREFIX + encodeBase64UrlString(proofJws) } function mkReq(headers: Record): IncomingMessage { return { headers: { host: 'alice.example.com', ...headers }, url: '/ws', socket: { remoteAddress: '1.2.3.4' }, } as unknown as IncomingMessage } describe('buildUpgradeRequest — DPoP transport', () => { it('reads the proof from the term.dpop. subprotocol when no header is set (the browser path)', () => { const req = mkReq({ 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${dpopEntry(PROOF)}` }) const upgrade = buildUpgradeRequest(req, 0) expect(upgrade.dpop.proof).toBe(PROOF) }) it('prefers the dpop HEADER over the subprotocol entry when both are present (header wins)', () => { const req = mkReq({ dpop: 'header-proof-jws', 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${dpopEntry(PROOF)}`, }) expect(buildUpgradeRequest(req, 0).dpop.proof).toBe('header-proof-jws') }) it('still honours the dpop header alone (header path unchanged)', () => { const req = mkReq({ dpop: 'header-only', 'sec-websocket-protocol': APP_SUBPROTOCOL }) expect(buildUpgradeRequest(req, 0).dpop.proof).toBe('header-only') }) it('yields proof=null when neither header nor subprotocol carries a proof', () => { const req = mkReq({ 'sec-websocket-protocol': APP_SUBPROTOCOL }) expect(buildUpgradeRequest(req, 0).dpop.proof).toBeNull() }) it('yields proof=null (fail-closed) for a malformed term.dpop. subprotocol entry', () => { const req = mkReq({ 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${DPOP_SUBPROTOCOL_PREFIX}not*b64u!` }) expect(buildUpgradeRequest(req, 0).dpop.proof).toBeNull() }) it('parses the comma-separated subprotocol list and always carries the app subprotocol through', () => { const req = mkReq({ 'sec-websocket-protocol': `${APP_SUBPROTOCOL}, ${dpopEntry(PROOF)}` }) expect(buildUpgradeRequest(req, 0).subprotocols).toContain(APP_SUBPROTOCOL) }) }) describe('buildUpgradeRequest — activeSessionCount plumbing (F2)', () => { it('carries the caller-supplied active-session count verbatim', () => { const req = mkReq({ 'sec-websocket-protocol': APP_SUBPROTOCOL }) expect(buildUpgradeRequest(req, 3).activeSessionCount).toBe(3) }) })