mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(server): permission (#10449)
This commit is contained in:
160
packages/backend/server/src/__tests__/models/doc-user.spec.ts
Normal file
160
packages/backend/server/src/__tests__/models/doc-user.spec.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { EventBus } from '../../base';
|
||||
import { DocRole, Models } from '../../models';
|
||||
import { createTestingModule, TestingModule } from '../utils';
|
||||
|
||||
let db: PrismaClient;
|
||||
let models: Models;
|
||||
let module: TestingModule;
|
||||
|
||||
test.before(async () => {
|
||||
module = await createTestingModule({
|
||||
tapModule: m => {
|
||||
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
|
||||
},
|
||||
});
|
||||
models = module.get(Models);
|
||||
db = module.get(PrismaClient);
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await module.initTestingDB();
|
||||
Sinon.reset();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
async function create() {
|
||||
return db.workspace.create({
|
||||
data: { public: false },
|
||||
});
|
||||
}
|
||||
|
||||
test('should set doc owner', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
const docId = 'fake-doc-id';
|
||||
|
||||
await models.docUser.setOwner(workspace.id, docId, user.id);
|
||||
const role = await models.docUser.get(workspace.id, docId, user.id);
|
||||
|
||||
t.is(role?.type, DocRole.Owner);
|
||||
});
|
||||
|
||||
test('should transfer doc owner', async t => {
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
const user2 = await models.user.create({ email: 'u2@affine.pro' });
|
||||
const workspace = await create();
|
||||
const docId = 'fake-doc-id';
|
||||
|
||||
await models.docUser.setOwner(workspace.id, docId, user.id);
|
||||
await models.docUser.setOwner(workspace.id, docId, user2.id);
|
||||
|
||||
const oldOwnerRole = await models.docUser.get(workspace.id, docId, user.id);
|
||||
const newOwnerRole = await models.docUser.get(workspace.id, docId, user2.id);
|
||||
|
||||
t.is(oldOwnerRole?.type, DocRole.Manager);
|
||||
t.is(newOwnerRole?.type, DocRole.Owner);
|
||||
});
|
||||
|
||||
test('should set doc user role', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
const docId = 'fake-doc-id';
|
||||
|
||||
await models.docUser.set(workspace.id, docId, user.id, DocRole.Manager);
|
||||
const role = await models.docUser.get(workspace.id, docId, user.id);
|
||||
|
||||
t.is(role?.type, DocRole.Manager);
|
||||
});
|
||||
|
||||
test('should not allow setting doc owner through setDocUserRole', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
const docId = 'fake-doc-id';
|
||||
|
||||
await t.throwsAsync(
|
||||
models.docUser.set(workspace.id, docId, user.id, DocRole.Owner),
|
||||
{ message: 'Cannot set Owner role of a doc to a user.' }
|
||||
);
|
||||
});
|
||||
|
||||
test('should delete doc user role', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
const docId = 'fake-doc-id';
|
||||
|
||||
await models.docUser.set(workspace.id, docId, user.id, DocRole.Manager);
|
||||
await models.docUser.delete(workspace.id, docId, user.id);
|
||||
|
||||
const role = await models.docUser.get(workspace.id, docId, user.id);
|
||||
t.is(role, null);
|
||||
});
|
||||
|
||||
test('should paginate doc user roles', async t => {
|
||||
const workspace = await create();
|
||||
const docId = 'fake-doc-id';
|
||||
await db.user.createMany({
|
||||
data: Array.from({ length: 200 }, (_, i) => ({
|
||||
id: String(i),
|
||||
name: `u${i}`,
|
||||
email: `${i}@affine.pro`,
|
||||
})),
|
||||
});
|
||||
|
||||
await db.workspaceDocUserRole.createMany({
|
||||
data: Array.from({ length: 200 }, (_, i) => ({
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
userId: String(i),
|
||||
type: DocRole.Editor,
|
||||
createdAt: new Date(Date.now() + i * 1000),
|
||||
})),
|
||||
});
|
||||
|
||||
const [roles, total] = await models.docUser.paginate(workspace.id, docId, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(roles.length, 10);
|
||||
t.is(total, 200);
|
||||
|
||||
const [roles2] = await models.docUser.paginate(workspace.id, docId, {
|
||||
after: roles.at(-1)?.createdAt.toISOString(),
|
||||
first: 50,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(roles2.length, 50);
|
||||
t.not(roles2[0].type, DocRole.Owner);
|
||||
t.deepEqual(
|
||||
roles2.map(r => r.userId),
|
||||
roles2
|
||||
.toSorted((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
.map(r => r.userId)
|
||||
);
|
||||
});
|
||||
|
||||
test('should count doc user roles', async t => {
|
||||
const workspace = await create();
|
||||
const docId = 'fake-doc-id';
|
||||
const users = await Promise.all([
|
||||
models.user.create({ email: 'u1@affine.pro' }),
|
||||
models.user.create({ email: 'u2@affine.pro' }),
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
users.map(user =>
|
||||
models.docUser.set(workspace.id, docId, user.id, DocRole.Manager)
|
||||
)
|
||||
);
|
||||
|
||||
const count = await models.docUser.count(workspace.id, docId);
|
||||
t.is(count, 2);
|
||||
});
|
||||
@@ -1,232 +0,0 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import ava, { TestFn } from 'ava';
|
||||
|
||||
import { Config } from '../../base/config';
|
||||
import { WorkspaceRole } from '../../core/permission';
|
||||
import { PublicPageMode } from '../../models/common';
|
||||
import { PageModel } from '../../models/page';
|
||||
import { type User, UserModel } from '../../models/user';
|
||||
import { type Workspace, WorkspaceModel } from '../../models/workspace';
|
||||
import { createTestingModule, type TestingModule } from '../utils';
|
||||
|
||||
interface Context {
|
||||
config: Config;
|
||||
module: TestingModule;
|
||||
db: PrismaClient;
|
||||
user: UserModel;
|
||||
workspace: WorkspaceModel;
|
||||
page: PageModel;
|
||||
}
|
||||
|
||||
const test = ava as TestFn<Context>;
|
||||
|
||||
test.before(async t => {
|
||||
const module = await createTestingModule();
|
||||
|
||||
t.context.user = module.get(UserModel);
|
||||
t.context.workspace = module.get(WorkspaceModel);
|
||||
t.context.page = module.get(PageModel);
|
||||
t.context.db = module.get(PrismaClient);
|
||||
t.context.config = module.get(Config);
|
||||
t.context.module = module;
|
||||
});
|
||||
|
||||
let user: User;
|
||||
let workspace: Workspace;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await t.context.module.initTestingDB();
|
||||
user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
workspace = await t.context.workspace.create(user.id);
|
||||
});
|
||||
|
||||
test.after(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
test('should create page with default mode and public false', async t => {
|
||||
const page = await t.context.page.upsert(workspace.id, 'page1');
|
||||
t.is(page.workspaceId, workspace.id);
|
||||
t.is(page.docId, 'page1');
|
||||
t.is(page.mode, PublicPageMode.Page);
|
||||
t.is(page.public, false);
|
||||
});
|
||||
|
||||
test('should update page', async t => {
|
||||
const page = await t.context.page.upsert(workspace.id, 'page1');
|
||||
const data = {
|
||||
mode: PublicPageMode.Edgeless,
|
||||
public: true,
|
||||
};
|
||||
await t.context.page.upsert(workspace.id, 'page1', data);
|
||||
const page1 = await t.context.page.get(workspace.id, 'page1');
|
||||
t.deepEqual(page1, {
|
||||
...page,
|
||||
...data,
|
||||
});
|
||||
});
|
||||
|
||||
test('should get null when page not exists', async t => {
|
||||
const page = await t.context.page.get(workspace.id, 'page1');
|
||||
t.is(page, null);
|
||||
});
|
||||
|
||||
test('should get page by id and public flag', async t => {
|
||||
await t.context.page.upsert(workspace.id, 'page1');
|
||||
await t.context.page.upsert(workspace.id, 'page2', {
|
||||
public: true,
|
||||
});
|
||||
let page1 = await t.context.page.get(workspace.id, 'page1');
|
||||
t.is(page1!.public, false);
|
||||
page1 = await t.context.page.get(workspace.id, 'page1', true);
|
||||
t.is(page1, null);
|
||||
let page2 = await t.context.page.get(workspace.id, 'page2', true);
|
||||
t.is(page2!.public, true);
|
||||
page2 = await t.context.page.get(workspace.id, 'page2', false);
|
||||
t.is(page2, null);
|
||||
});
|
||||
|
||||
test('should get public page count', async t => {
|
||||
await t.context.page.upsert(workspace.id, 'page1', {
|
||||
public: true,
|
||||
});
|
||||
await t.context.page.upsert(workspace.id, 'page2', {
|
||||
public: true,
|
||||
});
|
||||
await t.context.page.upsert(workspace.id, 'page3');
|
||||
const count = await t.context.page.getPublicsCount(workspace.id);
|
||||
t.is(count, 2);
|
||||
});
|
||||
|
||||
test('should get public pages of a workspace', async t => {
|
||||
await t.context.page.upsert(workspace.id, 'page1', {
|
||||
public: true,
|
||||
});
|
||||
await t.context.page.upsert(workspace.id, 'page2', {
|
||||
public: true,
|
||||
});
|
||||
await t.context.page.upsert(workspace.id, 'page3');
|
||||
const pages = await t.context.page.findPublics(workspace.id);
|
||||
t.is(pages.length, 2);
|
||||
t.deepEqual(pages.map(p => p.docId).sort(), ['page1', 'page2']);
|
||||
});
|
||||
|
||||
test('should grant a member to access a page', async t => {
|
||||
await t.context.page.upsert(workspace.id, 'page1', {
|
||||
public: true,
|
||||
});
|
||||
const member = await t.context.user.create({
|
||||
email: 'test1@affine.pro',
|
||||
});
|
||||
await t.context.page.grantMember(workspace.id, 'page1', member.id);
|
||||
let hasAccess = await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
member.id
|
||||
);
|
||||
t.true(hasAccess);
|
||||
hasAccess = await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
t.false(hasAccess);
|
||||
// grant write permission
|
||||
await t.context.page.grantMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
hasAccess = await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
t.true(hasAccess);
|
||||
hasAccess = await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
t.true(hasAccess);
|
||||
// delete member
|
||||
const count = await t.context.page.deleteMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id
|
||||
);
|
||||
t.is(count, 1);
|
||||
hasAccess = await t.context.page.isMember(workspace.id, 'page1', user.id);
|
||||
t.false(hasAccess);
|
||||
});
|
||||
|
||||
test('should change the page owner', async t => {
|
||||
await t.context.page.upsert(workspace.id, 'page1', {
|
||||
public: true,
|
||||
});
|
||||
await t.context.page.grantMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Owner
|
||||
);
|
||||
t.true(
|
||||
await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Owner
|
||||
)
|
||||
);
|
||||
|
||||
// change owner
|
||||
const otherUser = await t.context.user.create({
|
||||
email: 'test1@affine.pro',
|
||||
});
|
||||
await t.context.page.grantMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
otherUser.id,
|
||||
WorkspaceRole.Owner
|
||||
);
|
||||
t.true(
|
||||
await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
otherUser.id,
|
||||
WorkspaceRole.Owner
|
||||
)
|
||||
);
|
||||
t.false(
|
||||
await t.context.page.isMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Owner
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('should not delete owner from page', async t => {
|
||||
await t.context.page.upsert(workspace.id, 'page1', {
|
||||
public: true,
|
||||
});
|
||||
await t.context.page.grantMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id,
|
||||
WorkspaceRole.Owner
|
||||
);
|
||||
const count = await t.context.page.deleteMember(
|
||||
workspace.id,
|
||||
'page1',
|
||||
user.id
|
||||
);
|
||||
t.is(count, 0);
|
||||
});
|
||||
@@ -2,13 +2,13 @@ import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { EmailAlreadyUsed, EventBus } from '../../base';
|
||||
import { WorkspaceRole } from '../../core/permission';
|
||||
import { Models } from '../../models';
|
||||
import { UserModel } from '../../models/user';
|
||||
import { WorkspaceMemberStatus } from '../../models/workspace';
|
||||
import { createTestingModule, sleep, type TestingModule } from '../utils';
|
||||
|
||||
interface Context {
|
||||
module: TestingModule;
|
||||
models: Models;
|
||||
user: UserModel;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ test.before(async t => {
|
||||
const module = await createTestingModule({});
|
||||
|
||||
t.context.user = module.get(UserModel);
|
||||
t.context.models = module.get(Models);
|
||||
t.context.module = module;
|
||||
});
|
||||
|
||||
@@ -253,24 +254,13 @@ test('should trigger user.deleted event', async t => {
|
||||
|
||||
const user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
workspacePermissions: {
|
||||
create: {
|
||||
workspace: {
|
||||
create: {
|
||||
id: 'test-workspace',
|
||||
public: false,
|
||||
},
|
||||
},
|
||||
type: WorkspaceRole.Owner,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
},
|
||||
});
|
||||
const workspace = await t.context.models.workspace.create(user.id);
|
||||
|
||||
await t.context.user.delete(user.id);
|
||||
|
||||
t.true(
|
||||
spy.calledOnceWithExactly({ ...user, ownedWorkspaces: ['test-workspace'] })
|
||||
spy.calledOnceWithExactly({ ...user, ownedWorkspaces: [workspace.id] })
|
||||
);
|
||||
// await for 'user.deleted' event to be emitted and executed
|
||||
// avoid race condition cause database dead lock
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import test from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { EventBus } from '../../base';
|
||||
import { Models, WorkspaceMemberStatus, WorkspaceRole } from '../../models';
|
||||
import { createTestingModule, TestingModule } from '../utils';
|
||||
|
||||
let db: PrismaClient;
|
||||
let models: Models;
|
||||
let module: TestingModule;
|
||||
let event: Sinon.SinonStubbedInstance<EventBus>;
|
||||
|
||||
test.before(async () => {
|
||||
module = await createTestingModule({
|
||||
tapModule: m => {
|
||||
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
|
||||
},
|
||||
});
|
||||
models = module.get(Models);
|
||||
event = module.get(EventBus);
|
||||
db = module.get(PrismaClient);
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await module.initTestingDB();
|
||||
Sinon.reset();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
async function create() {
|
||||
return db.workspace.create({
|
||||
data: { public: false },
|
||||
});
|
||||
}
|
||||
|
||||
test('should set workspace owner', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
await models.workspaceUser.setOwner(workspace.id, user.id);
|
||||
const owner = await models.workspaceUser.getOwner(workspace.id);
|
||||
|
||||
t.is(owner.id, user.id);
|
||||
});
|
||||
|
||||
test('should transfer workespace owner', async t => {
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
const user2 = await models.user.create({ email: 'u2@affine.pro' });
|
||||
const workspace = await models.workspace.create(user.id);
|
||||
|
||||
await models.workspaceUser.setOwner(workspace.id, user2.id);
|
||||
|
||||
t.true(
|
||||
event.emit.lastCall.calledWith('workspace.owner.changed', {
|
||||
workspaceId: workspace.id,
|
||||
from: user.id,
|
||||
to: user2.id,
|
||||
})
|
||||
);
|
||||
|
||||
const owner2 = await models.workspaceUser.getOwner(workspace.id);
|
||||
t.is(owner2.id, user2.id);
|
||||
});
|
||||
|
||||
test('should get user role', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
await models.workspaceUser.set(workspace.id, user.id, WorkspaceRole.Admin);
|
||||
|
||||
const role = await models.workspaceUser.get(workspace.id, user.id);
|
||||
|
||||
t.is(role!.type, WorkspaceRole.Admin);
|
||||
});
|
||||
|
||||
test('should get active workspace role', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
await models.workspaceUser.set(
|
||||
workspace.id,
|
||||
user.id,
|
||||
WorkspaceRole.Admin,
|
||||
WorkspaceMemberStatus.Accepted
|
||||
);
|
||||
|
||||
const role = await models.workspaceUser.getActive(workspace.id, user.id);
|
||||
|
||||
t.is(role!.type, WorkspaceRole.Admin);
|
||||
});
|
||||
|
||||
test('should not get inactive workspace role', async t => {
|
||||
const workspace = await create();
|
||||
|
||||
const u1 = await models.user.create({ email: 'u1@affine.pro' });
|
||||
|
||||
await models.workspaceUser.set(workspace.id, u1.id, WorkspaceRole.Admin);
|
||||
|
||||
let role = await models.workspaceUser.getActive(workspace.id, u1.id);
|
||||
t.is(role, null);
|
||||
|
||||
await models.workspaceUser.setStatus(
|
||||
workspace.id,
|
||||
u1.id,
|
||||
WorkspaceMemberStatus.UnderReview
|
||||
);
|
||||
|
||||
role = await models.workspaceUser.getActive(workspace.id, u1.id);
|
||||
t.is(role, null);
|
||||
});
|
||||
|
||||
test('should update user role', async t => {
|
||||
const workspace = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
await models.workspaceUser.set(
|
||||
workspace.id,
|
||||
user.id,
|
||||
WorkspaceRole.Admin,
|
||||
WorkspaceMemberStatus.Accepted
|
||||
);
|
||||
const role = await models.workspaceUser.get(workspace.id, user.id);
|
||||
|
||||
t.is(role!.type, WorkspaceRole.Admin);
|
||||
|
||||
await models.workspaceUser.set(
|
||||
workspace.id,
|
||||
user.id,
|
||||
WorkspaceRole.Collaborator
|
||||
);
|
||||
|
||||
const role2 = await models.workspaceUser.get(workspace.id, user.id);
|
||||
|
||||
t.is(role2!.type, WorkspaceRole.Collaborator);
|
||||
t.deepEqual(event.emit.lastCall.args, [
|
||||
'workspace.members.roleChanged',
|
||||
{
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
role: WorkspaceRole.Collaborator,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return workspace role if status is Accepted', async t => {
|
||||
const workspace = await create();
|
||||
const u1 = await models.user.create({ email: 'u1@affine.pro' });
|
||||
|
||||
await models.workspaceUser.set(workspace.id, u1.id, WorkspaceRole.Admin);
|
||||
await models.workspaceUser.setStatus(
|
||||
workspace.id,
|
||||
u1.id,
|
||||
WorkspaceMemberStatus.Accepted
|
||||
);
|
||||
const role = await models.workspaceUser.get(workspace.id, u1.id);
|
||||
|
||||
t.is(role!.type, WorkspaceRole.Admin);
|
||||
});
|
||||
|
||||
test('should delete workspace user role', async t => {
|
||||
const workspace = await create();
|
||||
const u1 = await models.user.create({ email: 'u1@affine.pro' });
|
||||
|
||||
await models.workspaceUser.set(workspace.id, u1.id, WorkspaceRole.Admin);
|
||||
await models.workspaceUser.setStatus(
|
||||
workspace.id,
|
||||
u1.id,
|
||||
WorkspaceMemberStatus.Accepted
|
||||
);
|
||||
|
||||
let role = await models.workspaceUser.get(workspace.id, u1.id);
|
||||
t.is(role!.type, WorkspaceRole.Admin);
|
||||
|
||||
await models.workspaceUser.delete(workspace.id, u1.id);
|
||||
|
||||
role = await models.workspaceUser.get(workspace.id, u1.id);
|
||||
t.is(role, null);
|
||||
});
|
||||
|
||||
test('should get user workspace roles with filter', async t => {
|
||||
const ws1 = await create();
|
||||
const ws2 = await create();
|
||||
const user = await models.user.create({ email: 'u1@affine.pro' });
|
||||
|
||||
await db.workspaceUserRole.createMany({
|
||||
data: [
|
||||
{
|
||||
workspaceId: ws1.id,
|
||||
userId: user.id,
|
||||
type: WorkspaceRole.Admin,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
{
|
||||
workspaceId: ws2.id,
|
||||
userId: user.id,
|
||||
type: WorkspaceRole.Collaborator,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let roles = await models.workspaceUser.getUserActiveRoles(user.id, {
|
||||
role: WorkspaceRole.Admin,
|
||||
});
|
||||
t.is(roles.length, 1);
|
||||
t.is(roles[0].type, WorkspaceRole.Admin);
|
||||
|
||||
roles = await models.workspaceUser.getUserActiveRoles(user.id);
|
||||
t.is(roles.length, 2);
|
||||
});
|
||||
|
||||
test('should paginate workspace user roles', async t => {
|
||||
const workspace = await create();
|
||||
await db.user.createMany({
|
||||
data: Array.from({ length: 200 }, (_, i) => ({
|
||||
id: String(i),
|
||||
name: `u${i}`,
|
||||
email: `${i}@affine.pro`,
|
||||
})),
|
||||
});
|
||||
|
||||
await db.workspaceUserRole.createMany({
|
||||
data: Array.from({ length: 200 }, (_, i) => ({
|
||||
workspaceId: workspace.id,
|
||||
userId: String(i),
|
||||
type: WorkspaceRole.Collaborator,
|
||||
status: Object.values(WorkspaceMemberStatus)[
|
||||
Math.floor(Math.random() * Object.values(WorkspaceMemberStatus).length)
|
||||
],
|
||||
createdAt: new Date(Date.now() + i * 1000),
|
||||
})),
|
||||
});
|
||||
|
||||
const [roles, total] = await models.workspaceUser.paginate(workspace.id, {
|
||||
first: 10,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(roles.length, 10);
|
||||
t.is(total, 200);
|
||||
|
||||
const [roles2] = await models.workspaceUser.paginate(workspace.id, {
|
||||
after: roles.at(-1)?.createdAt.toISOString(),
|
||||
first: 50,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
t.is(roles2.length, 50);
|
||||
t.deepEqual(
|
||||
roles2.map(r => r.id),
|
||||
roles2
|
||||
.toSorted((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
.map(r => r.id)
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import { AppModule } from '../app.module';
|
||||
import { EventBus } from '../base';
|
||||
import { AuthService } from '../core/auth';
|
||||
import { DocReader } from '../core/doc';
|
||||
import { DocRole, PermissionService, WorkspaceRole } from '../core/permission';
|
||||
import { DocRole, WorkspaceRole } from '../core/permission';
|
||||
import { WorkspaceType } from '../core/workspaces';
|
||||
import { Models } from '../models';
|
||||
import {
|
||||
@@ -43,7 +43,6 @@ const test = ava as TestFn<{
|
||||
auth: AuthService;
|
||||
event: Sinon.SinonStubbedInstance<EventBus>;
|
||||
models: Models;
|
||||
permissions: PermissionService;
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
@@ -68,7 +67,6 @@ test.before(async t => {
|
||||
t.context.auth = app.get(AuthService);
|
||||
t.context.event = app.get(EventBus);
|
||||
t.context.models = app.get(Models);
|
||||
t.context.permissions = app.get(PermissionService);
|
||||
});
|
||||
|
||||
test.beforeEach(async t => {
|
||||
@@ -256,7 +254,7 @@ test('should be able to invite multiple users', async t => {
|
||||
});
|
||||
|
||||
test('should be able to check seat limit', async t => {
|
||||
const { app, permissions, models } = t.context;
|
||||
const { app, models } = t.context;
|
||||
const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 5);
|
||||
|
||||
{
|
||||
@@ -284,10 +282,8 @@ test('should be able to check seat limit', async t => {
|
||||
);
|
||||
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(
|
||||
ws.id,
|
||||
(await members1)[0][0].id
|
||||
),
|
||||
(await models.workspaceUser.get(ws.id, (await members1)[0][0].id))
|
||||
?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeat,
|
||||
'should be able to check member status'
|
||||
);
|
||||
@@ -295,18 +291,16 @@ test('should be able to check seat limit', async t => {
|
||||
// refresh seat, fifo
|
||||
await sleep(1000);
|
||||
const [[members2]] = await inviteBatch(['member6@affine.pro']);
|
||||
await permissions.refreshSeatStatus(ws.id, 7);
|
||||
await models.workspaceUser.refresh(ws.id, 7);
|
||||
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(
|
||||
ws.id,
|
||||
(await members1)[0][0].id
|
||||
),
|
||||
(await models.workspaceUser.get(ws.id, (await members1)[0][0].id))
|
||||
?.status,
|
||||
WorkspaceMemberStatus.Pending,
|
||||
'should become accepted after refresh'
|
||||
);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(ws.id, members2.id),
|
||||
(await models.workspaceUser.get(ws.id, members2.id))?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeat,
|
||||
'should not change status'
|
||||
);
|
||||
@@ -314,7 +308,7 @@ test('should be able to check seat limit', async t => {
|
||||
});
|
||||
|
||||
test('should be able to grant team member permission', async t => {
|
||||
const { app, permissions } = t.context;
|
||||
const { app, models } = t.context;
|
||||
const { owner, teamWorkspace: ws, write, read } = await init(app);
|
||||
|
||||
app.switchUser(read);
|
||||
@@ -335,11 +329,8 @@ test('should be able to grant team member permission', async t => {
|
||||
// owner should be able to grant permission
|
||||
app.switchUser(owner);
|
||||
t.true(
|
||||
await permissions.tryCheckWorkspaceIs(
|
||||
ws.id,
|
||||
read.id,
|
||||
WorkspaceRole.Collaborator
|
||||
),
|
||||
(await models.workspaceUser.get(ws.id, read.id))?.type ===
|
||||
WorkspaceRole.Collaborator,
|
||||
'should be able to check permission'
|
||||
);
|
||||
t.truthy(
|
||||
@@ -347,11 +338,8 @@ test('should be able to grant team member permission', async t => {
|
||||
'should be able to grant permission'
|
||||
);
|
||||
t.true(
|
||||
await permissions.tryCheckWorkspaceIs(
|
||||
ws.id,
|
||||
read.id,
|
||||
WorkspaceRole.Admin
|
||||
),
|
||||
(await models.workspaceUser.get(ws.id, read.id))?.type ===
|
||||
WorkspaceRole.Admin,
|
||||
'should be able to check permission'
|
||||
);
|
||||
}
|
||||
@@ -362,10 +350,9 @@ test('should be able to leave workspace', async t => {
|
||||
const { owner, teamWorkspace: ws, admin, write, read } = await init(app);
|
||||
|
||||
app.switchUser(owner);
|
||||
t.false(
|
||||
await leaveWorkspace(app, ws.id),
|
||||
'owner should not be able to leave workspace'
|
||||
);
|
||||
await t.throwsAsync(leaveWorkspace(app, ws.id), {
|
||||
message: 'Owner can not leave the workspace.',
|
||||
});
|
||||
|
||||
app.switchUser(admin);
|
||||
t.true(
|
||||
@@ -425,10 +412,9 @@ test('should be able to revoke team member', async t => {
|
||||
'owner should be able to revoke member'
|
||||
);
|
||||
|
||||
t.false(
|
||||
await revokeUser(app, ws.id, owner.id),
|
||||
'should not be able to revoke themselves'
|
||||
);
|
||||
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);
|
||||
@@ -508,7 +494,7 @@ test('should be able to approve team member', async t => {
|
||||
const memberInvite = members.find(m => m.id === member.id)!;
|
||||
t.is(memberInvite.status, 'UnderReview', 'should be under review');
|
||||
|
||||
t.is(await approveMember(app, tws.id, member.id), memberInvite.inviteId);
|
||||
t.true(await approveMember(app, tws.id, member.id));
|
||||
}
|
||||
|
||||
{
|
||||
@@ -536,7 +522,7 @@ test('should be able to approve team member', async t => {
|
||||
});
|
||||
|
||||
test('should be able to invite by link', async t => {
|
||||
const { app, permissions, models } = t.context;
|
||||
const { app, models } = t.context;
|
||||
const {
|
||||
createInviteLink,
|
||||
owner,
|
||||
@@ -562,10 +548,10 @@ test('should be able to invite by link', async t => {
|
||||
// invite link
|
||||
for (const [i] of Array.from({ length: 5 }).entries()) {
|
||||
const user = await invite(`test${i}@affine.pro`);
|
||||
const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id);
|
||||
const status = (await models.workspaceUser.get(ws.id, user.id))?.status;
|
||||
t.is(
|
||||
status,
|
||||
WorkspaceMemberStatus.Accepted,
|
||||
WorkspaceMemberStatus.UnderReview,
|
||||
'should be able to check status'
|
||||
);
|
||||
}
|
||||
@@ -587,12 +573,12 @@ test('should be able to invite by link', async t => {
|
||||
const [m3, m4] = members;
|
||||
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m3.id),
|
||||
(await models.workspaceUser.get(tws.id, m3.id))?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
||||
'should not change status'
|
||||
);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m4.id),
|
||||
(await models.workspaceUser.get(tws.id, m4.id))?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
||||
'should not change status'
|
||||
);
|
||||
@@ -600,14 +586,14 @@ test('should be able to invite by link', async t => {
|
||||
models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', {
|
||||
memberLimit: 6,
|
||||
});
|
||||
await permissions.refreshSeatStatus(tws.id, 6);
|
||||
await models.workspaceUser.refresh(tws.id, 6);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m3.id),
|
||||
(await models.workspaceUser.get(tws.id, m3.id))?.status,
|
||||
WorkspaceMemberStatus.UnderReview,
|
||||
'should not change status'
|
||||
);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m4.id),
|
||||
(await models.workspaceUser.get(tws.id, m4.id))?.status,
|
||||
WorkspaceMemberStatus.NeedMoreSeatAndReview,
|
||||
'should not change status'
|
||||
);
|
||||
@@ -615,9 +601,9 @@ test('should be able to invite by link', async t => {
|
||||
models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', {
|
||||
memberLimit: 7,
|
||||
});
|
||||
await permissions.refreshSeatStatus(tws.id, 7);
|
||||
await models.workspaceUser.refresh(tws.id, 7);
|
||||
t.is(
|
||||
await permissions.getWorkspaceMemberStatus(tws.id, m4.id),
|
||||
(await models.workspaceUser.get(tws.id, m4.id))?.status,
|
||||
WorkspaceMemberStatus.UnderReview,
|
||||
'should not change status'
|
||||
);
|
||||
@@ -665,6 +651,7 @@ test('should be able to emit events', async t => {
|
||||
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)!;
|
||||
t.deepEqual(
|
||||
@@ -698,7 +685,7 @@ test('should be able to emit events', async t => {
|
||||
{
|
||||
userId: read.id,
|
||||
workspaceId: tws.id,
|
||||
permission: WorkspaceRole.Admin,
|
||||
role: WorkspaceRole.Admin,
|
||||
},
|
||||
],
|
||||
'should emit role changed event'
|
||||
@@ -712,7 +699,7 @@ test('should be able to emit events', async t => {
|
||||
t.deepEqual(
|
||||
ownershipTransferred,
|
||||
[
|
||||
'workspace.members.ownershipTransferred',
|
||||
'workspace.owner.changed',
|
||||
{ from: owner.id, to: read.id, workspaceId: tws.id },
|
||||
],
|
||||
'should emit owner transferred event'
|
||||
@@ -880,20 +867,15 @@ test('should be able to grant and revoke doc user role', async t => {
|
||||
grantDocUserRoles: true,
|
||||
});
|
||||
|
||||
// external user now can manage the page
|
||||
// external user can never be able to manage the page
|
||||
{
|
||||
app.switchUser(external);
|
||||
const externalRes = await grantDocUserRoles(
|
||||
app,
|
||||
ws.id,
|
||||
docId,
|
||||
[read.id],
|
||||
DocRole.Manager
|
||||
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}.`,
|
||||
}
|
||||
);
|
||||
|
||||
t.deepEqual(externalRes, {
|
||||
grantDocUserRoles: true,
|
||||
});
|
||||
}
|
||||
|
||||
// revoke the role of the external user
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { type Blob } from '@prisma/client';
|
||||
|
||||
import { TestingApp } from './testing-app';
|
||||
|
||||
export async function listBlobs(
|
||||
app: TestingApp,
|
||||
workspaceId: string
|
||||
): Promise<string[]> {
|
||||
): Promise<Blob[]> {
|
||||
const res = await app.gql(`
|
||||
query {
|
||||
listBlobs(workspaceId: "${workspaceId}")
|
||||
workspace(id: "${workspaceId}") {
|
||||
blobs {
|
||||
key
|
||||
mime
|
||||
size
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
return res.listBlobs;
|
||||
return res.workspace.blobs;
|
||||
}
|
||||
|
||||
export async function getWorkspaceBlobsSize(
|
||||
|
||||
@@ -94,7 +94,7 @@ test('should visit public page', async t => {
|
||||
const docs2 = await getWorkspacePublicDocs(app, workspace.id);
|
||||
t.is(docs2.length, 0, 'failed to get shared docs');
|
||||
await t.throwsAsync(revokePublicDoc(app, workspace.id, 'doc3'), {
|
||||
message: 'Doc is not public',
|
||||
message: 'Doc is not public.',
|
||||
});
|
||||
|
||||
const docs3 = await getWorkspacePublicDocs(app, workspace.id);
|
||||
|
||||
@@ -77,7 +77,7 @@ test('should list blobs', async t => {
|
||||
const ret = await listBlobs(app, workspace.id);
|
||||
t.is(ret.length, 2, 'failed to list blobs');
|
||||
// list blob result is not ordered
|
||||
t.deepEqual(ret.sort(), [hash1, hash2].sort());
|
||||
t.deepEqual(ret.map(x => x.key).sort(), [hash1, hash2].sort());
|
||||
});
|
||||
|
||||
test('should auto delete blobs when workspace is deleted', async t => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import Sinon from 'sinon';
|
||||
|
||||
import { PgWorkspaceDocStorageAdapter } from '../../core/doc';
|
||||
import { WorkspaceBlobStorage } from '../../core/storage';
|
||||
import { Models, WorkspaceRole } from '../../models';
|
||||
import { createTestingApp, TestingApp, TestUser } from '../utils';
|
||||
|
||||
const test = ava as TestFn<{
|
||||
@@ -15,6 +16,7 @@ const test = ava as TestFn<{
|
||||
u1: TestUser;
|
||||
storage: Sinon.SinonStubbedInstance<WorkspaceBlobStorage>;
|
||||
workspace: Sinon.SinonStubbedInstance<PgWorkspaceDocStorageAdapter>;
|
||||
models: Models;
|
||||
}>;
|
||||
|
||||
test.before(async t => {
|
||||
@@ -34,6 +36,7 @@ test.before(async t => {
|
||||
t.context.app = app;
|
||||
t.context.storage = app.get(WorkspaceBlobStorage);
|
||||
t.context.workspace = app.get(PgWorkspaceDocStorageAdapter);
|
||||
t.context.models = app.get(Models);
|
||||
|
||||
await db.workspaceDoc.create({
|
||||
data: {
|
||||
@@ -155,17 +158,14 @@ test('should not be able to get private workspace with no public pages', async t
|
||||
});
|
||||
|
||||
test('should be able to get permission granted workspace', async t => {
|
||||
const { app, db, storage } = t.context;
|
||||
const { app, storage } = t.context;
|
||||
|
||||
await db.workspaceUserPermission.create({
|
||||
data: {
|
||||
workspaceId: 'totally-private',
|
||||
userId: t.context.u1.id,
|
||||
type: 1,
|
||||
accepted: true,
|
||||
status: WorkspaceMemberStatus.Accepted,
|
||||
},
|
||||
});
|
||||
await t.context.models.workspaceUser.set(
|
||||
'totally-private',
|
||||
t.context.u1.id,
|
||||
WorkspaceRole.Collaborator,
|
||||
WorkspaceMemberStatus.Accepted
|
||||
);
|
||||
|
||||
storage.get.resolves(blob());
|
||||
await app.login(t.context.u1);
|
||||
|
||||
Reference in New Issue
Block a user