plugin.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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. Order,
  9. OrderStateTransitionEvent,
  10. PluginCommonModule,
  11. ProcessContextModule,
  12. RequestContext,
  13. VendureEvent,
  14. } from '@vendure/core';
  15. import path from 'path';
  16. import { orderConfirmationHandler } from './default-email-handlers';
  17. import { EmailEventHandler } from './event-handler';
  18. import { EmailEventListener } from './event-listener';
  19. import { EmailPlugin } from './plugin';
  20. import { EmailPluginOptions } from './types';
  21. describe('EmailPlugin', () => {
  22. let eventBus: EventBus;
  23. let onSend: jest.Mock;
  24. let module: TestingModule;
  25. async function initPluginWithHandlers(
  26. handlers: Array<EmailEventHandler<string, any>>,
  27. options?: Partial<EmailPluginOptions>,
  28. ) {
  29. onSend = jest.fn();
  30. module = await Test.createTestingModule({
  31. imports: [
  32. TypeOrmModule.forRoot({
  33. type: 'sqljs',
  34. retryAttempts: 0,
  35. }),
  36. ProcessContextModule.forRoot(),
  37. PluginCommonModule,
  38. EmailPlugin.init({
  39. templatePath: path.join(__dirname, '../test-templates'),
  40. transport: {
  41. type: 'testing',
  42. onSend,
  43. },
  44. handlers,
  45. ...options,
  46. }),
  47. ],
  48. providers: [MockService],
  49. }).compile();
  50. const plugin = module.get(EmailPlugin);
  51. eventBus = module.get(EventBus);
  52. await plugin.onVendureBootstrap();
  53. return module;
  54. }
  55. afterEach(async () => {
  56. if (module) {
  57. await module.close();
  58. }
  59. });
  60. it('setting from, recipient, subject', async () => {
  61. const ctx = RequestContext.deserialize({
  62. _channel: { code: DEFAULT_CHANNEL_CODE },
  63. _languageCode: LanguageCode.en,
  64. } as any);
  65. const handler = new EmailEventListener('test')
  66. .on(MockEvent)
  67. .setFrom('"test from" <noreply@test.com>')
  68. .setRecipient(() => 'test@test.com')
  69. .setSubject('Hello')
  70. .setTemplateVars((event) => ({ subjectVar: 'foo' }));
  71. await initPluginWithHandlers([handler]);
  72. eventBus.publish(new MockEvent(ctx, true));
  73. await pause();
  74. expect(onSend.mock.calls[0][0].subject).toBe('Hello');
  75. expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
  76. expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  77. });
  78. describe('event filtering', () => {
  79. const ctx = RequestContext.deserialize({
  80. _channel: { code: DEFAULT_CHANNEL_CODE },
  81. _languageCode: LanguageCode.en,
  82. } as any);
  83. it('single filter', async () => {
  84. const handler = new EmailEventListener('test')
  85. .on(MockEvent)
  86. .filter((event) => event.shouldSend === true)
  87. .setRecipient(() => 'test@test.com')
  88. .setFrom('"test from" <noreply@test.com>')
  89. .setSubject('test subject');
  90. await initPluginWithHandlers([handler]);
  91. eventBus.publish(new MockEvent(ctx, false));
  92. await pause();
  93. expect(onSend).not.toHaveBeenCalled();
  94. eventBus.publish(new MockEvent(ctx, true));
  95. await pause();
  96. expect(onSend).toHaveBeenCalledTimes(1);
  97. });
  98. it('multiple filters', async () => {
  99. const handler = new EmailEventListener('test')
  100. .on(MockEvent)
  101. .filter((event) => event.shouldSend === true)
  102. .filter((event) => !!event.ctx.activeUserId)
  103. .setFrom('"test from" <noreply@test.com>')
  104. .setRecipient(() => 'test@test.com')
  105. .setSubject('test subject');
  106. await initPluginWithHandlers([handler]);
  107. eventBus.publish(new MockEvent(ctx, true));
  108. await pause();
  109. expect(onSend).not.toHaveBeenCalled();
  110. const ctxWithUser = RequestContext.deserialize({ ...ctx, _session: { user: { id: 42 } } } as any);
  111. eventBus.publish(new MockEvent(ctxWithUser, true));
  112. await pause();
  113. expect(onSend).toHaveBeenCalledTimes(1);
  114. });
  115. it('with .loadData() after .filter()', async () => {
  116. const handler = new EmailEventListener('test')
  117. .on(MockEvent)
  118. .filter((event) => event.shouldSend === true)
  119. .loadData((context) => Promise.resolve('loaded data'))
  120. .setRecipient(() => 'test@test.com')
  121. .setFrom('"test from" <noreply@test.com>')
  122. .setSubject('test subject');
  123. await initPluginWithHandlers([handler]);
  124. eventBus.publish(new MockEvent(ctx, false));
  125. await pause();
  126. expect(onSend).not.toHaveBeenCalled();
  127. eventBus.publish(new MockEvent(ctx, true));
  128. await pause();
  129. expect(onSend).toHaveBeenCalledTimes(1);
  130. });
  131. });
  132. describe('templateVars', () => {
  133. const ctx = RequestContext.deserialize({
  134. _channel: { code: DEFAULT_CHANNEL_CODE },
  135. _languageCode: LanguageCode.en,
  136. } as any);
  137. it('interpolates subject', async () => {
  138. const handler = new EmailEventListener('test')
  139. .on(MockEvent)
  140. .setFrom('"test from" <noreply@test.com>')
  141. .setRecipient(() => 'test@test.com')
  142. .setSubject('Hello {{ subjectVar }}')
  143. .setTemplateVars((event) => ({ subjectVar: 'foo' }));
  144. await initPluginWithHandlers([handler]);
  145. eventBus.publish(new MockEvent(ctx, true));
  146. await pause();
  147. expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
  148. });
  149. it('interpolates body', async () => {
  150. const handler = new EmailEventListener('test')
  151. .on(MockEvent)
  152. .setFrom('"test from" <noreply@test.com>')
  153. .setRecipient(() => 'test@test.com')
  154. .setSubject('Hello')
  155. .setTemplateVars((event) => ({ testVar: 'this is the test var' }));
  156. await initPluginWithHandlers([handler]);
  157. eventBus.publish(new MockEvent(ctx, true));
  158. await pause();
  159. expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
  160. });
  161. /**
  162. * Intended to test the ability for Handlebars to interpolate
  163. * getters on the Order entity prototype.
  164. * See https://github.com/vendure-ecommerce/vendure/issues/259
  165. */
  166. it('interpolates body with property from entity', async () => {
  167. const handler = new EmailEventListener('test')
  168. .on(MockEvent)
  169. .setFrom('"test from" <noreply@test.com>')
  170. .setRecipient(() => 'test@test.com')
  171. .setSubject('Hello')
  172. .setTemplateVars((event) => ({ order: new Order({ subTotal: 123 }) }));
  173. await initPluginWithHandlers([handler]);
  174. eventBus.publish(new MockEvent(ctx, true));
  175. await pause();
  176. expect(onSend.mock.calls[0][0].body).toContain('Total: 123');
  177. });
  178. it('interpolates globalTemplateVars', async () => {
  179. const handler = new EmailEventListener('test')
  180. .on(MockEvent)
  181. .setFrom('"test from" <noreply@test.com>')
  182. .setRecipient(() => 'test@test.com')
  183. .setSubject('Hello {{ globalVar }}');
  184. await initPluginWithHandlers([handler], {
  185. globalTemplateVars: { globalVar: 'baz' },
  186. });
  187. eventBus.publish(new MockEvent(ctx, true));
  188. await pause();
  189. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
  190. });
  191. it('interpolates from', async () => {
  192. const handler = new EmailEventListener('test')
  193. .on(MockEvent)
  194. .setFrom('"test from {{ globalVar }}" <noreply@test.com>')
  195. .setRecipient(() => 'test@test.com')
  196. .setSubject('Hello');
  197. await initPluginWithHandlers([handler], {
  198. globalTemplateVars: { globalVar: 'baz' },
  199. });
  200. eventBus.publish(new MockEvent(ctx, true));
  201. await pause();
  202. expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
  203. });
  204. it('globalTemplateVars available in setTemplateVars method', async () => {
  205. const handler = new EmailEventListener('test')
  206. .on(MockEvent)
  207. .setFrom('"test from" <noreply@test.com>')
  208. .setRecipient(() => 'test@test.com')
  209. .setSubject('Hello {{ testVar }}')
  210. .setTemplateVars((event, globals) => ({ testVar: globals.globalVar + ' quux' }));
  211. await initPluginWithHandlers([handler], {
  212. globalTemplateVars: { globalVar: 'baz' },
  213. });
  214. eventBus.publish(new MockEvent(ctx, true));
  215. await pause();
  216. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
  217. });
  218. it('setTemplateVars overrides globals', async () => {
  219. const handler = new EmailEventListener('test')
  220. .on(MockEvent)
  221. .setFrom('"test from" <noreply@test.com>')
  222. .setRecipient(() => 'test@test.com')
  223. .setSubject('Hello {{ name }}')
  224. .setTemplateVars((event, globals) => ({ name: 'quux' }));
  225. await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
  226. eventBus.publish(new MockEvent(ctx, true));
  227. await pause();
  228. expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
  229. });
  230. });
  231. describe('handlebars helpers', () => {
  232. const ctx = RequestContext.deserialize({
  233. _channel: { code: DEFAULT_CHANNEL_CODE },
  234. _languageCode: LanguageCode.en,
  235. } as any);
  236. it('formateDate', async () => {
  237. const handler = new EmailEventListener('test-helpers')
  238. .on(MockEvent)
  239. .setFrom('"test from" <noreply@test.com>')
  240. .setRecipient(() => 'test@test.com')
  241. .setSubject('Hello')
  242. .setTemplateVars((event) => ({ myDate: new Date('2020-01-01T10:00:00.000Z'), myPrice: 0 }));
  243. await initPluginWithHandlers([handler]);
  244. eventBus.publish(new MockEvent(ctx, true));
  245. await pause();
  246. expect(onSend.mock.calls[0][0].body).toContain('Date: Wed Jan 01 2020 10:00:00');
  247. });
  248. it('formateMoney', async () => {
  249. const handler = new EmailEventListener('test-helpers')
  250. .on(MockEvent)
  251. .setFrom('"test from" <noreply@test.com>')
  252. .setRecipient(() => 'test@test.com')
  253. .setSubject('Hello')
  254. .setTemplateVars((event) => ({ myDate: new Date(), myPrice: 123 }));
  255. await initPluginWithHandlers([handler]);
  256. eventBus.publish(new MockEvent(ctx, true));
  257. await pause();
  258. expect(onSend.mock.calls[0][0].body).toContain('Price: 1.23');
  259. });
  260. });
  261. describe('multiple configs', () => {
  262. const ctx = RequestContext.deserialize({
  263. _channel: { code: DEFAULT_CHANNEL_CODE },
  264. _languageCode: LanguageCode.en,
  265. } as any);
  266. it('additional LanguageCode', async () => {
  267. const handler = new EmailEventListener('test')
  268. .on(MockEvent)
  269. .setFrom('"test from" <noreply@test.com>')
  270. .setSubject('Hello, {{ name }}!')
  271. .setRecipient(() => 'test@test.com')
  272. .setTemplateVars(() => ({ name: 'Test' }))
  273. .addTemplate({
  274. channelCode: 'default',
  275. languageCode: LanguageCode.de,
  276. templateFile: 'body.de.hbs',
  277. subject: 'Servus, {{ name }}!',
  278. });
  279. await initPluginWithHandlers([handler]);
  280. const ctxTa = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.ta } as any);
  281. eventBus.publish(new MockEvent(ctxTa, true));
  282. await pause();
  283. expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
  284. expect(onSend.mock.calls[0][0].body).toContain('Default body.');
  285. const ctxDe = RequestContext.deserialize({ ...ctx, _languageCode: LanguageCode.de } as any);
  286. eventBus.publish(new MockEvent(ctxDe, true));
  287. await pause();
  288. expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
  289. expect(onSend.mock.calls[1][0].body).toContain('German body.');
  290. });
  291. });
  292. describe('loadData', () => {
  293. it('loads async data', async () => {
  294. const handler = new EmailEventListener('test')
  295. .on(MockEvent)
  296. .loadData(async ({ inject }) => {
  297. const service = inject(MockService);
  298. return service.someAsyncMethod();
  299. })
  300. .setFrom('"test from" <noreply@test.com>')
  301. .setSubject('Hello, {{ testData }}!')
  302. .setRecipient(() => 'test@test.com')
  303. .setTemplateVars((event) => ({ testData: event.data }));
  304. await initPluginWithHandlers([handler]);
  305. eventBus.publish(
  306. new MockEvent(
  307. RequestContext.deserialize({
  308. _channel: { code: DEFAULT_CHANNEL_CODE },
  309. _languageCode: LanguageCode.en,
  310. } as any),
  311. true,
  312. ),
  313. );
  314. await pause();
  315. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  316. });
  317. it('works when loadData is called after other setup', async () => {
  318. const handler = new EmailEventListener('test')
  319. .on(MockEvent)
  320. .setFrom('"test from" <noreply@test.com>')
  321. .setSubject('Hello, {{ testData }}!')
  322. .setRecipient(() => 'test@test.com')
  323. .loadData(async ({ inject }) => {
  324. const service = inject(MockService);
  325. return service.someAsyncMethod();
  326. })
  327. .setTemplateVars((event) => ({ testData: event.data }));
  328. await initPluginWithHandlers([handler]);
  329. eventBus.publish(
  330. new MockEvent(
  331. RequestContext.deserialize({
  332. _channel: { code: DEFAULT_CHANNEL_CODE },
  333. _languageCode: LanguageCode.en,
  334. } as any),
  335. true,
  336. ),
  337. );
  338. await pause();
  339. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  340. expect(onSend.mock.calls[0][0].from).toBe('"test from" <noreply@test.com>');
  341. expect(onSend.mock.calls[0][0].recipient).toBe('test@test.com');
  342. });
  343. });
  344. describe('orderConfirmationHandler', () => {
  345. beforeEach(async () => {
  346. await initPluginWithHandlers([orderConfirmationHandler], {
  347. templatePath: path.join(__dirname, '../templates'),
  348. });
  349. });
  350. const ctx = RequestContext.deserialize({
  351. _channel: { code: DEFAULT_CHANNEL_CODE },
  352. _languageCode: LanguageCode.en,
  353. } as any);
  354. const order = ({
  355. code: 'ABCDE',
  356. customer: {
  357. emailAddress: 'test@test.com',
  358. },
  359. } as Partial<Order>) as any;
  360. it('filters events with wrong order state', async () => {
  361. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order));
  362. await pause();
  363. expect(onSend).not.toHaveBeenCalled();
  364. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
  365. await pause();
  366. expect(onSend).not.toHaveBeenCalled();
  367. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order));
  368. await pause();
  369. expect(onSend).not.toHaveBeenCalled();
  370. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  371. await pause();
  372. expect(onSend).toHaveBeenCalledTimes(1);
  373. });
  374. it('sets the Order Customer emailAddress as recipient', async () => {
  375. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  376. await pause();
  377. expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
  378. });
  379. it('sets the subject', async () => {
  380. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  381. await pause();
  382. expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code}`);
  383. });
  384. });
  385. });
  386. const pause = () => new Promise((resolve) => setTimeout(resolve, 100));
  387. class MockEvent extends VendureEvent {
  388. constructor(public ctx: RequestContext, public shouldSend: boolean) {
  389. super();
  390. }
  391. }
  392. class MockService {
  393. someAsyncMethod() {
  394. return Promise.resolve('loaded data');
  395. }
  396. }