test(e2e): acl e2e test (#3249)
* test: acl test * test: acl action test * test: acl test * test: acl test * test: acl collection block test * test: acl e2e test * test: acl e2e test * test: acl test * test: acl collection test * test: acl collectin test * test: acl association test * test: association acl test * test: collection acl test * test: collection acl test * test: acl test * test: acl updateRole test * test: menu permission test * test: menu permission test * test: menu permission test * test: menu permission test * test: acl test * test: acl test * test: acl test * test: acl test * test: acl test * test: menu acl test * test: acl test * refactor: setDefaultRole * fix: merge bug * fix: merge bug * test: acl test
This commit is contained in:
parent
4121075a98
commit
c9d5292d03
@ -193,6 +193,7 @@ export const RolesResourcesActions = connect((props) => {
|
||||
render: (checked, field) => (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
aria-label={`${action.name}_checkbox`}
|
||||
onChange={() => {
|
||||
const item = actionMap[action.name] || {
|
||||
name: action.name,
|
||||
|
@ -67,6 +67,7 @@ export const StrategyActions = connect((props) => {
|
||||
render: (enabled, action) => (
|
||||
<Checkbox
|
||||
checked={enabled}
|
||||
aria-label={`${action.name}_checkbox`}
|
||||
onChange={(e) => {
|
||||
if (enabled) {
|
||||
delete scopes[action.name];
|
||||
|
@ -100,8 +100,8 @@ function ButtonEditor(props) {
|
||||
default: fieldSchema?.['x-component-props']?.danger
|
||||
? 'danger'
|
||||
: fieldSchema?.['x-component-props']?.type === 'primary'
|
||||
? 'primary'
|
||||
: 'default',
|
||||
? 'primary'
|
||||
: 'default',
|
||||
enum: [
|
||||
{ value: 'default', label: '{{t("Default")}}' },
|
||||
{ value: 'primary', label: '{{t("Highlight")}}' },
|
||||
|
@ -89,6 +89,38 @@ export interface CollectionSetting {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface AclActionsSetting {
|
||||
name: string; //操作标识,如cretae
|
||||
fields?: any[]; //有该操作权限的字段
|
||||
scope?: any; // 数据范围
|
||||
}
|
||||
interface AclResourcesSetting {
|
||||
name: string; //数据表标识
|
||||
usingActionsConfig: boolean; //是否开启单独配置
|
||||
actions?: AclActionsSetting[];
|
||||
}
|
||||
interface AclRoleSetting {
|
||||
name?: string;
|
||||
title?: string;
|
||||
/**
|
||||
* @default true
|
||||
*/
|
||||
allowNewMenu?: boolean;
|
||||
//配置权限,如 ["app", "pm", "pm.*", "ui.*"]
|
||||
snippets?: string[];
|
||||
//操作权限策略
|
||||
strategy?: any;
|
||||
//数据表单独操作权限配置
|
||||
resources?: AclResourcesSetting[];
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
default?: boolean;
|
||||
key?: string;
|
||||
//菜单权限配置
|
||||
menuUiSchemas?: string[];
|
||||
}
|
||||
|
||||
export interface PageConfig {
|
||||
/**
|
||||
* 页面类型
|
||||
@ -188,6 +220,14 @@ interface ExtendUtils {
|
||||
* @returns
|
||||
*/
|
||||
deletePage: (pageName: string) => Promise<void>;
|
||||
/**
|
||||
* 生成一个新的角色,并和admin关联上
|
||||
*/
|
||||
mockRole: <T = any>(roleSetting: AclRoleSetting) => Promise<T>;
|
||||
/**
|
||||
* 更新角色权限配置
|
||||
*/
|
||||
updateRole: <T = any>(roleSetting: AclRoleSetting) => Promise<T>;
|
||||
}
|
||||
|
||||
const PORT = process.env.APP_PORT || 20000;
|
||||
@ -232,7 +272,10 @@ export class NocoPage {
|
||||
await this._waitForInit;
|
||||
return this.url;
|
||||
}
|
||||
|
||||
async getUid() {
|
||||
await this._waitForInit;
|
||||
return this.uid;
|
||||
}
|
||||
async waitForInit(this: NocoPage) {
|
||||
await this._waitForInit;
|
||||
return this;
|
||||
@ -267,6 +310,7 @@ const _test = base.extend<ExtendUtils>({
|
||||
// 测试运行完自动销毁页面
|
||||
for (const nocoPage of nocoPages) {
|
||||
await nocoPage.destroy();
|
||||
await setDefaultRole('root');
|
||||
}
|
||||
},
|
||||
mockManualDestroyPage: async ({ browser }, use) => {
|
||||
@ -378,6 +422,20 @@ const _test = base.extend<ExtendUtils>({
|
||||
];
|
||||
await Promise.all(deletePromises);
|
||||
},
|
||||
mockRole: async ({ page }, use) => {
|
||||
const mockRole = async (roleSetting: AclRoleSetting) => {
|
||||
return createRole(roleSetting);
|
||||
};
|
||||
|
||||
await use(mockRole);
|
||||
},
|
||||
updateRole: async ({ page }, use) => {
|
||||
async (roleSetting: AclRoleSetting) => {
|
||||
return updateRole(roleSetting);
|
||||
};
|
||||
|
||||
await use(updateRole);
|
||||
},
|
||||
});
|
||||
|
||||
export const test = Object.assign(_test, {
|
||||
@ -592,6 +650,76 @@ const createCollections = async (collectionSettings: CollectionSetting | Collect
|
||||
return (await result.json()).data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据配置创建一个角色并将角色关联给superAdmin且切换到新角色
|
||||
* @param page 运行测试的 page 实例
|
||||
* @param AclRoleSetting
|
||||
* @returns
|
||||
*/
|
||||
const createRole = async (roleSetting: AclRoleSetting) => {
|
||||
const api = await request.newContext({
|
||||
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||
});
|
||||
|
||||
const state = await api.storageState();
|
||||
const headers = getHeaders(state);
|
||||
const name = roleSetting.name || uid();
|
||||
|
||||
const result = await api.post(`/api/users/1/roles:create`, {
|
||||
headers,
|
||||
data: { ...roleSetting, name, title: name },
|
||||
});
|
||||
|
||||
if (!result.ok()) {
|
||||
throw new Error(await result.text());
|
||||
}
|
||||
const roleData = (await result.json()).data;
|
||||
await setDefaultRole(name);
|
||||
return roleData;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据配置更新角色权限
|
||||
* @param page 运行测试的 page 实例
|
||||
* @param AclRoleSetting
|
||||
* @returns
|
||||
*/
|
||||
const updateRole = async (roleSetting: AclRoleSetting) => {
|
||||
const api = await request.newContext({
|
||||
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||
});
|
||||
const state = await api.storageState();
|
||||
const headers = getHeaders(state);
|
||||
const name = roleSetting.name;
|
||||
|
||||
const result = await api.post(`/api/users/1/roles:update?filterByTk=${name}`, {
|
||||
headers,
|
||||
data: { ...roleSetting },
|
||||
});
|
||||
|
||||
if (!result.ok()) {
|
||||
throw new Error(await result.text());
|
||||
}
|
||||
const roleData = (await result.json()).data;
|
||||
return roleData;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置默认角色
|
||||
* @param name
|
||||
*/
|
||||
const setDefaultRole = async (name) => {
|
||||
const api = await request.newContext({
|
||||
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
|
||||
});
|
||||
const state = await api.storageState();
|
||||
const headers = getHeaders(state);
|
||||
await api.post(`/api/users:setDefaultRole`, {
|
||||
headers,
|
||||
data: { roleName: name },
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据 collection 的配置生成 Faker 数据
|
||||
* @param collectionSetting
|
||||
|
@ -0,0 +1,242 @@
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { oneTableBlock } from './utils';
|
||||
|
||||
test.describe('view', () => {
|
||||
//关系字段有权限,关系目标表无权限
|
||||
test('association field accept, target collection denied', async ({ page, mockPage, mockRole, mockRecord }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'users',
|
||||
},
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view', fields: ['oneToOneBelongsTo'] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
//关系字段可见
|
||||
await expect(page.getByRole('button', { name: 'oneToOneBelongsTo' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'phone' })).not.toBeVisible();
|
||||
await page.getByLabel('action-Action.Link-Association block-customize:popup-general-table').click();
|
||||
//关系区块不可见
|
||||
await expect(await page.getByLabel('block-item-CardItem-users-form')).not.toBeVisible();
|
||||
});
|
||||
//关系字段有权限,关系目标表个别字段有权限
|
||||
test('association field accept, target collection accept with fields', async ({
|
||||
page,
|
||||
mockPage,
|
||||
mockRole,
|
||||
mockRecord,
|
||||
}) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'users',
|
||||
actions: [{ name: 'view', fields: ['email'] }],
|
||||
},
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view', fields: ['oneToOneBelongsTo'] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
//关系字段可见
|
||||
await expect(page.getByRole('button', { name: 'oneToOneBelongsTo' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'phone' })).not.toBeVisible();
|
||||
await page.getByLabel('action-Action.Link-Association block-customize:popup-general-table').click();
|
||||
//关系区块可见,个别字段可见
|
||||
await expect(await page.getByLabel('block-item-CardItem-users-form')).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.nickname')).not.toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.email')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('update', () => {
|
||||
//关系字段有权限,关系目标表无权限
|
||||
test('association field accept,target collection denied', async ({ page, mockPage, mockRole, mockRecord }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'users',
|
||||
},
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{ name: 'view', fields: ['oneToOneBelongsTo'] },
|
||||
{ name: 'update', fields: ['oneToOneBelongsTo'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await page.getByLabel('action-Action.Link-Association field-customize:popup-general-table').click();
|
||||
//关系字段组件可见,子表单/子表格中字段不可见
|
||||
await expect(
|
||||
await page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo'),
|
||||
).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.email-Email')).not.toBeVisible();
|
||||
});
|
||||
//关系字段有权限,关系目标表个别字段有权限
|
||||
test('association field accept, target collection accept with fields', async ({
|
||||
page,
|
||||
mockPage,
|
||||
mockRole,
|
||||
mockRecord,
|
||||
}) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'users',
|
||||
actions: [{ name: 'view' }, { name: 'update', fields: ['email'] }],
|
||||
},
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{ name: 'view', fields: ['oneToOneBelongsTo'] },
|
||||
{ name: 'update', fields: ['oneToOneBelongsTo'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await page.getByLabel('action-Action.Link-Association field-customize:popup-general-table').click();
|
||||
//关系字段组件可见,子表单/子表格中个别字段可见
|
||||
await expect(
|
||||
await page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo'),
|
||||
).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.email')).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.phone')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('create', () => {
|
||||
//关系字段有权限,关系目标表无权限
|
||||
test('association field accept,target collection denied', async ({ page, mockPage, mockRole, mockRecord }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
default: true,
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'users',
|
||||
},
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{ name: 'view', fields: ['oneToOneBelongsTo'] },
|
||||
{ name: 'create', fields: ['oneToOneBelongsTo'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await page.getByLabel('action-Action-Add new-create').click();
|
||||
//关系字段组件可见,子表单/子表格中字段不可见
|
||||
await expect(
|
||||
await page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo'),
|
||||
).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.email')).not.toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.phone')).not.toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.username')).not.toBeVisible();
|
||||
});
|
||||
//关系字段有权限,关系目标表个别字段有权限
|
||||
test('association field accept, target collection accept with fields', async ({
|
||||
page,
|
||||
mockPage,
|
||||
mockRole,
|
||||
mockRecord,
|
||||
}) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
default: true,
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'users',
|
||||
actions: [{ name: 'view' }, { name: 'create', fields: ['email'] }],
|
||||
},
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{ name: 'view', fields: ['oneToOneBelongsTo'] },
|
||||
{ name: 'create', fields: ['oneToOneBelongsTo'] },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await page.getByLabel('action-Action-Add new-create').click();
|
||||
//关系字段组件可见,子表单/子表格中个别字段可见
|
||||
await expect(
|
||||
await page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo'),
|
||||
).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.email')).toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.phone')).not.toBeVisible();
|
||||
await expect(await page.getByLabel('block-item-CollectionField-users-form-users.username')).not.toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,461 @@
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { oneTableBlock } from './utils';
|
||||
|
||||
test.describe('view', () => {
|
||||
test('general permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
});
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view', fields: [] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'singleLineText' })).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'phone' })).not.toBeVisible();
|
||||
});
|
||||
test('individual collection permission with fields', async ({ page, mockPage, mockRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view', fields: ['singleLineText'] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
//特定字段有权限
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'singleLineText' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'phone' })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('create', () => {
|
||||
test('general permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await expect(await page.getByLabel('action-Action-Add new-create-general-table')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
strategy: {
|
||||
actions: ['view', 'create'],
|
||||
},
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Add new-create-general-table')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
strategy: {
|
||||
actions: ['create'],
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{
|
||||
name: 'view',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await expect(await page.getByLabel('action-Action-Add new-create-general-table')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{
|
||||
name: 'view',
|
||||
},
|
||||
{ name: 'create' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Add new-create-general-table')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission width fields', async ({ page, mockPage, mockRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{
|
||||
name: 'view',
|
||||
fields: ['id', 'singleLineText'],
|
||||
scope: null,
|
||||
},
|
||||
{
|
||||
name: 'create',
|
||||
fields: ['singleLineText'],
|
||||
scope: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(await page.getByLabel('action-Action-Add new-create-general-table')).toBeVisible();
|
||||
await await page.getByLabel('action-Action-Add new-create-general-table').click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.singleLineText')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('update', () => {
|
||||
test('general permission', async ({ page, mockPage, mockRole, mockRecord, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action.Link-Edit')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
strategy: {
|
||||
actions: ['view', 'update'],
|
||||
},
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action.Link-Edit')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission', async ({ page, mockPage, mockRole, mockRecord, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
strategy: {
|
||||
actions: ['update'],
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await expect(await page.getByLabel('action-Action.Link-Edit')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{
|
||||
name: 'view',
|
||||
},
|
||||
{ name: 'update' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action.Link-Edit')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission with fields', async ({ page, mockPage, mockRole, mockRecord }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await mockRecord('general');
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view' }, { name: 'update', fields: ['singleLineText', 'phone', 'email'] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(await page.getByLabel('action-Action.Link-Edit')).toBeVisible();
|
||||
await page.getByLabel('action-Action.Link-Edit').click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.singleLineText')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.phone')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.email')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CollectionField-general-form-general.number')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('destroy', () => {
|
||||
test('general permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Delete-destroy-general-table')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
strategy: {
|
||||
actions: ['view', 'destroy'],
|
||||
},
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Delete-destroy-general-table')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
strategy: {
|
||||
actions: ['destroy'],
|
||||
},
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [{ name: 'view' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-table')).toBeVisible();
|
||||
await expect(await page.getByLabel('action-Action-Delete-destroy-general-table')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{
|
||||
name: 'view',
|
||||
},
|
||||
{ name: 'destroy' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Delete-destroy-general-table')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('export', () => {
|
||||
test('general permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage(oneTableBlock).goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Export')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
strategy: {
|
||||
actions: ['view', 'export'],
|
||||
},
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Export')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission', async ({ page, mockPage, mockRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{ name: 'view' },
|
||||
{
|
||||
name: 'export',
|
||||
scope: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(await page.getByLabel('action-Action-Export')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('import', () => {
|
||||
test('general permission', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
strategy: {
|
||||
actions: ['view'],
|
||||
},
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Import')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
strategy: {
|
||||
actions: ['view', 'importXlsx'],
|
||||
},
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('action-Action-Import')).toBeVisible();
|
||||
});
|
||||
test('individual collection permission', async ({ page, mockPage, mockRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
allowNewMenu: true,
|
||||
resources: [
|
||||
{
|
||||
usingActionsConfig: true,
|
||||
name: 'general',
|
||||
actions: [
|
||||
{ name: 'view' },
|
||||
{
|
||||
name: 'importXlsx',
|
||||
scope: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage(oneTableBlock).goto();
|
||||
await expect(await page.getByLabel('action-Action-Import')).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,151 @@
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { oneTableBlock } from './utils';
|
||||
|
||||
test('allows to configure interface', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
snippets: ['ui.*'],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByTestId('ui-editor-button')).toBeVisible();
|
||||
await expect(await page.getByTestId('schema-initializer-Menu-header')).toBeVisible();
|
||||
//更新权限,无ui配置权限
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
snippets: ['!ui.*'],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByTestId('ui-editor-button')).not.toBeVisible();
|
||||
await expect(await page.getByTestId('schema-initializer-Menu-header')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('allows to install ,install,disabled plugins ', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
snippets: ['pm'],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByTestId('plugin-manager-button')).toBeVisible();
|
||||
await page.getByTestId('plugin-manager-button').click();
|
||||
await expect(await page.url()).toContain('/pm/list/local');
|
||||
await expect(
|
||||
await page.locator('.ant-page-header-heading').getByTitle('Plugin manager', { exact: true }),
|
||||
).toBeVisible();
|
||||
//无插件管理权限时访问直接路由访问时404
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
snippets: ['!pm'],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(await page.getByTestId('plugin-manager-button')).not.toBeVisible();
|
||||
await page.goto('/pm/list/local');
|
||||
await expect(await page.getByText('Sorry, the page you visited')).toBeVisible();
|
||||
await expect(await page.getByRole('button', { name: 'Back Home' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('allows to confgiure plugins ', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
snippets: ['pm.*'],
|
||||
strategy: {
|
||||
actions: ['view', 'update'],
|
||||
},
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await page.getByTestId('plugin-settings-button').click();
|
||||
await page.getByLabel('acl').click();
|
||||
await page.getByLabel(`action-Action.Link-Configure-roles-${roleData.name}`).click();
|
||||
await expect(page.getByRole('tab').getByText('Plugin settings permissions')).toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
snippets: ['!pm.*'],
|
||||
});
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('plugin-settings-button')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('allows to clear cache,reboot application ', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
snippets: ['app'],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await page.getByTestId('user-center-button').hover();
|
||||
await expect(await page.getByRole('menuitem', { name: 'Clear cache' })).toBeVisible();
|
||||
await expect(await page.getByRole('menuitem', { name: 'Restart application' })).toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
snippets: ['!app'],
|
||||
});
|
||||
await page.reload();
|
||||
await page.getByTestId('user-center-button').hover();
|
||||
await expect(await page.getByRole('menuitem', { name: 'Clear cache' })).not.toBeVisible();
|
||||
await expect(await page.getByRole('menuitem', { name: 'Restart application' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('new menu items allow to be asscessed by default ', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
snippets: ['ui.*'],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await mockPage({ ...oneTableBlock, name: 'new page' }).goto();
|
||||
await expect(page.getByLabel('new page')).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
allowNewMenu: true,
|
||||
});
|
||||
await mockPage({ ...oneTableBlock, name: 'new page' }).goto();
|
||||
await expect(page.getByLabel('new page')).toBeVisible();
|
||||
});
|
||||
|
||||
test('plugin settings permissions', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
await mockPage().goto();
|
||||
//新建角色并切换到新角色
|
||||
const roleData = await mockRole({
|
||||
snippets: ['pm', 'pm.*', '!pm.auth.authenticators', '!pm.collection-manager', '!pm.collection-manager.collections'],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await page.getByTestId('plugin-settings-button').hover();
|
||||
await expect(await page.getByLabel('acl')).toBeVisible();
|
||||
await expect(await page.getByLabel('auth')).not.toBeVisible();
|
||||
await expect(await page.getByLabel('collection-manager')).not.toBeVisible();
|
||||
await page.getByLabel('acl').click();
|
||||
await expect(await page.getByRole('menuitem', { name: 'login Authentication' })).not.toBeVisible();
|
||||
await expect(await page.getByRole('menuitem', { name: 'database Collection manager' })).not.toBeVisible();
|
||||
await updateRole({
|
||||
name: roleData.name,
|
||||
snippets: ['pm', 'pm.*', 'pm.auth.authenticators', 'pm.collection-manager', 'pm.collection-manager.collections'],
|
||||
});
|
||||
await page.reload();
|
||||
await page.getByTestId('plugin-settings-button').hover();
|
||||
await expect(await page.getByLabel('acl')).toBeVisible();
|
||||
await expect(await page.getByLabel('auth')).toBeVisible();
|
||||
await expect(await page.getByLabel('collection-manager')).toBeVisible();
|
||||
await page.getByLabel('acl').click();
|
||||
await expect(await page.getByRole('menuitem', { name: 'login Authentication' })).toBeVisible();
|
||||
await expect(await page.getByRole('menuitem', { name: 'database Collection manager' })).toBeVisible();
|
||||
});
|
@ -0,0 +1,50 @@
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
|
||||
test('menu permission ', async ({ page, mockPage, mockRole, updateRole }) => {
|
||||
const page2 = await mockPage({ name: 'page2' });
|
||||
const page1 = await mockPage({ name: 'page1' });
|
||||
await page1.goto();
|
||||
const uid1 = await page1.getUid();
|
||||
const uid2 = await page2.getUid();
|
||||
//新建角色并切换到新角色,page1有权限,page2无权限
|
||||
const roleData = await mockRole({
|
||||
snippets: ['pm.*'],
|
||||
strategy: {
|
||||
actions: ['view', 'update'],
|
||||
},
|
||||
menuUiSchemas: [uid1],
|
||||
});
|
||||
await page.evaluate((roleData) => {
|
||||
window.localStorage.setItem('NOCOBASE_ROLE', roleData.name);
|
||||
}, roleData);
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('page1')).toBeVisible();
|
||||
await expect(await page.getByLabel('page2')).not.toBeVisible();
|
||||
await page.getByTestId('plugin-settings-button').hover();
|
||||
await page.getByLabel('acl').click();
|
||||
await page.getByLabel(`action-Action.Link-Configure-roles-${roleData.name}`).click();
|
||||
await page.getByRole('tab').getByText('Menu permissions').click();
|
||||
await page.waitForSelector('.ant-table');
|
||||
const page1Menu = await page.getByRole('row', { name: 'page1' }).locator('.ant-checkbox-input');
|
||||
const page2Menu = await page.getByRole('row', { name: 'page2' }).locator('.ant-checkbox-input');
|
||||
await expect(await page1Menu.isChecked()).toBe(true);
|
||||
await expect(await page2Menu.isChecked()).toBe(false);
|
||||
//修改菜单权限,page1无权限,page2有权限
|
||||
await updateRole({ name: roleData.name, menuUiSchemas: [uid2] });
|
||||
await page.reload();
|
||||
await expect(await page.getByLabel('page2')).toBeVisible();
|
||||
await expect(await page.getByLabel('page1')).not.toBeVisible();
|
||||
await page.getByTestId('plugin-settings-button').hover();
|
||||
await page.getByLabel('acl').click();
|
||||
await page.getByLabel(`action-Action.Link-Configure-roles-${roleData.name}`).click();
|
||||
await page.getByRole('tab').getByText('Menu permissions').click();
|
||||
await page.waitForSelector('.ant-table');
|
||||
const page1Menu1 = await page.getByRole('row', { name: 'page1' }).locator('.ant-checkbox-input');
|
||||
const page2Menu1 = await page.getByRole('row', { name: 'page2' }).locator('.ant-checkbox-input');
|
||||
await expect(await page1Menu1.isChecked()).toBe(false);
|
||||
await expect(await page2Menu1.isChecked()).toBe(true);
|
||||
//通过路由访问无权限的菜单,跳到有权限的第一个菜单
|
||||
await page.goto(`/admin/${uid1}`);
|
||||
await page.waitForSelector('.nb-page-wrapper');
|
||||
await expect(await page.url()).toContain(uid2);
|
||||
});
|
2551
packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/utils.ts
Normal file
2551
packages/plugins/@nocobase/plugin-acl/src/client/__e2e__/utils.ts
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user