feat(server): send invitation notification (#10219)

close PD-2307 CLOUD-150
This commit is contained in:
fengmk2
2025-03-24 02:27:23 +00:00
parent b59da65796
commit d7b3dc683b
15 changed files with 255 additions and 882 deletions
@@ -615,12 +615,16 @@ test('should be able to invite by link', async t => {
}
});
test('should be able to send mails', async t => {
test('should be able to invite batch and send notifications', async t => {
const { app } = t.context;
const { inviteBatch } = await init(app, 5);
const currentCount = app.queue.count('notification.sendInvitation');
await inviteBatch(['m3@affine.pro', 'm4@affine.pro'], true);
t.is(app.mails.count('MemberInvitation'), 2);
t.is(app.queue.count('notification.sendInvitation'), currentCount + 2);
const job = app.queue.last('notification.sendInvitation');
t.truthy(job.payload.inviteId);
t.truthy(job.payload.inviterId);
});
test('should be able to emit events', async t => {
@@ -1,174 +0,0 @@
import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
import { AuthService } from '../core/auth/service';
import { Models } from '../models';
import {
acceptInviteById,
createTestingApp,
createWorkspace,
getWorkspace,
inviteUser,
leaveWorkspace,
revokeUser,
TestingApp,
} from './utils';
const test = ava as TestFn<{
app: TestingApp;
client: PrismaClient;
auth: AuthService;
models: Models;
}>;
test.before(async t => {
const app = await createTestingApp();
t.context.app = app;
t.context.client = app.get(PrismaClient);
t.context.auth = app.get(AuthService);
t.context.models = app.get(Models);
});
test.beforeEach(async t => {
await t.context.app.initTestingDB();
});
test.after.always(async t => {
await t.context.app.close();
});
test('should invite a user', async t => {
const { app } = t.context;
const u2 = await app.signupV1('u2@affine.pro');
await app.signupV1('u1@affine.pro');
const workspace = await createWorkspace(app);
const invite = await inviteUser(app, workspace.id, u2.email);
t.truthy(invite, 'failed to invite user');
});
test('should leave a workspace', async t => {
const { app } = t.context;
const u2 = await app.signupV1('u2@affine.pro');
await app.signupV1('u1@affine.pro');
const workspace = await createWorkspace(app);
const invite = await inviteUser(app, workspace.id, u2.email);
app.switchUser(u2.id);
await acceptInviteById(app, workspace.id, invite);
const leave = await leaveWorkspace(app, workspace.id);
t.true(leave, 'failed to leave workspace');
});
test('should revoke a user', async t => {
const { app } = t.context;
const u2 = await app.signupV1('u2@affine.pro');
await app.signupV1('u1@affine.pro');
const workspace = await createWorkspace(app);
await inviteUser(app, workspace.id, u2.email);
const currWorkspace = await getWorkspace(app, workspace.id);
t.is(currWorkspace.members.length, 2, 'failed to invite user');
const revoke = await revokeUser(app, workspace.id, u2.id);
t.true(revoke, 'failed to revoke user');
});
test('should create user if not exist', async t => {
const { app, models } = t.context;
await app.signupV1('u1@affine.pro');
const workspace = await createWorkspace(app);
await inviteUser(app, workspace.id, 'u2@affine.pro');
const u2 = await models.user.getUserByEmail('u2@affine.pro');
t.not(u2, undefined, 'failed to create user');
t.is(u2?.name, 'u2', 'failed to create user');
});
test('should invite a user by link', async t => {
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 invite = await inviteUser(app, workspace.id, u2.email);
app.switchUser(u2.id);
const accept = await acceptInviteById(app, workspace.id, invite);
t.true(accept, 'failed to accept invite');
app.switchUser(u1.id);
const invite1 = await inviteUser(app, workspace.id, u2.email);
t.is(invite, invite1, 'repeat the invitation must return same id');
const currWorkspace = await getWorkspace(app, workspace.id);
const currMember = currWorkspace.members.find(u => u.email === u2.email);
t.not(currMember, undefined, 'failed to invite user');
t.is(currMember?.inviteId, invite, 'failed to check invite id');
});
test('should send email', async t => {
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 invite = await inviteUser(app, workspace.id, u2.email, true);
const invitationMail = app.mails.last('MemberInvitation');
t.is(invitationMail.name, 'MemberInvitation');
t.is(invitationMail.to, u2.email);
app.switchUser(u2.id);
await acceptInviteById(app, workspace.id, invite, true);
const acceptedMail = app.mails.last('MemberAccepted');
t.is(acceptedMail.to, u1.email);
t.is(acceptedMail.props.user.$$userId, u2.id);
await leaveWorkspace(app, workspace.id, true);
const leaveMail = app.mails.last('MemberLeave');
t.is(leaveMail.to, u1.email);
t.is(leaveMail.props.user.$$userId, u2.id);
});
test('should support pagination for member', async t => {
const { app } = t.context;
await app.signupV1('u1@affine.pro');
const workspace = await createWorkspace(app);
await inviteUser(app, workspace.id, 'u2@affine.pro');
await inviteUser(app, workspace.id, 'u3@affine.pro');
const firstPageWorkspace = await getWorkspace(app, workspace.id, 0, 2);
t.is(firstPageWorkspace.members.length, 2, 'failed to check invite id');
const secondPageWorkspace = await getWorkspace(app, workspace.id, 2, 2);
t.is(secondPageWorkspace.members.length, 1, 'failed to check invite id');
});
test('should limit member count correctly', async t => {
const { app } = t.context;
await app.signupV1('u1@affine.pro');
const workspace = await createWorkspace(app);
await Promise.allSettled(
Array.from({ length: 10 }).map(async (_, i) =>
inviteUser(app, workspace.id, `u${i}@affine.pro`)
)
);
const ws = await getWorkspace(app, workspace.id);
t.assert(ws.members.length <= 3, 'failed to check member list');
});