test(server): add more test for team workspace (#9182)

This commit is contained in:
darkskygit
2024-12-17 08:42:19 +00:00
parent 95d1a4a27d
commit 27d4aa7ca7
8 changed files with 486 additions and 48 deletions

View File

@@ -347,6 +347,11 @@ export const USER_FRIENDLY_ERRORS = {
args: { spaceId: 'string' }, args: { spaceId: 'string' },
message: ({ spaceId }) => `Space ${spaceId} not found.`, 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: { not_in_space: {
type: 'action_forbidden', type: 'action_forbidden',
args: { spaceId: 'string' }, args: { spaceId: 'string' },

View File

@@ -191,6 +191,16 @@ export class SpaceNotFound extends UserFriendlyError {
} }
} }
@ObjectType() @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 { class NotInSpaceDataType {
@Field() spaceId!: string @Field() spaceId!: string
} }
@@ -615,6 +625,7 @@ export enum ErrorNames {
ACCESS_DENIED, ACCESS_DENIED,
EMAIL_VERIFICATION_REQUIRED, EMAIL_VERIFICATION_REQUIRED,
SPACE_NOT_FOUND, SPACE_NOT_FOUND,
MEMBER_NOT_FOUND_IN_SPACE,
NOT_IN_SPACE, NOT_IN_SPACE,
ALREADY_IN_SPACE, ALREADY_IN_SPACE,
SPACE_ACCESS_DENIED, SPACE_ACCESS_DENIED,
@@ -674,5 +685,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({ export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion', name: 'ErrorDataUnion',
types: () => 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,
}); });

View File

