Explorar o código

refactor(email-plugin): Extract email sending logic to simplify tests

Michael Bromley %!s(int64=5) %!d(string=hai) anos
pai
achega
80c8c66c96

+ 29 - 0
packages/email-plugin/src/email-processor.controller.ts

@@ -0,0 +1,29 @@
+import { Controller, Inject, OnModuleInit } from '@nestjs/common';
+import { MessagePattern } from '@nestjs/microservices';
+import { asyncObservable } from '@vendure/core';
+import { Observable } from 'rxjs';
+
+import { EMAIL_PLUGIN_OPTIONS } from './constants';
+import { EmailProcessor } from './email-processor';
+import { EmailPluginOptions, EmailWorkerMessage } from './types';
+
+/**
+ * Runs on the Worker process and does the actual work of generating and sending the emails.
+ */
+@Controller()
+export class EmailProcessorController extends EmailProcessor implements OnModuleInit {
+    constructor(@Inject(EMAIL_PLUGIN_OPTIONS) protected options: EmailPluginOptions) {
+        super(options);
+    }
+
+    async onModuleInit() {
+        await super.init();
+    }
+
+    @MessagePattern(EmailWorkerMessage.pattern)
+    sendEmail(data: EmailWorkerMessage['data']): Observable<EmailWorkerMessage['response']> {
+        return asyncObservable(async () => {
+            return this.process(data);
+        });
+    }
+}

+ 64 - 0
packages/email-plugin/src/email-processor.ts

@@ -0,0 +1,64 @@
+import { InternalServerError } from '@vendure/core';
+import fs from 'fs-extra';
+
+import { isDevModeOptions } from './common';
+import { EmailSender } from './email-sender';
+import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
+import { TemplateLoader } from './template-loader';
+import { EmailPluginOptions, EmailTransportOptions, IntermediateEmailDetails } from './types';
+
+/**
+ * This class combines the template loading, generation, and email sending - the actual "work" of
+ * the EmailPlugin. It is arranged this way primarily to accomodate easier testing, so that the
+ * tests can be run without needing all the JobQueue stuff which would require a full e2e test.
+ */
+export class EmailProcessor {
+    protected templateLoader: TemplateLoader;
+    protected emailSender: EmailSender;
+    protected generator: HandlebarsMjmlGenerator;
+    protected transport: EmailTransportOptions;
+
+    constructor(protected options: EmailPluginOptions) {}
+
+    async init() {
+        this.templateLoader = new TemplateLoader(this.options.templatePath);
+        this.emailSender = new EmailSender();
+        this.generator = new HandlebarsMjmlGenerator();
+        if (this.generator.onInit) {
+            await this.generator.onInit.call(this.generator, this.options);
+        }
+        if (isDevModeOptions(this.options)) {
+            this.transport = {
+                type: 'file',
+                raw: false,
+                outputPath: this.options.outputPath,
+            };
+        } else {
+            if (!this.options.transport) {
+                throw new InternalServerError(
+                    `When devMode is not set to true, the 'transport' property must be set.`,
+                );
+            }
+            this.transport = this.options.transport;
+        }
+        if (this.transport.type === 'file') {
+            // ensure the configured directory exists before
+            // we attempt to write files to it
+            const emailPath = this.transport.outputPath;
+            await fs.ensureDir(emailPath);
+        }
+    }
+
+    async process(data: IntermediateEmailDetails) {
+        const bodySource = await this.templateLoader.loadTemplate(data.type, data.templateFile);
+        const generated = await this.generator.generate(
+            data.from,
+            data.subject,
+            bodySource,
+            data.templateVars,
+        );
+        const emailDetails = { ...generated, recipient: data.recipient };
+        await this.emailSender.send(emailDetails, this.transport);
+        return true;
+    }
+}

+ 0 - 67
packages/email-plugin/src/email-worker.controller.ts

@@ -1,67 +0,0 @@
-import { Controller, Inject, OnModuleInit } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { asyncObservable, InternalServerError } from '@vendure/core';
-import fs from 'fs-extra';
-import { Observable } from 'rxjs';
-
-import { isDevModeOptions } from './common';
-import { EMAIL_PLUGIN_OPTIONS } from './constants';
-import { EmailSender } from './email-sender';
-import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
-import { TemplateLoader } from './template-loader';
-import { EmailPluginOptions, EmailTransportOptions, EmailWorkerMessage } from './types';
-
-@Controller()
-export class EmailWorkerController implements OnModuleInit {
-    private templateLoader: TemplateLoader;
-    private emailSender: EmailSender;
-    private generator: HandlebarsMjmlGenerator;
-    private transport: EmailTransportOptions;
-
-    constructor(@Inject(EMAIL_PLUGIN_OPTIONS) private options: EmailPluginOptions) {}
-
-    async onModuleInit() {
-        this.templateLoader = new TemplateLoader(this.options.templatePath);
-        this.emailSender = new EmailSender();
-        this.generator = new HandlebarsMjmlGenerator();
-        if (this.generator.onInit) {
-            await this.generator.onInit.call(this.generator, this.options);
-        }
-        if (isDevModeOptions(this.options)) {
-            this.transport = {
-                type: 'file',
-                raw: false,
-                outputPath: this.options.outputPath,
-            };
-        } else {
-            if (!this.options.transport) {
-                throw new InternalServerError(
-                    `When devMode is not set to true, the 'transport' property must be set.`,
-                );
-            }
-            this.transport = this.options.transport;
-        }
-        if (this.transport.type === 'file') {
-            // ensure the configured directory exists before
-            // we attempt to write files to it
-            const emailPath = this.transport.outputPath;
-            await fs.ensureDir(emailPath);
-        }
-    }
-
-    @MessagePattern(EmailWorkerMessage.pattern)
-    sendEmail(data: EmailWorkerMessage['data']): Observable<EmailWorkerMessage['response']> {
-        return asyncObservable(async () => {
-            const bodySource = await this.templateLoader.loadTemplate(data.type, data.templateFile);
-            const generated = await this.generator.generate(
-                data.from,
-                data.subject,
-                bodySource,
-                data.templateVars,
-            );
-            const emailDetails = { ...generated, recipient: data.recipient };
-            await this.emailSender.send(emailDetails, this.transport);
-            return true;
-        });
-    }
-}

