plugin.spec.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  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 { createReadStream, readFileSync } from 'fs';
  17. import { readFile } from 'fs-extra';
  18. import path from 'path';
  19. import { Readable } from 'stream';
  20. import { orderConfirmationHandler } from './default-email-handlers';
  21. import { EmailEventHandler } from './event-handler';
  22. import { EmailEventListener } from './event-listener';
  23. import { EmailPlugin } from './plugin';
  24. import { EmailDetails, EmailPluginOptions, EmailSender, EmailTransportOptions } from './types';
  25. describe('EmailPlugin', () => {
  26. let eventBus: EventBus;
  27. let onSend: jest.Mock;
  28. let module: TestingModule;
  29. const testingLogger = new TestingLogger((...args) => jest.fn(...args));
  30. async function initPluginWithHandlers(
  31. handlers: Array<EmailEventHandler<string, any>>,
  32. options?: Partial<EmailPluginOptions>,
  33. ) {
  34. onSend = jest.fn();
  35. module = await Test.createTestingModule({
  36. imports: [
  37. TypeOrmModule.forRoot({
  38. type: 'sqljs',
  39. retryAttempts: 0,
  40. }),
  41. PluginCommonModule,
  42. EmailPlugin.init({
  43. templatePath: path.join(__dirname, '../test-templates'),
  44. transport: {
  45. type: 'testing',
  46. onSend,
  47. },
  48. handlers,
  49. ...options,
  50. }),
  51. ],
  52. providers: [MockService],
  53. }).compile();
  54. Logger.useLogger(testingLogger);
  55. module.useLogger(new Logger());
  56. const plugin = module.get(EmailPlugin);
  57. eventBus = module.get(EventBus);
  58. await plugin.onApplicationBootstrap();
  59. return module;
  60. }
  61. afterEach(async () => {
  62. if (module) {
  63. await module.close();
  64. }
  65. });
  66. it('setting from, recipient, subject', async () => {
  67. const ctx = RequestContext.deserialize({
  68. _channel: { code: DEFAULT_CHANNEL_CODE },
  69. _languageCode: LanguageCode.en,
  70. } as any);
  71. const handler = new EmailEventListener('test')
  72. .on(MockEvent)
  73. .setFrom('"test from" <noreply@test.com>')
  74. .setRecipient(() => 'test@test.com')
  75. .setSubject('Hello')
  76. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  77. await initPluginWithHandlers([handler]);
  78. eventBus.publish(new MockEvent(ctx, true));
  79. await pause();
  80. expect(onSend.mock.calls[0][0].subject).toBe('Hello');
  81. expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
  82. expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  83. });
  84. describe('event filtering', () => {
  85. const ctx = RequestContext.deserialize({
  86. _channel: { code: DEFAULT_CHANNEL_CODE },
  87. _languageCode: LanguageCode.en,
  88. } as any);
  89. it('single filter', async () => {
  90. const handler = new EmailEventListener('test')
  91. .on(MockEvent)
  92. .filter(event => event.shouldSend === true)
  93. .setRecipient(() => 'test@test.com')
  94. .setFrom('"test from" <noreply@test.com>')
  95. .setSubject('test subject');
  96. await initPluginWithHandlers([handler]);
  97. eventBus.publish(new MockEvent(ctx, false));
  98. await pause();
  99. expect(onSend).not.toHaveBeenCalled();
  100. eventBus.publish(new MockEvent(ctx, true));
  101. await pause();
  102. expect(onSend).toHaveBeenCalledTimes(1);
  103. });
  104. it('multiple filters', async () => {
  105. const handler = new EmailEventListener('test')
  106. .on(MockEvent)
  107. .filter(event => event.shouldSend === true)
  108. .filter(event => !!event.ctx.activeUserId)
  109. .setFrom('"test from" <noreply@test.com>')
  110. .setRecipient(() => 'test@test.com')
  111. .setSubject('test subject');
  112. await initPluginWithHandlers([handler]);
  113. eventBus.publish(new MockEvent(ctx, true));
  114. await pause();
  115. expect(onSend).not.toHaveBeenCalled();
  116. const ctxWithUser = RequestContext.deserialize({ ...ctx, _session: { user: { id: 42 } } } as any);
  117. eventBus.publish(new MockEvent(ctxWithUser, true));
  118. await pause();
  119. expect(onSend).toHaveBeenCalledTimes(1);
  120. });
  121. it('with .loadData() after .filter()', async () => {
  122. const handler = new EmailEventListener('test')
  123. .on(MockEvent)
  124. .filter(event => event.shouldSend === true)
  125. .loadData(context => Promise.resolve('loaded data'))
  126. .setRecipient(() => 'test@test.com')
  127. .setFrom('"test from" <noreply@test.com>')
  128. .setSubject('test subject');
  129. await initPluginWithHandlers([handler]);
  130. eventBus.publish(new MockEvent(ctx, false));
  131. await pause();
  132. expect(onSend).not.toHaveBeenCalled();
  133. eventBus.publish(new MockEvent(ctx, true));
  134. await pause();
  135. expect(onSend).toHaveBeenCalledTimes(1);
  136. });
  137. });
  138. describe('templateVars', () => {
  139. const ctx = RequestContext.deserialize({
  140. _channel: { code: DEFAULT_CHANNEL_CODE },
  141. _languageCode: LanguageCode.en,
  142. } as any);
  143. it('interpolates subject', async () => {
  144. const handler = new EmailEventListener('test')
  145. .on(MockEvent)
  146. .setFrom('"test from" <noreply@test.com>')
  147. .setRecipient(() => 'test@test.com')
  148. .setSubject('Hello {{ subjectVar }}')
  149. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  150. await initPluginWithHandlers([handler]);
  151. eventBus.publish(new MockEvent(ctx, true));
  152. await pause();
  153. expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
  154. });
  155. it('interpolates body', async () => {
  156. const handler = new EmailEventListener('test')
  157. .on(MockEvent)
  158. .setFrom('"test from" <noreply@test.com>')
  159. .setRecipient(() => 'test@test.com')
  160. .setSubject('Hello')
  161. .setTemplateVars(event => ({ testVar: 'this is the test var' }));
  162. await initPluginWithHandlers([handler]);
  163. eventBus.publish(new MockEvent(ctx, true));
  164. await pause();
  165. expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
  166. });
  167. /**
  168. * Intended to test the ability for Handlebars to interpolate
  169. * getters on the Order entity prototype.
  170. * See https://github.com/vendure-ecommerce/vendure/issues/259
  171. */
  172. it('interpolates body with property from entity', async () => {
  173. const handler = new EmailEventListener('test')
  174. .on(MockEvent)
  175. .setFrom('"test from" <noreply@test.com>')
  176. .setRecipient(() => 'test@test.com')
  177. .setSubject('Hello')
  178. .setTemplateVars(event => ({ order: new Order({ subTotal: 123 }) }));
  179. await initPluginWithHandlers([handler]);
  180. eventBus.publish(new MockEvent(ctx, true));
  181. await pause();
  182. expect(onSend.mock.calls[0][0].body).toContain('Total: 123');
  183. });
  184. it('interpolates globalTemplateVars', async () => {
  185. const handler = new EmailEventListener('test')
  186. .on(MockEvent)
  187. .setFrom('"test from" <noreply@test.com>')
  188. .setRecipient(() => 'test@test.com')
  189. .setSubject('Hello {{ globalVar }}');
  190. await initPluginWithHandlers([handler], {
  191. globalTemplateVars: { globalVar: 'baz' },
  192. });
  193. eventBus.publish(new MockEvent(ctx, true));
  194. await pause();
  195. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
  196. });
  197. it('interpolates from', async () => {
  198. const handler = new EmailEventListener('test')
  199. .on(MockEvent)
  200. .setFrom('"test from {{ globalVar }}" <noreply@test.com>')
  201. .setRecipient(() => 'test@test.com')
  202. .setSubject('Hello');
  203. await initPluginWithHandlers([handler], {
  204. globalTemplateVars: { globalVar: 'baz' },
  205. });
  206. eventBus.publish(new MockEvent(ctx, true));
  207. await pause();
  208. expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
  209. });
  210. // Test fix for https://github.com/vendure-ecommerce/vendure/issues/363
  211. it('does not escape HTML chars when interpolating "from"', async () => {
  212. const handler = new EmailEventListener('test')
  213. .on(MockEvent)
  214. .setFrom('{{ globalFrom }}')
  215. .setRecipient(() => 'Test <test@test.com>')
  216. .setSubject('Hello');
  217. await initPluginWithHandlers([handler], {
  218. globalTemplateVars: { globalFrom: 'Test <test@test.com>' },
  219. });
  220. eventBus.publish(new MockEvent(ctx, true));
  221. await pause();
  222. expect(onSend.mock.calls[0][0].from).toBe('Test <test@test.com>');
  223. });
  224. it('globalTemplateVars available in setTemplateVars method', async () => {
  225. const handler = new EmailEventListener('test')
  226. .on(MockEvent)
  227. .setFrom('"test from" <noreply@test.com>')
  228. .setRecipient(() => 'test@test.com')
  229. .setSubject('Hello {{ testVar }}')
  230. .setTemplateVars((event, globals) => ({ testVar: globals.globalVar + ' quux' }));
  231. await initPluginWithHandlers([handler], {
  232. globalTemplateVars: { globalVar: 'baz' },
  233. });
  234. eventBus.publish(new MockEvent(ctx, true));
  235. await pause();
  236. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
  237. });
  238. it('setTemplateVars overrides globals', async () => {
  239. const handler = new EmailEventListener('test')
  240. .on(MockEvent)
  241. .setFrom('"test from" <noreply@test.com>')
  242. .setRecipient(() => 'test@test.com')
  243. .setSubject('Hello {{ name }}')
  244. .setTemplateVars((event, globals) => ({ name: 'quux' }));
  245. await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
  246. eventBus.publish(new MockEvent(ctx, true));
  247. await pause();
  248. expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
  249. });
  250. });
  251. describe('handlebars helpers', () => {
  252. const ctx = RequestContext.deserialize({
  253. _channel: { code: DEFAULT_CHANNEL_CODE },
  254. _languageCode: LanguageCode.en,
  255. } as any);
  256. it('formateDate', async () => {
  257. const handler = new EmailEventListener('test-helpers')
  258. .on(MockEvent)
  259. .setFrom('"test from" <noreply@test.com>')
  260. .setRecipient(() => 'test@test.com')
  261. .setSubject('Hello')
  262. .setTemplateVars(event => ({ myDate: new Date('2020-01-01T10:00:00.000Z'), myPrice: 0 }));
  263. await initPluginWithHandlers([handler]);
  264. eventBus.publish(new MockEvent(ctx, true));
  265. await pause();
  266. expect(onSend.mock.calls[0][0].body).toContain('Date: Wed Jan 01 2020 10:00:00');
  267. });
  268. it('formatMoney', async () => {
  269. const handler = new EmailEventListener('test-helpers')
  270. .on(MockEvent)
  271. .setFrom('"test from" <noreply@test.com>')
  272. .setRecipient(() => 'test@test.com')
  273. .setSubject('Hello')
  274. .setTemplateVars(event => ({ myDate: new Date(), myPrice: 123 }));
  275. await initPluginWithHandlers([handler]);
  276. eventBus.publish(new MockEvent(ctx, true));
  277. await pause();
  278. expect(onSend.mock.calls[0][0].body).toContain('Price: 1.23');
  279. });
  280. });
  281. describe('multiple configs', () => {
  282. const ctx = RequestContext.deserialize({
  283. _channel: { code: DEFAULT_CHANNEL_CODE },
  284. _languageCode: LanguageCode.en,
  285. } as any);
  286. it('additional LanguageCode', async () => {
  287. const handler = new EmailEventListener('test')
  288. .on(MockEvent)
  289. .setFrom('"test from" <noreply@test.com>')
  290. .setSubject('Hello, {{ name }}!')
  291. .setRecipient(() => 'test@test.com')
  292. .setTemplateVars(() => ({ name: 'Test' }))
  293. .addTemplate({
  294. channelCode: 'default',
  295. languageCode: LanguageCode.de,
  296. templateFile: 'body.de.hbs',
  297. subject: 'Servus, {{ name }}!',
  298. });
  299. await initPluginWithHandlers([handler]);
  300. const ctxTa = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.ta } as any);
  301. eventBus.publish(new MockEvent(ctxTa, true));
  302. await pause();
  303. expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
  304. expect(onSend.mock.calls[0][0].body).toContain('Default body.');
  305. const ctxDe = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.de } as any);
  306. eventBus.publish(new MockEvent(ctxDe, true));
  307. await pause();
  308. expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
  309. expect(onSend.mock.calls[1][0].body).toContain('German body.');
  310. });
  311. it('set LanguageCode', async () => {
  312. const handler = new EmailEventListener('test')
  313. .on(MockEvent)
  314. .setFrom('"test from" <noreply@test.com>')
  315. .setSubject('Hello, {{ name }}!')
  316. .setRecipient(() => 'test@test.com')
  317. .setLanguageCode(() => LanguageCode.de)
  318. .setTemplateVars(() => ({ name: 'Test' }))
  319. .addTemplate({
  320. channelCode: 'default',
  321. languageCode: LanguageCode.de,
  322. templateFile: 'body.de.hbs',
  323. subject: 'Servus, {{ name }}!',
  324. });
  325. const ctxEn = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.en } as any);
  326. eventBus.publish(new MockEvent(ctxEn, true));
  327. await pause();
  328. expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
  329. expect(onSend.mock.calls[1][0].body).toContain('German body.');
  330. });
  331. });
  332. describe('loadData', () => {
  333. it('loads async data', async () => {
  334. const handler = new EmailEventListener('test')
  335. .on(MockEvent)
  336. .loadData(async ({ injector }) => {
  337. const service = injector.get(MockService);
  338. return service.someAsyncMethod();
  339. })
  340. .setFrom('"test from" <noreply@test.com>')
  341. .setSubject('Hello, {{ testData }}!')
  342. .setRecipient(() => 'test@test.com')
  343. .setTemplateVars(event => ({ testData: event.data }));
  344. await initPluginWithHandlers([handler]);
  345. eventBus.publish(
  346. new MockEvent(
  347. RequestContext.deserialize({
  348. _channel: { code: DEFAULT_CHANNEL_CODE },
  349. _languageCode: LanguageCode.en,
  350. } as any),
  351. true,
  352. ),
  353. );
  354. await pause();
  355. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  356. });
  357. it('works when loadData is called after other setup', async () => {
  358. const handler = new EmailEventListener('test')
  359. .on(MockEvent)
  360. .setFrom('"test from" <noreply@test.com>')
  361. .setSubject('Hello, {{ testData }}!')
  362. .setRecipient(() => 'test@test.com')
  363. .loadData(async ({ injector }) => {
  364. const service = injector.get(MockService);
  365. return service.someAsyncMethod();
  366. })
  367. .setTemplateVars(event => ({ testData: event.data }));
  368. await initPluginWithHandlers([handler]);
  369. eventBus.publish(
  370. new MockEvent(
  371. RequestContext.deserialize({
  372. _channel: { code: DEFAULT_CHANNEL_CODE },
  373. _languageCode: LanguageCode.en,
  374. } as any),
  375. true,
  376. ),
  377. );
  378. await pause();
  379. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  380. expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  381. expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
  382. });
  383. it('only executes for filtered events', async () => {
  384. let callCount = 0;
  385. const handler = new EmailEventListener('test')
  386. .on(MockEvent)
  387. .filter(event => event.shouldSend === true)
  388. .loadData(async ({ injector }) => {
  389. callCount++;
  390. });
  391. await initPluginWithHandlers([handler]);
  392. eventBus.publish(new MockEvent(RequestContext.empty(), false));
  393. eventBus.publish(new MockEvent(RequestContext.empty(), true));
  394. await pause();
  395. expect(callCount).toBe(1);
  396. });
  397. });
  398. describe('attachments', () => {
  399. const ctx = RequestContext.deserialize({
  400. _channel: { code: DEFAULT_CHANNEL_CODE },
  401. _languageCode: LanguageCode.en,
  402. } as any);
  403. const TEST_IMAGE_PATH = path.join(__dirname, '../test-fixtures/test.jpg');
  404. it('attachments are empty by default', async () => {
  405. const handler = new EmailEventListener('test')
  406. .on(MockEvent)
  407. .setFrom('"test from" <noreply@test.com>')
  408. .setRecipient(() => 'test@test.com')
  409. .setSubject('Hello {{ subjectVar }}');
  410. await initPluginWithHandlers([handler]);
  411. eventBus.publish(new MockEvent(ctx, true));
  412. await pause();
  413. expect(onSend.mock.calls[0][0].attachments).toEqual([]);
  414. });
  415. it('sync attachment', async () => {
  416. const handler = new EmailEventListener('test')
  417. .on(MockEvent)
  418. .setFrom('"test from" <noreply@test.com>')
  419. .setRecipient(() => 'test@test.com')
  420. .setSubject('Hello {{ subjectVar }}')
  421. .setAttachments(() => [
  422. {
  423. path: TEST_IMAGE_PATH,
  424. },
  425. ]);
  426. await initPluginWithHandlers([handler]);
  427. eventBus.publish(new MockEvent(ctx, true));
  428. await pause();
  429. expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
  430. });
  431. it('async attachment', async () => {
  432. const handler = new EmailEventListener('test')
  433. .on(MockEvent)
  434. .setFrom('"test from" <noreply@test.com>')
  435. .setRecipient(() => 'test@test.com')
  436. .setSubject('Hello {{ subjectVar }}')
  437. .setAttachments(async () => [
  438. {
  439. path: TEST_IMAGE_PATH,
  440. },
  441. ]);
  442. await initPluginWithHandlers([handler]);
  443. eventBus.publish(new MockEvent(ctx, true));
  444. await pause();
  445. expect(onSend.mock.calls[0][0].attachments).toEqual([{ path: TEST_IMAGE_PATH }]);
  446. });
  447. it('attachment content as a string buffer', async () => {
  448. const handler = new EmailEventListener('test')
  449. .on(MockEvent)
  450. .setFrom('"test from" <noreply@test.com>')
  451. .setRecipient(() => 'test@test.com')
  452. .setSubject('Hello {{ subjectVar }}')
  453. .setAttachments(() => [
  454. {
  455. content: Buffer.from('hello'),
  456. },
  457. ]);
  458. await initPluginWithHandlers([handler]);
  459. eventBus.publish(new MockEvent(ctx, true));
  460. await pause();
  461. const attachment = onSend.mock.calls[0][0].attachments[0].content;
  462. expect(Buffer.compare(attachment, Buffer.from('hello'))).toBe(0); // 0 = buffers are equal
  463. });
  464. it('attachment content as an image buffer', async () => {
  465. const imageFileBuffer = readFileSync(TEST_IMAGE_PATH);
  466. const handler = new EmailEventListener('test')
  467. .on(MockEvent)
  468. .setFrom('"test from" <noreply@test.com>')
  469. .setRecipient(() => 'test@test.com')
  470. .setSubject('Hello {{ subjectVar }}')
  471. .setAttachments(() => [
  472. {
  473. content: imageFileBuffer,
  474. },
  475. ]);
  476. await initPluginWithHandlers([handler]);
  477. eventBus.publish(new MockEvent(ctx, true));
  478. await pause();
  479. const attachment = onSend.mock.calls[0][0].attachments[0].content;
  480. expect(Buffer.compare(attachment, imageFileBuffer)).toBe(0); // 0 = buffers are equal
  481. });
  482. it('attachment content as a string', async () => {
  483. const handler = new EmailEventListener('test')
  484. .on(MockEvent)
  485. .setFrom('"test from" <noreply@test.com>')
  486. .setRecipient(() => 'test@test.com')
  487. .setSubject('Hello {{ subjectVar }}')
  488. .setAttachments(() => [
  489. {
  490. content: 'hello',
  491. },
  492. ]);
  493. await initPluginWithHandlers([handler]);
  494. eventBus.publish(new MockEvent(ctx, true));
  495. await pause();
  496. const attachment = onSend.mock.calls[0][0].attachments[0].content;
  497. expect(attachment).toBe('hello');
  498. });
  499. it('attachment content as a string stream', async () => {
  500. const handler = new EmailEventListener('test')
  501. .on(MockEvent)
  502. .setFrom('"test from" <noreply@test.com>')
  503. .setRecipient(() => 'test@test.com')
  504. .setSubject('Hello {{ subjectVar }}')
  505. .setAttachments(() => [
  506. {
  507. content: Readable.from(['hello']),
  508. },
  509. ]);
  510. await initPluginWithHandlers([handler]);
  511. eventBus.publish(new MockEvent(ctx, true));
  512. await pause();
  513. const attachment = onSend.mock.calls[0][0].attachments[0].content;
  514. expect(Buffer.compare(attachment, Buffer.from('hello'))).toBe(0); // 0 = buffers are equal
  515. });
  516. it('attachment content as an image stream', async () => {
  517. const imageFileBuffer = readFileSync(TEST_IMAGE_PATH);
  518. const imageFileStream = createReadStream(TEST_IMAGE_PATH);
  519. const handler = new EmailEventListener('test')
  520. .on(MockEvent)
  521. .setFrom('"test from" <noreply@test.com>')
  522. .setRecipient(() => 'test@test.com')
  523. .setSubject('Hello {{ subjectVar }}')
  524. .setAttachments(() => [
  525. {
  526. content: imageFileStream,
  527. },
  528. ]);
  529. await initPluginWithHandlers([handler]);
  530. eventBus.publish(new MockEvent(ctx, true));
  531. await pause();
  532. const attachment = onSend.mock.calls[0][0].attachments[0].content;
  533. expect(Buffer.compare(attachment, imageFileBuffer)).toBe(0); // 0 = buffers are equal
  534. });
  535. it('raises a warning for large content attachments', async () => {
  536. testingLogger.warnSpy.mockClear();
  537. const largeBuffer = Buffer.from(Array.from({ length: 65535, 0: 0 }));
  538. const handler = new EmailEventListener('test')
  539. .on(MockEvent)
  540. .setFrom('"test from" <noreply@test.com>')
  541. .setRecipient(() => 'test@test.com')
  542. .setSubject('Hello {{ subjectVar }}')
  543. .setAttachments(() => [
  544. {
  545. content: largeBuffer,
  546. },
  547. ]);
  548. await initPluginWithHandlers([handler]);
  549. eventBus.publish(new MockEvent(ctx, true));
  550. await pause();
  551. expect(testingLogger.warnSpy.mock.calls[0][0]).toContain(
  552. `Email has a large 'content' attachment (64k). Consider using the 'path' instead for improved performance.`,
  553. );
  554. });
  555. });
  556. describe('orderConfirmationHandler', () => {
  557. beforeEach(async () => {
  558. await initPluginWithHandlers([orderConfirmationHandler], {
  559. templatePath: path.join(__dirname, '../templates'),
  560. });
  561. });
  562. const ctx = RequestContext.deserialize({
  563. _channel: { code: DEFAULT_CHANNEL_CODE },
  564. _languageCode: LanguageCode.en,
  565. } as any);
  566. const order = {
  567. code: 'ABCDE',
  568. customer: {
  569. emailAddress: 'test@test.com',
  570. },
  571. lines: [],
  572. } as any;
  573. it('filters events with wrong order state', async () => {
  574. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order));
  575. await pause();
  576. expect(onSend).not.toHaveBeenCalled();
  577. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
  578. await pause();
  579. expect(onSend).not.toHaveBeenCalled();
  580. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order));
  581. await pause();
  582. expect(onSend).not.toHaveBeenCalled();
  583. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  584. await pause();
  585. expect(onSend).toHaveBeenCalledTimes(1);
  586. });
  587. it('sets the Order Customer emailAddress as recipient', async () => {
  588. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  589. await pause();
  590. expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
  591. });
  592. it('sets the subject', async () => {
  593. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  594. await pause();
  595. expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code}`);
  596. });
  597. });
  598. describe('error handling', () => {
  599. it('Logs an error if the template file is not found', async () => {
  600. const ctx = RequestContext.deserialize({
  601. _channel: { code: DEFAULT_CHANNEL_CODE },
  602. _languageCode: LanguageCode.en,
  603. } as any);
  604. testingLogger.errorSpy.mockClear();
  605. const handler = new EmailEventListener('no-template')
  606. .on(MockEvent)
  607. .setFrom('"test from" <noreply@test.com>')
  608. .setRecipient(() => 'test@test.com')
  609. .setSubject('Hello {{ subjectVar }}');
  610. await initPluginWithHandlers([handler]);
  611. eventBus.publish(new MockEvent(ctx, true));
  612. await pause();
  613. expect(testingLogger.errorSpy.mock.calls[0][0]).toContain(`ENOENT: no such file or directory`);
  614. });
  615. it('Logs a Handlebars error if the template is invalid', async () => {
  616. const ctx = RequestContext.deserialize({
  617. _channel: { code: DEFAULT_CHANNEL_CODE },
  618. _languageCode: LanguageCode.en,
  619. } as any);
  620. testingLogger.errorSpy.mockClear();
  621. const handler = new EmailEventListener('bad-template')
  622. .on(MockEvent)
  623. .setFrom('"test from" <noreply@test.com>')
  624. .setRecipient(() => 'test@test.com')
  625. .setSubject('Hello {{ subjectVar }}');
  626. await initPluginWithHandlers([handler]);
  627. eventBus.publish(new MockEvent(ctx, true));
  628. await pause();
  629. expect(testingLogger.errorSpy.mock.calls[0][0]).toContain(`Parse error on line 3:`);
  630. });
  631. it('Logs an error if the loadData method throws', async () => {
  632. const ctx = RequestContext.deserialize({
  633. _channel: { code: DEFAULT_CHANNEL_CODE },
  634. _languageCode: LanguageCode.en,
  635. } as any);
  636. testingLogger.errorSpy.mockClear();
  637. const handler = new EmailEventListener('bad-template')
  638. .on(MockEvent)
  639. .setFrom('"test from" <noreply@test.com>')
  640. .setRecipient(() => 'test@test.com')
  641. .setSubject('Hello {{ subjectVar }}')
  642. .loadData(context => {
  643. throw new Error('something went horribly wrong!');
  644. });
  645. await initPluginWithHandlers([handler]);
  646. eventBus.publish(new MockEvent(ctx, true));
  647. await pause();
  648. expect(testingLogger.errorSpy.mock.calls[0][0]).toContain(`something went horribly wrong!`);
  649. });
  650. });
  651. describe('custom sender', () => {
  652. it('should allow a custom sender to be utilized', async () => {
  653. const ctx = RequestContext.deserialize({
  654. _channel: { code: DEFAULT_CHANNEL_CODE },
  655. _languageCode: LanguageCode.en,
  656. } as any);
  657. const handler = new EmailEventListener('test')
  658. .on(MockEvent)
  659. .setFrom('"test from" <noreply@test.com>')
  660. .setRecipient(() => 'test@test.com')
  661. .setSubject('Hello')
  662. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  663. const fakeSender = new FakeCustomSender();
  664. const send = jest.fn();
  665. fakeSender.send = send;
  666. await initPluginWithHandlers([handler], {
  667. emailSender: fakeSender,
  668. });
  669. eventBus.publish(new MockEvent(ctx, true));
  670. await pause();
  671. expect(send.mock.calls[0][0].subject).toBe('Hello');
  672. expect(send.mock.calls[0][0].recipient).toBe('test@test.com');
  673. expect(send.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  674. expect(onSend).toHaveBeenCalledTimes(0);
  675. });
  676. });
  677. describe('optional address fields', () => {
  678. const ctx = RequestContext.deserialize({
  679. _channel: { code: DEFAULT_CHANNEL_CODE },
  680. _languageCode: LanguageCode.en,
  681. } as any);
  682. it('cc', async () => {
  683. const handler = new EmailEventListener('test')
  684. .on(MockEvent)
  685. .setFrom('"test from" <noreply@test.com>')
  686. .setRecipient(() => 'test@test.com')
  687. .setSubject('Hello {{ subjectVar }}')
  688. .setOptionalAddressFields(() => ({ cc: 'foo@bar.com' }));
  689. await initPluginWithHandlers([handler]);
  690. eventBus.publish(new MockEvent(ctx, true));
  691. await pause();
  692. expect(onSend.mock.calls[0][0].cc).toBe('foo@bar.com');
  693. });
  694. it('bcc', async () => {
  695. const handler = new EmailEventListener('test')
  696. .on(MockEvent)
  697. .setFrom('"test from" <noreply@test.com>')
  698. .setRecipient(() => 'test@test.com')
  699. .setSubject('Hello {{ subjectVar }}')
  700. .setOptionalAddressFields(() => ({ bcc: 'foo@bar.com' }));
  701. await initPluginWithHandlers([handler]);
  702. eventBus.publish(new MockEvent(ctx, true));
  703. await pause();
  704. expect(onSend.mock.calls[0][0].bcc).toBe('foo@bar.com');
  705. });
  706. it('replyTo', async () => {
  707. const handler = new EmailEventListener('test')
  708. .on(MockEvent)
  709. .setFrom('"test from" <noreply@test.com>')
  710. .setRecipient(() => 'test@test.com')
  711. .setSubject('Hello {{ subjectVar }}')
  712. .setOptionalAddressFields(() => ({ replyTo: 'foo@bar.com' }));
  713. await initPluginWithHandlers([handler]);
  714. eventBus.publish(new MockEvent(ctx, true));
  715. await pause();
  716. expect(onSend.mock.calls[0][0].replyTo).toBe('foo@bar.com');
  717. });
  718. });
  719. });
  720. class FakeCustomSender implements EmailSender {
  721. send: (email: EmailDetails<'unserialized'>, options: EmailTransportOptions) => void;
  722. }
  723. const pause = () => new Promise(resolve => setTimeout(resolve, 100));
  724. class MockEvent extends VendureEvent {
  725. constructor(public ctx: RequestContext, public shouldSend: boolean) {
  726. super();
  727. }
  728. }
  729. class MockService {
  730. someAsyncMethod() {
  731. return Promise.resolve('loaded data');
  732. }
  733. }