feat(server): new email template (#9528)

use `yarn af server dev:mail` to preview all mail template
fix CLOUD-93
This commit is contained in:
darkskygit
2025-01-22 02:55:24 +00:00
parent 2db9cc3922
commit 83ed215f4a
54 changed files with 4794 additions and 892 deletions
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,10 @@ test.before(async t => {
imports: [FeatureModule, UserModule, AuthModule],
tapModule: m => {
m.overrideProvider(MailService).useValue(
Sinon.createStubInstance(MailService)
Sinon.stub(
// @ts-expect-error safe
new MailService()
)
);
},
});
@@ -71,7 +74,7 @@ test('should be able to sign in with email', async t => {
t.is(res.body.email, u1.email);
t.true(mailer.sendSignInMail.calledOnce);
const [signInLink] = mailer.sendSignInMail.firstCall.args;
const [, { url: signInLink }] = mailer.sendSignInMail.firstCall.args;
const url = new URL(signInLink);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
@@ -99,7 +102,7 @@ test('should be able to sign up with email', async t => {
t.is(res.body.email, 'u2@affine.pro');
t.true(mailer.sendSignUpMail.calledOnce);
const [signUpLink] = mailer.sendSignUpMail.firstCall.args;
const [, { url: signUpLink }] = mailer.sendSignUpMail.firstCall.args;
const url = new URL(signUpLink);
const email = url.searchParams.get('email');
const token = url.searchParams.get('token');
@@ -1,54 +0,0 @@
/// <reference types="../global.d.ts" />
// This test case is for testing the mailer service.
// Please use local SMTP server for testing.
// See: https://github.com/mailhog/MailHog
import {
getCurrentMailMessageCount,
getLatestMailMessage,
} from '@affine-test/kit/utils/cloud';
import { TestingModule } from '@nestjs/testing';
import type { TestFn } from 'ava';
import ava from 'ava';
import { ConfigModule } from '../base/config';
import { AuthService } from '../core/auth/service';
import { createTestingModule } from './utils';
const test = ava as TestFn<{
auth: AuthService;
module: TestingModule;
skip: boolean;
}>;
test.beforeEach(async t => {
t.context.module = await createTestingModule({
imports: [ConfigModule.forRoot({})],
});
t.context.auth = t.context.module.get(AuthService);
});
test.afterEach.always(async t => {
await t.context.module.close();
});
test('should include callbackUrl in sending email', async t => {
const { auth } = t.context;
await auth.signUp('test@affine.pro', '123456');
for (const fn of [
'sendSetPasswordEmail',
'sendChangeEmail',
'sendChangePasswordEmail',
'sendVerifyChangeEmail',
] as const) {
const prev = await getCurrentMailMessageCount();
await auth[fn]('test@affine.pro', 'https://test.com/callback');
const current = await getCurrentMailMessageCount();
const mail = await getLatestMailMessage();
t.regex(
mail?.Content?.Body,
/https:\/\/test.com\/callback/,
`should include callbackUrl when calling ${fn}`
);
t.is(current, prev + 1, `calling ${fn}`);
}
});
@@ -11,6 +11,7 @@ const test = ava as TestFn<{
app: INestApplication;
mail: MailService;
}>;
import * as renderers from '../mails';
test.beforeEach(async t => {
const { module, app } = await createTestingApp({
@@ -42,7 +43,7 @@ test('should send invite email', async t => {
const workspace = await createWorkspace(app, u1.token.token);
const stub = Sinon.stub(mail, 'sendMail');
const stub = Sinon.stub(mail, 'send');
await inviteUser(app, u1.token.token, workspace.id, u2.email, true);
@@ -53,9 +54,17 @@ test('should send invite email', async t => {
t.is(args.to, u2.email);
t.true(
args.subject!.startsWith(
`${u1.name} invited you to join` /* we don't know the name of mocked workspace */
`${u1.email} invited you to join` /* we don't know the name of mocked workspace */
)
);
}
t.pass();
});
test('should render emails', async t => {
for (const render of Object.values(renderers)) {
// @ts-expect-error use [PreviewProps]
const content = await render();
t.snapshot(content.html, content.subject);
}
});
@@ -703,23 +703,15 @@ test('should be able to emit events', async t => {
);
await grantMember(app, owner.token.token, tws.id, read.id, 'Owner');
const [ownerTransferred, roleChanged] = event.emit
const [ownershipTransferred] = event.emit
.getCalls()
.map(call => call.args)
.toReversed();
t.deepEqual(
roleChanged,
ownershipTransferred,
[
'workspace.members.roleChanged',
{ userId: read.id, workspaceId: tws.id, permission: Permission.Owner },
],
'should emit role changed event'
);
t.deepEqual(
ownerTransferred,
[
'workspace.members.ownerTransferred',
{ email: owner.email, workspaceId: tws.id },
'workspace.members.ownershipTransferred',
{ from: owner.id, to: read.id, workspaceId: tws.id },
],
'should emit owner transferred event'
);
@@ -174,12 +174,13 @@ test('should send email', async t => {
await leaveWorkspace(app, u2.token.token, workspace.id, true);
const afterLeaveMailCount = await getCurrentMailMessageCount();
t.is(
afterAcceptMailCount + 1,
afterLeaveMailCount,
'failed to send leave email to owner'
);
// TODO(@darkskygit): enable this after cluster event system is ready
// const afterLeaveMailCount = await getCurrentMailMessageCount();
// t.is(
// afterAcceptMailCount + 1,
// afterLeaveMailCount,
// 'failed to send leave email to owner'
// );
const leaveEmailContent = await getLatestMailMessage();
t.not(
leaveEmailContent.To.find((item: any) => {