UNPKG

@email-service/email-service

Version:

email-service is a versatile npm package designed to simplify the integration and standardization of email communications across multiple Email Service Providers (ESPs).

216 lines (201 loc) 7.87 kB
// Test manuel sendBulk — après `npm run build` // node test/manual-bulk.mjs // // On hérite de ESP pour override doSendMail → aucun appel HTTP réel. // Permet de vérifier : // - boucle + hooks // - validation marketing (unsubscribeUrl obligatoire) // - règles de blocage stream (transactional vs marketing) // - interpolation mergeVars // - injection List-Unsubscribe en marketing // - auto-generation text depuis html import { ESP } from '../dist/esm/models/esp.js' function check(label, cond, detail = '') { console.log(`${cond ? 'OK ' : 'FAIL'} | ${label}${detail ? ' — ' + detail : ''}`) } function mkFakeESP(opts, simulate) { class FakeESP extends ESP { history = [] async doSendMail(options) { this.history.push(options) return simulate ? simulate(options) : { success: true, status: 200, data: { to: options.to, submittedAt: new Date().toISOString(), messageId: 'fake-' + Math.random() }, } } } const cfg = { esp: 'emailserviceviewer', apiToken: 'test', webhook: 'test', logger: false, rateLimit: { perSecond: 10000 } } return new FakeESP(cfg, opts) } // === 1. Transactional, pas de hooks, 3 destinataires === { const esp = mkFakeESP({}) const report = await esp.sendBulk({ stream: 'transactional', campaignId: 'c-1', from: 'sender@x.com', recipients: [ { email: 'a@x.com' }, { email: 'b@x.com' }, { email: 'c@x.com' }, ], template: { subject: 'Hi', html: '<p>Hello</p>' }, }) check('1a. 3 sent', report.sent === 3) check('1b. 0 skipped', report.skipped.length === 0) check('1c. 0 failed', report.failed.length === 0) check('1d. campaignId propagé', esp.history[0].metaData.campaignId === 'c-1') check('1e. stream dans metaData', esp.history[0].metaData.stream === 'transactional') check('1f. text auto-généré', esp.history[0].text === 'Hello', esp.history[0].text) check('1g. pas de List-Unsubscribe', !esp.history[0].headers?.some((h) => h.name === 'List-Unsubscribe')) } // === 2. Marketing sans unsubscribeUrl → throw === { const esp = mkFakeESP({}) let threw = false try { await esp.sendBulk({ stream: 'marketing', campaignId: 'c-2', from: 'sender@x.com', recipients: [{ email: 'a@x.com' }], template: { subject: 'Promo', html: '<p>Deal</p>' }, // unsubscribeUrl absent → doit throw }) } catch (err) { threw = err.message.includes('unsubscribeUrl') } check('2. marketing sans unsubscribeUrl throw', threw) } // === 3. Marketing avec unsubscribeUrl → injecte List-Unsubscribe === { const esp = mkFakeESP({}) const report = await esp.sendBulk({ stream: 'marketing', campaignId: 'c-3', from: 'sender@x.com', recipients: [{ email: 'a@x.com' }], template: { subject: 'Promo', html: '<p>Deal</p>' }, unsubscribeUrl: (email) => `https://app.example/unsub?email=${encodeURIComponent(email)}`, }) check('3a. 1 sent', report.sent === 1) const h = esp.history[0].headers ?? [] const listUnsub = h.find((x) => x.name === 'List-Unsubscribe') check('3b. List-Unsubscribe présent', !!listUnsub, listUnsub?.value) check('3c. URL par destinataire', listUnsub?.value.includes('a%40x.com'), listUnsub?.value) check('3d. List-Unsubscribe-Post présent', h.some((x) => x.name === 'List-Unsubscribe-Post')) } // === 4. checkSuppression bloque selon stream === { // Hook qui retourne unsubscribe_marketing pour b@x.com const hook = async (email) => email === 'b@x.com' ? 'unsubscribe_marketing' : null // 4a. Transactional : unsubscribe_marketing ne bloque PAS { const esp = mkFakeESP({ hooks: { checkSuppression: hook } }) const report = await esp.sendBulk({ stream: 'transactional', campaignId: 'c-4a', from: 'sender@x.com', recipients: [{ email: 'a@x.com' }, { email: 'b@x.com' }, { email: 'c@x.com' }], template: { subject: 'Hi', html: '<p>x</p>' }, }) check('4a. transactional : unsubscribe_marketing passe', report.sent === 3 && report.skipped.length === 0) } // 4b. Marketing : unsubscribe_marketing BLOQUE { const esp = mkFakeESP({ hooks: { checkSuppression: hook } }) const report = await esp.sendBulk({ stream: 'marketing', campaignId: 'c-4b', from: 'sender@x.com', recipients: [{ email: 'a@x.com' }, { email: 'b@x.com' }, { email: 'c@x.com' }], template: { subject: 'Hi', html: '<p>x</p>' }, unsubscribeUrl: (e) => `https://u/${e}`, }) check('4b. marketing : unsubscribe_marketing bloque', report.sent === 2 && report.skipped.length === 1) check(' skipped email correct', report.skipped[0]?.email === 'b@x.com') check(' skipped reason correct', report.skipped[0]?.reason === 'unsubscribe_marketing') } // 4c. hard_bounce bloque dans les 2 streams { const esp = mkFakeESP({ hooks: { checkSuppression: async () => 'hard_bounce' } }) const report = await esp.sendBulk({ stream: 'transactional', campaignId: 'c-4c', from: 'sender@x.com', recipients: [{ email: 'a@x.com' }], template: { subject: 'Hi', html: '<p>x</p>' }, }) check('4c. hard_bounce bloque en transactional', report.sent === 0 && report.skipped.length === 1) } } // === 5. Hooks onSent / onFailed appelés === { let sentCount = 0, failedCount = 0, skippedCount = 0 const hooks = { onSent: async () => { sentCount++ }, onFailed: async () => { failedCount++ }, onSkipped: async () => { skippedCount++ }, checkSuppression: async (email) => email === 's@x.com' ? 'hard_bounce' : null, } const simulate = (options) => { const toEmail = typeof options.to === 'string' ? options.to : options.to?.[0]?.email ?? options.to if (toEmail === 'f@x.com') { return { success: false, status: 400, error: { name: 'REJECTED', message: 'bad' } } } return { success: true, status: 200, data: { to: options.to, submittedAt: '', messageId: 'm' } } } const esp = mkFakeESP({ hooks }, simulate) const report = await esp.sendBulk({ stream: 'transactional', campaignId: 'c-5', from: 'sender@x.com', recipients: [ { email: 'ok@x.com' }, { email: 'f@x.com' }, // failed { email: 's@x.com' }, // skipped (hard_bounce) { email: 'ok2@x.com' }, ], template: { subject: 'Hi', html: '<p>x</p>' }, }) check('5a. onSent appelé 2 fois', sentCount === 2, `sent=${sentCount}`) check('5b. onFailed appelé 1 fois', failedCount === 1, `failed=${failedCount}`) check('5c. onSkipped appelé 1 fois', skippedCount === 1, `skipped=${skippedCount}`) check('5d. report.sent = 2', report.sent === 2) check('5e. report.failed.length = 1', report.failed.length === 1) check('5f. report.skipped.length = 1', report.skipped.length === 1) } // === 6. mergeVars interpolés === { const esp = mkFakeESP({}) await esp.sendBulk({ stream: 'transactional', campaignId: 'c-6', from: 'sender@x.com', recipients: [ { email: 'alice@x.com', mergeVars: { user: { firstName: 'Alice' } } }, { email: 'bob@x.com', mergeVars: { user: { firstName: 'Bob' } } }, ], template: { subject: 'Hello {{user.firstName}}', html: '<p>Hi {{user.firstName}}!</p>' }, }) check('6a. subject interpolé Alice', esp.history[0].subject === 'Hello Alice', esp.history[0].subject) check('6b. html interpolé Alice', esp.history[0].html === '<p>Hi Alice!</p>') check('6c. subject interpolé Bob', esp.history[1].subject === 'Hello Bob') check('6d. text stripped', esp.history[0].text === 'Hi Alice!', esp.history[0].text) } // === 7. Report structure === { const esp = mkFakeESP({}) const report = await esp.sendBulk({ stream: 'transactional', campaignId: 'c-7', from: 'sender@x.com', recipients: [{ email: 'a@x.com' }], template: { subject: 'X', html: '<p>X</p>' }, }) check('7a. campaignId dans report', report.campaignId === 'c-7') check('7b. startedAt/endedAt présents', !!report.startedAt && !!report.endedAt) check('7c. durationMs >= 0', typeof report.durationMs === 'number' && report.durationMs >= 0) } console.log('\nTests sendBulk terminés.')