diff --git a/packages/backend/server/src/__tests__/mocks/index.ts b/packages/backend/server/src/__tests__/mocks/index.ts index 162f4d92d..d210f228f 100644 --- a/packages/backend/server/src/__tests__/mocks/index.ts +++ b/packages/backend/server/src/__tests__/mocks/index.ts @@ -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 }; diff --git a/packages/backend/server/src/__tests__/mocks/user-settings.mock.ts b/packages/backend/server/src/__tests__/mocks/user-settings.mock.ts new file mode 100644 index 000000000..75b90948f --- /dev/null +++ b/packages/backend/server/src/__tests__/mocks/user-settings.mock.ts @@ -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'), + }, + }, + }); + } +} diff --git a/packages/backend/server/src/core/notification/__tests__/service.spec.ts b/packages/backend/server/src/core/notification/__tests__/service.spec.ts index aabb4d527..daeef5048 100644 --- a/packages/backend/server/src/core/notification/__tests__/service.spec.ts +++ b/packages/backend/server/src/core/notification/__tests__/service.spec.ts @@ -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); }); diff --git a/packages/backend/server/src/core/notification/service.ts b/packages/backend/server/src/core/notification/service.ts index f52d17119..b923eab3c 100644 --- a/packages/backend/server/src/core/notification/service.ts +++ b/packages/backend/server/src/core/notification/service.ts @@ -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);