| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- import { after, before, test } from 'node:test';
- import assert from 'node:assert/strict';
- import { prepareTestDatabase, startServer, type RunningServer } from '../helpers/testServer.js';
- import { applyTestConfig } from '../helpers/testConfig.js';
- import {
- incomingEmailMessage,
- noLabelsPayload,
- outgoingMessage,
- privateNote,
- statusUpdate,
- } from '../fixtures/chatwoot.js';
- const dbUrl = prepareTestDatabase('test-webhook');
- // Imported after the DATABASE_URL is in place.
- const { createApp } = await import('../../src/http/app.js');
- const { db, disconnectDb } = await import('../../src/store/db.js');
- let running: RunningServer;
- before(async () => {
- applyTestConfig({ DATABASE_URL: dbUrl });
- running = await startServer(createApp());
- });
- after(async () => {
- await running.close();
- await disconnectDb();
- });
- async function post(path: string, body: unknown, headers: Record<string, string> = {}) {
- const res = await fetch(`${running.baseUrl}${path}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', ...headers },
- body: JSON.stringify(body),
- });
- return { status: res.status, body: (await res.json()) as Record<string, unknown> };
- }
- test('GET /health returns 200 without touching any dependency', async () => {
- const res = await fetch(`${running.baseUrl}/health`);
- assert.equal(res.status, 200);
- const body = (await res.json()) as Record<string, unknown>;
- assert.equal(body.ok, true);
- assert.equal(body.service, 'eks-relay');
- });
- test('an incoming message is accepted with 202 and recorded', async () => {
- const res = await post('/webhooks/chatwoot', incomingEmailMessage);
- assert.equal(res.status, 202);
- assert.equal(res.body.accepted, true);
- assert.equal(res.body.conversationId, 1311);
- assert.ok(res.body.jobId);
- const row = await db().processedMessage.findUnique({
- where: { source_messageId: { source: 'chatwoot', messageId: '90210' } },
- });
- assert.ok(row, 'ProcessedMessage row should exist');
- assert.equal(row.conversationId, 1311);
- assert.equal(row.status, 'queued');
- const jobs = await db().job.findMany({ where: { type: 'chatwoot_message' } });
- assert.equal(jobs.length, 1);
- });
- test('the same message id a second time is a no-op duplicate', async () => {
- const res = await post('/webhooks/chatwoot', incomingEmailMessage);
- assert.equal(res.status, 200);
- assert.equal(res.body.duplicate, true);
- const jobs = await db().job.findMany({ where: { type: 'chatwoot_message' } });
- assert.equal(jobs.length, 1, 'a duplicate must not create a second job');
- const rows = await db().processedMessage.findMany({ where: { messageId: '90210' } });
- assert.equal(rows.length, 1);
- });
- test('concurrent deliveries of the same message create exactly one job', async () => {
- const payload = { ...incomingEmailMessage, id: 99001 };
- const results = await Promise.all([
- post('/webhooks/chatwoot', payload),
- post('/webhooks/chatwoot', payload),
- post('/webhooks/chatwoot', payload),
- ]);
- const accepted = results.filter((r) => r.status === 202);
- assert.equal(accepted.length, 1, 'exactly one delivery may be accepted');
- const jobs = await db().job.findMany({});
- const forMessage = jobs.filter((j) => j.payloadJson.includes('"messageId":"99001"'));
- assert.equal(forMessage.length, 1);
- });
- test('the webhook stores an audit event', async () => {
- const events = await db().auditEvent.findMany({ where: { messageId: '90210' } });
- assert.ok(events.length >= 1);
- assert.equal(events[0]?.eventType, 'webhook_accepted');
- });
- test('outgoing messages, private notes and status events are skipped with 200', async () => {
- for (const payload of [outgoingMessage, privateNote, statusUpdate]) {
- const res = await post('/webhooks/chatwoot', payload);
- assert.equal(res.status, 200);
- assert.equal(res.body.skipped, true);
- }
- const skippedRows = await db().processedMessage.findMany({
- where: { messageId: { in: ['90211', '90212', '90213'] } },
- });
- assert.equal(skippedRows.length, 0, 'skipped events must not be recorded as processed');
- });
- test('a payload without labels is still accepted and flagged for a fetch', async () => {
- const res = await post('/webhooks/chatwoot', noLabelsPayload);
- assert.equal(res.status, 202);
- const job = await db().job.findFirst({
- where: { payloadJson: { contains: '"messageId":"90217"' } },
- });
- assert.ok(job);
- const payload = JSON.parse(job.payloadJson) as { needsConversationFetch: boolean };
- assert.equal(payload.needsConversationFetch, true);
- });
- test('garbage bodies are answered 200 without creating work', async () => {
- const before = await db().job.count();
- const res = await post('/webhooks/chatwoot', { totally: 'unrelated' });
- assert.equal(res.status, 200);
- assert.equal(res.body.skipped, true);
- assert.equal(await db().job.count(), before);
- });
|