import { afterEach, describe, expect, it, vi } from 'vitest' import { EventEmitter } from 'node:events' const dns = vi.hoisted(() => ({ lookup: vi.fn() })) const https = vi.hoisted(() => ({ request: vi.fn() })) vi.mock('node:dns/promises', () => dns) vi.mock('node:https', () => https) import { postWebhook } from './webhook-transport.js' afterEach(() => vi.resetAllMocks()) describe('pinned webhook POST transport', () => { it('rejects mixed DNS answers and rebinding on subsequent attempts without opening a socket', async () => { dns.lookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }, { address: '127.0.0.1', family: 4 }]) await expect(postWebhook('https://hooks.example.com', 'secret', '{}', 'event', new AbortController().signal)).rejects.toThrow('INVALID_DESTINATION') expect(https.request).not.toHaveBeenCalled() dns.lookup.mockResolvedValue([{ address: '::ffff:10.0.0.1', family: 6 }]) await expect(postWebhook('https://hooks.example.com', 'secret', '{}', 'event', new AbortController().signal)).rejects.toThrow('INVALID_DESTINATION') expect(dns.lookup).toHaveBeenCalledTimes(2) }) it('pins the socket lookup, keeps TLS hostname, signs POST bytes and never follows redirects', async () => { dns.lookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]) const sent: string[] = [] https.request.mockImplementation((url, options, callback) => { expect(url.hostname).toBe('hooks.example.com') expect(options.method).toBe('POST') expect(options.agent).toBe(false) expect(options.rejectUnauthorized).not.toBe(false) const selected = vi.fn() options.lookup('hooks.example.com', {}, selected) expect(selected).toHaveBeenCalledWith(null, '93.184.216.34', 4) expect(options.headers['x-evofw-signature']).toMatch(/^v1=[a-f0-9]{64}$/) expect(options.headers['x-evofw-event-id']).toBe('event') const request = new EventEmitter() as EventEmitter & { end: (body: string) => void } request.end = body => { sent.push(body) const response = Object.assign(new EventEmitter(), { statusCode: 302 }) callback(response) queueMicrotask(() => response.emit('end')) } return request }) await expect(postWebhook('https://hooks.example.com/path', 'secret', '{"id":1}', 'event', new AbortController().signal)).rejects.toThrow('HTTP_302') expect(sent).toEqual(['{"id":1}']) expect(https.request).toHaveBeenCalledTimes(1) }) it('stops a DNS lookup when shutdown aborts', async () => { dns.lookup.mockImplementation(() => new Promise(() => {})) const controller = new AbortController() const pending = postWebhook('https://hooks.example.com', 'secret', '{}', 'event', controller.signal) controller.abort() await expect(pending).rejects.toThrow('DELIVERY_TIMEOUT') expect(https.request).not.toHaveBeenCalled() }) })