plugin.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. VendureEvent,
  12. } from '@vendure/core';
  13. import path from 'path';
  14. import { orderConfirmationHandler } from './default-email-handlers';
  15. import { EmailEventHandler } from './event-handler';
  16. import { EmailEventListener } from './event-listener';
  17. import { EmailPlugin } from './plugin';
  18. import { EmailPluginOptions } from './types';
  19. describe('EmailPlugin', () => {
  20. let plugin: EmailPlugin;
  21. let eventBus: EventBus;
  22. let onSend: jest.Mock;
  23. async function initPluginWithHandlers(
  24. handlers: Array<EmailEventHandler<string, any>>,
  25. options?: Partial<EmailPluginOptions>,
  26. ) {
  27. onSend = jest.fn();
  28. const module = await Test.createTestingModule({
  29. imports: [
  30. TypeOrmModule.forRoot({
  31. type: 'sqljs',
  32. }),
  33. PluginCommonModule,
  34. EmailPlugin.init({
  35. templatePath: path.join(__dirname, '../test-templates'),
  36. transport: {
  37. type: 'testing',
  38. onSend,
  39. },
  40. handlers,
  41. ...options,
  42. }),
  43. ],
  44. providers: [MockService],
  45. }).compile();
  46. plugin = module.get(EmailPlugin);
  47. eventBus = module.get(EventBus);
  48. await plugin.onVendureBootstrap();
  49. return module;
  50. }
  51. describe('event filtering', () => {
  52. const ctx = {
  53. channel: { code: DEFAULT_CHANNEL_CODE },
  54. languageCode: LanguageCode.en,
  55. } as any;
  56. it('single filter', async () => {
  57. const handler = new EmailEventListener('test')
  58. .on(MockEvent)
  59. .filter(event => event.shouldSend === true)
  60. .setRecipient(() => 'test@test.com')
  61. .setFrom('"test from" <noreply@test.com>')
  62. .setSubject('test subject');
  63. const module = await initPluginWithHandlers([handler]);
  64. eventBus.publish(new MockEvent(ctx, false));
  65. await pause();
  66. expect(onSend).not.toHaveBeenCalled();
  67. eventBus.publish(new MockEvent(ctx, true));
  68. await pause();
  69. expect(onSend).toHaveBeenCalledTimes(1);
  70. await module.close();
  71. });
  72. it('multiple filters', async () => {
  73. const handler = new EmailEventListener('test')
  74. .on(MockEvent)
  75. .filter(event => event.shouldSend === true)
  76. .filter(event => !!event.ctx.user)
  77. .setFrom('"test from" <noreply@test.com>')
  78. .setRecipient(() => 'test@test.com')
  79. .setSubject('test subject');
  80. const module = await initPluginWithHandlers([handler]);
  81. eventBus.publish(new MockEvent(ctx, true));
  82. await pause();
  83. expect(onSend).not.toHaveBeenCalled();
  84. eventBus.publish(new MockEvent({ ...ctx, user: 'joe' }, true));
  85. await pause();
  86. expect(onSend).toHaveBeenCalledTimes(1);
  87. await module.close();
  88. });
  89. });
  90. describe('templateVars', () => {
  91. const ctx = {
  92. channel: { code: DEFAULT_CHANNEL_CODE },
  93. languageCode: LanguageCode.en,
  94. } as any;
  95. it('interpolates subject', async () => {
  96. const handler = new EmailEventListener('test')
  97. .on(MockEvent)
  98. .setFrom('"test from" <noreply@test.com>')
  99. .setRecipient(() => 'test@test.com')
  100. .setSubject('Hello {{ subjectVar }}')
  101. .setTemplateVars(event => ({ subjectVar: 'foo' }));
  102. const module = await initPluginWithHandlers([handler]);
  103. eventBus.publish(new MockEvent(ctx, true));
  104. await pause();
  105. expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
  106. await module.close();
  107. });
  108. it('interpolates body', async () => {
  109. const handler = new EmailEventListener('test')
  110. .on(MockEvent)
  111. .setFrom('"test from" <noreply@test.com>')
  112. .setRecipient(() => 'test@test.com')
  113. .setSubject('Hello')
  114. .setTemplateVars(event => ({ testVar: 'this is the test var' }));
  115. const module = await initPluginWithHandlers([handler]);
  116. eventBus.publish(new MockEvent(ctx, true));
  117. await pause();
  118. expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
  119. await module.close();
  120. });
  121. it('interpolates globalTemplateVars', async () => {
  122. const handler = new EmailEventListener('test')
  123. .on(MockEvent)
  124. .setFrom('"test from" <noreply@test.com>')
  125. .setRecipient(() => 'test@test.com')
  126. .setSubject('Hello {{ globalVar }}');
  127. const module = await initPluginWithHandlers([handler], {
  128. globalTemplateVars: { globalVar: 'baz' },
  129. });
  130. eventBus.publish(new MockEvent(ctx, true));
  131. await pause();
  132. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz');
  133. await module.close();
  134. });
  135. it('interpolates from', async () => {
  136. const handler = new EmailEventListener('test')
  137. .on(MockEvent)
  138. .setFrom('"test from {{ globalVar }}" <noreply@test.com>')
  139. .setRecipient(() => 'test@test.com')
  140. .setSubject('Hello');
  141. const module = await initPluginWithHandlers([handler], {
  142. globalTemplateVars: { globalVar: 'baz' },
  143. });
  144. eventBus.publish(new MockEvent(ctx, true));
  145. await pause();
  146. expect(onSend.mock.calls[0][0].from).toBe('"test from baz" <noreply@test.com>');
  147. await module.close();
  148. });
  149. it('globalTemplateVars available in setTemplateVars method', 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 {{ testVar }}')
  155. .setTemplateVars((event, globals) => ({ testVar: globals.globalVar + ' quux' }));
  156. const module = await initPluginWithHandlers([handler], {
  157. globalTemplateVars: { globalVar: 'baz' },
  158. });
  159. eventBus.publish(new MockEvent(ctx, true));
  160. await pause();
  161. expect(onSend.mock.calls[0][0].subject).toBe('Hello baz quux');
  162. await module.close();
  163. });
  164. it('setTemplateVars overrides globals', async () => {
  165. const handler = new EmailEventListener('test')
  166. .on(MockEvent)
  167. .setFrom('"test from" <noreply@test.com>')
  168. .setRecipient(() => 'test@test.com')
  169. .setSubject('Hello {{ name }}')
  170. .setTemplateVars((event, globals) => ({ name: 'quux' }));
  171. const module = await initPluginWithHandlers([handler], { globalTemplateVars: { name: 'baz' } });
  172. eventBus.publish(new MockEvent(ctx, true));
  173. await pause();
  174. expect(onSend.mock.calls[0][0].subject).toBe('Hello quux');
  175. await module.close();
  176. });
  177. });
  178. describe('multiple configs', () => {
  179. const ctx = {
  180. channel: { code: DEFAULT_CHANNEL_CODE },
  181. languageCode: LanguageCode.en,
  182. } as any;
  183. it('additional LanguageCode', async () => {
  184. const handler = new EmailEventListener('test')
  185. .on(MockEvent)
  186. .setFrom('"test from" <noreply@test.com>')
  187. .setSubject('Hello, {{ name }}!')
  188. .setRecipient(() => 'test@test.com')
  189. .setTemplateVars(() => ({ name: 'Test' }))
  190. .addTemplate({
  191. channelCode: 'default',
  192. languageCode: LanguageCode.de,
  193. templateFile: 'body.de.hbs',
  194. subject: 'Servus, {{ name }}!',
  195. });
  196. const module = await initPluginWithHandlers([handler]);
  197. eventBus.publish(new MockEvent({ ...ctx, languageCode: LanguageCode.ta }, true));
  198. await pause();
  199. expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
  200. expect(onSend.mock.calls[0][0].body).toContain('Default body.');
  201. eventBus.publish(new MockEvent({ ...ctx, languageCode: LanguageCode.de }, true));
  202. await pause();
  203. expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
  204. expect(onSend.mock.calls[1][0].body).toContain('German body.');
  205. await module.close();
  206. });
  207. });
  208. describe('loadData', () => {
  209. it('loads async data', async () => {
  210. const handler = new EmailEventListener('test')
  211. .on(MockEvent)
  212. .loadData(async ({ inject }) => {
  213. const service = inject(MockService);
  214. return service.someAsyncMethod();
  215. })
  216. .setFrom('"test from" <noreply@test.com>')
  217. .setSubject('Hello, {{ testData }}!')
  218. .setRecipient(() => 'test@test.com')
  219. .setTemplateVars(event => ({ testData: event.data }));
  220. const module = await initPluginWithHandlers([handler]);
  221. eventBus.publish(
  222. new MockEvent(
  223. { channel: { code: DEFAULT_CHANNEL_CODE }, languageCode: LanguageCode.en },
  224. true,
  225. ),
  226. );
  227. await pause();
  228. expect(onSend.mock.calls[0][0].subject).toBe('Hello, loaded data!');
  229. await module.close();
  230. });
  231. });
  232. describe('orderConfirmationHandler', () => {
  233. let module: TestingModule;
  234. beforeEach(async () => {
  235. module = await initPluginWithHandlers([orderConfirmationHandler], {
  236. templatePath: path.join(__dirname, '../templates'),
  237. });
  238. });
  239. afterEach(async () => {
  240. await module.close();
  241. });
  242. const ctx = {
  243. channel: { code: DEFAULT_CHANNEL_CODE },
  244. languageCode: LanguageCode.en,
  245. } as any;
  246. const order = ({
  247. code: 'ABCDE',
  248. customer: {
  249. emailAddress: 'test@test.com',
  250. },
  251. } as Partial<Order>) as any;
  252. it('filters events with wrong order state', async () => {
  253. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order));
  254. await pause();
  255. expect(onSend).not.toHaveBeenCalled();
  256. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
  257. await pause();
  258. expect(onSend).not.toHaveBeenCalled();
  259. eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'PaymentAuthorized', ctx, order));
  260. await pause();
  261. expect(onSend).not.toHaveBeenCalled();
  262. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  263. await pause();
  264. expect(onSend).toHaveBeenCalledTimes(1);
  265. });
  266. it('sets the Order Customer emailAddress as recipient', async () => {
  267. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  268. await pause();
  269. expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
  270. });
  271. it('sets the subject', async () => {
  272. eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
  273. await pause();
  274. expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code}`);
  275. });
  276. });
  277. });
  278. const pause = () => new Promise(resolve => setTimeout(resolve, 50));
  279. class MockEvent extends VendureEvent {
  280. constructor(public ctx: any, public shouldSend: boolean) {
  281. super();
  282. }
  283. }
  284. class MockService {
  285. someAsyncMethod() {
  286. return Promise.resolve('loaded data');
  287. }
  288. }