@@ -13,7 +13,7 @@ import {
Cache, Cache,
EventEmitter, EventEmitter,
type EventPayload, type EventPayload,
NotInSpace, MemberNotFoundInSpace,
OnEvent, OnEvent,
RequestMutex, RequestMutex,
TooManyRequest, TooManyRequest,
@@ -152,7 +152,16 @@ export class TeamWorkspaceResolver {
description: 'invite link for workspace', description: 'invite link for workspace',
nullable: true, 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 cacheId = `workspace:inviteLink:${workspace.id}`;
const id = await this.cache.get<{ inviteId: string }>(cacheId); const id = await this.cache.get<{ inviteId: string }>(cacheId);
if (id) { if (id) {
@@ -261,7 +270,7 @@ export class TeamWorkspaceResolver {
} }
return new TooManyRequest(); return new TooManyRequest();
} else { } else {
return new NotInSpace({ spaceId: workspaceId }); return new MemberNotFoundInSpace({ spaceId: workspaceId });
} }
} catch (e) { } catch (e) {
this.logger.error('failed to invite user', e); this.logger.error('failed to invite user', e);
@@ -307,7 +316,7 @@ export class TeamWorkspaceResolver {
return result; return result;
} else { } else {
return new NotInSpace({ spaceId: workspaceId }); return new MemberNotFoundInSpace({ spaceId: workspaceId });
} }
} catch (e) { } catch (e) {
this.logger.error('failed to invite user', e); this.logger.error('failed to invite user', e);

View File

@@ -209,7 +209,7 @@ type EditorType {
name: String! 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 { enum ErrorNames {
ACCESS_DENIED ACCESS_DENIED
@@ -257,6 +257,7 @@ 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_QUOTA_EXCEEDED MEMBER_QUOTA_EXCEEDED
MISSING_OAUTH_QUERY_PARAMETER MISSING_OAUTH_QUERY_PARAMETER
NOT_FOUND NOT_FOUND
@@ -472,6 +473,10 @@ input ManageUserInput {
name: String name: String
} }
type MemberNotFoundInSpaceDataType {
spaceId: String!
}
type MissingOauthQueryParameterDataType { type MissingOauthQueryParameterDataType {
name: String! name: String!
} }

View File

@@ -1,11 +1,16 @@
/// <reference types="../src/global.d.ts" /> /// <reference types="../src/global.d.ts" />
import { randomUUID } from 'node:crypto';
import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { WorkspaceMemberStatus } from '@prisma/client'; import { WorkspaceMemberStatus } from '@prisma/client';
import type { TestFn } from 'ava'; import type { TestFn } from 'ava';
import ava from 'ava'; import ava from 'ava';
import Sinon from 'sinon';
import { AppModule } from '../src/app.module'; import { AppModule } from '../src/app.module';
import { EventEmitter } from '../src/base';
import { AuthService } from '../src/core/auth'; import { AuthService } from '../src/core/auth';
import { DocContentService } from '../src/core/doc-renderer'; import { DocContentService } from '../src/core/doc-renderer';
import { Permission, PermissionService } from '../src/core/permission'; import { Permission, PermissionService } from '../src/core/permission';
@@ -17,15 +22,20 @@ import {
import { WorkspaceType } from '../src/core/workspaces'; import { WorkspaceType } from '../src/core/workspaces';
import { import {
acceptInviteById, acceptInviteById,
approveMember,
createInviteLink, createInviteLink,
createTestingApp, createTestingApp,
createWorkspace, createWorkspace,
getInviteInfo, getInviteInfo,
getInviteLink,
getWorkspace,
grantMember, grantMember,
inviteUser, inviteUser,
inviteUsers, inviteUsers,
leaveWorkspace, leaveWorkspace,
PermissionEnum, PermissionEnum,
revokeInviteLink,
revokeUser,
signUp, signUp,
sleep, sleep,
UserAuthedType, UserAuthedType,
@@ -34,6 +44,7 @@ import {
const test = ava as TestFn<{ const test = ava as TestFn<{
app: INestApplication; app: INestApplication;
auth: AuthService; auth: AuthService;
event: Sinon.SinonStubbedInstance<EventEmitter>;
quota: QuotaService; quota: QuotaService;
quotaManager: QuotaManagementService; quotaManager: QuotaManagementService;
permissions: PermissionService; permissions: PermissionService;
@@ -43,6 +54,9 @@ test.beforeEach(async t => {
const { app } = await createTestingApp({ const { app } = await createTestingApp({
imports: [AppModule], imports: [AppModule],
tapModule: module => { tapModule: module => {
module
.overrideProvider(EventEmitter)
.useValue(Sinon.createStubInstance(EventEmitter));
module.overrideProvider(DocContentService).useValue({ module.overrideProvider(DocContentService).useValue({
getWorkspaceContent() { getWorkspaceContent() {
return { 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.app = app;
t.context.quota = quota; t.context.auth = app.get(AuthService);
t.context.quotaManager = quotaManager; t.context.event = app.get(EventEmitter);
t.context.permissions = permissions; t.context.quota = app.get(QuotaService);
t.context.auth = auth; t.context.quotaManager = app.get(QuotaManagementService);
t.context.permissions = app.get(PermissionService);
}); });
test.afterEach.always(async t => { test.afterEach.always(async t => {
await t.context.app.close(); await t.context.app.close();
}); });
const init = async (app: INestApplication, memberLimit = 10) => { const init = async (
const owner = await signUp(app, 'test', 'test@affine.pro', '123456'); app: INestApplication,
const workspace = await createWorkspace(app, owner.token.token); 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 teamWorkspace = await createWorkspace(app, owner.token.token);
const quota = app.get(QuotaManagementService); {
await quota.addTeamWorkspace(teamWorkspace.id, 'test'); const quota = app.get(QuotaManagementService);
await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, { await quota.addTeamWorkspace(teamWorkspace.id, 'test');
memberLimit, await quota.updateWorkspaceConfig(teamWorkspace.id, QuotaType.TeamPlanV1, {
}); memberLimit,
});
}
const invite = async ( const invite = async (
email: string, email: string,
permission: PermissionEnum = 'Write' permission: PermissionEnum = 'Write',
shouldSendEmail: boolean = false
) => { ) => {
const member = await signUp(app, email.split('@')[0], email, '123456'); const member = await signUp(app, email.split('@')[0], email, '123456');
const inviteId = await inviteUser(
app, {
owner.token.token, // normal workspace
teamWorkspace.id, const inviteId = await inviteUser(
member.email, app,
permission owner.token.token,
); workspace.id,
await acceptInviteById(app, teamWorkspace.id, inviteId); 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; return member;
}; };
const inviteBatch = async (emails: string[]) => { const inviteBatch = async (
emails: string[],
shouldSendEmail: boolean = false
) => {
const members = []; const members = [];
for (const email of emails) { for (const email of emails) {
const member = await signUp(app, email.split('@')[0], email, '123456'); const member = await signUp(app, email.split('@')[0], email, '123456');
@@ -107,7 +155,8 @@ const init = async (app: INestApplication, memberLimit = 10) => {
app, app,
owner.token.token, owner.token.token,
teamWorkspace.id, teamWorkspace.id,
emails emails,
shouldSendEmail
); );
return [members, invites] as const; return [members, invites] as const;
}; };
@@ -122,9 +171,18 @@ const init = async (app: INestApplication, memberLimit = 10) => {
const inviteId = link.split('/').pop()!; const inviteId = link.split('/').pop()!;
return [ return [
inviteId, inviteId,
async (email: string): Promise<UserAuthedType> => { async (
email: string,
shouldSendEmail: boolean = false
): Promise<UserAuthedType> => {
const member = await signUp(app, email.split('@')[0], email, '123456'); 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; return member;
}, },
async (token: string) => { async (token: string) => {
@@ -133,9 +191,9 @@ const init = async (app: INestApplication, memberLimit = 10) => {
] as const; ] as const;
}; };
const admin = await invite('admin@affine.pro', 'Admin'); const admin = await invite(`${prefix}admin@affine.pro`, 'Admin');
const write = await invite('member1@affine.pro'); const write = await invite(`${prefix}write@affine.pro`);
const read = await invite('member2@affine.pro', 'Read'); const read = await invite(`${prefix}read@affine.pro`, 'Read');
return { return {
invite, 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 => { test('should be able to check seat limit', async t => {
const { app, permissions, quotaManager } = t.context; const { app, permissions, quotaManager } = t.context;
const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 4); 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 => { test('should be able to invite by link', async t => {
const { app, permissions, quotaManager } = t.context; const { app, permissions, quotaManager } = t.context;
const { const {
@@ -291,19 +555,21 @@ test('should be able to invite by link', async t => {
{ {
// invite link // invite link
const t1 = await invite('test1@affine.pro'); for (const [i] of Array.from({ length: 6 }).entries()) {
const t2 = await invite('test2@affine.pro'); 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( await t.throwsAsync(
invite('test3@affine.pro'), invite('exceed@affine.pro'),
{ message: 'You have exceeded your workspace member quota.' }, { message: 'You have exceeded your workspace member quota.' },
'should throw error if exceed member limit' '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'
);
}
});

View File

@@ -65,6 +65,34 @@ export async function inviteUsers(
return res.body.data.inviteBatch; 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( export async function createInviteLink(
app: INestApplication, app: INestApplication,
token: string, token: string,
@@ -92,6 +120,29 @@ export async function createInviteLink(
return res.body.data.createInviteLink; return res.body.data.createInviteLink;
} }
export async function revokeInviteLink(
app: INestApplication,
token: string,
workspaceId: string
): Promise<boolean> {
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( export async function acceptInviteById(
app: INestApplication, app: INestApplication,
workspaceId: string, workspaceId: string,
@@ -119,6 +170,32 @@ export async function acceptInviteById(
return res.body.data.acceptInviteById; return res.body.data.acceptInviteById;
} }
export async function approveMember(
app: INestApplication,
token: string,
workspaceId: string,
userId: string
): Promise<string> {
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( export async function leaveWorkspace(
app: INestApplication, app: INestApplication,
token: string, token: string,
@@ -161,6 +238,11 @@ export async function revokeUser(
`, `,
}) })
.expect(200); .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; return res.body.data.revoke;
} }

View File

@@ -71,7 +71,7 @@ export async function getWorkspace(
query: ` query: `
query { query {
workspace(id: "${workspaceId}") { 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 }
} }
} }
`, `,

View File

@@ -273,6 +273,7 @@ export type ErrorDataUnion =
| InvalidHistoryTimestampDataType | InvalidHistoryTimestampDataType
| InvalidPasswordLengthDataType | InvalidPasswordLengthDataType
| InvalidRuntimeConfigTypeDataType | InvalidRuntimeConfigTypeDataType
| MemberNotFoundInSpaceDataType
| MissingOauthQueryParameterDataType | MissingOauthQueryParameterDataType
| NotInSpaceDataType | NotInSpaceDataType
| RuntimeConfigNotFoundDataType | RuntimeConfigNotFoundDataType
@@ -334,6 +335,7 @@ export enum ErrorNames {
INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS', INVALID_SUBSCRIPTION_PARAMETERS = 'INVALID_SUBSCRIPTION_PARAMETERS',
LINK_EXPIRED = 'LINK_EXPIRED', LINK_EXPIRED = 'LINK_EXPIRED',
MAILER_SERVICE_IS_NOT_CONFIGURED = 'MAILER_SERVICE_IS_NOT_CONFIGURED', 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', MEMBER_QUOTA_EXCEEDED = 'MEMBER_QUOTA_EXCEEDED',
MISSING_OAUTH_QUERY_PARAMETER = 'MISSING_OAUTH_QUERY_PARAMETER', MISSING_OAUTH_QUERY_PARAMETER = 'MISSING_OAUTH_QUERY_PARAMETER',
NOT_FOUND = 'NOT_FOUND', NOT_FOUND = 'NOT_FOUND',
@@ -541,6 +543,11 @@ export interface ManageUserInput {
name?: InputMaybe<Scalars['String']['input']>; name?: InputMaybe<Scalars['String']['input']>;
} }
export interface MemberNotFoundInSpaceDataType {
__typename?: 'MemberNotFoundInSpaceDataType';
spaceId: Scalars['String']['output'];
}
export interface MissingOauthQueryParameterDataType { export interface MissingOauthQueryParameterDataType {
__typename?: 'MissingOauthQueryParameterDataType'; __typename?: 'MissingOauthQueryParameterDataType';
name: Scalars['String']['output']; name: Scalars['String']['output'];