messageNormalizer.test.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { cleanEmailBody, normalizeChatwootWebhook } from '../src/domain/messageNormalizer.js';
  4. import {
  5. bounceMessage,
  6. incomingEmailMessage,
  7. missingConversationId,
  8. noLabelsPayload,
  9. outgoingMessage,
  10. privateNote,
  11. statusUpdate,
  12. } from './fixtures/chatwoot.js';
  13. test('accepts an incoming e-mail message', () => {
  14. const result = normalizeChatwootWebhook(incomingEmailMessage);
  15. assert.ok(result.ok);
  16. assert.equal(result.event.conversationId, 1311);
  17. assert.equal(result.event.messageId, '90210');
  18. assert.equal(result.event.senderEmail, 'jan.kowalski@example.com');
  19. assert.equal(result.event.senderName, 'Jan Kowalski');
  20. assert.equal(result.event.channel, 'email');
  21. assert.equal(result.event.subject, 'Pytanie o klimatyzację');
  22. assert.equal(result.event.needsConversationFetch, false);
  23. });
  24. test('rejects outgoing messages', () => {
  25. const result = normalizeChatwootWebhook(outgoingMessage);
  26. assert.equal(result.ok, false);
  27. assert.match((result as { reason: string }).reason, /not_incoming/);
  28. });
  29. test('rejects private notes', () => {
  30. const result = normalizeChatwootWebhook(privateNote);
  31. assert.equal(result.ok, false);
  32. assert.equal((result as { reason: string }).reason, 'private_note');
  33. });
  34. test('rejects non message_created events', () => {
  35. const result = normalizeChatwootWebhook(statusUpdate);
  36. assert.equal(result.ok, false);
  37. assert.match((result as { reason: string }).reason, /unsupported_event/);
  38. });
  39. test('rejects payloads without a conversation id', () => {
  40. const result = normalizeChatwootWebhook(missingConversationId);
  41. assert.equal(result.ok, false);
  42. assert.equal((result as { reason: string }).reason, 'missing_conversation_id');
  43. });
  44. test('flags payloads that need a conversation fetch', () => {
  45. const result = normalizeChatwootWebhook(noLabelsPayload);
  46. assert.ok(result.ok);
  47. assert.equal(result.event.needsConversationFetch, true);
  48. });
  49. test('derives a stable surrogate id when the payload has no message id', () => {
  50. const { id, ...withoutId } = incomingEmailMessage;
  51. void id;
  52. const a = normalizeChatwootWebhook(withoutId);
  53. const b = normalizeChatwootWebhook(withoutId);
  54. assert.ok(a.ok && b.ok);
  55. assert.match(a.event.messageId, /^hash:1311:[0-9a-f]{32}$/);
  56. assert.equal(a.event.messageId, b.event.messageId);
  57. });
  58. test('a different body yields a different surrogate id', () => {
  59. const { id, ...withoutId } = incomingEmailMessage;
  60. void id;
  61. const a = normalizeChatwootWebhook(withoutId);
  62. const b = normalizeChatwootWebhook({ ...withoutId, content: 'inna treść' });
  63. assert.ok(a.ok && b.ok);
  64. assert.notEqual(a.event.messageId, b.event.messageId);
  65. });
  66. test('bounce payloads still normalise (the spam gate decides, not the parser)', () => {
  67. const result = normalizeChatwootWebhook(bounceMessage);
  68. assert.ok(result.ok);
  69. assert.equal(result.event.senderEmail, 'mailer-daemon@example.com');
  70. });
  71. test('cleanEmailBody strips quoted replies and signatures', () => {
  72. const raw = [
  73. 'Dzień dobry, mam pytanie o zamówienie.',
  74. '',
  75. 'W dniu 2026-08-19 Jan Kowalski napisał:',
  76. '> poprzednia wiadomość',
  77. '> druga linia cytatu',
  78. ].join('\n');
  79. assert.equal(cleanEmailBody(raw), 'Dzień dobry, mam pytanie o zamówienie.');
  80. });
  81. test('cleanEmailBody converts HTML e-mails to text', () => {
  82. const raw = '<p>Witam,<br>czy produkt jest dostępny?</p><style>p{color:red}</style>';
  83. const cleaned = cleanEmailBody(raw);
  84. assert.match(cleaned, /Witam/);
  85. assert.match(cleaned, /czy produkt jest dostępny\?/);
  86. assert.ok(!cleaned.includes('<'));
  87. assert.ok(!cleaned.includes('color:red'));
  88. });