plugin.spec.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. /* tslint:disable:no-non-null-assertion */
  2. import { Test, TestingModule } from '@nestjs/testing';
  3. import { TypeOrmModule } from '@nestjs/typeorm';
  4. import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
  5. import {
  6. EventBus,
  7. LanguageCode,
  8. Logger,
  9. Order,
  10. OrderStateTransitionEvent,
  11. PluginCommonModule,
  12. RequestContext,
  13. VendureEvent,
  14. } from '@vendure/core';
  15. import { TestingLogger } from '@vendure/testing';
  16. import path from 'path';
  17. import { orderConfirmationHandler } from './default-email-handlers';
  18. import { EmailEventHandler } from './event-handler';
  19. import { EmailEventListener } from './event-listener';
  20. import { EmailPlugin } from './plugin';
  21. import { EmailDetails, EmailPluginOptions, EmailSender, EmailTransportOptions } from './types';
  22. describe('EmailPlugin', () => {
  23. let eventBus: EventBus;
  24. let onSend: jest.Mock;
  25. let module: TestingModule;
  26. const testingLogger = new TestingLogger(() => jest.fn());
  27. async function initPluginWithHandlers(
  28. handlers: Array<EmailEventHandler<string, any>>,
  29. options?: Partial<EmailPluginOptions>,
  30. ) {
  31. onSend = jest.fn();
  32. module = await Test.createTestingModule({
  33. imports: [
  34. TypeOrmModule.forRoot({
  35. type: 'sqljs',
  36. retryAttempts: 0,
  37. }),
  38. PluginCommonModule,
  39. EmailPlugin.init({
  40. templatePath: path.join(__dirname, '../test-templates'),
  41. transport: {
  42. type: 'testing',
  43. onSend,
  44. },
  45. handlers,
  46. ...options,
  47. }),
  48. ],
  49. providers: [MockService],
  50. }).compile();
  51. Logger.useLogger(testingLogger);
  52. module.useLogger(new Logger());
  53. const plugin = module.get(EmailPlugin);
  54. eventBus = module.get(EventBus);
  55. await plugin.onApplicationBootstrap();
  56. return module;
  57. }
  58. afterEach(async () => {
  59. if (module) {
  60. await module.close();
  61. }
  62. });
  63. it('setting from, recipient, subject', async () => {
  64. const ctx = RequestContext.deserialize({
  65. _channel: { code: DEFAULT_CHANNEL_CODE },
  66. _languageCode: LanguageCode.en,
  67. } as any);
  68. const handler = new EmailEventListener('test')
  69. .on(MockEvent)
  70. .setFrom('"test from" <noreply@test.com>')
  71. .setRecipient(() => 'test@test.com')
  72. .setSubject('Hello')
  73. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  74. await initPluginWithHandlers([handler]);
  75. eventBus.publish(new MockEvent(ctx, true));
  76. await pause();
  77. expect(onSend.mock.calls[0][0].subject).toBe('Hello');
  78. expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
  79. expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  80. });
  81. describe('event filtering', () => {
  82. const ctx = RequestContext.deserialize({
  83. _channel: { code: DEFAULT_CHANNEL_CODE },
  84. _languageCode: LanguageCode.en,
  85. } as any);
  86. it('single filter', async () => {
  87. const handler = new EmailEventListener('test')
  88. .on(MockEvent)
  89. .filter(event => event.shouldSend === true)
  90. .setRecipient(() => 'test@test.com')
  91. .setFrom('"test from" <noreply@test.com>')
  92. .setSubject('test subject');
  93. await initPluginWithHandlers([handler]);
  94. eventBus.publish(new MockEvent(ctx, false));
  95. await pause();
  96. expect(onSend).not.toHaveBeenCalled();
  97. eventBus.publish(new MockEvent(ctx, true));
  98. await pause();
  99. expect(onSend).toHaveBeenCalledTimes(1);
  100. });
  101. it('multiple filters', async () => {
  102. const handler = new EmailEventListener('test')
  103. .on(MockEvent)
  104. .filter(event => event.shouldSend === true)
  105. .filter(event => !!event.ctx.activeUserId)
  106. .setFrom('"test from" <noreply@test.com>')
  107. .setRecipient(() => 'test@test.com')
  108. .setSubject('test subject');
  109. await initPluginWithHandlers([handler]);
  110. eventBus.publish(new MockEvent(ctx, true));
  111. await pause();
  112. expect(onSend).not.toHaveBeenCalled();
  113. const ctxWithUser = RequestContext.deserialize({ ...ctx, _session: { user: { id: 42 } } } as any);
  114. eventBus.publish(new MockEvent(ctxWithUser, true));
  115. await pause();
  116. expect(onSend).toHaveBeenCalledTimes(1);
  117. });
  118. it('with .loadData() after .filter()', async () => {
  119. const handler = new EmailEventListener('test')
  120. .on(MockEvent)
  121. .filter(event => event.shouldSend === true)
  122. .loadData(context => Promise.resolve('loaded data'))
  123. .setRecipient(() => 'test@test.com')
  124. .setFrom('"test from" <noreply@test.com>')
  125. .setSubject('test subject');
  126. await initPluginWithHandlers([handler]);
  127. eventBus.publish(new MockEvent(ctx, false));
  128. await pause();
  129. expect(onSend).not.toHaveBeenCalled();
  130. eventBus.publish(new MockEvent(ctx, true));
  131. await pause();
  132. expect(onSend).toHaveBeenCalledTimes(1);
  133. });
  134. });
  135. describe('templateVars', () => {
  136. const ctx = RequestContext.deserialize({
  137. _channel: { code: DEFAULT_CHANNEL_CODE },
  138. _languageCode: LanguageCode.en,
  139. } as any);
  140. it('interpolates subject', async () => {
  141. const handler = new EmailEventListener('test')
  142. .on(MockEvent)
  143. .setFrom('"test from" <noreply@test.com>')
  144. .setRecipient(() => 'test@test.com')
  145. .setSubject('Hello {{ subjectVar }}')
  146. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  147. await initPluginWithHandlers([handler]);
  148. eventBus.publish(new MockEvent(ctx, true));
  149. await pause();
  150. expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
  151. });
  152. it('interpolates body', async () => {
  153. const handler = new EmailEventListener('test')
  154. .on(MockEvent)
  155. .setFrom('"test from" <noreply@test.com>')
  156. .setRecipient(() => 'test@test.com')
  157. .setSubject('Hello')
  158. .setTemplateVars(event => ({ testVar: 'this is the test var' }));
  159. await initPluginWithHandlers([handler]);
  160. eventBus.publish(new MockEvent(ctx, true));
  161. await pause();
  162. expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
  163. });
  164. /**
  165. * Intended to test the ability for Handlebars to interpolate
  166. * getters on the Order entity prototype.
  167. * See https://github.com/vendure-ecommerce/vendure/issues/259
  168. */
  169. it('interpolates body with property from entity', async () => {
  170. const handler = new EmailEventListener('test')
  171. .on(MockEvent)
  172. .setFrom('"test from" <noreply@test.com>')
  173. .setRecipient(() => 'test@test.com')
  174. .setSubject('Hello')
  175. .setTemplateVars(event => ({ order: new Order({ subTotal: 123 }) }));
  176. await initPluginWithHandlers([handler]);
  177. eventBus.publish(new MockEvent(ctx, true));
  178. await pause();
  179. expect(onSend.mock.calls[0][0].body).toContain('Total: 123');
  180. });
  181. it('interpolates globalTemplateVars', async () => {
  182. const handler = new EmailEventListener('test')
  183. .on(MockEvent)
  184. .setFrom('"test from" <noreply@test.com>')
  185. .setRecipient(() => 'test@test.com')
  186. .setSubject('Hello {{ globalVar }}');
  187. await initPluginWithHandlers([handler], {
  188. globalTemplateVars: { globalVar: 'baz' },
  189. });
  190. eventBus.publish(new MockEvent(ctx, true));
  191. await pause();
  192. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
  193. });
  194. it('interpolates from', async () => {
  195. const handler = new EmailEventListener('test')
  196. .on(MockEvent)
  197. .setFrom('"test from {{ globalVar }}" <noreply@test.com>')
  198. .setRecipient(() => 'test@test.com')
  199. .setSubject('Hello');
  200. await initPluginWithHandlers([handler], {
  201. globalTemplateVars: { globalVar: 'baz' },
  202. });
  203. eventBus.publish(new MockEvent(ctx, true));
  204. await pause();
  205. expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
  206. });
  207. // Test fix for https://github.com/vendure-ecommerce/vendure/issues/363
  208. it('does not escape HTML chars when interpolating "from"', async () => {
  209. const handler = new EmailEventListener('test')
  210. .on(MockEvent)
  211. .setFrom('{{ globalFrom }}')
  212. .setRecipient(() => 'Test <test@test.com>')
  213. .setSubject('Hello');
  214. await initPluginWithHandlers([handler], {
  215. globalTemplateVars: { globalFrom: 'Test <test@test.com>' },
  216. });
  217. eventBus.publish(new MockEvent(ctx, true));
  218. await pause();
  219. expect(onSend.mock.calls[0][0].from).toBe('Test <test@test.com>');
  220. });
  221. it('globalTemplateVars available in setTemplateVars method', async () => {
  222. const handler = new EmailEventListener('test')
  223. .on(MockEvent)
  224. .setFrom('"test from" <noreply@test.com>')
  225. .setRecipient(() => 'test@test.com')
  226. .setSubject('Hello {{ testVar }}')
  227. .setTemplateVars((event, globals) => ({ testVar: globals.globalVar + ' quux' }));
  228. await initPluginWithHandlers([handler], {
  229. globalTemplateVars: { globalVar: 'baz' },
  230. });
  231. eventBus.publish(new MockEvent(ctx, true));
  232. await pause();
  233. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
  234. });
  235. it('setTemplateVars overrides globals', async () => {
  236. const handler = new EmailEventListener('test')
  237. .on(MockEvent)
  238. .setFrom('"test from" <noreply@test.com>')
  239. .setRecipient(() => 'test@test.com')
  240. .setSubject('Hello {{ name }}')
  241. .setTemplateVars((event, globals) => ({ name: 'quux' }));
  242. await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
  243. eventBus.publish(new MockEvent(ctx, true));
  244. await pause();
  245. expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
  246. });
  247. });
  248. describe('handlebars helpers', () => {
  249. const ctx = RequestContext.deserialize({
  250. _channel: { code: DEFAULT_CHANNEL_CODE },
  251. _languageCode: LanguageCode.en,
  252. } as any);
  253. it('formateDate', async () => {
  254. const handler = new EmailEventListener('test-helpers')
  255. .on(MockEvent)
  256. .setFrom('"test from" <noreply@test.com>')
  257. .setRecipient(() => 'test@test.com')
  258. .setSubject('Hello')
  259. .setTemplateVars(event => ({ myDate: new Date('2020-01-01T10:00:00.000Z'), myPrice: 0 }));
  260. await initPluginWithHandlers([handler]);
  261. eventBus.publish(new MockEvent(ctx, true));
  262. await pause();
  263. expect(onSend.mock.calls[0][0].body).toContain('Date: Wed Jan 01 2020 10:00:00');
  264. });
  265. it('formatMoney', async () => {
  266. const handler = new EmailEventListener('test-helpers')
  267. .on(MockEvent)
  268. .setFrom('"test from" <noreply@test.com>')
  269. .setRecipient(() => 'test@test.com')
  270. .setSubject('Hello')
  271. .setTemplateVars(event => ({ myDate: new Date(), myPrice: 123 }));
  272. await initPluginWithHandlers([handler]);
  273. eventBus.publish(new MockEvent(ctx, true));
  274. await pause();
  275. expect(onSend.mock.calls[0][0].body).toContain('Price: 1.23');
  276. });
  277. });
  278. describe('multiple configs', () => {
  279. const ctx = RequestContext.deserialize({
  280. _channel: { code: DEFAULT_CHANNEL_CODE },
  281. _languageCode: LanguageCode.en,
  282. } as any);
  283. it('additional LanguageCode', async () => {
  284. const handler = new EmailEventListener('test')
  285. .on(MockEvent)
  286. .setFrom('"test from" <noreply@test.com>')
  287. .setSubject('Hello, {{ name }}!')
  288. .setRecipient(() => 'test@test.com')
  289. .setTemplateVars(() => ({ name: 'Test' }))
  290. .addTemplate({
  291. channelCode: 'default',
  292. languageCode: LanguageCode.de,
  293. templateFile: 'body.de.hbs',
  294. subject: 'Servus, {{ name }}!',
  295. });
  296. await initPluginWithHandlers([handler]);
  297. const ctxTa = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.ta } as any);
  298. eventBus.publish(new MockEvent(ctxTa, true));
  299. await pause();
  300. expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
  301. expect(onSend.mock.calls[0][0].body).toContain('Default body.');
  302. const ctxDe = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.de } as any);
  303. eventBus.publish(new MockEvent(ctxDe, true));
  304. await pause();
  305. expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
  306. expect(onSend.mock.calls[1][0].body).toContain('German body.');
  307. });
  308. });
  309. describe('loadData', () => {
  310. it('loads async data', async () => {
  311. const handler = new EmailEventListener('test')
  312. .on(MockEvent)
  313. .loadData(async ({ injector }) => {
  314. const service = injector.get(MockService);
  315. return service.someAsyncMethod();
  316. })
  317. .setFrom('"test from" <noreply@test.com>')
  318. .setSubject('Hello, {{ testData }}!')
  319. .setRecipient(() => 'test@test.com')
  320. .setTemplateVars(event => ({ testData: event.data }));
  321. await initPluginWithHandlers([handler]);
  322. eventBus.publish(
  323. new MockEvent(
  324. RequestContext.deserialize({
  325. _channel: { code: DEFAULT_CHANNEL_CODE },
  326. _languageCode: LanguageCode.en,
  327. } as any),
  328. true,
  329. ),
  330. );
  331. await pause();
  332. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  333. });
  334. it('works when loadData is called after other setup', async () => {
  335. const handler = new EmailEventListener('test')
  336. .on(MockEvent)
  337. .setFrom('"test from" <noreply@test.com>')
  338. .setSubject('Hello, {{ testData }}!')
  339. .setRecipient(() => 'test@test.com')
  340. .loadData(async ({ injector }) => {
  341. const service = injector.get(MockService);
  342. return service.someAsyncMethod();
  343. })
  344. .setTemplateVars(event => ({ testData: event.data }));
  345. await initPluginWithHandlers([handler]);
  346. eventBus.publish(
  347. new MockEvent(
  348. RequestContext.deserialize({
  349. _channel: { code: DEFAULT_CHANNEL_CODE },
  350. _languageCode: LanguageCode.en,
  351. } as any),
  352. true,
  353. ),
  354. );
  355. await pause();
  356. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  357. expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  358. expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
  359. });
  360. it('only executes for filtered events', async () => {
  361. let callCount = 0;
  362. const handler = new EmailEventListener('test')
  363. .on(MockEvent)
  364. .filter(event => event.shouldSend === true)
  365. .loadData(async ({ injector }) => {
  366. callCount++;
  367. });
  368. await initPluginWithHandlers([handler]);
  369. eventBus.publish(new MockEvent(RequestContext.empty(), false));
  370. eventBus.publish(new MockEvent(RequestContext.empty(), true));
  371. await pause();
  372. expect(callCount).toBe(1);
  373. });
  374. });
  375. describe('attachments', () => {
  376. const ctx = RequestContext.deserialize({
  377. _channel: { code: DEFAULT_CHANNEL_CODE },
  378. _languageCode: LanguageCode.en,
  379. } as any);
  380. const TEST_IMAGE_PATH = path.join(__dirname, '../test-fixtures/test.jpg');
  381. it('attachments are empty by default', async () => {
  382. const handler = new EmailEventListener('test')
  383. .on(MockEvent)
  384. .setFrom('"test from" <noreply@test.com>')
  385. .setRecipient(() => 'test@test.com')
  386. .setSubject('Hello {{ subjectVar }}');
  387. await initPluginWithHandlers([handler]);
  388. eventBus.publish(new MockEvent(ctx, true));
  389. await pause();
  390. expect(onSend.mock.calls[0][0].attachments).toEqual([]);
  391. });
  392. it('sync attachment', async () => {
  393. const handler = new EmailEventListener('test')
  394. .on(MockEvent)
  395. .setFrom('"test from" <noreply@test.com>')
  396. .setRecipient(() => 'test@test.com')
  397. .setSubject('Hello {{ subjectVar }}')
  398. .setAttachments(() => [
  399. {
  400. path: TEST_IMAGE_PATH,
  401. },
  402. ]);
  403. await initPluginWithHandlers([handler]);
  404. eventBus.publish(new MockEvent(ctx, true));
  405. await pause();
  406. expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
  407. });
  408. it('async attachment', async () => {
  409. const handler = new EmailEventListener('test')
  410. .on(MockEvent)
  411. .setFrom('"test from" <noreply@test.com>')
  412. .setRecipient(() => 'test@test.com')
  413. .setSubject('Hello {{ subjectVar }}')
  414. .setAttachments(async () => [
  415. {
  416. path: TEST_IMAGE_PATH,
  417. },
  418. ]);
  419. await initPluginWithHandlers([handler]);
  420. eventBus.publish(new MockEvent(ctx, true));
  421. await pause();
  422. expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
  423. });
  424. });
  425. describe('orderConfirmationHandler', () => {
  426. beforeEach(async () => {
  427. await initPluginWithHandlers([orderConfirmationHandler], {
  428. templatePath: path.join(__dirname, '../templates'),
  429. });
  430. });
  431. const ctx = RequestContext.deserialize({
  432. _channel: { code: DEFAULT_CHANNEL_CODE },
  433. _languageCode: LanguageCode.en,
  434. } as any);
  435. const order = ({
  436. code: 'ABCDE',
  437. customer: {
  438. emailAddress: 'test@test.com',
  439. },
  440. } as Partial<Order>) as any;
  441. it('filters events with wrong order state', async () => {
  442. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order));
  443. await pause();
  444. expect(onSend).not.toHaveBeenCalled();
  445. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
  446. await pause();
  447. expect(onSend).not.toHaveBeenCalled();
  448. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order));
  449. await pause();
  450. expect(onSend).not.toHaveBeenCalled();
  451. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  452. await pause();
  453. expect(onSend).toHaveBeenCalledTimes(1);
  454. });
  455. it('sets the Order Customer emailAddress as recipient', async () => {
  456. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  457. await pause();
  458. expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
  459. });
  460. it('sets the subject', async () => {
  461. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  462. await pause();
  463. expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code}`);
  464. });
  465. });
  466. describe('error handling', () => {
  467. it('Logs an error if the template file is not found', async () => {
  468. const ctx = RequestContext.deserialize({
  469. _channel: { code: DEFAULT_CHANNEL_CODE },
  470. _languageCode: LanguageCode.en,
  471. } as any);
  472. testingLogger.errorSpy.mockClear();
  473. const handler = new EmailEventListener('no-template')
  474. .on(MockEvent)
  475. .setFrom('"test from" <noreply@test.com>')
  476. .setRecipient(() => 'test@test.com')
  477. .setSubject('Hello {{ subjectVar }}');
  478. await initPluginWithHandlers([handler]);
  479. eventBus.publish(new MockEvent(ctx, true));
  480. await pause();
  481. expect(testingLogger.errorSpy.mock.calls[0][0]).toContain(`ENOENT: no such file or directory`);
  482. });
  483. it('Logs a Handlebars error if the template is invalid', async () => {
  484. const ctx = RequestContext.deserialize({
  485. _channel: { code: DEFAULT_CHANNEL_CODE },
  486. _languageCode: LanguageCode.en,
  487. } as any);
  488. testingLogger.errorSpy.mockClear();
  489. const handler = new EmailEventListener('bad-template')
  490. .on(MockEvent)
  491. .setFrom('"test from" <noreply@test.com>')
  492. .setRecipient(() => 'test@test.com')
  493. .setSubject('Hello {{ subjectVar }}');
  494. await initPluginWithHandlers([handler]);
  495. eventBus.publish(new MockEvent(ctx, true));
  496. await pause();
  497. expect(testingLogger.errorSpy.mock.calls[0][0]).toContain(`Parse error on line 3:`);
  498. });
  499. it('Logs an error if the loadData method throws', async () => {
  500. const ctx = RequestContext.deserialize({
  501. _channel: { code: DEFAULT_CHANNEL_CODE },
  502. _languageCode: LanguageCode.en,
  503. } as any);
  504. testingLogger.errorSpy.mockClear();
  505. const handler = new EmailEventListener('bad-template')
  506. .on(MockEvent)
  507. .setFrom('"test from" <noreply@test.com>')
  508. .setRecipient(() => 'test@test.com')
  509. .setSubject('Hello {{ subjectVar }}')
  510. .loadData(context => {
  511. throw new Error('something went horribly wrong!');
  512. });
  513. await initPluginWithHandlers([handler]);
  514. eventBus.publish(new MockEvent(ctx, true));
  515. await pause();
  516. expect(testingLogger.errorSpy.mock.calls[0][0]).toContain(`something went horribly wrong!`);
  517. });
  518. });
  519. describe('custom sender', () => {
  520. it('should allow a custom sender to be utilized', async () => {
  521. const ctx = RequestContext.deserialize({
  522. _channel: { code: DEFAULT_CHANNEL_CODE },
  523. _languageCode: LanguageCode.en,
  524. } as any);
  525. const handler = new EmailEventListener('test')
  526. .on(MockEvent)
  527. .setFrom('"test from" <noreply@test.com>')
  528. .setRecipient(() => 'test@test.com')
  529. .setSubject('Hello')
  530. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  531. const fakeSender = new FakeCustomSender();
  532. const send = jest.fn();
  533. fakeSender.send = send;
  534. await initPluginWithHandlers([handler], {
  535. emailSender: fakeSender,
  536. });
  537. eventBus.publish(new MockEvent(ctx, true));
  538. await pause();
  539. expect(send.mock.calls[0][0].subject).toBe('Hello');
  540. expect(send.mock.calls[0][0].recipient).toBe('test@test.com');
  541. expect(send.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  542. expect(onSend).toHaveBeenCalledTimes(0);
  543. });
  544. });
  545. });
  546. class FakeCustomSender implements EmailSender {
  547. send: (email: EmailDetails<'unserialized'>, options: EmailTransportOptions) => void;
  548. }
  549. const pause = () => new Promise(resolve => setTimeout(resolve, 100));
  550. class MockEvent extends VendureEvent {
  551. constructor(public ctx: RequestContext, public shouldSend: boolean) {
  552. super();
  553. }
  554. }
  555. class MockService {
  556. someAsyncMethod() {
  557. return Promise.resolve('loaded data');
  558. }
  559. }