plugin.spec.ts 18 KB

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