diff --git a/backend/src/mail/mail.module.ts b/backend/src/mail/mail.module.ts new file mode 100644 index 0000000..789a903 --- /dev/null +++ b/backend/src/mail/mail.module.ts @@ -0,0 +1,29 @@ +import { Module } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { MailerModule } from "@nestjs-modules/mailer"; +import { MailService } from "./mail.service"; + +@Module({ + imports: [ + MailerModule.forRootAsync({ + useFactory: async (config: ConfigService) => ({ + transport: { + host: config.get("MAIL_HOST"), + port: Number(config.get("MAIL_PORT")), + secure: config.get("MAIL_SECURE") === "true", + auth: { + user: config.get("MAIL_USER"), + pass: config.get("MAIL_PASS"), + }, + }, + defaults: { + from: `"No Reply" <${config.get("MAIL_FROM")}>`, + }, + }), + inject: [ConfigService], + }), + ], + providers: [MailService], + exports: [MailService], +}) +export class MailModule {} diff --git a/backend/src/mail/mail.service.spec.ts b/backend/src/mail/mail.service.spec.ts new file mode 100644 index 0000000..f01a7d6 --- /dev/null +++ b/backend/src/mail/mail.service.spec.ts @@ -0,0 +1,87 @@ +import { Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { Test, TestingModule } from "@nestjs/testing"; +import { MailerService } from "@nestjs-modules/mailer"; +import { MailService } from "./mail.service"; + +describe("MailService", () => { + let service: MailService; + let mailerService: MailerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MailService, + { + provide: MailerService, + useValue: { + sendMail: jest.fn().mockResolvedValue({}), + }, + }, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockReturnValue("test.io"), + }, + }, + ], + }).compile(); + + service = module.get(MailService); + mailerService = module.get(MailerService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + it("should send validation email with correct URL", async () => { + const email = "test@example.com"; + const token = "token123"; + await service.sendEmailValidation(email, token); + + expect(mailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + to: email, + subject: "Validation de votre adresse email", + html: expect.stringContaining( + "https://test.io/api/auth/verify-email?token=token123", + ), + }), + ); + }); + + it("should send password reset email with correct URL", async () => { + const email = "test@example.com"; + const token = "token123"; + await service.sendPasswordReset(email, token); + + expect(mailerService.sendMail).toHaveBeenCalledWith( + expect.objectContaining({ + to: email, + subject: "Réinitialisation de votre mot de passe", + html: expect.stringContaining( + "https://test.io/api/auth/reset-password?token=token123", + ), + }), + ); + }); + + it("should throw and log error when sending fails", async () => { + const email = "test@example.com"; + const token = "token123"; + const error = new Error("SMTP Error"); + (mailerService.sendMail as jest.Mock).mockRejectedValueOnce(error); + + const loggerSpy = jest + .spyOn(Logger.prototype, "error") + .mockImplementation(() => {}); + + await expect(service.sendEmailValidation(email, token)).rejects.toThrow( + "SMTP Error", + ); + + expect(loggerSpy).toHaveBeenCalled(); + loggerSpy.mockRestore(); + }); +}); diff --git a/backend/src/mail/mail.service.ts b/backend/src/mail/mail.service.ts new file mode 100644 index 0000000..135c2fc --- /dev/null +++ b/backend/src/mail/mail.service.ts @@ -0,0 +1,62 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { MailerService } from "@nestjs-modules/mailer"; + +@Injectable() +export class MailService { + private readonly logger = new Logger(MailService.name); + private readonly domain: string; + + constructor( + private mailerService: MailerService, + private configService: ConfigService, + ) { + this.domain = this.configService.get("DOMAIN_NAME", "memegoat.io"); + } + + async sendEmailValidation(email: string, token: string) { + const url = `https://${this.domain}/api/auth/verify-email?token=${token}`; + + try { + await this.mailerService.sendMail({ + to: email, + subject: "Validation de votre adresse email", + html: ` +

Bonjour,

+

Veuillez cliquer sur le lien ci-dessous pour valider votre adresse email :

+

${url}

+

Si vous n'avez pas demandé cette validation, vous pouvez ignorer cet email.

+ `, + }); + } catch (error) { + this.logger.error( + `Failed to send validation email to ${email}: ${error.message}`, + error.stack, + ); + throw error; + } + } + + async sendPasswordReset(email: string, token: string) { + const url = `https://${this.domain}/api/auth/reset-password?token=${token}`; + + try { + await this.mailerService.sendMail({ + to: email, + subject: "Réinitialisation de votre mot de passe", + html: ` +

Bonjour,

+

Veuillez cliquer sur le lien ci-dessous pour réinitialiser votre mot de passe :

+

${url}

+

Si vous n'avez pas demandé de réinitialisation, vous pouvez ignorer cet email.

+ `, + }); + } catch (error) { + this.logger.error( + `Failed to send password reset email to ${email}: ${error.message}`, + error.stack, + ); + throw error; + } + } +}