plugin.spec.ts 33 KB

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