fix(server): send invitation review notification (#11203)

This commit is contained in:
fengmk2
2025-03-26 09:44:03 +00:00
parent c23b8f604b
commit 07e098aef1
4 changed files with 106 additions and 64 deletions
@@ -8,6 +8,7 @@ import { MockMailer } from './mailer.mock';
import { MockJobQueue } from './queue.mock'; import { MockJobQueue } from './queue.mock';
import { MockTeamWorkspace } from './team-workspace.mock'; import { MockTeamWorkspace } from './team-workspace.mock';
import { MockUser } from './user.mock'; import { MockUser } from './user.mock';
import { MockUserSettings } from './user-settings.mock';
import { MockWorkspace } from './workspace.mock'; import { MockWorkspace } from './workspace.mock';
import { MockWorkspaceUser } from './workspace-user.mock'; import { MockWorkspaceUser } from './workspace-user.mock';
@@ -16,6 +17,7 @@ export const Mockers = {
Workspace: MockWorkspace, Workspace: MockWorkspace,
TeamWorkspace: MockTeamWorkspace, TeamWorkspace: MockTeamWorkspace,
WorkspaceUser: MockWorkspaceUser, WorkspaceUser: MockWorkspaceUser,
UserSettings: MockUserSettings,
}; };
export { MockJobQueue, MockMailer }; export { MockJobQueue, MockMailer };
@@ -0,0 +1,27 @@
import type { UserSettings } from '@prisma/client';
import { omit } from 'lodash-es';
import { UserSettingsInput } from '../../models';
import { Mocker } from './factory';
export type MockUserSettingsInput = UserSettingsInput & {
userId: string;
};
export type MockedUserSettings = UserSettings;
export class MockUserSettings extends Mocker<
MockUserSettingsInput,
MockedUserSettings
> {
override async create(input: MockUserSettingsInput) {
return await this.db.userSettings.create({
data: {
userId: input.userId,
payload: {
...omit(input, 'userId'),
},
},
});
}
}
@@ -3,6 +3,7 @@ import { mock } from 'node:test';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import { Mockers } from '../../../__tests__/mocks';
import { import {
createTestingModule, createTestingModule,
type TestingModule, type TestingModule,
@@ -15,6 +16,7 @@ import {
NotificationType, NotificationType,
User, User,
Workspace, Workspace,
WorkspaceMemberStatus,
} from '../../../models'; } from '../../../models';
import { DocReader } from '../../doc'; import { DocReader } from '../../doc';
import { NotificationService } from '../service'; import { NotificationService } from '../service';
@@ -41,15 +43,14 @@ let member: User;
let workspace: Workspace; let workspace: Workspace;
test.beforeEach(async t => { test.beforeEach(async t => {
await t.context.module.initTestingDB(); const { module } = t.context;
owner = await t.context.models.user.create({ await module.initTestingDB();
email: `${randomUUID()}@affine.pro`, owner = await module.create(Mockers.User);
}); member = await module.create(Mockers.User);
member = await t.context.models.user.create({ workspace = await module.create(Mockers.Workspace, {
email: `${randomUUID()}@affine.pro`, owner: {
}); id: owner.id,
workspace = await t.context.models.workspace.create(owner.id); },
await t.context.models.workspace.update(workspace.id, {
name: 'Test Workspace', name: 'Test Workspace',
avatarKey: 'test-avatar-key', avatarKey: 'test-avatar-key',
}); });
@@ -87,12 +88,13 @@ test('should create invitation notification and email', async t => {
}); });
test('should not send invitation email if user setting is not to receive invitation email', async t => { test('should not send invitation email if user setting is not to receive invitation email', async t => {
const { notificationService } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const inviteId = randomUUID();
await t.context.models.userSettings.set(member.id, { await module.create(Mockers.UserSettings, {
userId: member.id,
receiveInvitationEmail: false, receiveInvitationEmail: false,
}); });
const invitationMailCount = t.context.module.mails.count('MemberInvitation'); const invitationMailCount = module.mails.count('MemberInvitation');
const notification = await notificationService.createInvitation({ const notification = await notificationService.createInvitation({
userId: member.id, userId: member.id,
body: { body: {
@@ -107,11 +109,11 @@ test('should not send invitation email if user setting is not to receive invitat
}); });
test('should not create invitation notification if user is already a member', async t => { test('should not create invitation notification if user is already a member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
mock.method(models.workspaceUser, 'getActive', async () => ({ workspaceId: workspace.id,
id: inviteId, userId: member.id,
})); });
const notification = await notificationService.createInvitation({ const notification = await notificationService.createInvitation({
userId: member.id, userId: member.id,
body: { body: {
@@ -124,8 +126,11 @@ test('should not create invitation notification if user is already a member', as
}); });
test('should create invitation accepted notification and email', async t => { test('should create invitation accepted notification and email', async t => {
const { notificationService } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
const notification = await notificationService.createInvitationAccepted({ const notification = await notificationService.createInvitationAccepted({
userId: owner.id, userId: owner.id,
body: { body: {
@@ -142,15 +147,19 @@ test('should create invitation accepted notification and email', async t => {
t.is(notification!.body.inviteId, inviteId); t.is(notification!.body.inviteId, inviteId);
// should send email // should send email
const invitationAcceptedMail = t.context.module.mails.last('MemberAccepted'); const invitationAcceptedMail = module.mails.last('MemberAccepted');
t.is(invitationAcceptedMail.to, owner.email); t.is(invitationAcceptedMail.to, owner.email);
}); });
test('should not send invitation accepted email if user settings is not receive invitation email', async t => { test('should not send invitation accepted email if user settings is not receive invitation email', async t => {
const { notificationService } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
// should not send email if user settings is not receive invitation email // should not send email if user settings is not receive invitation email
await t.context.models.userSettings.set(owner.id, { await module.create(Mockers.UserSettings, {
userId: owner.id,
receiveInvitationEmail: false, receiveInvitationEmail: false,
}); });
const invitationAcceptedMailCount = const invitationAcceptedMailCount =
@@ -172,9 +181,8 @@ test('should not send invitation accepted email if user settings is not receive
}); });
test('should not create invitation accepted notification if user is not an active member', async t => { test('should not create invitation accepted notification if user is not an active member', async t => {
const { notificationService, models } = t.context; const { notificationService } = t.context;
const inviteId = randomUUID(); const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationAccepted({ const notification = await notificationService.createInvitationAccepted({
userId: owner.id, userId: owner.id,
body: { body: {
@@ -225,9 +233,8 @@ test('should create invitation rejected notification', async t => {
}); });
test('should create invitation review request notification if user is not an active member', async t => { test('should create invitation review request notification if user is not an active member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationReviewRequest({ const notification = await notificationService.createInvitationReviewRequest({
userId: owner.id, userId: owner.id,
body: { body: {
@@ -244,18 +251,18 @@ test('should create invitation review request notification if user is not an act
t.is(notification!.body.inviteId, inviteId); t.is(notification!.body.inviteId, inviteId);
// should send email // should send email
const invitationReviewRequestMail = t.context.module.mails.last( const invitationReviewRequestMail = module.mails.last(
'LinkInvitationReviewRequest' 'LinkInvitationReviewRequest'
); );
t.is(invitationReviewRequestMail.to, owner.email); t.is(invitationReviewRequestMail.to, owner.email);
}); });
test('should not create invitation review request notification if user is an active member', async t => { test('should not create invitation review request notification if user is an active member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
mock.method(models.workspaceUser, 'getActive', async () => ({ workspaceId: workspace.id,
id: inviteId, userId: member.id,
})); });
const notification = await notificationService.createInvitationReviewRequest({ const notification = await notificationService.createInvitationReviewRequest({
userId: owner.id, userId: owner.id,
body: { body: {
@@ -268,11 +275,11 @@ test('should not create invitation review request notification if user is an act
}); });
test('should create invitation review approved notification if user is an active member', async t => { test('should create invitation review approved notification if user is an active member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
mock.method(models.workspaceUser, 'getActive', async () => ({ workspaceId: workspace.id,
id: inviteId, userId: member.id,
})); });
const notification = await notificationService.createInvitationReviewApproved( const notification = await notificationService.createInvitationReviewApproved(
{ {
userId: member.id, userId: member.id,
@@ -298,15 +305,18 @@ test('should create invitation review approved notification if user is an active
}); });
test('should not create invitation review approved notification if user is not an active member', async t => { test('should not create invitation review approved notification if user is not an active member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
mock.method(models.workspaceUser, 'getActive', async () => null); workspaceId: workspace.id,
userId: member.id,
status: WorkspaceMemberStatus.Pending,
});
const notification = await notificationService.createInvitationReviewApproved( const notification = await notificationService.createInvitationReviewApproved(
{ {
userId: owner.id, userId: member.id,
body: { body: {
workspaceId: workspace.id, workspaceId: workspace.id,
createdByUserId: member.id, createdByUserId: owner.id,
inviteId, inviteId,
}, },
} }
@@ -315,8 +325,7 @@ test('should not create invitation review approved notification if user is not a
}); });
test('should create invitation review declined notification if user is not an active member', async t => { test('should create invitation review declined notification if user is not an active member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationReviewDeclined( const notification = await notificationService.createInvitationReviewDeclined(
{ {
userId: member.id, userId: member.id,
@@ -333,18 +342,18 @@ test('should create invitation review declined notification if user is not an ac
t.is(notification!.body.createdByUserId, owner.id); t.is(notification!.body.createdByUserId, owner.id);
// should send email // should send email
const invitationReviewDeclinedMail = t.context.module.mails.last( const invitationReviewDeclinedMail = module.mails.last(
'LinkInvitationDecline' 'LinkInvitationDecline'
); );
t.is(invitationReviewDeclinedMail.to, member.email); t.is(invitationReviewDeclinedMail.to, member.email);
}); });
test('should not create invitation review declined notification if user is an active member', async t => { test('should not create invitation review declined notification if user is an active member', async t => {
const { notificationService, models } = t.context; const { notificationService, module } = t.context;
const inviteId = randomUUID(); await module.create(Mockers.WorkspaceUser, {
mock.method(models.workspaceUser, 'getActive', async () => ({ workspaceId: workspace.id,
id: inviteId, userId: member.id,
})); });
const notification = await notificationService.createInvitationReviewDeclined( const notification = await notificationService.createInvitationReviewDeclined(
{ {
userId: owner.id, userId: owner.id,
@@ -413,7 +422,7 @@ test('should throw error on mark notification as read if notification is not fou
}); });
test('should throw error on mark notification as read if notification user is not the same', async t => { test('should throw error on mark notification as read if notification user is not the same', async t => {
const { notificationService } = t.context; const { notificationService, module } = t.context;
const notification = await notificationService.createInvitation({ const notification = await notificationService.createInvitation({
userId: member.id, userId: member.id,
body: { body: {
@@ -422,9 +431,7 @@ test('should throw error on mark notification as read if notification user is no
inviteId: randomUUID(), inviteId: randomUUID(),
}, },
}); });
const otherUser = await t.context.models.user.create({ const otherUser = await module.create(Mockers.User);
email: `${randomUUID()}@affine.pro`,
});
await t.throwsAsync( await t.throwsAsync(
notificationService.markAsRead(otherUser.id, notification!.id), notificationService.markAsRead(otherUser.id, notification!.id),
{ {
@@ -538,7 +545,7 @@ test('should raw doc title in mention notification if no doc found', async t =>
}); });
test('should send mention email by user setting', async t => { test('should send mention email by user setting', async t => {
const { notificationService } = t.context; const { notificationService, module } = t.context;
const docId = randomUUID(); const docId = randomUUID();
const notification = await notificationService.createMention({ const notification = await notificationService.createMention({
userId: member.id, userId: member.id,
@@ -555,12 +562,13 @@ test('should send mention email by user setting', async t => {
}); });
t.truthy(notification); t.truthy(notification);
// should send mention email // should send mention email
const mentionMail = t.context.module.mails.last('Mention'); const mentionMail = module.mails.last('Mention');
t.is(mentionMail.to, member.email); t.is(mentionMail.to, member.email);
// update user setting to not receive mention email // update user setting to not receive mention email
const mentionMailCount = t.context.module.mails.count('Mention'); const mentionMailCount = module.mails.count('Mention');
await t.context.models.userSettings.set(member.id, { await module.create(Mockers.UserSettings, {
userId: member.id,
receiveMentionEmail: false, receiveMentionEmail: false,
}); });
await notificationService.createMention({ await notificationService.createMention({
@@ -577,5 +585,5 @@ test('should send mention email by user setting', async t => {
}, },
}); });
// should not send mention email // should not send mention email
t.is(t.context.module.mails.count('Mention'), mentionMailCount); t.is(module.mails.count('Mention'), mentionMailCount);
}); });
@@ -132,8 +132,12 @@ export class NotificationService {
async createInvitationAccepted(input: InvitationNotificationCreate) { async createInvitationAccepted(input: InvitationNotificationCreate) {
const workspaceId = input.body.workspaceId; const workspaceId = input.body.workspaceId;
const userId = input.userId; if (
if (!(await this.isActiveWorkspaceUser(workspaceId, userId))) { !(await this.isActiveWorkspaceUser(
workspaceId,
input.body.createdByUserId
))
) {
return; return;
} }
await this.ensureWorkspaceContentExists(workspaceId); await this.ensureWorkspaceContentExists(workspaceId);
@@ -194,8 +198,9 @@ export class NotificationService {
async createInvitationReviewRequest(input: InvitationNotificationCreate) { async createInvitationReviewRequest(input: InvitationNotificationCreate) {
const workspaceId = input.body.workspaceId; const workspaceId = input.body.workspaceId;
const userId = input.userId; if (
if (await this.isActiveWorkspaceUser(workspaceId, userId)) { await this.isActiveWorkspaceUser(workspaceId, input.body.createdByUserId)
) {
return; return;
} }
await this.ensureWorkspaceContentExists(workspaceId); await this.ensureWorkspaceContentExists(workspaceId);