+ 2 - 3
packages/email-plugin/src/plugin.spec.ts

@@ -21,7 +21,6 @@ import { EmailPlugin } from './plugin';
 import { EmailPluginOptions } from './types';
 
 describe('EmailPlugin', () => {
-    let plugin: EmailPlugin;
     let eventBus: EventBus;
     let onSend: jest.Mock;
     let module: TestingModule;
@@ -52,7 +51,7 @@ describe('EmailPlugin', () => {
             providers: [MockService],
         }).compile();
 
-        plugin = module.get(EmailPlugin);
+        const plugin = module.get(EmailPlugin);
         eventBus = module.get(EventBus);
         await plugin.onVendureBootstrap();
         return module;
@@ -409,7 +408,7 @@ describe('EmailPlugin', () => {
 
     describe('orderConfirmationHandler', () => {
         beforeEach(async () => {
-            module = await initPluginWithHandlers([orderConfirmationHandler], {
+            await initPluginWithHandlers([orderConfirmationHandler], {
                 templatePath: path.join(__dirname, '../templates'),
             });
         });

+ 27 - 14
packages/email-plugin/src/plugin.ts

@@ -19,7 +19,8 @@ import { Connection } from 'typeorm';
 import { isDevModeOptions } from './common';
 import { EMAIL_PLUGIN_OPTIONS } from './constants';
 import { DevMailbox } from './dev-mailbox';
-import { EmailWorkerController } from './email-worker.controller';
+import { EmailProcessor } from './email-processor';
+import { EmailProcessorController } from './email-processor.controller';
 import { EmailEventHandler, EmailEventHandlerWithAsyncData } from './event-handler';
 import {
     EmailPluginDevModeOptions,
@@ -141,13 +142,14 @@ import {
 @VendurePlugin({
     imports: [PluginCommonModule],
     providers: [{ provide: EMAIL_PLUGIN_OPTIONS, useFactory: () => EmailPlugin.options }],
-    workers: [EmailWorkerController],
+    workers: [EmailProcessorController],
     configuration: (config) => EmailPlugin.configure(config),
 })
 export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
     private static options: EmailPluginOptions | EmailPluginDevModeOptions;
     private devMailbox: DevMailbox | undefined;
-    private jobQueue: JobQueue<IntermediateEmailDetails>;
+    private jobQueue: JobQueue<IntermediateEmailDetails> | undefined;
+    private testingProcessor: EmailProcessor | undefined;
 
     /** @internal */
     constructor(
@@ -190,16 +192,23 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
 
         await this.setupEventSubscribers();
 
-        this.jobQueue = this.jobQueueService.createQueue({
-            name: 'send-email',
-            concurrency: 5,
-            process: (job) => {
-                this.workerService.send(new EmailWorkerMessage(job.data)).subscribe({
-                    complete: () => job.complete(),
-                    error: (err) => job.fail(err),
-                });
-            },
-        });
+        if (!isDevModeOptions(options) && options.transport.type === 'testing') {
+            // When running tests, we don't want to go through the JobQueue system,
+            // so we just call the email sending logic directly.
+            this.testingProcessor = new EmailProcessor(options);
+            await this.testingProcessor.init();
+        } else {
+            this.jobQueue = this.jobQueueService.createQueue({
+                name: 'send-email',
+                concurrency: 5,
+                process: (job) => {
+                    this.workerService.send(new EmailWorkerMessage(job.data)).subscribe({
+                        complete: () => job.complete(),
+                        error: (err) => job.fail(err),
+                    });
+                },
+            });
+        }
     }
 
     /** @internal */
@@ -235,7 +244,11 @@ export class EmailPlugin implements OnVendureBootstrap, OnVendureClose {
             if (!result) {
                 return;
             }
-            await this.jobQueue.add(result);
+            if (this.jobQueue) {
+                await this.jobQueue.add(result);
+            } else if (this.testingProcessor) {
+                await this.testingProcessor.process(result);
+            }
         } catch (e) {
             Logger.error(e.message, 'EmailPlugin', e.stack);
         }