/** * Receiver-server integration tests. Spins up the real Bun.serve * receiver against a temp SQLite bus, fires signed envelopes at it, * asserts the local bus actually received the emit. */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { defineEvents, openBus } from '@celilo/event-bus'; import type { PublishEvent } from '@celilo/event-bus/build-bus'; import { signEvent } from '@celilo/event-bus/build-bus'; import { type ReceiverServer, startReceiverServer } from './receiver-server'; const NO_SCHEMAS = defineEvents({}); function buildEvent(overrides: Partial = {}): PublishEvent { return { eventId: `evt-${Math.random().toString(36).slice(2)}`, timestamp: new Date().toISOString(), registry: 'npm', tag: 'latest', package: { name: '@celilo/cli', version: '0.4.0' }, ...overrides, }; } describe('receiver-server', () => { let tmpDir: string; let busPath: string; let server: ReceiverServer; const SECRET = 'shared-receiver-secret'; beforeEach(async () => { tmpDir = mkdtempSync(join(tmpdir(), 'celilo-build-bus-receiver-')); busPath = join(tmpDir, 'bus.db'); server = startReceiverServer({ port: 0, secret: SECRET, busPath }); }); afterEach(async () => { await server.stop(); rmSync(tmpDir, { recursive: true, force: true }); }); test('GET /health returns 200 ok', async () => { const r = await fetch(`${server.url}health`); expect(r.status).toBe(200); expect(await r.text()).toBe('ok'); }); test('GET / returns 405 method-not-allowed', async () => { const r = await fetch(server.url); expect(r.status).toBe(405); }); test('POST to unknown path returns 404', async () => { const r = await fetch(`${server.url}other`, { method: 'POST', body: '{}' }); expect(r.status).toBe(404); }); test('properly signed envelope: 200 + event emitted on local bus', async () => { const event = buildEvent(); const envelope = { event, signature: signEvent(event, SECRET) }; const r = await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(envelope), }); expect(r.status).toBe(200); const body = (await r.json()) as { ok: boolean; eventId: string }; expect(body.ok).toBe(true); expect(body.eventId).toBe(event.eventId); // Open the same bus and confirm the event landed. const bus = openBus({ dbPath: busPath, events: NO_SCHEMAS }); try { const recent = bus.recentEvents({ limit: 10, type: 'build-bus.publish' }); expect(recent).toHaveLength(1); const payload = recent[0].payload as PublishEvent; expect(payload.eventId).toBe(event.eventId); expect(payload.package.name).toBe('@celilo/cli'); } finally { bus.close(); } }); test('wrong-secret signature: 401 + nothing emitted', async () => { const event = buildEvent(); const envelope = { event, signature: signEvent(event, 'attacker-secret') }; const r = await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(envelope), }); expect(r.status).toBe(401); const bus = openBus({ dbPath: busPath, events: NO_SCHEMAS }); try { const recent = bus.recentEvents({ limit: 10, type: 'build-bus.publish' }); expect(recent).toHaveLength(0); } finally { bus.close(); } }); test('stale timestamp: 401', async () => { const event = buildEvent({ timestamp: '2020-01-01T00:00:00.000Z' }); const envelope = { event, signature: signEvent(event, SECRET) }; const r = await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(envelope), }); expect(r.status).toBe(401); }); test('malformed JSON body: 400', async () => { const r = await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: 'not json at all', }); expect(r.status).toBe(400); }); test('duplicate eventId dedupes via bus dedupKey (publisher retry safety)', async () => { const event = buildEvent(); const envelope = { event, signature: signEvent(event, SECRET) }; const r1 = await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(envelope), }); expect(r1.status).toBe(200); const r2 = await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(envelope), }); expect(r2.status).toBe(200); // still ok — publisher's idempotent retry // Only one event on the bus. const bus = openBus({ dbPath: busPath, events: NO_SCHEMAS }); try { const recent = bus.recentEvents({ limit: 10, type: 'build-bus.publish' }); expect(recent).toHaveLength(1); } finally { bus.close(); } }); test('onEvent hook fires after a verified emit', async () => { const seen: string[] = []; await server.stop(); server = startReceiverServer({ port: 0, secret: SECRET, busPath, onEvent: (envelope) => { seen.push(envelope.event.eventId); }, }); const event = buildEvent(); const envelope = { event, signature: signEvent(event, SECRET) }; await fetch(server.url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(envelope), }); expect(seen).toEqual([event.eventId]); }); });