'use strict'; // Set env BEFORE requiring the middleware (ALLOWED array is built at load time) process.env.ORIGIN_GUARD_DISABLED = 'false'; process.env.NODE_ENV = 'development'; process.env.ALLOWED_ORIGINS = 'http://localhost:5173,http://localhost:3024'; const originGuard = require('../../middleware/originGuard.middleware'); // ── Mock helpers ────────────────────────────────────────────────────────────── function makeReq({ method = 'GET', headers = {} } = {}) { return { method, headers }; } function makeRes() { const res = { _status: null, _body: null, status(code) { this._status = code; return this; }, json(body) { this._body = body; return this; }, }; return res; } // Minimal headers that represent a real browser fetch() call (SPA → API). // localhost:5173 → localhost:3024 is same-site (same eTLD+1, different port). const BROWSER_HEADERS = { 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'accept-language': 'en-US,en;q=0.9', }; // ── Tests ───────────────────────────────────────────────────────────────────── describe('originGuard middleware', () => { // ── Layer 1a: Sec-Fetch-Site presence + value ───────────────────────────── test('1. GET with no Sec-Fetch-Site → 403 (Layer 1a: header absent)', () => { const req = makeReq({ method: 'GET', headers: {} }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); expect(res._body).toMatchObject({ status: 'error', message: 'Forbidden.' }); }); test('2. GET with Sec-Fetch-Site: cross-site → 403 (Layer 1a: cross-site rejected)', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'cross-site' } }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); expect(res._body).toMatchObject({ status: 'error', message: 'Forbidden.' }); }); // ── Layer 1b: Fetch Metadata family completeness + valid combo ──────────── test('3. GET with Sec-Fetch-Site but missing Mode and Dest → 403 (Layer 1b: incomplete family)', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'same-site' } }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); expect(res._body).toMatchObject({ status: 'error', message: 'Forbidden.' }); }); test('4. GET with Sec-Fetch-Site + Mode but missing Dest → 403 (Layer 1b: partial family)', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'cors' }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); }); test('5. GET with impossible combo (cors + document) → 403 (Layer 1b: invalid combination)', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'document', 'accept-language': 'en-US', }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); }); test('6. GET with no-cors mode → 403 (Layer 1b: no-cors not expected on API server)', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'no-cors', 'sec-fetch-dest': 'empty', 'accept-language': 'en-US', }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); }); // ── Layer 2: Browser presence signals ──────────────────────────────────── test('7. GET with valid Fetch Metadata but no Sec-CH-UA and no Accept-Language → 403 (Layer 2: no browser fingerprint)', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); expect(res._body).toMatchObject({ status: 'error', message: 'Forbidden.' }); }); // ── Layer 3: Origin allowlist ───────────────────────────────────────────── test('8. POST with full browser headers but foreign Origin → 403 (Layer 3: unlisted origin)', () => { const req = makeReq({ method: 'POST', headers: { ...BROWSER_HEADERS, 'origin': 'http://attacker.com' }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); expect(res._body).toMatchObject({ status: 'error', message: 'Forbidden.' }); }); test('9. POST with full browser headers but missing Origin → 403 (Layer 3: no origin header)', () => { const req = makeReq({ method: 'POST', headers: { ...BROWSER_HEADERS } }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).not.toHaveBeenCalled(); expect(res._status).toBe(403); }); // ── Happy paths ─────────────────────────────────────────────────────────── test('10. GET with full browser headers (Accept-Language path) → passes', () => { const req = makeReq({ method: 'GET', headers: { ...BROWSER_HEADERS } }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).toHaveBeenCalledTimes(1); expect(res._status).toBeNull(); }); test('11. GET with Sec-CH-UA instead of Accept-Language (Chromium path) → passes', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'same-site', 'sec-fetch-mode': 'cors', 'sec-fetch-dest': 'empty', 'sec-ch-ua': '"Chromium";v="137", "Not/A)Brand";v="24"', }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).toHaveBeenCalledTimes(1); expect(res._status).toBeNull(); }); test('12. POST with full browser headers and allowed Origin → passes', () => { const req = makeReq({ method: 'POST', headers: { ...BROWSER_HEADERS, 'origin': 'http://localhost:5173' }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).toHaveBeenCalledTimes(1); expect(res._status).toBeNull(); }); test('13. Direct browser navigation (navigate + document, site: none) → passes', () => { const req = makeReq({ method: 'GET', headers: { 'sec-fetch-site': 'none', 'sec-fetch-mode': 'navigate', 'sec-fetch-dest': 'document', 'sec-fetch-user': '?1', 'accept-language': 'en-US,en;q=0.9', 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).toHaveBeenCalledTimes(1); expect(res._status).toBeNull(); }); test('14. OPTIONS preflight → passes immediately (handled by cors())', () => { const req = makeReq({ method: 'OPTIONS', headers: {} }); const res = makeRes(); const next = jest.fn(); originGuard(req, res, next); expect(next).toHaveBeenCalledTimes(1); expect(res._status).toBeNull(); }); });