mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
941 lines
24 KiB
TypeScript
941 lines
24 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
|
|
import { User, WorkspaceMemberStatus } from '@prisma/client';
|
|
import type { TestFn } from 'ava';
|
|
import ava from 'ava';
|
|
import { nanoid } from 'nanoid';
|
|
import Sinon from 'sinon';
|
|
|
|
import { AppModule } from '../app.module';
|
|
import { EventBus } from '../base';
|
|
import { AuthService } from '../core/auth';
|
|
import { DocReader } from '../core/doc';
|
|
import { DocRole, WorkspaceRole } from '../core/permission';
|
|
import { WorkspaceType } from '../core/workspaces';
|
|
import { Models } from '../models';
|
|
import {
|
|
acceptInviteById,
|
|
approveMember,
|
|
createInviteLink,
|
|
createTestingApp,
|
|
createWorkspace,
|
|
docGrantedUsersList,
|
|
getInviteInfo,
|
|
getInviteLink,
|
|
getWorkspace,
|
|
grantDocUserRoles,
|
|
grantMember,
|
|
inviteUser,
|
|
inviteUsers,
|
|
leaveWorkspace,
|
|
revokeDocUserRoles,
|
|
revokeInviteLink,
|
|
revokeMember,
|
|
revokeUser,
|
|
sleep,
|
|
TestingApp,
|
|
updateDocDefaultRole,
|
|
} from './utils';
|
|
|
|
const test = ava as TestFn<{
|
|
app: TestingApp;
|
|
auth: AuthService;
|
|
event: Sinon.SinonStubbedInstance<EventBus>;
|
|
models: Models;
|
|
}>;
|
|
|
|
test.before(async t => {
|
|
const app = await createTestingApp({
|
|
imports: [AppModule],
|
|
tapModule: module => {
|
|
module
|
|
.overrideProvider(EventBus)
|
|
.useValue(Sinon.createStubInstance(EventBus));
|
|
module.overrideProvider(DocReader).useValue({
|
|
getWorkspaceContent() {
|
|
return {
|
|
name: 'test',
|
|
avatarKey: null,
|
|
};
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
t.context.app = app;
|
|
t.context.auth = app.get(AuthService);
|
|
t.context.event = app.get(EventBus);
|
|
t.context.models = app.get(Models);
|
|
});
|
|
|
|
test.beforeEach(async t => {
|
|
await t.context.app.initTestingDB();
|
|
});
|
|
|
|
test.after.always(async t => {
|
|
await t.context.app.close();
|
|
});
|
|
|
|
const init = async (
|
|
app: TestingApp,
|
|
memberLimit = 10,
|
|
prefix = randomUUID()
|
|
) => {
|
|
const owner = await app.signupV1(`${prefix}owner@affine.pro`);
|
|
const models = app.get(Models);
|
|
{
|
|
await models.userFeature.add(owner.id, 'pro_plan_v1', 'test');
|
|
}
|
|
|
|
const workspace = await createWorkspace(app);
|
|
const teamWorkspace = await createWorkspace(app);
|
|
{
|
|
models.workspaceFeature.add(teamWorkspace.id, 'team_plan_v1', 'test', {
|
|
memberLimit,
|
|
});
|
|
}
|
|
|
|
const invite = async (
|
|
email: string,
|
|
permission: WorkspaceRole = WorkspaceRole.Collaborator,
|
|
shouldSendEmail: boolean = false
|
|
) => {
|
|
const member = await app.signupV1(email);
|
|
|
|
{
|
|
// normal workspace
|
|
app.switchUser(owner);
|
|
const inviteId = await inviteUser(
|
|
app,
|
|
workspace.id,
|
|
member.email,
|
|
shouldSendEmail
|
|
);
|
|
app.switchUser(member);
|
|
await acceptInviteById(app, workspace.id, inviteId, shouldSendEmail);
|
|
}
|
|
|
|
{
|
|
// team workspace
|
|
app.switchUser(owner);
|
|
const inviteId = await inviteUser(
|
|
app,
|
|
teamWorkspace.id,
|
|
member.email,
|
|
shouldSendEmail
|
|
);
|
|
app.switchUser(member);
|
|
await acceptInviteById(app, teamWorkspace.id, inviteId, shouldSendEmail);
|
|
app.switchUser(owner);
|
|
await grantMember(app, teamWorkspace.id, member.id, permission);
|
|
}
|
|
|
|
return member;
|
|
};
|
|
|
|
const inviteBatch = async (
|
|
emails: string[],
|
|
shouldSendEmail: boolean = false
|
|
) => {
|
|
const members = [];
|
|
for (const email of emails) {
|
|
const member = await app.signupV1(email);
|
|
members.push(member);
|
|
}
|
|
|
|
app.switchUser(owner);
|
|
const invites = await inviteUsers(
|
|
app,
|
|
teamWorkspace.id,
|
|
emails,
|
|
shouldSendEmail
|
|
);
|
|
return [members, invites] as const;
|
|
};
|
|
|
|
const getCreateInviteLinkFetcher = async (ws: WorkspaceType) => {
|
|
app.switchUser(owner);
|
|
const { link } = await createInviteLink(app, ws.id, 'OneDay');
|
|
const inviteId = link.split('/').pop()!;
|
|
return [
|
|
inviteId,
|
|
async (email: string, shouldSendEmail: boolean = false) => {
|
|
const member = await app.signupV1(email);
|
|
await acceptInviteById(app, ws.id, inviteId, shouldSendEmail);
|
|
return member;
|
|
},
|
|
async (userId: string) => {
|
|
app.switchUser(userId);
|
|
await acceptInviteById(app, ws.id, inviteId, false);
|
|
},
|
|
] as const;
|
|
};
|
|
|
|
const admin = await invite(`${prefix}admin@affine.pro`, WorkspaceRole.Admin);
|
|
const write = await invite(`${prefix}write@affine.pro`);
|
|
const read = await invite(
|
|
`${prefix}read@affine.pro`,
|
|
WorkspaceRole.Collaborator
|
|
);
|
|
|
|
const external = await invite(
|
|
`${prefix}external@affine.pro`,
|
|
WorkspaceRole.External
|
|
);
|
|
|
|
app.switchUser(owner.id);
|
|
return {
|
|
invite,
|
|
inviteBatch,
|
|
createInviteLink: getCreateInviteLinkFetcher,
|
|
owner,
|
|
workspace,
|
|
teamWorkspace,
|
|
admin,
|
|
write,
|
|
read,
|
|
external,
|
|
};
|
|
};
|
|
|
|
test('should be able to invite multiple users', async t => {
|
|
const { app } = t.context;
|
|
const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 5);
|
|
|
|
{
|
|
// no permission
|
|
app.switchUser(read);
|
|
await t.throwsAsync(
|
|
inviteUsers(app, ws.id, ['test@affine.pro']),
|
|
{ instanceOf: Error },
|
|
'should throw error if not manager'
|
|
);
|
|
app.switchUser(write);
|
|
await t.throwsAsync(
|
|
inviteUsers(app, ws.id, ['test@affine.pro']),
|
|
{ instanceOf: Error },
|
|
'should throw error if not manager'
|
|
);
|
|
}
|
|
|
|
{
|
|
// manager
|
|
const m1 = await app.signupV1('m1@affine.pro');
|
|
const m2 = await app.signupV1('m2@affine.pro');
|
|
app.switchUser(owner);
|
|
t.is(
|
|
(await inviteUsers(app, ws.id, [m1.email])).length,
|
|
1,
|
|
'should be able to invite user'
|
|
);
|
|
app.switchUser(admin);
|
|
t.is(
|
|
(await inviteUsers(app, ws.id, [m2.email])).length,
|
|
1,
|
|
'should be able to invite user'
|
|
);
|
|
t.is(
|
|
(await inviteUsers(app, ws.id, [m2.email])).length,
|
|
0,
|
|
'should not be able to invite user if already in workspace'
|
|
);
|
|
|
|
await t.throwsAsync(
|
|
inviteUsers(
|
|
app,
|
|
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, models } = t.context;
|
|
const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 5);
|
|
|
|
{
|
|
// invite
|
|
await t.throwsAsync(
|
|
invite('member3@affine.pro', WorkspaceRole.Collaborator),
|
|
{ message: 'You have exceeded your workspace member quota.' },
|
|
'should throw error if exceed member limit'
|
|
);
|
|
models.workspaceFeature.add(ws.id, 'team_plan_v1', 'test', {
|
|
memberLimit: 6,
|
|
});
|
|
await t.notThrowsAsync(
|
|
invite('member4@affine.pro', WorkspaceRole.Collaborator),
|
|
'should not throw error if not exceed member limit'
|
|
);
|
|
}
|
|
|
|
{
|
|
const members1 = inviteBatch(['member5@affine.pro']);
|
|
// invite batch
|
|
await t.notThrowsAsync(
|
|
members1,
|
|
'should not throw error in batch invite event reach limit'
|
|
);
|
|
|
|
t.is(
|
|
(await models.workspaceUser.get(ws.id, (await members1)[0][0].id))
|
|
?.status,
|
|
WorkspaceMemberStatus.NeedMoreSeat,
|
|
'should be able to check member status'
|
|
);
|
|
|
|
// refresh seat, fifo
|
|
await sleep(1000);
|
|
const [[members2]] = await inviteBatch(['member6@affine.pro']);
|
|
await models.workspaceUser.refresh(ws.id, 7);
|
|
|
|
t.is(
|
|
(await models.workspaceUser.get(ws.id, (await members1)[0][0].id))
|
|
?.status,
|
|
WorkspaceMemberStatus.Pending,
|
|
'should become accepted after refresh'
|
|
);
|
|
t.is(
|
|
(await models.workspaceUser.get(ws.id, members2.id))?.status,
|
|
WorkspaceMemberStatus.NeedMoreSeat,
|
|
'should not change status'
|
|
);
|
|
}
|
|
});
|
|
|
|
test('should be able to grant team member permission', async t => {
|
|
const { app, models } = t.context;
|
|
const { owner, teamWorkspace: ws, write, read } = await init(app);
|
|
|
|
app.switchUser(read);
|
|
await t.throwsAsync(
|
|
grantMember(app, ws.id, write.id, WorkspaceRole.Collaborator),
|
|
{ instanceOf: Error },
|
|
'should throw error if not owner'
|
|
);
|
|
|
|
app.switchUser(write);
|
|
await t.throwsAsync(
|
|
grantMember(app, ws.id, read.id, WorkspaceRole.Collaborator),
|
|
{ instanceOf: Error },
|
|
'should throw error if not owner'
|
|
);
|
|
|
|
{
|
|
// owner should be able to grant permission
|
|
app.switchUser(owner);
|
|
t.true(
|
|
(await models.workspaceUser.get(ws.id, read.id))?.type ===
|
|
WorkspaceRole.Collaborator,
|
|
'should be able to check permission'
|
|
);
|
|
t.truthy(
|
|
await grantMember(app, ws.id, read.id, WorkspaceRole.Admin),
|
|
'should be able to grant permission'
|
|
);
|
|
t.true(
|
|
(await models.workspaceUser.get(ws.id, read.id))?.type ===
|
|
WorkspaceRole.Admin,
|
|
'should be able to check permission'
|
|
);
|
|
}
|
|
});
|
|
|
|
test('should be able to leave workspace', async t => {
|
|
const { app } = t.context;
|
|
const { owner, teamWorkspace: ws, admin, write, read } = await init(app);
|
|
|
|
app.switchUser(owner);
|
|
await t.throwsAsync(leaveWorkspace(app, ws.id), {
|
|
message: 'Owner can not leave the workspace.',
|
|
});
|
|
|
|
app.switchUser(admin);
|
|
t.true(
|
|
await leaveWorkspace(app, ws.id),
|
|
'admin should be able to leave workspace'
|
|
);
|
|
|
|
app.switchUser(write);
|
|
t.true(
|
|
await leaveWorkspace(app, ws.id),
|
|
'write should be able to leave workspace'
|
|
);
|
|
|
|
app.switchUser(read);
|
|
t.true(
|
|
await leaveWorkspace(app, ws.id),
|
|
'read should be able to leave workspace'
|
|
);
|
|
});
|
|
|
|
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
|
|
app.switchUser(read);
|
|
await t.throwsAsync(
|
|
revokeUser(app, ws.id, read.id),
|
|
{ instanceOf: Error },
|
|
'should throw error if not admin'
|
|
);
|
|
await t.throwsAsync(
|
|
revokeUser(app, ws.id, write.id),
|
|
{ instanceOf: Error },
|
|
'should throw error if not admin'
|
|
);
|
|
}
|
|
|
|
{
|
|
// manager
|
|
app.switchUser(admin);
|
|
t.true(
|
|
await revokeUser(app, ws.id, read.id),
|
|
'admin should be able to revoke member'
|
|
);
|
|
|
|
await t.throwsAsync(
|
|
revokeUser(app, ws.id, admin.id),
|
|
{ instanceOf: Error },
|
|
'should not be able to revoke themselves'
|
|
);
|
|
|
|
app.switchUser(owner);
|
|
t.true(
|
|
await revokeUser(app, ws.id, write.id),
|
|
'owner should be able to revoke member'
|
|
);
|
|
|
|
await t.throwsAsync(revokeUser(app, ws.id, owner.id), {
|
|
message: 'You can not revoke your own permission.',
|
|
});
|
|
|
|
await revokeUser(app, ws.id, admin.id);
|
|
app.switchUser(admin);
|
|
await t.throwsAsync(
|
|
revokeUser(app, ws.id, read.id),
|
|
{ instanceOf: Error },
|
|
'should not be able to revoke member not in workspace after revoked'
|
|
);
|
|
}
|
|
});
|
|
|
|
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);
|
|
|
|
for (const [workspace, managers] of [
|
|
[ws, [owner]],
|
|
[tws, [owner, admin]],
|
|
] as const) {
|
|
for (const manager of managers) {
|
|
app.switchUser(manager.id);
|
|
const { link } = await createInviteLink(app, workspace.id, 'OneDay');
|
|
const { link: currLink } = await getInviteLink(app, workspace.id);
|
|
t.is(link, currLink, 'should be able to get invite link');
|
|
|
|
t.true(
|
|
await revokeInviteLink(app, workspace.id),
|
|
'should be able to revoke invite link'
|
|
);
|
|
}
|
|
|
|
for (const collaborator of [write, read]) {
|
|
app.switchUser(collaborator.id);
|
|
await t.throwsAsync(
|
|
createInviteLink(app, workspace.id, 'OneDay'),
|
|
{ instanceOf: Error },
|
|
'should throw error if not manager'
|
|
);
|
|
await t.throwsAsync(
|
|
getInviteLink(app, workspace.id),
|
|
{ instanceOf: Error },
|
|
'should throw error if not manager'
|
|
);
|
|
await t.throwsAsync(
|
|
revokeInviteLink(app, 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, 6);
|
|
|
|
{
|
|
app.switchUser(owner);
|
|
const { link } = await createInviteLink(app, tws.id, 'OneDay');
|
|
const inviteId = link.split('/').pop()!;
|
|
|
|
const member = await app.signupV1('newmember@affine.pro');
|
|
t.true(
|
|
await acceptInviteById(app, tws.id, inviteId, false),
|
|
'should be able to accept invite'
|
|
);
|
|
|
|
app.switchUser(owner);
|
|
const { members } = await getWorkspace(app, tws.id);
|
|
const memberInvite = members.find(m => m.id === member.id)!;
|
|
t.is(memberInvite.status, 'UnderReview', 'should be under review');
|
|
|
|
t.true(await approveMember(app, tws.id, member.id));
|
|
const requestApprovedNotification = app.queue.last(
|
|
'notification.sendInvitationReviewApproved'
|
|
);
|
|
t.truthy(requestApprovedNotification);
|
|
t.deepEqual(
|
|
requestApprovedNotification.payload,
|
|
{
|
|
inviteId: memberInvite.inviteId,
|
|
reviewerId: owner.id,
|
|
},
|
|
'should send review approved notification'
|
|
);
|
|
}
|
|
|
|
{
|
|
app.switchUser(admin);
|
|
await t.throwsAsync(
|
|
approveMember(app, tws.id, 'not_exists_id'),
|
|
{ instanceOf: Error },
|
|
'should throw error if member not exists'
|
|
);
|
|
|
|
app.switchUser(write);
|
|
await t.throwsAsync(
|
|
approveMember(app, tws.id, 'not_exists_id'),
|
|
{ instanceOf: Error },
|
|
'should throw error if not manager'
|
|
);
|
|
|
|
app.switchUser(read);
|
|
await t.throwsAsync(
|
|
approveMember(app, 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, models } = t.context;
|
|
const {
|
|
createInviteLink,
|
|
owner,
|
|
workspace: ws,
|
|
teamWorkspace: tws,
|
|
} = await init(app, 5);
|
|
const [inviteId, invite] = await createInviteLink(ws);
|
|
const [teamInviteId, teamInvite, acceptTeamInvite] =
|
|
await createInviteLink(tws);
|
|
|
|
const member = await app.signup();
|
|
{
|
|
// check invite link
|
|
app.switchUser(member);
|
|
const info = await getInviteInfo(app, inviteId);
|
|
t.is(info.workspace.id, ws.id, 'should be able to get invite info');
|
|
t.falsy(info.status);
|
|
|
|
// check team invite link
|
|
const teamInfo = await getInviteInfo(app, teamInviteId);
|
|
t.is(teamInfo.workspace.id, tws.id, 'should be able to get invite info');
|
|
t.falsy(info.status);
|
|
}
|
|
|
|
{
|
|
// invite link
|
|
for (const [i] of Array.from({ length: 5 }).entries()) {
|
|
const user = await invite(`test${i}@affine.pro`);
|
|
const role = await models.workspaceUser.get(ws.id, user.id);
|
|
t.truthy(role);
|
|
const status = role!.status;
|
|
t.is(
|
|
status,
|
|
WorkspaceMemberStatus.UnderReview,
|
|
'should be able to check status'
|
|
);
|
|
const info = await getInviteInfo(app, role!.id);
|
|
t.is(info.status, WorkspaceMemberStatus.UnderReview);
|
|
}
|
|
|
|
await t.throwsAsync(
|
|
invite('exceed@affine.pro'),
|
|
{ message: 'You have exceeded your workspace member quota.' },
|
|
'should throw error if exceed member limit'
|
|
);
|
|
}
|
|
|
|
{
|
|
// team invite link
|
|
const members: User[] = [];
|
|
await t.notThrowsAsync(async () => {
|
|
members.push(await teamInvite('member3@affine.pro'));
|
|
members.push(await teamInvite('member4@affine.pro'));
|
|
}, 'should not throw error even exceed member limit');
|
|
const [m3, m4] = members;
|
|
|
|
t.is(
|
|
(await models.workspaceUser.get(tws.id, m3.id))?.status,
|
|
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
|
'should not change status'
|
|
);
|
|
t.is(
|
|
(await models.workspaceUser.get(tws.id, m4.id))?.status,
|
|
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
|
'should not change status'
|
|
);
|
|
|
|
models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', {
|
|
memberLimit: 6,
|
|
});
|
|
await models.workspaceUser.refresh(tws.id, 6);
|
|
t.is(
|
|
(await models.workspaceUser.get(tws.id, m3.id))?.status,
|
|
WorkspaceMemberStatus.UnderReview,
|
|
'should not change status'
|
|
);
|
|
t.is(
|
|
(await models.workspaceUser.get(tws.id, m4.id))?.status,
|
|
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
|
'should not change status'
|
|
);
|
|
|
|
models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', {
|
|
memberLimit: 7,
|
|
});
|
|
await models.workspaceUser.refresh(tws.id, 7);
|
|
t.is(
|
|
(await models.workspaceUser.get(tws.id, m4.id))?.status,
|
|
WorkspaceMemberStatus.UnderReview,
|
|
'should not change status'
|
|
);
|
|
|
|
{
|
|
await t.throwsAsync(acceptTeamInvite(owner.id), {
|
|
message: `You have already joined in Space ${tws.id}.`,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
test('should be able to invite batch and send notifications', async t => {
|
|
const { app } = t.context;
|
|
const { inviteBatch } = await init(app, 5);
|
|
|
|
const currentCount = app.queue.count('notification.sendInvitation');
|
|
await inviteBatch(['m3@affine.pro', 'm4@affine.pro'], true);
|
|
t.is(app.queue.count('notification.sendInvitation'), currentCount + 2);
|
|
const job = app.queue.last('notification.sendInvitation');
|
|
t.truthy(job.payload.inviteId);
|
|
t.truthy(job.payload.inviterId);
|
|
});
|
|
|
|
test('should be able to emit events and send notifications', async t => {
|
|
const { app, event } = t.context;
|
|
|
|
{
|
|
const { teamWorkspace: tws, inviteBatch } = await init(app, 5);
|
|
|
|
await inviteBatch(['m1@affine.pro', 'm2@affine.pro']);
|
|
const [membersUpdated] = event.emit
|
|
.getCalls()
|
|
.map(call => call.args)
|
|
.toReversed();
|
|
t.deepEqual(membersUpdated, [
|
|
'workspace.members.updated',
|
|
{
|
|
workspaceId: tws.id,
|
|
count: 7,
|
|
},
|
|
]);
|
|
}
|
|
|
|
{
|
|
const { teamWorkspace: tws, owner, createInviteLink } = await init(app);
|
|
const [, invite] = await createInviteLink(tws);
|
|
const user = await invite('m3@affine.pro');
|
|
app.switchUser(owner);
|
|
const { members } = await getWorkspace(app, tws.id);
|
|
const memberInvite = members.find(m => m.id === user.id)!;
|
|
const requestRequestNotification = app.queue.last(
|
|
'notification.sendInvitationReviewRequest'
|
|
);
|
|
t.truthy(requestRequestNotification);
|
|
// find admin
|
|
const admins = await t.context.models.workspaceUser.getAdmins(tws.id);
|
|
t.deepEqual(
|
|
requestRequestNotification.payload,
|
|
{
|
|
inviteId: memberInvite.inviteId,
|
|
reviewerId: admins[0].id,
|
|
},
|
|
'should send review request notification'
|
|
);
|
|
|
|
app.switchUser(owner);
|
|
await revokeUser(app, tws.id, user.id);
|
|
const requestDeclinedNotification = app.queue.last(
|
|
'notification.sendInvitationReviewDeclined'
|
|
);
|
|
t.truthy(requestDeclinedNotification);
|
|
t.deepEqual(
|
|
requestDeclinedNotification.payload,
|
|
{
|
|
userId: user.id,
|
|
workspaceId: tws.id,
|
|
reviewerId: owner.id,
|
|
},
|
|
'should send review declined notification'
|
|
);
|
|
}
|
|
|
|
{
|
|
const { teamWorkspace: tws, owner, read } = await init(app);
|
|
await grantMember(app, tws.id, read.id, WorkspaceRole.Admin);
|
|
t.deepEqual(
|
|
event.emit.lastCall.args,
|
|
[
|
|
'workspace.members.roleChanged',
|
|
{
|
|
userId: read.id,
|
|
workspaceId: tws.id,
|
|
role: WorkspaceRole.Admin,
|
|
},
|
|
],
|
|
'should emit role changed event'
|
|
);
|
|
|
|
await grantMember(app, tws.id, read.id, WorkspaceRole.Owner);
|
|
const [ownershipTransferred] = event.emit
|
|
.getCalls()
|
|
.map(call => call.args)
|
|
.toReversed();
|
|
t.deepEqual(
|
|
ownershipTransferred,
|
|
[
|
|
'workspace.owner.changed',
|
|
{ from: owner.id, to: read.id, workspaceId: tws.id },
|
|
],
|
|
'should emit owner transferred event'
|
|
);
|
|
|
|
app.switchUser(read);
|
|
await revokeMember(app, tws.id, owner.id);
|
|
const [memberRemoved, memberUpdated] = event.emit
|
|
.getCalls()
|
|
.map(call => call.args)
|
|
.toReversed();
|
|
t.deepEqual(
|
|
memberRemoved,
|
|
[
|
|
'workspace.members.removed',
|
|
{
|
|
userId: owner.id,
|
|
workspaceId: tws.id,
|
|
},
|
|
],
|
|
'should emit owner transferred event'
|
|
);
|
|
t.deepEqual(
|
|
memberUpdated,
|
|
[
|
|
'workspace.members.updated',
|
|
{
|
|
count: 4,
|
|
workspaceId: tws.id,
|
|
},
|
|
],
|
|
'should emit role changed event'
|
|
);
|
|
}
|
|
});
|
|
|
|
test('should be able to grant and revoke users role in page', async t => {
|
|
const { app } = t.context;
|
|
const {
|
|
teamWorkspace: ws,
|
|
admin,
|
|
write,
|
|
read,
|
|
external,
|
|
} = await init(app, 5);
|
|
const docId = nanoid();
|
|
|
|
app.switchUser(admin);
|
|
const res = await grantDocUserRoles(
|
|
app,
|
|
ws.id,
|
|
docId,
|
|
[read.id, write.id],
|
|
DocRole.Manager
|
|
);
|
|
|
|
t.deepEqual(res, {
|
|
grantDocUserRoles: true,
|
|
});
|
|
|
|
// should not downgrade the role if role exists
|
|
{
|
|
await grantDocUserRoles(app, ws.id, docId, [read.id], DocRole.Reader);
|
|
|
|
// read still be the Manager of this doc
|
|
app.switchUser(read);
|
|
const res = await grantDocUserRoles(
|
|
app,
|
|
ws.id,
|
|
docId,
|
|
[external.id],
|
|
DocRole.Editor
|
|
);
|
|
t.deepEqual(res, {
|
|
grantDocUserRoles: true,
|
|
});
|
|
|
|
app.switchUser(admin);
|
|
const docUsersList = await docGrantedUsersList(app, ws.id, docId);
|
|
t.is(docUsersList.workspace.doc.grantedUsersList.totalCount, 3);
|
|
const externalRole = docUsersList.workspace.doc.grantedUsersList.edges.find(
|
|
(edge: any) => edge.node.user.id === external.id
|
|
)?.node.role;
|
|
t.is(externalRole, DocRole[DocRole.Editor]);
|
|
}
|
|
});
|
|
|
|
test('should be able to change the default role in page', async t => {
|
|
const { app } = t.context;
|
|
const { teamWorkspace: ws, admin } = await init(app, 5);
|
|
const docId = nanoid();
|
|
app.switchUser(admin);
|
|
const res = await updateDocDefaultRole(app, ws.id, docId, DocRole.Reader);
|
|
|
|
t.deepEqual(res, {
|
|
updateDocDefaultRole: true,
|
|
});
|
|
});
|
|
|
|
test('default page role should be able to override the workspace role', async t => {
|
|
const { app } = t.context;
|
|
const {
|
|
teamWorkspace: workspace,
|
|
admin,
|
|
read,
|
|
external,
|
|
} = await init(app, 5);
|
|
|
|
const docId = nanoid();
|
|
|
|
app.switchUser(admin);
|
|
const res = await updateDocDefaultRole(
|
|
app,
|
|
workspace.id,
|
|
docId,
|
|
DocRole.Manager
|
|
);
|
|
|
|
t.deepEqual(res, {
|
|
updateDocDefaultRole: true,
|
|
});
|
|
|
|
// reader can manage the page if the page default role is Manager
|
|
{
|
|
app.switchUser(read);
|
|
const readerRes = await updateDocDefaultRole(
|
|
app,
|
|
workspace.id,
|
|
docId,
|
|
DocRole.Manager
|
|
);
|
|
|
|
t.deepEqual(readerRes, {
|
|
updateDocDefaultRole: true,
|
|
});
|
|
}
|
|
|
|
// external can't manage the page even if the page default role is Manager
|
|
{
|
|
app.switchUser(external);
|
|
await t.throwsAsync(
|
|
updateDocDefaultRole(app, workspace.id, docId, DocRole.Manager),
|
|
{
|
|
message: `You do not have permission to perform Doc.Users.Manage action on doc ${docId}.`,
|
|
}
|
|
);
|
|
}
|
|
});
|
|
|
|
test('should be able to grant and revoke doc user role', async t => {
|
|
const { app } = t.context;
|
|
const { teamWorkspace: ws, admin, read, external } = await init(app, 5);
|
|
const docId = nanoid();
|
|
|
|
app.switchUser(admin);
|
|
const res = await grantDocUserRoles(
|
|
app,
|
|
ws.id,
|
|
docId,
|
|
[external.id],
|
|
DocRole.Manager
|
|
);
|
|
|
|
t.deepEqual(res, {
|
|
grantDocUserRoles: true,
|
|
});
|
|
|
|
// external user can never be able to manage the page
|
|
{
|
|
app.switchUser(external);
|
|
await t.throwsAsync(
|
|
grantDocUserRoles(app, ws.id, docId, [read.id], DocRole.Manager),
|
|
{
|
|
message: `You do not have permission to perform Doc.Users.Manage action on doc ${docId}.`,
|
|
}
|
|
);
|
|
}
|
|
|
|
// revoke the role of the external user
|
|
{
|
|
app.switchUser(admin);
|
|
const revokeRes = await revokeDocUserRoles(app, ws.id, docId, external.id);
|
|
|
|
t.deepEqual(revokeRes, {
|
|
revokeDocUserRoles: true,
|
|
});
|
|
|
|
// external user can't manage the page
|
|
app.switchUser(external);
|
|
await t.throwsAsync(revokeDocUserRoles(app, ws.id, docId, read.id), {
|
|
message: `You do not have permission to perform Doc.Users.Manage action on doc ${docId}.`,
|
|
});
|
|
}
|
|
});
|
|
|
|
test('update page default role should throw error if the space does not exist', async t => {
|
|
const { app } = t.context;
|
|
const { admin } = await init(app, 5);
|
|
const docId = nanoid();
|
|
const nonExistWorkspaceId = 'non-exist-workspace';
|
|
app.switchUser(admin);
|
|
await t.throwsAsync(
|
|
updateDocDefaultRole(app, nonExistWorkspaceId, docId, DocRole.Manager),
|
|
{
|
|
message: `You do not have permission to perform Doc.Users.Manage action on doc ${docId}.`,
|
|
}
|
|
);
|
|
});
|