fix(mail): remove dead return before sendMail, add firstName personalization
- Remove I18n dependency injection from MailService constructor - Hardcode German email subject/body strings directly in the service - Add optional firstName field to userSignUp and forgotPassword methods - Wire firstName from user object through auth.service.ts call sites - Add comprehensive unit tests for MailService Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -192,6 +192,7 @@ export class AuthService {
|
|||||||
to: user.email,
|
to: user.email,
|
||||||
data: {
|
data: {
|
||||||
hash,
|
hash,
|
||||||
|
firstName: user.firstName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -247,6 +248,7 @@ export class AuthService {
|
|||||||
to: email,
|
to: email,
|
||||||
data: {
|
data: {
|
||||||
hash,
|
hash,
|
||||||
|
firstName: user.firstName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
61
myteamwallet_backend/src/mail/mail.service.spec.ts
Normal file
61
myteamwallet_backend/src/mail/mail.service.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
|
||||||
|
describe('MailService', () => {
|
||||||
|
let service: MailService;
|
||||||
|
let sendMail: jest.Mock;
|
||||||
|
let configGet: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sendMail = jest.fn().mockResolvedValue(undefined);
|
||||||
|
configGet = jest.fn().mockReturnValue('https://app.example.com');
|
||||||
|
|
||||||
|
service = new MailService(
|
||||||
|
{ sendMail } as unknown as MailerService,
|
||||||
|
{ get: configGet } as unknown as ConfigService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends the activation mail with the confirm-email link', async () => {
|
||||||
|
await service.userSignUp({
|
||||||
|
to: 'user@example.com',
|
||||||
|
data: { hash: 'abc123', firstName: 'Max' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMail).toHaveBeenCalledTimes(1);
|
||||||
|
const call = sendMail.mock.calls[0][0];
|
||||||
|
expect(call.to).toBe('user@example.com');
|
||||||
|
expect(call.template).toBe('activation');
|
||||||
|
expect(call.context.url).toBe(
|
||||||
|
'https://app.example.com/confirm-email/abc123',
|
||||||
|
);
|
||||||
|
expect(call.context.firstName).toBe('Max');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends the reset-password mail with the password-change link', async () => {
|
||||||
|
await service.forgotPassword({
|
||||||
|
to: 'user@example.com',
|
||||||
|
data: { hash: 'xyz789', firstName: 'Erika' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sendMail).toHaveBeenCalledTimes(1);
|
||||||
|
const call = sendMail.mock.calls[0][0];
|
||||||
|
expect(call.to).toBe('user@example.com');
|
||||||
|
expect(call.template).toBe('reset-password');
|
||||||
|
expect(call.context.url).toBe(
|
||||||
|
'https://app.example.com/password-change/xyz789',
|
||||||
|
);
|
||||||
|
expect(call.context.firstName).toBe('Erika');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works without a firstName (optional personalization)', async () => {
|
||||||
|
await service.userSignUp({
|
||||||
|
to: 'user@example.com',
|
||||||
|
data: { hash: 'abc123' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = sendMail.mock.calls[0][0];
|
||||||
|
expect(call.context.firstName).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,61 +1,57 @@
|
|||||||
import { MailerService } from '@nestjs-modules/mailer';
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { I18n, I18nRequestScopeService } from 'nestjs-i18n';
|
|
||||||
import { MailData } from './interfaces/mail-data.interface';
|
import { MailData } from './interfaces/mail-data.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MailService {
|
export class MailService {
|
||||||
constructor(
|
constructor(
|
||||||
@I18n()
|
|
||||||
private i18n: I18nRequestScopeService,
|
|
||||||
private mailerService: MailerService,
|
private mailerService: MailerService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async userSignUp(mailData: MailData<{ hash: string }>) {
|
async userSignUp(
|
||||||
return;
|
mailData: MailData<{ hash: string; firstName?: string | null }>,
|
||||||
|
) {
|
||||||
|
const actionTitle = 'E-Mail bestätigen';
|
||||||
|
const url = `${this.configService.get('app.frontendDomain')}/confirm-email/${
|
||||||
|
mailData.data.hash
|
||||||
|
}`;
|
||||||
|
|
||||||
await this.mailerService.sendMail({
|
await this.mailerService.sendMail({
|
||||||
to: mailData.to,
|
to: mailData.to,
|
||||||
subject: await this.i18n.t('common.confirmEmail'),
|
subject: 'Bestätige deine E-Mail-Adresse',
|
||||||
text: `${this.configService.get('app.frontendDomain')}/confirm-email/${
|
text: `${url} ${actionTitle}`,
|
||||||
mailData.data.hash
|
|
||||||
} ${await this.i18n.t('common.confirmEmail')}`,
|
|
||||||
template: 'activation',
|
template: 'activation',
|
||||||
context: {
|
context: {
|
||||||
title: await this.i18n.t('common.confirmEmail'),
|
title: 'Bestätige deine E-Mail-Adresse',
|
||||||
url: `${this.configService.get('app.frontendDomain')}/confirm-email/${
|
year: new Date().getFullYear(),
|
||||||
mailData.data.hash
|
firstName: mailData.data.firstName,
|
||||||
}`,
|
url,
|
||||||
actionTitle: await this.i18n.t('common.confirmEmail'),
|
actionTitle,
|
||||||
app_name: this.configService.get('app.name'),
|
|
||||||
text1: await this.i18n.t('confirm-email.text1'),
|
|
||||||
text2: await this.i18n.t('confirm-email.text2'),
|
|
||||||
text3: await this.i18n.t('confirm-email.text3'),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async forgotPassword(mailData: MailData<{ hash: string }>) {
|
async forgotPassword(
|
||||||
return;
|
mailData: MailData<{ hash: string; firstName?: string | null }>,
|
||||||
|
) {
|
||||||
|
const actionTitle = 'Passwort zurücksetzen';
|
||||||
|
const url = `${this.configService.get('app.frontendDomain')}/password-change/${
|
||||||
|
mailData.data.hash
|
||||||
|
}`;
|
||||||
|
|
||||||
await this.mailerService.sendMail({
|
await this.mailerService.sendMail({
|
||||||
to: mailData.to,
|
to: mailData.to,
|
||||||
subject: await this.i18n.t('common.resetPassword'),
|
subject: actionTitle,
|
||||||
text: `${this.configService.get('app.frontendDomain')}/password-change/${
|
text: `${url} ${actionTitle}`,
|
||||||
mailData.data.hash
|
|
||||||
} ${await this.i18n.t('common.resetPassword')}`,
|
|
||||||
template: 'reset-password',
|
template: 'reset-password',
|
||||||
context: {
|
context: {
|
||||||
title: await this.i18n.t('common.resetPassword'),
|
title: actionTitle,
|
||||||
url: `${this.configService.get('app.frontendDomain')}/password-change/${
|
year: new Date().getFullYear(),
|
||||||
mailData.data.hash
|
firstName: mailData.data.firstName,
|
||||||
}`,
|
url,
|
||||||
actionTitle: await this.i18n.t('common.resetPassword'),
|
actionTitle,
|
||||||
app_name: this.configService.get('app.name'),
|
|
||||||
text1: await this.i18n.t('reset-password.text1'),
|
|
||||||
text2: await this.i18n.t('reset-password.text2'),
|
|
||||||
text3: await this.i18n.t('reset-password.text3'),
|
|
||||||
text4: await this.i18n.t('reset-password.text4'),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user