import { describe, expect, it, vi } from 'vitest'; import { SafeUpstreamGateway } from './safe-upstream-gateway.js'; describe('SafeUpstreamGateway', () => { it('dispatches only the audited zero-body network registration operation through pinned POST', async () => { const calls: unknown[] = []; const gateway = new SafeUpstreamGateway({ transport: { get: async () => ({ status: 200, headers: {}, body: '' }), post: async (url, headers, body) => { calls.push({ url, headers, body }); return { status: 204, headers: {}, body: '' }; }, }, }); const response = await gateway.postNetworkRegisterAuto('http://192.168.1.20:8080'); expect(response.status).toBe(204); expect(calls).toEqual([ { url: 'http://192.168.1.20:8080/api/network/register-auto', headers: {}, body: '' }, ]); }); it('rejects malformed operation origins before calling transport', async () => { const post = vi.fn(async () => ({ status: 204, headers: {}, body: '' })); const gateway = new SafeUpstreamGateway({ transport: { get: async () => ({ status: 200, headers: {}, body: '' }), post }, }); await expect( gateway.postNetworkRegisterAuto('http://192.168.1.20/base?next=x'), ).rejects.toMatchObject({ code: 'UPSTREAM_REQUEST_INVALID', dispatched: false }); expect(post).not.toHaveBeenCalled(); }); it('sends login password only as JSON through the pinned POST transport', async () => { const calls: unknown[] = []; const gateway = new SafeUpstreamGateway({ transport: { get: async () => ({ status: 200, headers: {}, body: '' }), post: async (url, headers, body) => { calls.push({ url, headers, body }); return { status: 200, headers: { 'set-cookie': 'simadmin_session=opaque' }, body: '' }; }, }, }); await gateway.request({ url: 'https://192.168.1.20:8080/api/auth/login', method: 'POST', headers: { 'content-type': 'application/json' }, secret: '[REDACTED]', body: '[REDACTED]', }); expect(calls).toEqual([ { url: 'https://192.168.1.20:8080/api/auth/login', headers: { 'content-type': 'application/json' }, body: '{"password":"[REDACTED]"}', }, ]); }); it('rejects HTTP login and logout so credentials and cookies are never sent in cleartext', async () => { const gateway = new SafeUpstreamGateway({ transport: { get: async () => ({ status: 200, headers: {}, body: '' }), post: async () => ({ status: 200, headers: {}, body: '' }), }, }); await expect( gateway.request({ url: 'http://192.168.1.20:8080/api/auth/login', method: 'POST', headers: { 'content-type': 'application/json' }, secret: '[REDACTED]', body: '[REDACTED]', }), ).rejects.toThrow('UPSTREAM_INSECURE_AUTH'); await expect( gateway.request({ url: 'http://192.168.1.20:8080/api/auth/logout', method: 'POST', headers: { cookie: 'simadmin_session=opaque' }, }), ).rejects.toThrow('UPSTREAM_INSECURE_AUTH'); }); it('does not allow a supplied redacted body marker to become a network request body', async () => { const gateway = new SafeUpstreamGateway({ transport: { get: async () => ({ status: 200, headers: {}, body: '' }), post: async () => ({ status: 200, headers: {}, body: '' }), }, }); await expect( gateway.request({ url: 'https://192.168.1.20:8080/api/auth/login', method: 'POST', headers: {}, body: '[REDACTED]', }), ).rejects.toThrow('UPSTREAM_REQUEST_INVALID'); }); });