feat(server): refactor mail queue (#15204)

This commit is contained in:
DarkSky
2026-07-07 08:38:16 +08:00
committed by GitHub
parent 9581432d21
commit 998b255afd
72 changed files with 8386 additions and 763 deletions
@@ -110,7 +110,7 @@ test('set and change password', async t => {
const u1 = await app.signupV1(u1Email);
await sendSetPasswordEmail(app, u1Email, '/password-change');
const setPasswordMail = app.mails.last('ChangePassword');
const setPasswordMail = app.mails.last('SetPassword');
const link = new URL(setPasswordMail.props.url);
const setPasswordToken = link.searchParams.get('token');
@@ -4,7 +4,9 @@ import {
getCurrentUserQuery,
getWorkspaceQuery,
} from '@affine/graphql';
import { WorkspaceMemberStatus } from '@prisma/client';
import { WorkspaceRole } from '../../../models';
import { app, e2e, Mockers } from '../test';
const admin = await app.create(Mockers.User, {
@@ -107,6 +109,31 @@ e2e('should ban account', async t => {
t.is(banUser.disabled, true);
});
e2e('should ban account with pending workspace invitation', async t => {
const owner = await app.create(Mockers.User);
const user = await app.create(Mockers.User);
const workspace = await app.create(Mockers.Workspace, { owner });
await app.models.workspaceInvitation.set(
workspace.id,
user.id,
WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending,
{ inviterId: owner.id }
);
await app.login(admin);
const { banUser } = await app.gql({
query: disableUserMutation,
variables: {
id: user.id,
},
});
t.is(banUser.disabled, true);
});
e2e('should not login banned account', async t => {
const user = await app.create(Mockers.User);
@@ -2,17 +2,17 @@ import { interval, map, take, takeUntil } from 'rxjs';
import Sinon from 'sinon';
import { Mailer } from '../../core/mail';
import type { SendMailCommand } from '../../core/mail/types';
import { MailName } from '../../mails';
export class MockMailer {
send = Sinon.createStubInstance(Mailer).send.resolves(true);
trySend(command: Jobs['notification.sendMail']) {
skip = Sinon.createStubInstance(Mailer).skip.resolves(false);
trySend(command: SendMailCommand) {
return this.send(command, true);
}
last<Mail extends MailName>(
name: Mail
): Extract<Jobs['notification.sendMail'], { name: Mail }> {
last<Mail extends MailName>(name: Mail): SendMailCommand & { name: Mail } {
const last = this.send.lastCall.args[0];
if (!last) {
@@ -23,22 +23,26 @@ export class MockMailer {
throw new Error(`Mail name mismatch: ${last.name} !== ${name}`);
}
return last as any;
return last as SendMailCommand & { name: Mail };
}
waitFor<Mail extends MailName>(
name: Mail,
timeout: number = 1000
): Promise<Extract<Jobs['notification.sendMail'], { name: Mail }>> {
const { promise, reject, resolve } = Promise.withResolvers<any>();
): Promise<SendMailCommand & { name: Mail }> {
const { promise, reject, resolve } = Promise.withResolvers<
SendMailCommand & { name: Mail }
>();
interval(10)
.pipe(
take(Math.floor(timeout / 10)),
takeUntil(promise),
map(() => {
const last = this.send.lastCall.args[0];
return last.name === name ? last : undefined;
const last = this.send.lastCall?.args[0];
return last?.name === name
? (last as SendMailCommand & { name: Mail })
: undefined;
})
)
.subscribe({