| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- import { test } from 'node:test';
- import assert from 'node:assert/strict';
- import { cleanEmailBody, normalizeChatwootWebhook } from '../src/domain/messageNormalizer.js';
- import {
- bounceMessage,
- incomingEmailMessage,
- missingConversationId,
- noLabelsPayload,
- outgoingMessage,
- privateNote,
- statusUpdate,
- } from './fixtures/chatwoot.js';
- test('accepts an incoming e-mail message', () => {
- const result = normalizeChatwootWebhook(incomingEmailMessage);
- assert.ok(result.ok);
- assert.equal(result.event.conversationId, 1311);
- assert.equal(result.event.messageId, '90210');
- assert.equal(result.event.senderEmail, 'jan.kowalski@example.com');
- assert.equal(result.event.senderName, 'Jan Kowalski');
- assert.equal(result.event.channel, 'email');
- assert.equal(result.event.subject, 'Pytanie o klimatyzację');
- assert.equal(result.event.needsConversationFetch, false);
- });
- test('rejects outgoing messages', () => {
- const result = normalizeChatwootWebhook(outgoingMessage);
- assert.equal(result.ok, false);
- assert.match((result as { reason: string }).reason, /not_incoming/);
- });
- test('rejects private notes', () => {
- const result = normalizeChatwootWebhook(privateNote);
- assert.equal(result.ok, false);
- assert.equal((result as { reason: string }).reason, 'private_note');
- });
- test('rejects non message_created events', () => {
- const result = normalizeChatwootWebhook(statusUpdate);
- assert.equal(result.ok, false);
- assert.match((result as { reason: string }).reason, /unsupported_event/);
- });
- test('rejects payloads without a conversation id', () => {
- const result = normalizeChatwootWebhook(missingConversationId);
- assert.equal(result.ok, false);
- assert.equal((result as { reason: string }).reason, 'missing_conversation_id');
- });
- test('flags payloads that need a conversation fetch', () => {
- const result = normalizeChatwootWebhook(noLabelsPayload);
- assert.ok(result.ok);
- assert.equal(result.event.needsConversationFetch, true);
- });
- test('derives a stable surrogate id when the payload has no message id', () => {
- const { id, ...withoutId } = incomingEmailMessage;
- void id;
- const a = normalizeChatwootWebhook(withoutId);
- const b = normalizeChatwootWebhook(withoutId);
- assert.ok(a.ok && b.ok);
- assert.match(a.event.messageId, /^hash:1311:[0-9a-f]{32}$/);
- assert.equal(a.event.messageId, b.event.messageId);
- });
- test('a different body yields a different surrogate id', () => {
- const { id, ...withoutId } = incomingEmailMessage;
- void id;
- const a = normalizeChatwootWebhook(withoutId);
- const b = normalizeChatwootWebhook({ ...withoutId, content: 'inna treść' });
- assert.ok(a.ok && b.ok);
- assert.notEqual(a.event.messageId, b.event.messageId);
- });
- test('bounce payloads still normalise (the spam gate decides, not the parser)', () => {
- const result = normalizeChatwootWebhook(bounceMessage);
- assert.ok(result.ok);
- assert.equal(result.event.senderEmail, 'mailer-daemon@example.com');
- });
- test('cleanEmailBody strips quoted replies and signatures', () => {
- const raw = [
- 'Dzień dobry, mam pytanie o zamówienie.',
- '',
- 'W dniu 2026-08-19 Jan Kowalski napisał:',
- '> poprzednia wiadomość',
- '> druga linia cytatu',
- ].join('\n');
- assert.equal(cleanEmailBody(raw), 'Dzień dobry, mam pytanie o zamówienie.');
- });
- test('cleanEmailBody converts HTML e-mails to text', () => {
- const raw = '<p>Witam,<br>czy produkt jest dostępny?</p><style>p{color:red}</style>';
- const cleaned = cleanEmailBody(raw);
- assert.match(cleaned, /Witam/);
- assert.match(cleaned, /czy produkt jest dostępny\?/);
- assert.ok(!cleaned.includes('<'));
- assert.ok(!cleaned.includes('color:red'));
- });
|