From 27d4aa7ca7620f6fa53f5e07dd67cedb067f477b Mon Sep 17 00:00:00 2001 From: darkskygit Date: Tue, 17 Dec 2024 08:42:19 +0000 Subject: [PATCH] test(server): add more test for team workspace (#9182) --- packages/backend/server/src/base/error/def.ts | 5 + .../server/src/base/error/errors.gen.ts | 13 +- .../src/core/workspaces/resolvers/team.ts | 17 +- packages/backend/server/src/schema.gql | 7 +- packages/backend/server/tests/team.e2e.ts | 401 ++++++++++++++++-- packages/backend/server/tests/utils/invite.ts | 82 ++++ .../backend/server/tests/utils/workspace.ts | 2 +- packages/frontend/graphql/src/schema.ts | 7 + 8 files changed, 486 insertions(+), 48 deletions(-) diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 21b9e354f3..87b620f0b3 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -347,6 +347,11 @@ export const USER_FRIENDLY_ERRORS = { args: { spaceId: 'string' }, message: ({ spaceId }) => `Space ${spaceId} not found.`, }, + member_not_found_in_space: { + type: 'action_forbidden', + args: { spaceId: 'string' }, + message: ({ spaceId }) => `Member not found in Space ${spaceId}.`, + }, not_in_space: { type: 'action_forbidden', args: { spaceId: 'string' }, diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index e87010b0be..01273cb870 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -191,6 +191,16 @@ export class SpaceNotFound extends UserFriendlyError { } } @ObjectType() +class MemberNotFoundInSpaceDataType { + @Field() spaceId!: string +} + +export class MemberNotFoundInSpace extends UserFriendlyError { + constructor(args: MemberNotFoundInSpaceDataType, message?: string | ((args: MemberNotFoundInSpaceDataType) => string)) { + super('action_forbidden', 'member_not_found_in_space', message, args); + } +} +@ObjectType() class NotInSpaceDataType { @Field() spaceId!: string } @@ -615,6 +625,7 @@ export enum ErrorNames { ACCESS_DENIED, EMAIL_VERIFICATION_REQUIRED, SPACE_NOT_FOUND, + MEMBER_NOT_FOUND_IN_SPACE, NOT_IN_SPACE, ALREADY_IN_SPACE, SPACE_ACCESS_DENIED, @@ -674,5 +685,5 @@ registerEnumType(ErrorNames, { export const ErrorDataUnionType = createUnionType({ name: 'ErrorDataUnion', types: () => - [WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const, + [WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const, }); diff --git a/packages/backend/server/src/core/workspaces/resolvers/team.ts b/packages/backend/server/src/core/workspaces/resolvers/team.ts index 2d584ffddc..87897f8834 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/team.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/team.ts @@ -13,7 +13,7 @@ import { Cache, EventEmitter, type EventPayload, - NotInSpace, + MemberNotFoundInSpace, OnEvent, RequestMutex, TooManyRequest, @@ -152,7 +152,16 @@ export class TeamWorkspaceResolver { description: 'invite link for workspace', nullable: true, }) - async inviteLink(@Parent() workspace: WorkspaceType) { + async inviteLink( + @Parent() workspace: WorkspaceType, + @CurrentUser() user: CurrentUser + ) { + await this.permissions.checkWorkspace( + workspace.id, + user.id, + Permission.Admin + ); + const cacheId = `workspace:inviteLink:${workspace.id}`; const id = await this.cache.get<{ inviteId: string }>(cacheId); if (id) { @@ -261,7 +270,7 @@ export class TeamWorkspaceResolver { } return new TooManyRequest(); } else { - return new NotInSpace({ spaceId: workspaceId }); + return new MemberNotFoundInSpace({ spaceId: workspaceId }); } } catch (e) { this.logger.error('failed to invite user', e); @@ -307,7 +316,7 @@ export class TeamWorkspaceResolver { return result; } else { - return new NotInSpace({ spaceId: workspaceId }); + return new MemberNotFoundInSpace({ spaceId: workspaceId }); } } catch (e) { this.logger.error('failed to invite user', e); diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 19763fda7e..aa59d72efc 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -209,7 +209,7 @@ type EditorType { name: String! } -union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType +union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WrongSignInCredentialsDataType enum ErrorNames { ACCESS_DENIED @@ -257,6 +257,7 @@ enum ErrorNames { INVALID_SUBSCRIPTION_PARAMETERS LINK_EXPIRED MAILER_SERVICE_IS_NOT_CONFIGURED + MEMBER_NOT_FOUND_IN_SPACE MEMBER_QUOTA_EXCEEDED MISSING_OAUTH_QUERY_PARAMETER NOT_FOUND @@ -472,6 +473,10 @@ input ManageUserInput { name: String } +type MemberNotFoundInSpaceDataType { + spaceId: String! +} + type MissingOauthQueryParameterDataType { name: String! } diff --git a/packages/backend/server/tests/team.e2e.ts b/packages/backend/server/tests/team.e2e.ts index e48d3f96f4..4be4b0c0a2 100644 --- a/packages/backend/server/tests/team.e2e.ts +++ b/packages/backend/server/tests/team.e2e.ts @@ -1,11 +1,16 @@ /// +import { randomUUID } from 'node:crypto'; + +import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud'; import { INestApplication } from '@nestjs/common'; import { WorkspaceMemberStatus } from '@prisma/client'; import type { TestFn } from 'ava'; import ava from 'ava'; +import Sinon from 'sinon'; import { AppModule } from '../src/app.module'; +import { EventEmitter } from '../src/base'; import { AuthService } from '../src/core/auth'; import { DocContentService } from '../src/core/doc-renderer'; import { Permission, PermissionService } from '../src/core/permission'; @@ -17,15 +22,20 @@ import { import { WorkspaceType } from '../src/core/workspaces'; import { acceptInviteById, + approveMember, createInviteLink, createTestingApp, createWorkspace, getInviteInfo, + getInviteLink, + getWorkspace, grantMember, inviteUser, inviteUsers, leaveWorkspace, PermissionEnum, + revokeInviteLink, + revokeUser, signUp, sleep, UserAuthedType, @@ -34,6 +44,7 @@ import { const test = ava as TestFn<{ app: INestApplication; auth: AuthService; + event: Sinon.SinonStubbedInstance; quota: QuotaService; quotaManager: QuotaManagementService; permissions: PermissionService; @@ -43,6 +54,9 @@ test.beforeEach(async t => { const { app } = await createTestingApp({ imports: [AppModule], tapModule: module => { + module + .overrideProvider(EventEmitter) + .useValue(Sinon.createStubInstance(EventEmitter)); module.overrideProvider(DocContentService).useValue({ getWorkspaceContent() { return { @@ -54,50 +68,84 @@ test.beforeEach(async t => { }, }); - const quota = app.get(QuotaService); - const quotaManager = app.get(QuotaManagementService); - const permissions = app.get(PermissionService); - const auth = app.get(AuthService); - t.context.app = app; - t.context.quota = quota; - t.context.quotaManager = quotaManager; - t.context.permissions = permissions; - t.context.auth = auth; + t.context.auth = app.get(AuthService); + t.context.event = app.get(EventEmitter); + t.context.quota = app.get(QuotaService); + t.context.quotaManager = app.get(QuotaManagementService); + t.context.permissions = app.get(PermissionService); }); test.afterEach.always(async t => { await t.context.app.close(); }); -const init = async (app: INestApplication, memberLimit = 10) => { - const owner = await signUp(app, 'test', 'test@affine.pro', '123456'); - const workspace = await createWorkspace(app, owner.token.token); +const init = async ( + app: INestApplication, + memberLimit = 10, + prefix = randomUUID() +) => { + const owner = await signUp( + app, + 'owner', + `${prefix}owner@affine.pro`, + '123456' + ); + { + const quota = app.get(QuotaService); + await quota.switchUserQuota(owner.id, QuotaType.ProPlanV1); + } + const workspace = await createWorkspace(app, owner.token.token); const teamWorkspace = await createWorkspace(app, owner.token.token); - const quota = app.get(QuotaManagementService); - await quota.addTeamWorkspace(teamWorkspace.id, 'test'); - await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, { - memberLimit, - }); + { + const quota = app.get(QuotaManagementService); + await quota.addTeamWorkspace(teamWorkspace.id, 'test'); + await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, { + memberLimit, + }); + } const invite = async ( email: string, - permission: PermissionEnum = 'Write' + permission: PermissionEnum = 'Write', + shouldSendEmail: boolean = false ) => { const member = await signUp(app, email.split('@')[0], email, '123456'); - const inviteId = await inviteUser( - app, - owner.token.token, - teamWorkspace.id, - member.email, - permission - ); - await acceptInviteById(app, teamWorkspace.id, inviteId); + + { + // normal workspace + const inviteId = await inviteUser( + app, + owner.token.token, + workspace.id, + member.email, + permission, + shouldSendEmail + ); + await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail); + } + + { + // team workspace + const inviteId = await inviteUser( + app, + owner.token.token, + teamWorkspace.id, + member.email, + permission, + shouldSendEmail + ); + await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail); + } + return member; }; - const inviteBatch = async (emails: string[]) => { + const inviteBatch = async ( + emails: string[], + shouldSendEmail: boolean = false + ) => { const members = []; for (const email of emails) { const member = await signUp(app, email.split('@')[0], email, '123456'); @@ -107,7 +155,8 @@ const init = async (app: INestApplication, memberLimit = 10) => { app, owner.token.token, teamWorkspace.id, - emails + emails, + shouldSendEmail ); return [members, invites] as const; }; @@ -122,9 +171,18 @@ const init = async (app: INestApplication, memberLimit = 10) => { const inviteId = link.split('/').pop()!; return [ inviteId, - async (email: string): Promise => { + async ( + email: string, + shouldSendEmail: boolean = false + ): Promise => { const member = await signUp(app, email.split('@')[0], email, '123456'); - await acceptInviteById(app, ws.id, inviteId, false, member.token.token); + await acceptInviteById( + app, + ws.id, + inviteId, + shouldSendEmail, + member.token.token + ); return member; }, async (token: string) => { @@ -133,9 +191,9 @@ const init = async (app: INestApplication, memberLimit = 10) => { ] as const; }; - const admin = await invite('admin@affine.pro', 'Admin'); - const write = await invite('member1@affine.pro'); - const read = await invite('member2@affine.pro', 'Read'); + const admin = await invite(`${prefix}admin@affine.pro`, 'Admin'); + const write = await invite(`${prefix}write@affine.pro`); + const read = await invite(`${prefix}read@affine.pro`, 'Read'); return { invite, @@ -150,6 +208,57 @@ const init = async (app: INestApplication, memberLimit = 10) => { }; }; +test('should be able to invite multiple users', async t => { + const { app } = t.context; + const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 4); + + { + // no permission + await t.throwsAsync( + inviteUsers(app, read.token.token, ws.id, ['test@affine.pro']), + { instanceOf: Error }, + 'should throw error if not manager' + ); + await t.throwsAsync( + inviteUsers(app, write.token.token, ws.id, ['test@affine.pro']), + { instanceOf: Error }, + 'should throw error if not manager' + ); + } + + { + // manager + const m1 = await signUp(app, 'm1', 'm1@affine.pro', '123456'); + const m2 = await signUp(app, 'm2', 'm2@affine.pro', '123456'); + t.is( + (await inviteUsers(app, owner.token.token, ws.id, [m1.email])).length, + 1, + 'should be able to invite user' + ); + t.is( + (await inviteUsers(app, admin.token.token, ws.id, [m2.email])).length, + 1, + 'should be able to invite user' + ); + t.is( + (await inviteUsers(app, admin.token.token, ws.id, [m2.email])).length, + 0, + 'should not be able to invite user if already in workspace' + ); + + await t.throwsAsync( + inviteUsers( + app, + admin.token.token, + ws.id, + Array.from({ length: 513 }, (_, i) => `m${i}@affine.pro`) + ), + { message: 'Too many requests.' }, + 'should throw error if exceed maximum number of invitations per request' + ); + } +}); + test('should be able to check seat limit', async t => { const { app, permissions, quotaManager } = t.context; const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 4); @@ -267,6 +376,161 @@ test('should be able to leave workspace', async t => { ); }); +test('should be able to revoke team member', async t => { + const { app } = t.context; + const { teamWorkspace: ws, owner, admin, write, read } = await init(app); + + { + // no permission + t.throwsAsync( + revokeUser(app, read.token.token, ws.id, read.id), + { instanceOf: Error }, + 'should throw error if not admin' + ); + t.throwsAsync( + revokeUser(app, read.token.token, ws.id, write.id), + { instanceOf: Error }, + 'should throw error if not admin' + ); + } + + { + // manager + t.true( + await revokeUser(app, admin.token.token, ws.id, read.id), + 'admin should be able to revoke member' + ); + + t.true( + await revokeUser(app, owner.token.token, ws.id, write.id), + 'owner should be able to revoke member' + ); + + await t.throwsAsync( + revokeUser(app, admin.token.token, ws.id, admin.id), + { instanceOf: Error }, + 'should not be able to revoke themselves' + ); + + t.false( + await revokeUser(app, owner.token.token, ws.id, owner.id), + 'should not be able to revoke themselves' + ); + + await revokeUser(app, owner.token.token, ws.id, admin.id); + await t.throwsAsync( + revokeUser(app, admin.token.token, ws.id, read.id), + { instanceOf: Error }, + 'should not be able to revoke member not in workspace' + ); + } +}); + +test('should be able to manage invite link', async t => { + const { app } = t.context; + const { + workspace: ws, + teamWorkspace: tws, + owner, + admin, + write, + read, + } = await init(app, 4); + + for (const workspace of [ws, tws]) { + for (const manager of [owner, admin]) { + const { link } = await createInviteLink( + app, + manager.token.token, + workspace.id, + 'OneDay' + ); + const { link: currLink } = await getInviteLink( + app, + manager.token.token, + workspace.id + ); + t.is(link, currLink, 'should be able to get invite link'); + + t.true( + await revokeInviteLink(app, manager.token.token, workspace.id), + 'should be able to revoke invite link' + ); + } + + for (const collaborator of [write, read]) { + await t.throwsAsync( + createInviteLink(app, collaborator.token.token, workspace.id, 'OneDay'), + { instanceOf: Error }, + 'should throw error if not manager' + ); + await t.throwsAsync( + getInviteLink(app, collaborator.token.token, workspace.id), + { instanceOf: Error }, + 'should throw error if not manager' + ); + await t.throwsAsync( + revokeInviteLink(app, collaborator.token.token, workspace.id), + { instanceOf: Error }, + 'should throw error if not manager' + ); + } + } +}); + +test('should be able to approve team member', async t => { + const { app } = t.context; + const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 5); + + { + const { link } = await createInviteLink( + app, + owner.token.token, + tws.id, + 'OneDay' + ); + const inviteId = link.split('/').pop()!; + + const member = await signUp( + app, + 'newmember', + 'newmember@affine.pro', + '123456' + ); + t.true( + await acceptInviteById(app, tws.id, inviteId, false, member.token.token), + 'should be able to accept invite' + ); + + const { members } = await getWorkspace(app, owner.token.token, tws.id); + const memberInvite = members.find(m => m.id === member.id)!; + t.is(memberInvite.status, 'UnderReview', 'should be under review'); + + t.is( + await approveMember(app, admin.token.token, tws.id, member.id), + memberInvite.inviteId + ); + } + + { + await t.throwsAsync( + approveMember(app, admin.token.token, tws.id, 'not_exists_id'), + { instanceOf: Error }, + 'should throw error if member not exists' + ); + await t.throwsAsync( + approveMember(app, write.token.token, tws.id, 'not_exists_id'), + { instanceOf: Error }, + 'should throw error if not manager' + ); + await t.throwsAsync( + approveMember(app, read.token.token, tws.id, 'not_exists_id'), + { instanceOf: Error }, + 'should throw error if not manager' + ); + } +}); + test('should be able to invite by link', async t => { const { app, permissions, quotaManager } = t.context; const { @@ -291,19 +555,21 @@ test('should be able to invite by link', async t => { { // invite link - const t1 = await invite('test1@affine.pro'); - const t2 = await invite('test2@affine.pro'); + for (const [i] of Array.from({ length: 6 }).entries()) { + const user = await invite(`test${i}@affine.pro`); + const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id); + t.is( + status, + WorkspaceMemberStatus.Accepted, + 'should be able to check status' + ); + } await t.throwsAsync( - invite('test3@affine.pro'), + invite('exceed@affine.pro'), { message: 'You have exceeded your workspace member quota.' }, 'should throw error if exceed member limit' ); - - const s1 = await permissions.getWorkspaceMemberStatus(ws.id, t1.id); - t.is(s1, WorkspaceMemberStatus.Accepted, 'should be able to check status'); - const s2 = await permissions.getWorkspaceMemberStatus(ws.id, t2.id); - t.is(s2, WorkspaceMemberStatus.Accepted, 'should be able to check status'); } { @@ -361,3 +627,56 @@ test('should be able to invite by link', async t => { } } }); + +test('should be able to send mails', async t => { + const { app } = t.context; + const { inviteBatch } = await init(app, 4); + const primitiveMailCount = await getCurrentMailMessageCount(); + + { + await inviteBatch(['m3@affine.pro', 'm4@affine.pro'], true); + t.is(await getCurrentMailMessageCount(), primitiveMailCount + 2); + } +}); + +test('should be able to emit events', async t => { + const { app, event } = t.context; + + { + const { teamWorkspace: tws, inviteBatch } = await init(app, 4); + + await inviteBatch(['m1@affine.pro', 'm2@affine.pro']); + t.true( + event.emit.calledOnceWith('workspace.members.updated', { + workspaceId: tws.id, + count: 6, + }) + ); + } + + { + const { teamWorkspace: tws, owner, createInviteLink } = await init(app, 10); + const [, invite] = await createInviteLink(tws); + const user = await invite('m3@affine.pro'); + const { members } = await getWorkspace(app, owner.token.token, tws.id); + const memberInvite = members.find(m => m.id === user.id)!; + t.deepEqual( + event.emit.lastCall.args, + [ + 'workspace.members.reviewRequested', + { inviteId: memberInvite.inviteId }, + ], + 'should emit review requested event' + ); + + await revokeUser(app, owner.token.token, tws.id, user.id); + t.deepEqual( + event.emit.lastCall.args, + [ + 'workspace.members.requestDeclined', + { inviteId: memberInvite.inviteId }, + ], + 'should emit review requested event' + ); + } +}); diff --git a/packages/backend/server/tests/utils/invite.ts b/packages/backend/server/tests/utils/invite.ts index a995476e90..218ddad371 100644 --- a/packages/backend/server/tests/utils/invite.ts +++ b/packages/backend/server/tests/utils/invite.ts @@ -65,6 +65,34 @@ export async function inviteUsers( return res.body.data.inviteBatch; } +export async function getInviteLink( + app: INestApplication, + token: string, + workspaceId: string +): Promise<{ link: string; expireTime: string }> { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + workspace(id: "${workspaceId}") { + inviteLink { + link + expireTime + } + } + } + `, + }) + .expect(200); + if (res.body.errors) { + throw new Error(res.body.errors[0].message); + } + return res.body.data.workspace.inviteLink; +} + export async function createInviteLink( app: INestApplication, token: string, @@ -92,6 +120,29 @@ export async function createInviteLink( return res.body.data.createInviteLink; } +export async function revokeInviteLink( + app: INestApplication, + token: string, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + revokeInviteLink(workspaceId: "${workspaceId}") + } + `, + }) + .expect(200); + if (res.body.errors) { + throw new Error(res.body.errors[0].message); + } + return res.body.data.revokeInviteLink; +} + export async function acceptInviteById( app: INestApplication, workspaceId: string, @@ -119,6 +170,32 @@ export async function acceptInviteById( return res.body.data.acceptInviteById; } +export async function approveMember( + app: INestApplication, + token: string, + workspaceId: string, + userId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .auth(token, { type: 'bearer' }) + .send({ + query: ` + mutation { + approveMember(workspaceId: "${workspaceId}", userId: "${userId}") + } + `, + }) + .expect(200); + if (res.body.errors) { + throw new Error(res.body.errors[0].message, { + cause: res.body.errors[0].cause, + }); + } + return res.body.data.approveMember; +} + export async function leaveWorkspace( app: INestApplication, token: string, @@ -161,6 +238,11 @@ export async function revokeUser( `, }) .expect(200); + if (res.body.errors) { + throw new Error(res.body.errors[0].message, { + cause: res.body.errors[0].cause, + }); + } return res.body.data.revoke; } diff --git a/packages/backend/server/tests/utils/workspace.ts b/packages/backend/server/tests/utils/workspace.ts index a2cf6c2716..8fcdde1553 100644 --- a/packages/backend/server/tests/utils/workspace.ts +++ b/packages/backend/server/tests/utils/workspace.ts @@ -71,7 +71,7 @@ export async function getWorkspace( query: ` query { workspace(id: "${workspaceId}") { - id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId } + id, members(skip: ${skip}, take: ${take}) { id, name, email, permission, inviteId, status } } } `, diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 4baeb351ad..7085df1e23 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -273,6 +273,7 @@ export type ErrorDataUnion = | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType + | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType @@ -334,6 +335,7 @@ export enum ErrorNames { INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS', LINK_EXPIRED = 'LINK_EXPIRED', MAILER_SERVICE_IS_NOT_CONFIGURED = 'MAILER_SERVICE_IS_NOT_CONFIGURED', + MEMBER_NOT_FOUND_IN_SPACE = 'MEMBER_NOT_FOUND_IN_SPACE', MEMBER_QUOTA_EXCEEDED = 'MEMBER_QUOTA_EXCEEDED', MISSING_OAUTH_QUERY_PARAMETER = 'MISSING_OAUTH_QUERY_PARAMETER', NOT_FOUND = 'NOT_FOUND', @@ -541,6 +543,11 @@ export interface ManageUserInput { name?: InputMaybe; } +export interface MemberNotFoundInSpaceDataType { + __typename?: 'MemberNotFoundInSpaceDataType'; + spaceId: Scalars['String']['output']; +} + export interface MissingOauthQueryParameterDataType { __typename?: 'MissingOauthQueryParameterDataType'; name: Scalars['String']['output'];