frontend-analytics-model.test.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import assert from 'node:assert/strict';
  2. import { test } from 'node:test';
  3. import {
  4. buildDashboardSummary,
  5. buildDeliveryFunnel,
  6. buildEventTimeline,
  7. buildDomainRanking,
  8. buildHourlyHeatmap,
  9. buildStatusDistribution,
  10. buildTrendSeries
  11. } from '../src/frontend/analytics-model.js';
  12. test('builds dashboard summary from analytics, DNS health, and SMTP state', () => {
  13. const summary = buildDashboardSummary({
  14. analytics: {
  15. summary: {
  16. total: 20,
  17. queued: 18,
  18. failed: 2,
  19. recipients: 25,
  20. today: 8,
  21. successRate: 90,
  22. verifiedDomains: 1
  23. }
  24. },
  25. domains: [
  26. domain(true, [
  27. record('verification', 'ok'),
  28. record('dkim', 'ok'),
  29. record('spf', 'ok'),
  30. record('dmarc', 'ok'),
  31. record('sender-a', 'ok')
  32. ]),
  33. domain(false, [
  34. record('verification', 'ok'),
  35. record('dkim', 'warn'),
  36. record('spf', 'missing')
  37. ])
  38. ],
  39. events: [{ createdAt: '2026-07-08T12:30:00.000Z' }],
  40. config: { submission: { enabled: true } },
  41. smtpCredential: { passwordSet: true }
  42. });
  43. assert.equal(summary.verifiedDomains, 1);
  44. assert.equal(summary.today, 8);
  45. assert.equal(summary.successRate, 90);
  46. assert.equal(summary.bounceRate, 10);
  47. assert.equal(summary.complaintRate, 0);
  48. assert.equal(summary.dnsIssues, 4);
  49. assert.equal(summary.smtpReady, true);
  50. assert.equal(summary.lastSentAt, '2026-07-08T12:30:00.000Z');
  51. });
  52. test('normalizes chart datasets for trend, status, ranking, and hourly views', () => {
  53. const analytics = {
  54. byDay: [
  55. { day: '2026-07-07', total: 6, queued: 5, failed: 1, recipients: 8 },
  56. { day: '2026-07-08', total: 9, queued: 9, failed: 0, recipients: 10 }
  57. ],
  58. byStatus: [
  59. { status: 'queued', total: 14 },
  60. { status: 'failed', total: 1 }
  61. ],
  62. byDomain: [
  63. { domain: 'b.example.com', total: 2, queued: 2, failed: 0, recipients: 2 },
  64. { domain: 'a.example.com', total: 8, queued: 7, failed: 1, recipients: 12 }
  65. ],
  66. hourly: [
  67. { hour: 0, total: 0, queued: 0, failed: 0 },
  68. { hour: 9, total: 4, queued: 3, failed: 1 }
  69. ]
  70. };
  71. assert.deepEqual(buildTrendSeries(analytics), [
  72. { date: '2026-07-07', total: 6, accepted: 5, failed: 1, recipients: 8 },
  73. { date: '2026-07-08', total: 9, accepted: 9, failed: 0, recipients: 10 }
  74. ]);
  75. assert.deepEqual(buildStatusDistribution(analytics), [
  76. { status: 'queued', label: 'queued', value: 14 },
  77. { status: 'failed', label: 'failed', value: 1 }
  78. ]);
  79. assert.deepEqual(buildDomainRanking(analytics).map((item) => item.domain), ['a.example.com', 'b.example.com']);
  80. assert.deepEqual(buildHourlyHeatmap(analytics).find((item) => item.hour === '09:00'), {
  81. hour: '09:00',
  82. total: 4,
  83. accepted: 3,
  84. failed: 1
  85. });
  86. });
  87. test('builds delivery funnel and event timeline models', () => {
  88. const analytics = {
  89. deliveryFunnel: [
  90. { stage: 'submitted', total: 5, rate: 100 },
  91. { stage: 'accepted', total: 4, rate: 80 },
  92. { stage: 'delivered', total: 2, rate: 40 },
  93. { stage: 'pending', total: 1, rate: 20 },
  94. { stage: 'failed', total: 2, rate: 40 }
  95. ]
  96. };
  97. assert.deepEqual(buildDeliveryFunnel(analytics), [
  98. { stage: 'submitted', total: 5, rate: 100, tone: 'info' },
  99. { stage: 'accepted', total: 4, rate: 80, tone: 'info' },
  100. { stage: 'delivered', total: 2, rate: 40, tone: 'success' },
  101. { stage: 'pending', total: 1, rate: 20, tone: 'warning' },
  102. { stage: 'failed', total: 2, rate: 40, tone: 'error' }
  103. ]);
  104. const timeline = buildEventTimeline({
  105. id: 7,
  106. status: 'sent',
  107. queueId: 'Q7',
  108. createdAt: '2026-07-09T11:59:00.000Z',
  109. deliveredAt: '2026-07-09T12:00:00.000Z',
  110. deliveryAttempts: [{
  111. at: '2026-07-09T12:00:00.000Z',
  112. queueId: 'Q7',
  113. status: 'sent',
  114. recipient: 'user@example.com',
  115. relay: 'mx.example.net',
  116. response: '250 ok'
  117. }],
  118. webhookDeliveries: [{
  119. id: 11,
  120. webhookId: 3,
  121. userId: 1,
  122. sendEventId: 7,
  123. eventType: 'sent',
  124. status: 'success',
  125. attemptCount: 1,
  126. createdAt: '2026-07-09T12:00:01.000Z',
  127. lastAttemptAt: '2026-07-09T12:00:02.000Z',
  128. responseStatus: 200
  129. }]
  130. });
  131. assert.deepEqual(timeline.map((item) => item.stage), ['submitted', 'accepted', 'delivered', 'webhook']);
  132. assert.equal(timeline[1].queueId, 'Q7');
  133. assert.equal(timeline[2].tone, 'success');
  134. assert.equal(timeline[3].status, 'success');
  135. });
  136. function domain(verified, records) {
  137. return {
  138. status: {
  139. verified,
  140. records
  141. }
  142. };
  143. }
  144. function record(key, status) {
  145. return { key, status };
  146. }