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

View File

@@ -8,6 +8,7 @@ import { MockMailer } from './mailer.mock';
import { MockJobQueue } from './queue.mock';
import { MockTeamWorkspace } from './team-workspace.mock';
import { MockUser } from './user.mock';
import { MockUserSettings } from './user-settings.mock';
import { MockWorkspace } from './workspace.mock';
import { MockWorkspaceUser } from './workspace-user.mock';
@@ -16,6 +17,7 @@ export const Mockers = {
Workspace: MockWorkspace,
TeamWorkspace: MockTeamWorkspace,
WorkspaceUser: MockWorkspaceUser,
UserSettings: MockUserSettings,
};
export { MockJobQueue, MockMailer };

View File

@@ -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'),
},
},
});
}
}

View File

@@ -3,6 +3,7 @@ import { mock } from 'node:test';
import ava, { TestFn } from 'ava';
import { Mockers } from '../../../__tests__/mocks';
import {
createTestingModule,
type TestingModule,
@@ -15,6 +16,7 @@ import {
NotificationType,
User,
Workspace,
WorkspaceMemberStatus,
} from '../../../models';
import { DocReader } from '../../doc';
import { NotificationService } from '../service';
@@ -41,15 +43,14 @@ let member: User;
let workspace: Workspace;
test.beforeEach(async t => {
await t.context.module.initTestingDB();
owner = await t.context.models.user.create({
email: `${randomUUID()}@affine.pro`,
});
member = await t.context.models.user.create({
email: `${randomUUID()}@affine.pro`,
});
workspace = await t.context.models.workspace.create(owner.id);
await t.context.models.workspace.update(workspace.id, {
const { module } = t.context;
await module.initTestingDB();
owner = await module.create(Mockers.User);
member = await module.create(Mockers.User);
workspace = await module.create(Mockers.Workspace, {
owner: {
id: owner.id,
},
name: 'Test Workspace',
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 => {
const { notificationService } = t.context;
const { notificationService, module } = t.context;
const inviteId = randomUUID();
await t.context.models.userSettings.set(member.id, {
await module.create(Mockers.UserSettings, {
userId: member.id,
receiveInvitationEmail: false,
});
const invitationMailCount = t.context.module.mails.count('MemberInvitation');
const invitationMailCount = module.mails.count('MemberInvitation');
const notification = await notificationService.createInvitation({
userId: member.id,
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 => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const { notificationService, module } = t.context;
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
const notification = await notificationService.createInvitation({
userId: member.id,
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 => {
const { notificationService } = t.context;
const inviteId = randomUUID();
const { notificationService, module } = t.context;
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
const notification = await notificationService.createInvitationAccepted({
userId: owner.id,
body: {
@@ -142,15 +147,19 @@ test('should create invitation accepted notification and email', async t => {
t.is(notification!.body.inviteId, inviteId);
// should send email
const invitationAcceptedMail = t.context.module.mails.last('MemberAccepted');
const invitationAcceptedMail = module.mails.last('MemberAccepted');
t.is(invitationAcceptedMail.to, owner.email);
});
test('should not send invitation accepted email if user settings is not receive invitation email', async t => {
const { notificationService } = t.context;
const inviteId = randomUUID();
const { notificationService, module } = t.context;
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
await t.context.models.userSettings.set(owner.id, {
await module.create(Mockers.UserSettings, {
userId: owner.id,
receiveInvitationEmail: false,
});
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 => {
const { notificationService, models } = t.context;
const { notificationService } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationAccepted({
userId: owner.id,
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 => {
const { notificationService, models } = t.context;
const { notificationService, module } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const notification = await notificationService.createInvitationReviewRequest({
userId: owner.id,
body: {
@@ -244,18 +251,18 @@ test('should create invitation review request notification if user is not an act
t.is(notification!.body.inviteId, inviteId);
// should send email
const invitationReviewRequestMail = t.context.module.mails.last(
const invitationReviewRequestMail = module.mails.last(
'LinkInvitationReviewRequest'
);
t.is(invitationReviewRequestMail.to, owner.email);
});
test('should not create invitation review request notification if user is an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const { notificationService, module } = t.context;
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
const notification = await notificationService.createInvitationReviewRequest({
userId: owner.id,
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 => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const { notificationService, module } = t.context;
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
const notification = await notificationService.createInvitationReviewApproved(
{
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 => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => null);
const { notificationService, module } = t.context;
const { id: inviteId } = await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
status: WorkspaceMemberStatus.Pending,
});
const notification = await notificationService.createInvitationReviewApproved(
{
userId: owner.id,
userId: member.id,
body: {
workspaceId: workspace.id,
createdByUserId: member.id,
createdByUserId: owner.id,
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 => {
const { notificationService, models } = t.context;
mock.method(models.workspaceUser, 'getActive', async () => null);
const { notificationService, module } = t.context;
const notification = await notificationService.createInvitationReviewDeclined(
{
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);
// should send email
const invitationReviewDeclinedMail = t.context.module.mails.last(
const invitationReviewDeclinedMail = module.mails.last(
'LinkInvitationDecline'
);
t.is(invitationReviewDeclinedMail.to, member.email);
});
test('should not create invitation review declined notification if user is an active member', async t => {
const { notificationService, models } = t.context;
const inviteId = randomUUID();
mock.method(models.workspaceUser, 'getActive', async () => ({
id: inviteId,
}));
const { notificationService, module } = t.context;
await module.create(Mockers.WorkspaceUser, {
workspaceId: workspace.id,
userId: member.id,
});
const notification = await notificationService.createInvitationReviewDeclined(
{
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 => {
const { notificationService } = t.context;
const { notificationService, module } = t.context;
const notification = await notificationService.createInvitation({
userId: member.id,
body: {
@@ -422,9 +431,7 @@ test('should throw error on mark notification as read if notification user is no
inviteId: randomUUID(),
},
});
const otherUser = await t.context.models.user.create({
email: `${randomUUID()}@affine.pro`,
});
const otherUser = await module.create(Mockers.User);
await t.throwsAsync(
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 => {
const { notificationService } = t.context;
const { notificationService, module } = t.context;
const docId = randomUUID();
const notification = await notificationService.createMention({
userId: member.id,
@@ -555,12 +562,13 @@ test('should send mention email by user setting', async t => {
});
t.truthy(notification);
// should send mention email
const mentionMail = t.context.module.mails.last('Mention');
const mentionMail = module.mails.last('Mention');
t.is(mentionMail.to, member.email);
// update user setting to not receive mention email
const mentionMailCount = t.context.module.mails.count('Mention');
await t.context.models.userSettings.set(member.id, {
const mentionMailCount = module.mails.count('Mention');
await module.create(Mockers.UserSettings, {
userId: member.id,
receiveMentionEmail: false,
});
await notificationService.createMention({
@@ -577,5 +585,5 @@ test('should send mention email by user setting', async t => {
},
});
// should not send mention email
t.is(t.context.module.mails.count('Mention'), mentionMailCount);
t.is(module.mails.count('Mention'), mentionMailCount);
});

View File

@@ -132,8 +132,12 @@ export class NotificationService {
async createInvitationAccepted(input: InvitationNotificationCreate) {
const workspaceId = input.body.workspaceId;
const userId = input.userId;
if (!(await this.isActiveWorkspaceUser(workspaceId, userId))) {
if (
!(await this.isActiveWorkspaceUser(
workspaceId,
input.body.createdByUserId
))
) {
return;
}
await this.ensureWorkspaceContentExists(workspaceId);
@@ -194,8 +198,9 @@ export class NotificationService {
async createInvitationReviewRequest(input: InvitationNotificationCreate) {
const workspaceId = input.body.workspaceId;
const userId = input.userId;
if (await this.isActiveWorkspaceUser(workspaceId, userId)) {
if (
await this.isActiveWorkspaceUser(workspaceId, input.body.createdByUserId)
) {
return;
}
await this.ensureWorkspaceContentExists(workspaceId);