mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 06:47:02 +08:00
refactor(server): mail service (#10934)
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,13 +1,8 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
|
||||
import {
|
||||
getCurrentMailMessageCount,
|
||||
getTokenFromLatestMailMessage,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
|
||||
import { MailService } from '../../base/mailer';
|
||||
import {
|
||||
changeEmail,
|
||||
changePassword,
|
||||
@@ -21,14 +16,11 @@ import {
|
||||
|
||||
const test = ava as TestFn<{
|
||||
app: TestingApp;
|
||||
mail: MailService;
|
||||
}>;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const app = await createTestingApp();
|
||||
const mail = app.get(MailService);
|
||||
t.context.app = app;
|
||||
t.context.mail = mail;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
@@ -36,182 +28,106 @@ test.afterEach.always(async t => {
|
||||
});
|
||||
|
||||
test('change email', async t => {
|
||||
const { mail, app } = t.context;
|
||||
if (mail.hasConfigured()) {
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const u2Email = 'u2@affine.pro';
|
||||
const { app } = t.context;
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const u2Email = 'u2@affine.pro';
|
||||
|
||||
await app.signupV1(u1Email);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
await sendChangeEmail(app, u1Email, 'affine.pro');
|
||||
const user = await app.signupV1(u1Email);
|
||||
await sendChangeEmail(app, u1Email, 'affine.pro');
|
||||
|
||||
const afterSendChangeMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
primitiveMailCount + 1,
|
||||
afterSendChangeMailCount,
|
||||
'failed to send change email'
|
||||
);
|
||||
const changeMail = app.mails.last('ChangeEmail');
|
||||
|
||||
const changeEmailToken = await getTokenFromLatestMailMessage();
|
||||
t.is(changeMail.to, u1Email);
|
||||
|
||||
t.not(
|
||||
changeEmailToken,
|
||||
null,
|
||||
'fail to get change email token from email content'
|
||||
);
|
||||
let link = new URL(changeMail.props.url);
|
||||
|
||||
await sendVerifyChangeEmail(
|
||||
app,
|
||||
changeEmailToken as string,
|
||||
u2Email,
|
||||
'affine.pro'
|
||||
);
|
||||
const changeEmailToken = link.searchParams.get('token');
|
||||
|
||||
const afterSendVerifyMailCount = await getCurrentMailMessageCount();
|
||||
t.not(
|
||||
changeEmailToken,
|
||||
null,
|
||||
'fail to get change email token from email content'
|
||||
);
|
||||
|
||||
t.is(
|
||||
afterSendChangeMailCount + 1,
|
||||
afterSendVerifyMailCount,
|
||||
'failed to send verify email'
|
||||
);
|
||||
await sendVerifyChangeEmail(
|
||||
app,
|
||||
changeEmailToken as string,
|
||||
u2Email,
|
||||
'affine.pro'
|
||||
);
|
||||
|
||||
const verifyEmailToken = await getTokenFromLatestMailMessage();
|
||||
const verifyMail = app.mails.last('VerifyChangeEmail');
|
||||
|
||||
t.not(
|
||||
verifyEmailToken,
|
||||
null,
|
||||
'fail to get verify change email token from email content'
|
||||
);
|
||||
t.is(verifyMail.to, u2Email);
|
||||
|
||||
await changeEmail(app, verifyEmailToken as string, u2Email);
|
||||
link = new URL(verifyMail.props.url);
|
||||
|
||||
const afterNotificationMailCount = await getCurrentMailMessageCount();
|
||||
const verifyEmailToken = link.searchParams.get('token');
|
||||
|
||||
t.is(
|
||||
afterSendVerifyMailCount + 1,
|
||||
afterNotificationMailCount,
|
||||
'failed to send notification email'
|
||||
);
|
||||
}
|
||||
t.pass();
|
||||
t.not(
|
||||
verifyEmailToken,
|
||||
null,
|
||||
'fail to get verify change email token from email content'
|
||||
);
|
||||
|
||||
await changeEmail(app, verifyEmailToken as string, u2Email);
|
||||
|
||||
const changedMail = app.mails.last('EmailChanged');
|
||||
|
||||
t.is(changedMail.to, u2Email);
|
||||
t.is(changedMail.props.to, u2Email);
|
||||
|
||||
await app.logout();
|
||||
await app.login({
|
||||
...user,
|
||||
email: u2Email,
|
||||
});
|
||||
|
||||
const me = await currentUser(app);
|
||||
|
||||
t.not(me, null, 'failed to get current user');
|
||||
t.is(me?.email, u2Email, 'failed to get current user');
|
||||
});
|
||||
|
||||
test('set and change password', async t => {
|
||||
const { mail, app } = t.context;
|
||||
if (mail.hasConfigured()) {
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const { app } = t.context;
|
||||
const u1Email = 'u1@affine.pro';
|
||||
|
||||
const u1 = await app.signupV1(u1Email);
|
||||
const u1 = await app.signupV1(u1Email);
|
||||
await sendSetPasswordEmail(app, u1Email, 'affine.pro');
|
||||
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
const setPasswordMail = app.mails.last('ChangePassword');
|
||||
const link = new URL(setPasswordMail.props.url);
|
||||
const setPasswordToken = link.searchParams.get('token');
|
||||
|
||||
await sendSetPasswordEmail(app, u1Email, 'affine.pro');
|
||||
t.is(setPasswordMail.to, u1Email);
|
||||
t.not(
|
||||
setPasswordToken,
|
||||
null,
|
||||
'fail to get set password token from email content'
|
||||
);
|
||||
|
||||
const afterSendSetMailCount = await getCurrentMailMessageCount();
|
||||
const newPassword = randomBytes(16).toString('hex');
|
||||
const success = await changePassword(
|
||||
app,
|
||||
u1.id,
|
||||
setPasswordToken as string,
|
||||
newPassword
|
||||
);
|
||||
|
||||
t.is(
|
||||
primitiveMailCount + 1,
|
||||
afterSendSetMailCount,
|
||||
'failed to send set email'
|
||||
);
|
||||
t.true(success, 'failed to change password');
|
||||
|
||||
const setPasswordToken = await getTokenFromLatestMailMessage();
|
||||
let user = await currentUser(app);
|
||||
|
||||
t.not(
|
||||
setPasswordToken,
|
||||
null,
|
||||
'fail to get set password token from email content'
|
||||
);
|
||||
t.is(user, null);
|
||||
|
||||
const newPassword = randomBytes(16).toString('hex');
|
||||
const success = await changePassword(
|
||||
app,
|
||||
u1.id,
|
||||
setPasswordToken as string,
|
||||
newPassword
|
||||
);
|
||||
await app.login({
|
||||
...u1,
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
t.true(success, 'failed to change password');
|
||||
user = await currentUser(app);
|
||||
|
||||
await app.login({
|
||||
...u1,
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
const user = await currentUser(app);
|
||||
|
||||
t.not(user, null, 'failed to get current user');
|
||||
t.is(user?.email, u1Email, 'failed to get current user');
|
||||
}
|
||||
t.pass();
|
||||
});
|
||||
test('should revoke token after change user identify', async t => {
|
||||
const { mail, app } = t.context;
|
||||
if (mail.hasConfigured()) {
|
||||
// change email
|
||||
{
|
||||
const u1Email = 'u1@affine.pro';
|
||||
const u2Email = 'u2@affine.pro';
|
||||
|
||||
const u1 = await app.signupV1(u1Email);
|
||||
|
||||
{
|
||||
const user = await currentUser(app);
|
||||
t.is(user?.email, u1Email, 'failed to get current user');
|
||||
}
|
||||
|
||||
await sendChangeEmail(app, u1Email, 'affine.pro');
|
||||
|
||||
const changeEmailToken = await getTokenFromLatestMailMessage();
|
||||
await sendVerifyChangeEmail(
|
||||
app,
|
||||
changeEmailToken as string,
|
||||
u2Email,
|
||||
'affine.pro'
|
||||
);
|
||||
|
||||
const verifyEmailToken = await getTokenFromLatestMailMessage();
|
||||
await changeEmail(app, verifyEmailToken as string, u2Email);
|
||||
|
||||
let user = await currentUser(app);
|
||||
t.is(user, null, 'token should be revoked');
|
||||
|
||||
await app.login({
|
||||
...u1,
|
||||
email: u2Email,
|
||||
});
|
||||
|
||||
user = await currentUser(app);
|
||||
t.is(user?.email, u2Email, 'failed to sign in with new email');
|
||||
}
|
||||
|
||||
// change password
|
||||
{
|
||||
const u3Email = 'u3333@affine.pro';
|
||||
|
||||
await app.logout();
|
||||
const u3 = await app.signupV1(u3Email);
|
||||
|
||||
{
|
||||
const user = await currentUser(app);
|
||||
t.is(user?.email, u3Email, 'failed to get current user');
|
||||
}
|
||||
|
||||
await sendSetPasswordEmail(app, u3Email, 'affine.pro');
|
||||
const token = await getTokenFromLatestMailMessage();
|
||||
const newPassword = randomBytes(16).toString('hex');
|
||||
await changePassword(app, u3.id, token as string, newPassword);
|
||||
|
||||
let user = await currentUser(app);
|
||||
t.is(user, null, 'token should be revoked');
|
||||
|
||||
await app.login({
|
||||
...u3,
|
||||
password: newPassword,
|
||||
});
|
||||
user = await currentUser(app);
|
||||
t.is(user?.email, u3Email, 'failed to sign in with new password');
|
||||
}
|
||||
}
|
||||
t.pass();
|
||||
t.not(user, null, 'failed to get current user');
|
||||
t.is(user?.email, u1Email, 'failed to get current user');
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { MailService } from '../../base';
|
||||
import { AuthModule } from '../../core/auth';
|
||||
import { AuthService } from '../../core/auth/service';
|
||||
import { FeatureModule } from '../../core/features';
|
||||
@@ -20,26 +19,16 @@ import {
|
||||
const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
db: PrismaClient;
|
||||
mailer: Sinon.SinonStubbedInstance<MailService>;
|
||||
app: TestingApp;
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
const app = await createTestingApp({
|
||||
imports: [FeatureModule, UserModule, AuthModule],
|
||||
tapModule: m => {
|
||||
m.overrideProvider(MailService).useValue(
|
||||
Sinon.stub(
|
||||
// @ts-expect-error safe
|
||||
new MailService()
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
t.context.auth = app.get(AuthService);
|
||||
t.context.db = app.get(PrismaClient);
|
||||
t.context.mailer = app.get(MailService);
|
||||
t.context.app = app;
|
||||
});
|
||||
|
||||
@@ -67,11 +56,9 @@ test('should be able to sign in with credential', async t => {
|
||||
});
|
||||
|
||||
test('should be able to sign in with email', async t => {
|
||||
const { app, mailer } = t.context;
|
||||
const { app } = t.context;
|
||||
|
||||
const u1 = await app.createUser('u1@affine.pro');
|
||||
// @ts-expect-error mock
|
||||
mailer.sendSignInMail.resolves({ rejected: [] });
|
||||
|
||||
const res = await app
|
||||
.POST('/api/auth/sign-in')
|
||||
@@ -79,10 +66,11 @@ test('should be able to sign in with email', async t => {
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.email, u1.email);
|
||||
t.true(mailer.sendSignInMail.calledOnce);
|
||||
const signInMail = app.mails.last('SignIn');
|
||||
|
||||
const [, { url: signInLink }] = mailer.sendSignInMail.firstCall.args;
|
||||
const url = new URL(signInLink);
|
||||
t.is(signInMail.to, u1.email);
|
||||
|
||||
const url = new URL(signInMail.props.url);
|
||||
const email = url.searchParams.get('email');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
@@ -93,10 +81,7 @@ test('should be able to sign in with email', async t => {
|
||||
});
|
||||
|
||||
test('should be able to sign up with email', async t => {
|
||||
const { app, mailer } = t.context;
|
||||
|
||||
// @ts-expect-error mock
|
||||
mailer.sendSignUpMail.resolves({ rejected: [] });
|
||||
const { app } = t.context;
|
||||
|
||||
const res = await app
|
||||
.POST('/api/auth/sign-in')
|
||||
@@ -104,10 +89,11 @@ test('should be able to sign up with email', async t => {
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.email, 'u2@affine.pro');
|
||||
t.true(mailer.sendSignUpMail.calledOnce);
|
||||
const signUpMail = app.mails.last('SignUp');
|
||||
|
||||
const [, { url: signUpLink }] = mailer.sendSignUpMail.firstCall.args;
|
||||
const url = new URL(signUpLink);
|
||||
t.is(signUpMail.to, 'u2@affine.pro');
|
||||
|
||||
const url = new URL(signUpMail.props.url);
|
||||
const email = url.searchParams.get('email');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
@@ -129,7 +115,7 @@ test('should not be able to sign in if email is invalid', async t => {
|
||||
});
|
||||
|
||||
test('should not be able to sign in if forbidden', async t => {
|
||||
const { app, auth, mailer } = t.context;
|
||||
const { app, auth } = t.context;
|
||||
|
||||
const u1 = await app.createUser('u1@affine.pro');
|
||||
const canSignInStub = Sinon.stub(auth, 'canSignIn').resolves(false);
|
||||
@@ -139,9 +125,8 @@ test('should not be able to sign in if forbidden', async t => {
|
||||
.send({ email: u1.email })
|
||||
.expect(HttpStatus.FORBIDDEN);
|
||||
|
||||
t.true(mailer.sendSignInMail.notCalled);
|
||||
|
||||
canSignInStub.restore();
|
||||
t.pass();
|
||||
});
|
||||
|
||||
test('should be able to sign out', async t => {
|
||||
@@ -253,12 +238,10 @@ test('should be able to sign out multiple accounts in one session', async t => {
|
||||
});
|
||||
|
||||
test('should be able to sign in with email and client nonce', async t => {
|
||||
const { app, mailer } = t.context;
|
||||
const { app } = t.context;
|
||||
|
||||
const clientNonce = randomUUID();
|
||||
const u1 = await app.createUser();
|
||||
// @ts-expect-error mock
|
||||
mailer.sendSignInMail.resolves({ rejected: [] });
|
||||
|
||||
const res = await app
|
||||
.POST('/api/auth/sign-in')
|
||||
@@ -266,10 +249,11 @@ test('should be able to sign in with email and client nonce', async t => {
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.email, u1.email);
|
||||
t.true(mailer.sendSignInMail.calledOnce);
|
||||
const signInMail = app.mails.last('SignIn');
|
||||
|
||||
const [, { url: signInLink }] = mailer.sendSignInMail.firstCall.args;
|
||||
const url = new URL(signInLink);
|
||||
t.is(signInMail.to, u1.email);
|
||||
|
||||
const url = new URL(signInMail.props.url);
|
||||
const email = url.searchParams.get('email');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
@@ -283,12 +267,10 @@ test('should be able to sign in with email and client nonce', async t => {
|
||||
});
|
||||
|
||||
test('should not be able to sign in with email and client nonce if invalid', async t => {
|
||||
const { app, mailer } = t.context;
|
||||
const { app } = t.context;
|
||||
|
||||
const clientNonce = randomUUID();
|
||||
const u1 = await app.createUser();
|
||||
// @ts-expect-error mock
|
||||
mailer.sendSignInMail.resolves({ rejected: [] });
|
||||
|
||||
const res = await app
|
||||
.POST('/api/auth/sign-in')
|
||||
@@ -296,10 +278,11 @@ test('should not be able to sign in with email and client nonce if invalid', asy
|
||||
.expect(200);
|
||||
|
||||
t.is(res.body.email, u1.email);
|
||||
t.true(mailer.sendSignInMail.calledOnce);
|
||||
const signInMail = app.mails.last('SignIn');
|
||||
|
||||
const [, { url: signInLink }] = mailer.sendSignInMail.firstCall.args;
|
||||
const url = new URL(signInLink);
|
||||
t.is(signInMail.to, u1.email);
|
||||
|
||||
const url = new URL(signInMail.props.url);
|
||||
const email = url.searchParams.get('email');
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from '../../base';
|
||||
import { SocketIoAdapter } from '../../base/websocket';
|
||||
import { AuthGuard } from '../../core/auth';
|
||||
import { Mailer } from '../../core/mail';
|
||||
import { MockMailer } from '../mocks';
|
||||
import { TEST_LOG_LEVEL } from '../utils';
|
||||
|
||||
interface TestingAppMetadata {
|
||||
@@ -21,6 +23,10 @@ interface TestingAppMetadata {
|
||||
}
|
||||
|
||||
export class TestingApp extends NestApplication {
|
||||
get mails() {
|
||||
return this.get(Mailer, { strict: false }) as MockMailer;
|
||||
}
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { MailService } from '../base/mailer';
|
||||
import {
|
||||
createTestingApp,
|
||||
createWorkspace,
|
||||
inviteUser,
|
||||
TestingApp,
|
||||
} from './utils';
|
||||
const test = ava as TestFn<{
|
||||
app: TestingApp;
|
||||
mail: MailService;
|
||||
}>;
|
||||
import * as renderers from '../mails';
|
||||
|
||||
test.beforeEach(async t => {
|
||||
const app = await createTestingApp();
|
||||
|
||||
const mail = app.get(MailService);
|
||||
t.context.app = app;
|
||||
t.context.mail = mail;
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
await t.context.app.close();
|
||||
});
|
||||
|
||||
test('should send invite email', async t => {
|
||||
const { mail, app } = t.context;
|
||||
|
||||
if (mail.hasConfigured()) {
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
const stub = Sinon.stub(mail, 'send');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
await inviteUser(app, workspace.id, u2.email, true);
|
||||
|
||||
t.true(stub.calledOnce);
|
||||
|
||||
const args = stub.args[0][0];
|
||||
|
||||
t.is(args.to, u2.email);
|
||||
t.true(
|
||||
args.subject!.startsWith(
|
||||
`${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);
|
||||
}
|
||||
});
|
||||
11
packages/backend/server/src/__tests__/mails.spec.ts
Normal file
11
packages/backend/server/src/__tests__/mails.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import test from 'ava';
|
||||
|
||||
import { Renderers } from '../mails';
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ export * from './team-workspace.mock';
|
||||
export * from './user.mock';
|
||||
export * from './workspace.mock';
|
||||
|
||||
import { MockMailer } from './mailer.mock';
|
||||
import { MockTeamWorkspace } from './team-workspace.mock';
|
||||
import { MockUser } from './user.mock';
|
||||
import { MockWorkspace } from './workspace.mock';
|
||||
@@ -12,3 +13,5 @@ export const Mockers = {
|
||||
Workspace: MockWorkspace,
|
||||
TeamWorkspace: MockTeamWorkspace,
|
||||
};
|
||||
|
||||
export { MockMailer };
|
||||
|
||||
29
packages/backend/server/src/__tests__/mocks/mailer.mock.ts
Normal file
29
packages/backend/server/src/__tests__/mocks/mailer.mock.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { Mailer } from '../../core/mail';
|
||||
import { MailName } from '../../mails';
|
||||
|
||||
export class MockMailer {
|
||||
send = Sinon.createStubInstance(Mailer).send.resolves(true);
|
||||
|
||||
last<Mail extends MailName>(
|
||||
name: Mail
|
||||
): Extract<Jobs['notification.sendMail'], { name: Mail }> {
|
||||
const last = this.send.lastCall.args[0];
|
||||
|
||||
if (!last) {
|
||||
throw new Error('No mail ever sent');
|
||||
}
|
||||
|
||||
if (last.name !== name) {
|
||||
throw new Error(`Mail name mismatch: ${last.name} !== ${name}`);
|
||||
}
|
||||
|
||||
return last as any;
|
||||
}
|
||||
|
||||
count(name: MailName) {
|
||||
return this.send.getCalls().filter(call => call.args[0].name === name)
|
||||
.length;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud';
|
||||
import { User, WorkspaceMemberStatus } from '@prisma/client';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
@@ -619,12 +618,9 @@ test('should be able to invite by link', async t => {
|
||||
test('should be able to send mails', async t => {
|
||||
const { app } = t.context;
|
||||
const { inviteBatch } = await init(app, 5);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
|
||||
{
|
||||
await inviteBatch(['m3@affine.pro', 'm4@affine.pro'], true);
|
||||
t.is(await getCurrentMailMessageCount(), primitiveMailCount + 2);
|
||||
}
|
||||
await inviteBatch(['m3@affine.pro', 'm4@affine.pro'], true);
|
||||
t.is(app.mails.count('MemberInvitation'), 2);
|
||||
});
|
||||
|
||||
test('should be able to emit events', async t => {
|
||||
|
||||
@@ -10,8 +10,10 @@ import supertest from 'supertest';
|
||||
|
||||
import { AFFiNELogger, ApplyType, GlobalExceptionFilter } from '../../base';
|
||||
import { AuthService } from '../../core/auth';
|
||||
import { Mailer } from '../../core/mail';
|
||||
import { UserModel } from '../../models';
|
||||
import { createFactory, MockedUser, MockUser, MockUserInput } from '../mocks';
|
||||
import { MockMailer } from '../mocks/mailer.mock';
|
||||
import { createTestingModule } from './testing-module';
|
||||
import { initTestingDB, TEST_LOG_LEVEL } from './utils';
|
||||
|
||||
@@ -82,6 +84,7 @@ export class TestingApp extends ApplyType<INestApplication>() {
|
||||
private readonly userCookies: Set<string> = new Set();
|
||||
|
||||
readonly create!: ReturnType<typeof createFactory>;
|
||||
readonly mails!: MockMailer;
|
||||
|
||||
[Symbol.asyncDispose](): Promise<void> {
|
||||
return this.close();
|
||||
@@ -280,6 +283,8 @@ function makeTestingApp(app: INestApplication): TestingApp {
|
||||
|
||||
// @ts-expect-error allow
|
||||
testingApp.create = createFactory(app.get(PrismaClient, { strict: false }));
|
||||
// @ts-expect-error allow
|
||||
testingApp.mails = app.get(Mailer, { strict: false }) as MockMailer;
|
||||
|
||||
return new Proxy(testingApp, {
|
||||
get(target, prop) {
|
||||
|
||||
@@ -12,11 +12,13 @@ import { AppModule, FunctionalityModules } from '../../app.module';
|
||||
import { AFFiNELogger, Runtime } from '../../base';
|
||||
import { GqlModule } from '../../base/graphql';
|
||||
import { AuthGuard, AuthModule } from '../../core/auth';
|
||||
import { Mailer, MailModule } from '../../core/mail';
|
||||
import { ModelsModule } from '../../models';
|
||||
// for jsdoc inference
|
||||
// oxlint-disable-next-line no-unused-vars
|
||||
import type { createModule } from '../create-module';
|
||||
import { createFactory } from '../mocks';
|
||||
import { MockMailer } from '../mocks/mailer.mock';
|
||||
import { initTestingDB, TEST_LOG_LEVEL } from './utils';
|
||||
|
||||
interface TestingModuleMeatdata extends ModuleMetadata {
|
||||
@@ -26,6 +28,7 @@ interface TestingModuleMeatdata extends ModuleMetadata {
|
||||
export interface TestingModule extends BaseTestingModule {
|
||||
initTestingDB(): Promise<void>;
|
||||
create: ReturnType<typeof createFactory>;
|
||||
mails: MockMailer;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -68,6 +71,7 @@ export async function createTestingModule(
|
||||
ModelsModule,
|
||||
AuthModule,
|
||||
GqlModule,
|
||||
MailModule,
|
||||
...imports,
|
||||
]);
|
||||
|
||||
@@ -87,6 +91,7 @@ export async function createTestingModule(
|
||||
if (moduleDef.tapModule) {
|
||||
moduleDef.tapModule(builder);
|
||||
}
|
||||
builder.overrideProvider(Mailer).useClass(MockMailer);
|
||||
|
||||
const module = await builder.compile();
|
||||
|
||||
@@ -108,6 +113,8 @@ export async function createTestingModule(
|
||||
await module.close();
|
||||
};
|
||||
|
||||
testingModule.mails = module.get(Mailer, { strict: false }) as MockMailer;
|
||||
|
||||
const logger = new AFFiNELogger();
|
||||
// we got a lot smoking tests try to break nestjs
|
||||
// can't tolerate the noisy logs
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
getCurrentMailMessageCount,
|
||||
getLatestMailMessage,
|
||||
} from '@affine-test/kit/utils/cloud';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import type { TestFn } from 'ava';
|
||||
import ava from 'ava';
|
||||
|
||||
import { MailService } from '../base/mailer';
|
||||
import { AuthService } from '../core/auth/service';
|
||||
import { Models } from '../models';
|
||||
import {
|
||||
@@ -24,7 +19,6 @@ const test = ava as TestFn<{
|
||||
app: TestingApp;
|
||||
client: PrismaClient;
|
||||
auth: AuthService;
|
||||
mail: MailService;
|
||||
models: Models;
|
||||
}>;
|
||||
|
||||
@@ -33,7 +27,6 @@ test.before(async t => {
|
||||
t.context.app = app;
|
||||
t.context.client = app.get(PrismaClient);
|
||||
t.context.auth = app.get(AuthService);
|
||||
t.context.mail = app.get(MailService);
|
||||
t.context.models = app.get(Models);
|
||||
});
|
||||
|
||||
@@ -125,70 +118,31 @@ test('should invite a user by link', async t => {
|
||||
});
|
||||
|
||||
test('should send email', async t => {
|
||||
const { mail, app } = t.context;
|
||||
if (mail.hasConfigured()) {
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
await app.signupV1('u1@affine.pro');
|
||||
const { app } = t.context;
|
||||
const u2 = await app.signupV1('u2@affine.pro');
|
||||
const u1 = await app.signupV1('u1@affine.pro');
|
||||
|
||||
const workspace = await createWorkspace(app);
|
||||
const primitiveMailCount = await getCurrentMailMessageCount();
|
||||
const workspace = await createWorkspace(app);
|
||||
const invite = await inviteUser(app, workspace.id, u2.email, true);
|
||||
|
||||
const invite = await inviteUser(app, workspace.id, u2.email, true);
|
||||
const invitationMail = app.mails.last('MemberInvitation');
|
||||
|
||||
const afterInviteMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
primitiveMailCount + 1,
|
||||
afterInviteMailCount,
|
||||
'failed to send invite email'
|
||||
);
|
||||
const inviteEmailContent = await getLatestMailMessage();
|
||||
t.is(invitationMail.name, 'MemberInvitation');
|
||||
t.is(invitationMail.to, u2.email);
|
||||
|
||||
t.not(
|
||||
inviteEmailContent.To.find((item: any) => {
|
||||
return item.Mailbox === 'u2';
|
||||
}),
|
||||
undefined,
|
||||
'invite email address was incorrectly sent'
|
||||
);
|
||||
app.switchUser(u2.id);
|
||||
await acceptInviteById(app, workspace.id, invite, true);
|
||||
|
||||
app.switchUser(u2.id);
|
||||
const accept = await acceptInviteById(app, workspace.id, invite, true);
|
||||
t.true(accept, 'failed to accept invite');
|
||||
const acceptedMail = app.mails.last('MemberAccepted');
|
||||
t.is(acceptedMail.to, u1.email);
|
||||
t.is(acceptedMail.props.user.$$userId, u2.id);
|
||||
|
||||
const afterAcceptMailCount = await getCurrentMailMessageCount();
|
||||
t.is(
|
||||
afterInviteMailCount + 1,
|
||||
afterAcceptMailCount,
|
||||
'failed to send accepted email to owner'
|
||||
);
|
||||
const acceptEmailContent = await getLatestMailMessage();
|
||||
t.not(
|
||||
acceptEmailContent.To.find((item: any) => {
|
||||
return item.Mailbox === 'u1';
|
||||
}),
|
||||
undefined,
|
||||
'accept email address was incorrectly sent'
|
||||
);
|
||||
await leaveWorkspace(app, workspace.id, true);
|
||||
|
||||
await leaveWorkspace(app, workspace.id, true);
|
||||
const leaveMail = app.mails.last('MemberLeave');
|
||||
|
||||
// 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) => {
|
||||
return item.Mailbox === 'u1';
|
||||
}),
|
||||
undefined,
|
||||
'leave email address was incorrectly sent'
|
||||
);
|
||||
}
|
||||
t.pass();
|
||||
t.is(leaveMail.to, u1.email);
|
||||
t.is(leaveMail.props.user.$$userId, u2.id);
|
||||
});
|
||||
|
||||
test('should support pagination for member', async t => {
|
||||
|
||||
Reference in New Issue
Block a user