diff --git a/packages/backend/server/src/__tests__/e2e/workspace/team.spec.ts b/packages/backend/server/src/__tests__/e2e/workspace/team.spec.ts new file mode 100644 index 0000000000..4564c180b9 --- /dev/null +++ b/packages/backend/server/src/__tests__/e2e/workspace/team.spec.ts @@ -0,0 +1,95 @@ +import { + acceptInviteByInviteIdMutation, + createInviteLinkMutation, + WorkspaceInviteLinkExpireTime, +} from '@affine/graphql'; + +import { Mockers } from '../../mocks'; +import { app, e2e } from '../test'; + +async function createTeamWorkspace(quantity = 10) { + const owner = await app.create(Mockers.User); + + const workspace = await app.create(Mockers.Workspace, { + owner: { id: owner.id }, + }); + await app.create(Mockers.TeamWorkspace, { + id: workspace.id, + quantity, + }); + + return { + owner, + workspace, + }; +} + +e2e( + 'should invite by link and send review request notification below quota limit', + async t => { + const { owner, workspace } = await createTeamWorkspace(); + + await app.login(owner); + const { createInviteLink } = await app.gql({ + query: createInviteLinkMutation, + variables: { + workspaceId: workspace.id, + expireTime: WorkspaceInviteLinkExpireTime.OneDay, + }, + }); + t.truthy(createInviteLink, 'failed to create invite link'); + const link = createInviteLink.link; + const inviteId = link.split('/').pop()!; + + // accept invite by link + await app.signup(); + const result = await app.gql({ + query: acceptInviteByInviteIdMutation, + variables: { + workspaceId: workspace.id, + inviteId, + }, + }); + t.truthy(result, 'failed to accept invite'); + const notification = app.queue.last( + 'notification.sendInvitationReviewRequest' + ); + t.is(notification.payload.reviewerId, owner.id); + t.truthy(notification.payload.inviteId); + } +); + +e2e( + 'should invite by link and send review request notification over quota limit', + async t => { + const { owner, workspace } = await createTeamWorkspace(1); + + await app.login(owner); + const { createInviteLink } = await app.gql({ + query: createInviteLinkMutation, + variables: { + workspaceId: workspace.id, + expireTime: WorkspaceInviteLinkExpireTime.OneDay, + }, + }); + t.truthy(createInviteLink, 'failed to create invite link'); + const link = createInviteLink.link; + const inviteId = link.split('/').pop()!; + + // accept invite by link + await app.signup(); + const result = await app.gql({ + query: acceptInviteByInviteIdMutation, + variables: { + workspaceId: workspace.id, + inviteId, + }, + }); + t.truthy(result, 'failed to accept invite'); + const notification = app.queue.last( + 'notification.sendInvitationReviewRequest' + ); + t.is(notification.payload.reviewerId, owner.id); + t.truthy(notification.payload.inviteId); + } +); diff --git a/packages/backend/server/src/__tests__/mocks/team-workspace.mock.ts b/packages/backend/server/src/__tests__/mocks/team-workspace.mock.ts index 3e84f28117..338c0825da 100644 --- a/packages/backend/server/src/__tests__/mocks/team-workspace.mock.ts +++ b/packages/backend/server/src/__tests__/mocks/team-workspace.mock.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; -import { Feature } from '../../models'; +import { Feature, FeatureType } from '../../models'; import { Mocker } from './factory'; interface MockTeamWorkspaceInput { @@ -46,6 +46,8 @@ export class MockTeamWorkspace extends Mocker< featureId: feature.id, reason: 'test', activated: true, + name: Feature.TeamPlan, + type: FeatureType.Quota, configs: { memberLimit: quantity, }, diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts index b9e8371e43..614c05b2f0 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts @@ -610,38 +610,8 @@ export class WorkspaceResolver { `workspace:inviteLink:${workspaceId}` ); if (invite?.inviteId === inviteId) { - const seatAvailable = await this.quota.tryCheckSeat(workspaceId); - if (seatAvailable) { - const invite = await this.models.workspaceUser.set( - workspaceId, - user.id, - WorkspaceRole.Collaborator, - WorkspaceMemberStatus.UnderReview - ); - await this.workspaceService.sendReviewRequestNotification(invite.id); - return true; - } else { - const isTeam = - await this.workspaceService.isTeamWorkspace(workspaceId); - // only team workspace allow over limit - if (isTeam) { - await this.models.workspaceUser.set( - workspaceId, - user.id, - WorkspaceRole.Collaborator, - WorkspaceMemberStatus.NeedMoreSeatAndReview - ); - const memberCount = - await this.models.workspaceUser.count(workspaceId); - this.event.emit('workspace.members.updated', { - workspaceId, - count: memberCount, - }); - return true; - } else { - throw new MemberQuotaExceeded(); - } - } + await this.acceptInviteByLink(user, workspaceId); + return true; } } @@ -650,6 +620,40 @@ export class WorkspaceResolver { return true; } + private async acceptInviteByLink(user: CurrentUser, workspaceId: string) { + const seatAvailable = await this.quota.tryCheckSeat(workspaceId); + if (seatAvailable) { + const role = await this.models.workspaceUser.set( + workspaceId, + user.id, + WorkspaceRole.Collaborator, + WorkspaceMemberStatus.UnderReview + ); + await this.workspaceService.sendReviewRequestNotification(role.id); + return; + } + + const isTeam = await this.workspaceService.isTeamWorkspace(workspaceId); + // only team workspace allow over limit + if (isTeam) { + const role = await this.models.workspaceUser.set( + workspaceId, + user.id, + WorkspaceRole.Collaborator, + WorkspaceMemberStatus.NeedMoreSeatAndReview + ); + await this.workspaceService.sendReviewRequestNotification(role.id); + const memberCount = await this.models.workspaceUser.count(workspaceId); + this.event.emit('workspace.members.updated', { + workspaceId, + count: memberCount, + }); + return; + } + + throw new MemberQuotaExceeded(); + } + @Mutation(() => Boolean) async leaveWorkspace( @CurrentUser() user: CurrentUser,