///
import { INestApplication } from '@nestjs/common';
import { WorkspaceMemberStatus } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
import { AppModule } from '../src/app.module';
import { AuthService } from '../src/core/auth';
import { DocContentService } from '../src/core/doc-renderer';
import { Permission, PermissionService } from '../src/core/permission';
import {
QuotaManagementService,
QuotaService,
QuotaType,
} from '../src/core/quota';
import {
acceptInviteById,
createInviteLink,
createTestingApp,
createWorkspace,
getInviteInfo,
grantMember,
inviteUser,
inviteUsers,
leaveWorkspace,
PermissionEnum,
signUp,
sleep,
UserAuthedType,
} from './utils';
const test = ava as TestFn<{
app: INestApplication;
auth: AuthService;
quota: QuotaService;
quotaManager: QuotaManagementService;
permissions: PermissionService;
}>;
test.beforeEach(async t => {
const { app } = await createTestingApp({
imports: [AppModule],
tapModule: module => {
module.overrideProvider(DocContentService).useValue({
getWorkspaceContent() {
return {
name: 'test',
avatarKey: null,
};
},
});
},
});
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;
});
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 ws = await createWorkspace(app, owner.token.token);
const quota = app.get(QuotaManagementService);
await quota.addTeamWorkspace(ws.id, 'test');
await quota.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, {
memberLimit,
});
const invite = async (
email: string,
permission: PermissionEnum = 'Write'
) => {
const member = await signUp(app, email.split('@')[0], email, '123456');
const inviteId = await inviteUser(
app,
owner.token.token,
ws.id,
member.email,
permission
);
await acceptInviteById(app, ws.id, inviteId);
return member;
};
const inviteBatch = async (emails: string[]) => {
const members = [];
for (const email of emails) {
const member = await signUp(app, email.split('@')[0], email, '123456');
members.push(member);
}
const invites = await inviteUsers(app, owner.token.token, ws.id, emails);
return [members, invites] as const;
};
const getCreateInviteLinkFetcher = async () => {
const { link } = await createInviteLink(
app,
owner.token.token,
ws.id,
'OneDay'
);
const inviteId = link.split('/').pop()!;
return [
inviteId,
async (email: string): Promise => {
const member = await signUp(app, email.split('@')[0], email, '123456');
await acceptInviteById(app, ws.id, inviteId, false, member.token.token);
return member;
},
async (token: string) => {
await acceptInviteById(app, ws.id, inviteId, false, token);
},
] 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');
return {
invite,
inviteBatch,
createInviteLink: getCreateInviteLinkFetcher,
owner,
ws,
admin,
write,
read,
};
};
test('should be able to check seat limit', async t => {
const { app, permissions, quotaManager } = t.context;
const { invite, inviteBatch, ws } = await init(app, 4);
{
// invite
await t.throwsAsync(
invite('member3@affine.pro', 'Read'),
{ message: 'You have exceeded your workspace member quota.' },
'should throw error if exceed member limit'
);
await quotaManager.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, {
memberLimit: 5,
});
await t.notThrowsAsync(
invite('member4@affine.pro', 'Read'),
'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 permissions.getWorkspaceMemberStatus(
ws.id,
(await members1)[0][0].id
),
WorkspaceMemberStatus.NeedMoreSeat,
'should be able to check member status'
);
// refresh seat, fifo
sleep(1000);
const [[members2]] = await inviteBatch(['member6@affine.pro']);
await permissions.refreshSeatStatus(ws.id, 6);
t.is(
await permissions.getWorkspaceMemberStatus(
ws.id,
(await members1)[0][0].id
),
WorkspaceMemberStatus.Pending,
'should become accepted after refresh'
);
t.is(
await permissions.getWorkspaceMemberStatus(ws.id, members2.id),
WorkspaceMemberStatus.NeedMoreSeat,
'should not change status'
);
}
});
test('should be able to grant team member permission', async t => {
const { app, permissions } = t.context;
const { owner, ws, admin, write, read } = await init(app);
await t.throwsAsync(
grantMember(app, read.token.token, ws.id, write.id, 'Write'),
{ instanceOf: Error },
'should throw error if not owner'
);
await t.throwsAsync(
grantMember(app, write.token.token, ws.id, read.id, 'Write'),
{ instanceOf: Error },
'should throw error if not owner'
);
await t.throwsAsync(
grantMember(app, admin.token.token, ws.id, read.id, 'Write'),
{ instanceOf: Error },
'should throw error if not owner'
);
{
// owner should be able to grant permission
t.true(
await permissions.tryCheckWorkspaceIs(ws.id, read.id, Permission.Read),
'should be able to check permission'
);
t.truthy(
await grantMember(app, owner.token.token, ws.id, read.id, 'Admin'),
'should be able to grant permission'
);
t.true(
await permissions.tryCheckWorkspaceIs(ws.id, read.id, Permission.Admin),
'should be able to check permission'
);
}
});
test('should be able to leave workspace', async t => {
const { app } = t.context;
const { owner, ws, admin, write, read } = await init(app);
t.false(
await leaveWorkspace(app, owner.token.token, ws.id),
'owner should not be able to leave workspace'
);
t.true(
await leaveWorkspace(app, admin.token.token, ws.id),
'admin should be able to leave workspace'
);
t.true(
await leaveWorkspace(app, write.token.token, ws.id),
'write should be able to leave workspace'
);
t.true(
await leaveWorkspace(app, read.token.token, ws.id),
'read should be able to leave workspace'
);
});
test('should be able to invite by link', async t => {
const { app, permissions, quotaManager } = t.context;
const { createInviteLink, owner, ws } = await init(app, 4);
const [inviteId, invite, acceptInvite] = await createInviteLink();
{
// check invite link
const info = await getInviteInfo(app, owner.token.token, inviteId);
t.is(info.workspace.id, ws.id, 'should be able to get invite info');
}
{
// invite link
const members: UserAuthedType[] = [];
await t.notThrowsAsync(async () => {
members.push(await invite('member3@affine.pro'));
members.push(await invite('member4@affine.pro'));
}, 'should not throw error even exceed member limit');
const [m3, m4] = members;
t.is(
await permissions.getWorkspaceMemberStatus(ws.id, m3.id),
WorkspaceMemberStatus.NeedMoreSeatAndReview,
'should not change status'
);
t.is(
await permissions.getWorkspaceMemberStatus(ws.id, m4.id),
WorkspaceMemberStatus.NeedMoreSeatAndReview,
'should not change status'
);
await quotaManager.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, {
memberLimit: 5,
});
await permissions.refreshSeatStatus(ws.id, 5);
t.is(
await permissions.getWorkspaceMemberStatus(ws.id, m3.id),
WorkspaceMemberStatus.UnderReview,
'should not change status'
);
t.is(
await permissions.getWorkspaceMemberStatus(ws.id, m4.id),
WorkspaceMemberStatus.NeedMoreSeatAndReview,
'should not change status'
);
await quotaManager.updateWorkspaceConfig(ws.id, QuotaType.TeamPlanV1, {
memberLimit: 6,
});
await permissions.refreshSeatStatus(ws.id, 6);
t.is(
await permissions.getWorkspaceMemberStatus(ws.id, m4.id),
WorkspaceMemberStatus.UnderReview,
'should not change status'
);
{
const message = `You have already joined in Space ${ws.id}.`;
await t.throwsAsync(
acceptInvite(owner.token.token),
{ message },
'should throw error if member already in workspace'
);
}
}
});