fix: register mail layout partial on handlebars singleton for real mailer wiring
The HandlebarsAdapter reads partials config from a top-level sibling of
`template` (mailerOptions.options.partials), not from
template.options.partials where it was nested. Because the mail templates
use partial blocks ({{#> layout}}...{{/layout}}), the unregistered partial
rendered silently as an unstyled fragment instead of throwing, so this went
unnoticed. A config-only fix also breaks on Windows because the adapter's
glob-based directory loader mishandles backslash path separators.
Fix registers the shared `layout` partial directly on the handlebars module
singleton in MailConfigService, bypassing the broken glob loader entirely.
Also:
- add mail-config.service.spec.ts, an integration test that drives the real
MailerOptions + HandlebarsAdapter wiring (would have caught this bug,
unlike the existing template-only spec which registers the partial itself)
- remove stale nestjs-i18n references from .env.example, env-example, and
the backend README (i18n was already removed from the code)
- add missing trailing newlines to activation.hbs and reset-password.hbs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,6 @@ NODE_ENV=production
|
||||
APP_PORT=3999
|
||||
APP_NAME="NestJS API"
|
||||
API_PREFIX=api
|
||||
APP_FALLBACK_LANGUAGE=en
|
||||
APP_HEADER_LANGUAGE=x-custom-lang
|
||||
FRONTEND_DOMAIN=http://localhost:3999
|
||||
BACKEND_DOMAIN=http://localhost:3999
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ NODE_ENV=development
|
||||
APP_PORT=3000
|
||||
APP_NAME="NestJS API"
|
||||
API_PREFIX=api
|
||||
APP_FALLBACK_LANGUAGE=en
|
||||
APP_HEADER_LANGUAGE=x-custom-lang
|
||||
FRONTEND_DOMAIN=http://localhost:3000
|
||||
BACKEND_DOMAIN=http://localhost:3000
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ Seeden: npm run seed:run
|
||||
- [x] Sign in and sign up via email.
|
||||
- [x] Social sign in (Apple, Facebook, Google, Twitter).
|
||||
- [x] Admin and User roles.
|
||||
- [x] I18N ([nestjs-i18n](https://www.npmjs.com/package/nestjs-i18n)).
|
||||
- [x] File uploads. Support local and Amazon S3 drivers.
|
||||
- [x] Swagger.
|
||||
- [x] E2E and units tests.
|
||||
|
||||
56
myteamwallet_backend/src/mail/mail-config.service.spec.ts
Normal file
56
myteamwallet_backend/src/mail/mail-config.service.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as path from 'path';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MailConfigService } from './mail-config.service';
|
||||
|
||||
describe('MailConfigService integration', () => {
|
||||
it('produces mailer options whose adapter actually renders the shared layout partial', (done) => {
|
||||
const workingDirectory = path.join(__dirname, '..', '..');
|
||||
const configValues: Record<string, unknown> = {
|
||||
'app.workingDirectory': workingDirectory,
|
||||
'mail.host': 'localhost',
|
||||
'mail.port': 1025,
|
||||
'mail.ignoreTLS': true,
|
||||
'mail.secure': false,
|
||||
'mail.requireTLS': false,
|
||||
'mail.user': '',
|
||||
'mail.password': '',
|
||||
'mail.defaultName': 'TeamWallet',
|
||||
'mail.defaultEmail': 'test@example.com',
|
||||
};
|
||||
const configService = {
|
||||
get: (key: string) => configValues[key],
|
||||
} as unknown as ConfigService;
|
||||
|
||||
const options = new MailConfigService(configService).createMailerOptions();
|
||||
|
||||
const mail: {
|
||||
data: {
|
||||
template: string;
|
||||
context: Record<string, unknown>;
|
||||
html?: string;
|
||||
};
|
||||
} = {
|
||||
data: {
|
||||
template: 'activation',
|
||||
context: {
|
||||
title: 'Test',
|
||||
year: 2026,
|
||||
firstName: 'Max',
|
||||
url: 'https://example.com/confirm-email/abc',
|
||||
actionTitle: 'Jetzt bestätigen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
options.template.adapter.compile(
|
||||
mail,
|
||||
(err?: Error) => {
|
||||
expect(err).toBeUndefined();
|
||||
expect(mail.data.html).toContain('TeamWallet');
|
||||
expect(mail.data.html).toContain('tw-wordmark');
|
||||
done();
|
||||
},
|
||||
options,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as handlebars from 'handlebars';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MailerOptions, MailerOptionsFactory } from '@nestjs-modules/mailer';
|
||||
@@ -9,6 +11,21 @@ export class MailConfigService implements MailerOptionsFactory {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
createMailerOptions(): MailerOptions {
|
||||
const templatesDir = path.join(
|
||||
this.configService.get('app.workingDirectory'),
|
||||
'src',
|
||||
'mail',
|
||||
'mail-templates',
|
||||
);
|
||||
|
||||
handlebars.registerPartial(
|
||||
'layout',
|
||||
fs.readFileSync(
|
||||
path.join(templatesDir, 'partials', 'layout.hbs'),
|
||||
'utf-8',
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
transport: {
|
||||
host: this.configService.get('mail.host'),
|
||||
@@ -27,24 +44,10 @@ export class MailConfigService implements MailerOptionsFactory {
|
||||
)}" <${this.configService.get('mail.defaultEmail')}>`,
|
||||
},
|
||||
template: {
|
||||
dir: path.join(
|
||||
this.configService.get('app.workingDirectory'),
|
||||
'src',
|
||||
'mail',
|
||||
'mail-templates',
|
||||
),
|
||||
dir: templatesDir,
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
partials: {
|
||||
dir: path.join(
|
||||
this.configService.get('app.workingDirectory'),
|
||||
'src',
|
||||
'mail',
|
||||
'mail-templates',
|
||||
'partials',
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as MailerOptions;
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
<a class="tw-button" href="{{url}}">{{actionTitle}}</a>
|
||||
</div>
|
||||
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
|
||||
{{/layout}}
|
||||
{{/layout}}
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
</div>
|
||||
<p>Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail einfach ignorieren — es wird nichts verändert.</p>
|
||||
<p>Falls der Button nicht funktioniert, kopiere diesen Link in deinen Browser:<br><a href="{{url}}">{{url}}</a></p>
|
||||
{{/layout}}
|
||||
{{/layout}}
|
||||
|
||||
Reference in New Issue
Block a user