| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027 |
- /* eslint-disable @typescript-eslint/no-non-null-assertion */
- import { Test, TestingModule } from '@nestjs/testing';
- import { TypeOrmModule } from '@nestjs/typeorm';
- import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
- import {
- EventBus,
- Injector,
- JobQueueService,
- LanguageCode,
- Logger,
- Order,
- OrderStateTransitionEvent,
- PluginCommonModule,
- RequestContext,
- VendureEvent,
- } from '@vendure/core';
- import { ensureConfigLoaded } from '@vendure/core/dist/config/config-helpers';
- import { TestingLogger } from '@vendure/testing';
- import { createReadStream, readFileSync } from 'fs';
- import path from 'path';
- import { Readable } from 'stream';
- import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
- import { EmailProcessor } from './email-processor';
- import { EmailEventListener } from './event-listener';
- import { orderConfirmationHandler } from './handler/default-email-handlers';
- import { EmailEventHandler } from './handler/event-handler';
- import { EmailPlugin } from './plugin';
- import { EmailSender } from './sender/email-sender';
- import { FileBasedTemplateLoader } from './template-loader/file-based-template-loader';
- import { EmailDetails, EmailPluginOptions, EmailTransportOptions } from './types';
- describe('EmailPlugin', () => {
- let eventBus: EventBus;
- let onSend: Mock;
- let module: TestingModule;
- const testingLogger = new TestingLogger((...args) => vi.fn(...args));
- async function initPluginWithHandlers(
- handlers: Array<EmailEventHandler<string, any>>,
- options?: Partial<EmailPluginOptions>,
- ) {
- await ensureConfigLoaded();
- onSend = vi.fn();
- module = await Test.createTestingModule({
- imports: [
- TypeOrmModule.forRoot({
- type: 'sqljs',
- retryAttempts: 0,
- }),
- PluginCommonModule,
- EmailPlugin.init({
- templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../test-templates')),
- transport: {
- type: 'testing',
- onSend,
- },
- handlers,
- ...options,
- }),
- ],
- providers: [MockService],
- }).compile();
- Logger.useLogger(testingLogger);
- module.useLogger(new Logger());
- const plugin = module.get(EmailPlugin);
- eventBus = module.get(EventBus);
- await plugin.onApplicationBootstrap();
- return module;
- }
- afterEach(async () => {
- if (module) {
- await module.close();
- }
- });
- it('setting from, recipient, subject', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ subjectVar: 'foo' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello');
- expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
- expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
- });
- describe('event filtering', () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- it('single filter', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .filter(event => event.shouldSend === true)
- .setRecipient(() => 'test@test.com')
- .setFrom('"test from" <noreply@test.com>')
- .setSubject('test subject');
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, false));
- await pause();
- expect(onSend).not.toHaveBeenCalled();
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend).toHaveBeenCalledTimes(1);
- });
- it('multiple filters', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .filter(event => event.shouldSend === true)
- .filter(event => !!event.ctx.activeUserId)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('test subject');
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend).not.toHaveBeenCalled();
- const ctxWithUser = RequestContext.deserialize({ ...ctx, _session: { user: { id: 42 } } } as any);
- await eventBus.publish(new MockEvent(ctxWithUser, true));
- await pause();
- expect(onSend).toHaveBeenCalledTimes(1);
- });
- it('with .loadData() after .filter()', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .filter(event => event.shouldSend === true)
- .loadData(context => Promise.resolve('loaded data'))
- .setRecipient(() => 'test@test.com')
- .setFrom('"test from" <noreply@test.com>')
- .setSubject('test subject');
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, false));
- await pause();
- expect(onSend).not.toHaveBeenCalled();
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend).toHaveBeenCalledTimes(1);
- });
- });
- describe('templateVars', () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- it('interpolates subject', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setTemplateVars(event => ({ subjectVar: 'foo' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
- });
- it('interpolates body', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ testVar: 'this is the test var' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
- });
- /**
- * Intended to test the ability for Handlebars to interpolate
- * getters on the Order entity prototype.
- * See https://github.com/vendure-ecommerce/vendure/issues/259
- */
- it('interpolates body with property from entity', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ order: new Order({ subTotal: 123 }) }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].body).toContain('Total: 123');
- });
- it('interpolates globalTemplateVars', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ globalVar }}');
- await initPluginWithHandlers([handler], {
- globalTemplateVars: { globalVar: 'baz' },
- });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
- });
- it('loads globalTemplateVars async', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Job {{ name }}, {{ primaryColor }}');
- await initPluginWithHandlers([handler], {
- globalTemplateVars: async (_ctxLocal: RequestContext, injector: Injector) => {
- const jobQueueService = injector.get(JobQueueService);
- const jobQueue = await jobQueueService.createQueue({
- name: 'hello-service',
- // eslint-disable-next-line
- process: async job => {
- return 'hello';
- },
- });
- const name = jobQueue.name;
- return {
- name,
- primaryColor: 'blue',
- };
- },
- });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe(`Job hello-service, blue`);
- });
- it('interpolates from', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from {{ globalVar }}" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello');
- await initPluginWithHandlers([handler], {
- globalTemplateVars: { globalVar: 'baz' },
- });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
- });
- // Test fix for https://github.com/vendure-ecommerce/vendure/issues/363
- it('does not escape HTML chars when interpolating "from"', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('{{ globalFrom }}')
- .setRecipient(() => 'Test <test@test.com>')
- .setSubject('Hello');
- await initPluginWithHandlers([handler], {
- globalTemplateVars: { globalFrom: 'Test <test@test.com>' },
- });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].from).toBe('Test <test@test.com>');
- });
- it('globalTemplateVars available in setTemplateVars method', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ testVar }}')
- .setTemplateVars((event, globals) => ({ testVar: (globals.globalVar as string) + ' quux' }));
- await initPluginWithHandlers([handler], {
- globalTemplateVars: { globalVar: 'baz' },
- });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
- });
- it('setTemplateVars overrides globals', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ name }}')
- .setTemplateVars((event, globals) => ({ name: 'quux' }));
- await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
- });
- });
- describe('handlebars helpers', () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- it('formateDate', async () => {
- const handler = new EmailEventListener('test-helpers')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ myDate: new Date('2020-01-01T10:00:00.000Z'), myPrice: 0 }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].body).toContain('Date: Wed Jan 01 2020 10:00:00');
- });
- it('formatMoney', async () => {
- const handler = new EmailEventListener('test-helpers')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ myDate: new Date(), myPrice: 123 }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].body).toContain('Price: 1.23');
- expect(onSend.mock.calls[0][0].body).toContain('Price: €1.23');
- expect(onSend.mock.calls[0][0].body).toContain('Price: £1.23');
- });
- });
- describe('multiple configs', () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- it('additional LanguageCode', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setSubject('Hello, {{ name }}!')
- .setRecipient(() => 'test@test.com')
- .setTemplateVars(() => ({ name: 'Test' }))
- .addTemplate({
- channelCode: 'default',
- languageCode: LanguageCode.de,
- templateFile: 'body.de.hbs',
- subject: 'Servus, {{ name }}!',
- });
- await initPluginWithHandlers([handler]);
- const ctxTa = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.ta } as any);
- await eventBus.publish(new MockEvent(ctxTa, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
- expect(onSend.mock.calls[0][0].body).toContain('Default body.');
- const ctxDe = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.de } as any);
- await eventBus.publish(new MockEvent(ctxDe, true));
- await pause();
- expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
- expect(onSend.mock.calls[1][0].body).toContain('German body.');
- });
- it('set LanguageCode', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setSubject('Hello, {{ name }}!')
- .setRecipient(() => 'test@test.com')
- .setLanguageCode(() => LanguageCode.de)
- .setTemplateVars(() => ({ name: 'Test' }))
- .addTemplate({
- channelCode: 'default',
- languageCode: LanguageCode.de,
- templateFile: 'body.de.hbs',
- subject: 'Servus, {{ name }}!',
- });
- const ctxEn = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.en } as any);
- await eventBus.publish(new MockEvent(ctxEn, true));
- await pause();
- expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
- expect(onSend.mock.calls[1][0].body).toContain('German body.');
- });
- });
- describe('loadData', () => {
- it('loads async data', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .loadData(async ({ injector }) => {
- const service = injector.get(MockService);
- return service.someAsyncMethod();
- })
- .setFrom('"test from" <noreply@test.com>')
- .setSubject('Hello, {{ testData }}!')
- .setRecipient(() => 'test@test.com')
- .setTemplateVars(event => ({ testData: event.data }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(
- new MockEvent(
- RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any),
- true,
- ),
- );
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
- });
- it('works when loadData is called after other setup', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setSubject('Hello, {{ testData }}!')
- .setRecipient(() => 'test@test.com')
- .loadData(async ({ injector }) => {
- const service = injector.get(MockService);
- return service.someAsyncMethod();
- })
- .setTemplateVars(event => ({ testData: event.data }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(
- new MockEvent(
- RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any),
- true,
- ),
- );
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
- expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
- expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
- });
- it('only executes for filtered events', async () => {
- let callCount = 0;
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .filter(event => event.shouldSend === true)
- .loadData(async ({ injector }) => {
- callCount++;
- });
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(RequestContext.empty(), false));
- await eventBus.publish(new MockEvent(RequestContext.empty(), true));
- await pause();
- expect(callCount).toBe(1);
- });
- });
- describe('attachments', () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- const TEST_IMAGE_PATH = path.join(__dirname, '../test-fixtures/test.jpg');
- it('attachments are empty by default', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}');
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].attachments).toEqual([]);
- });
- it('sync attachment', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- path: TEST_IMAGE_PATH,
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
- });
- it('async attachment', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(async () => [
- {
- path: TEST_IMAGE_PATH,
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
- });
- it('attachment content as a string buffer', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- content: Buffer.from('hello'),
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- const attachment = onSend.mock.calls[0][0].attachments[0].content;
- expect(Buffer.compare(attachment, Buffer.from('hello'))).toBe(0); // 0 = buffers are equal
- });
- it('attachment content as an image buffer', async () => {
- const imageFileBuffer = readFileSync(TEST_IMAGE_PATH);
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- content: imageFileBuffer,
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- const attachment = onSend.mock.calls[0][0].attachments[0].content;
- expect(Buffer.compare(attachment, imageFileBuffer)).toBe(0); // 0 = buffers are equal
- });
- it('attachment content as a string', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- content: 'hello',
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- const attachment = onSend.mock.calls[0][0].attachments[0].content;
- expect(attachment).toBe('hello');
- });
- it('attachment content as a string stream', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- content: Readable.from(['hello']),
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- const attachment = onSend.mock.calls[0][0].attachments[0].content;
- expect(Buffer.compare(attachment, Buffer.from('hello'))).toBe(0); // 0 = buffers are equal
- });
- it('attachment content as an image stream', async () => {
- const imageFileBuffer = readFileSync(TEST_IMAGE_PATH);
- const imageFileStream = createReadStream(TEST_IMAGE_PATH);
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- content: imageFileStream,
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- const attachment = onSend.mock.calls[0][0].attachments[0].content;
- expect(Buffer.compare(attachment, imageFileBuffer)).toBe(0); // 0 = buffers are equal
- });
- it('raises a warning for large content attachments', async () => {
- testingLogger.warnSpy.mockClear();
- const largeBuffer = Buffer.from(Array.from({ length: 65535, 0: 0 }));
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setAttachments(() => [
- {
- content: largeBuffer,
- },
- ]);
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(testingLogger.warnSpy.mock.calls[0][0]).toContain(
- "Email has a large 'content' attachment (64k). Consider using the 'path' instead for improved performance.",
- );
- });
- });
- describe('orderConfirmationHandler', () => {
- beforeEach(async () => {
- await initPluginWithHandlers([orderConfirmationHandler], {
- templateLoader: new FileBasedTemplateLoader(path.join(__dirname, '../templates')),
- });
- });
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- const order = {
- code: 'ABCDE',
- customer: {
- emailAddress: 'test@test.com',
- },
- lines: [],
- } as any;
- it('filters events with wrong order state', async () => {
- await eventBus.publish(
- new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order),
- );
- await pause();
- expect(onSend).not.toHaveBeenCalled();
- await eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
- await pause();
- expect(onSend).not.toHaveBeenCalled();
- await eventBus.publish(
- new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order),
- );
- await pause();
- expect(onSend).not.toHaveBeenCalled();
- await eventBus.publish(
- new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order),
- );
- await pause();
- expect(onSend).toHaveBeenCalledTimes(1);
- });
- it('sets the Order Customer emailAddress as recipient', async () => {
- await eventBus.publish(
- new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order),
- );
- await pause();
- expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
- });
- it('sets the subject', async () => {
- await eventBus.publish(
- new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order),
- );
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code as string}`);
- });
- });
- describe('error handling', () => {
- it('Logs an error if the template file is not found', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- testingLogger.errorSpy.mockClear();
- const handler = new EmailEventListener('no-template')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}');
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('ENOENT: no such file or directory');
- });
- it('Logs a Handlebars error if the template is invalid', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- testingLogger.errorSpy.mockClear();
- const handler = new EmailEventListener('bad-template')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}');
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('Parse error on line 3:');
- });
- it('Logs an error if the loadData method throws', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- testingLogger.errorSpy.mockClear();
- const handler = new EmailEventListener('bad-template')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .loadData(context => {
- throw new Error('something went horribly wrong!');
- });
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(testingLogger.errorSpy.mock.calls[0][0]).toContain('something went horribly wrong!');
- });
- });
- describe('custom sender', () => {
- it('should allow a custom sender to be utilized', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ subjectVar: 'foo' }));
- const fakeSender = new FakeCustomSender();
- const send = vi.fn();
- fakeSender.send = send;
- await initPluginWithHandlers([handler], {
- emailSender: fakeSender,
- });
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(send.mock.calls[0][0].subject).toBe('Hello');
- expect(send.mock.calls[0][0].recipient).toBe('test@test.com');
- expect(send.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
- expect(onSend).toHaveBeenCalledTimes(0);
- });
- });
- describe('optional address fields', () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- it('cc', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setOptionalAddressFields(() => ({ cc: 'foo@bar.com' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].cc).toBe('foo@bar.com');
- });
- it('bcc', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setOptionalAddressFields(() => ({ bcc: 'foo@bar.com' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].bcc).toBe('foo@bar.com');
- });
- it('replyTo', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello {{ subjectVar }}')
- .setOptionalAddressFields(() => ({ replyTo: 'foo@bar.com' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].replyTo).toBe('foo@bar.com');
- });
- });
- describe('Dynamic transport settings', () => {
- let injectorArg: Injector | undefined;
- let ctxArg: RequestContext | undefined;
- it('Initializes with async transport settings', async () => {
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ subjectVar: 'foo' }));
- module = await initPluginWithHandlers([handler], {
- transport: async (injector, _ctx) => {
- injectorArg = injector;
- ctxArg = _ctx;
- return {
- type: 'testing',
- onSend: () => {
- /* */
- },
- };
- },
- });
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- await module!.get(EventBus).publish(new MockEvent(ctx, true));
- await pause();
- expect(module).toBeDefined();
- expect(typeof module.get(EmailPlugin).options.transport).toBe('function');
- });
- it('Passes injector and context to transport function', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- await module!.get(EventBus).publish(new MockEvent(ctx, true));
- await pause();
- expect(injectorArg?.constructor.name).toBe('Injector');
- expect(ctxArg?.constructor.name).toBe('RequestContext');
- });
- it('Resolves async transport settings', async () => {
- const transport = await module!.get(EmailProcessor).getTransportSettings();
- expect(transport.type).toBe('testing');
- });
- });
- describe('Dynamic subject handling', () => {
- it('With string', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject('Hello')
- .setTemplateVars(event => ({ subjectVar: 'foo' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello');
- expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
- expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
- });
- it('With callback function', async () => {
- const ctx = RequestContext.deserialize({
- _channel: { code: DEFAULT_CHANNEL_CODE },
- _languageCode: LanguageCode.en,
- } as any);
- const handler = new EmailEventListener('test')
- .on(MockEvent)
- .setFrom('"test from" <noreply@test.com>')
- .setRecipient(() => 'test@test.com')
- .setSubject(async (_e, _ctx, _i) => {
- const service = _i.get(MockService);
- const mockData = await service.someAsyncMethod();
- return `Hello from ${mockData} and {{ subjectVar }}`;
- })
- .setTemplateVars(event => ({ subjectVar: 'foo' }));
- await initPluginWithHandlers([handler]);
- await eventBus.publish(new MockEvent(ctx, true));
- await pause();
- expect(onSend.mock.calls[0][0].subject).toBe('Hello from loaded data and foo');
- expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
- expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
- });
- });
- });
- class FakeCustomSender implements EmailSender {
- send: (email: EmailDetails<'unserialized'>, options: EmailTransportOptions) => void;
- }
- const pause = () => new Promise(resolve => setTimeout(resolve, 100));
- class MockEvent extends VendureEvent {
- constructor(
- public ctx: RequestContext,
- public shouldSend: boolean,
- ) {
- super();
- }
- }
- class MockService {
- someAsyncMethod() {
- return Promise.resolve('loaded data');
- }
- }
|