Playwright Tests coverage for Entity Permissions ,ServiceEntity, Domains and Glossary Operation Permissions (#22435)

* added test

* update permission for entities

* minor refactor

* minor code refactor

* code refactor

* minor fix

* minor fix

* update permissions for service entities

* code refactor

* fix tests

* address pr comments

* fix failing test

* fix dashboard test

* roles test fix

* fix dashboard data model  test

* added glossary and domains test

* fix test

* fix test

* fix test

* fix test

* remove cleanup

* fix entity test

* fix test

* minor fix

* minor fix

* update cleanup

---------

Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
This commit is contained in:
Shrushti Polekar 2025-08-03 17:36:32 +05:30 committed by GitHub
parent 1573470cac
commit d43b9d28dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1417 additions and 7 deletions

View File

@ -0,0 +1,228 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page, test as base } from '@playwright/test';
import { SidebarItem } from '../../../constant/sidebar';
import { Domain } from '../../../support/domain/Domain';
import { EntityDataClass } from '../../../support/entity/EntityDataClass';
import { UserClass } from '../../../support/user/UserClass';
import { performAdminLogin } from '../../../utils/admin';
import { redirectToHomePage, uuid } from '../../../utils/common';
import { addCustomPropertiesForEntity } from '../../../utils/customProperty';
import {
assignRoleToUser,
initializePermissions,
} from '../../../utils/permission';
import {
settingClick,
SettingOptionsType,
sidebarClick,
} from '../../../utils/sidebar';
const adminUser = new UserClass();
const testUser = new UserClass();
const test = base.extend<{
page: Page;
testUserPage: Page;
}>({
page: async ({ browser }, use) => {
const adminPage = await browser.newPage();
await adminUser.login(adminPage);
await use(adminPage);
await adminPage.close();
},
testUserPage: async ({ browser }, use) => {
const page = await browser.newPage();
await testUser.login(page);
await use(page);
await page.close();
},
});
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
await testUser.create(apiContext);
await afterAction();
});
const domain = new Domain();
const customPropertyName = `pwDomainCustomProperty${uuid()}`;
test.beforeAll('Setup domain and custom property', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await EntityDataClass.preRequisitesForTests(apiContext);
await domain.create(apiContext);
// Create custom property for domain once
const page = await browser.newPage();
await adminUser.login(page);
await settingClick(page, 'domains' as SettingOptionsType, true);
await addCustomPropertiesForEntity({
page,
propertyName: customPropertyName,
customPropertyData: { description: 'Test domain custom property' },
customType: 'String',
});
await page.close();
await afterAction();
});
test('Domain allow operations', async ({ testUserPage, browser }) => {
test.slow(true);
// Setup allow permissions
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'allow', [
'EditDescription',
'EditOwners',
'EditTags',
'Delete',
'EditDisplayName',
'Create',
'Delete',
'EditCustomFields',
]);
await assignRoleToUser(page, testUser);
await page.close();
// Navigate to domain page
await redirectToHomePage(testUserPage);
await sidebarClick(testUserPage, SidebarItem.DOMAIN);
await domain.visitEntityPage(testUserPage);
// Test that domain operation elements are visible
const directElements = [
'edit-description',
'add-owner',
'add-tag',
'edit-icon-right-panel',
'add-domain',
];
const manageButtonElements = ['delete-button', 'rename-button'];
await testUserPage.waitForLoadState('networkidle');
// Test direct elements first
for (const testId of directElements) {
let element;
if (testId === 'add-tag') {
// For add-tag, target the button within tags-container
element = testUserPage
.getByTestId('tags-container')
.getByTestId('add-tag');
} else {
element = testUserPage.getByTestId(testId).first();
}
await expect(element).toBeVisible();
}
// Click manage button once and test elements inside it
const manageButton = testUserPage.getByTestId('manage-button');
if (await manageButton.isVisible()) {
await manageButton.click();
for (const testId of manageButtonElements) {
const element = testUserPage.getByTestId(testId);
await expect(element).toBeVisible();
}
}
});
test('Domain deny operations', async ({ testUserPage, browser }) => {
test.slow(true);
// Setup deny permissions
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'deny', [
'EditDescription',
'EditOwners',
'EditTags',
'Delete',
'EditDisplayName',
'Create',
'Delete',
'EditCustomFields',
]);
await assignRoleToUser(page, testUser);
await page.close();
// Navigate to domain page
await redirectToHomePage(testUserPage);
await sidebarClick(testUserPage, SidebarItem.DOMAIN);
await domain.visitEntityPage(testUserPage);
// Test that domain operation elements are visible
const directElements = [
'edit-description',
'add-owner',
'add-tag',
'edit-icon-right-panel',
'add-domain',
];
const manageButtonElements = ['delete-button', 'rename-button'];
await testUserPage.waitForLoadState('networkidle');
for (const testId of directElements) {
let element;
if (testId === 'add-tag') {
// For add-tag, target the button within tags-container
element = testUserPage
.getByTestId('tags-container')
.getByTestId('add-tag');
} else {
element = testUserPage.getByTestId(testId).first();
}
await expect(element).not.toBeVisible();
}
// Click manage button once and test elements inside it
const manageButton = testUserPage.getByTestId('manage-button');
if (await manageButton.isVisible()) {
await manageButton.click();
for (const testId of manageButtonElements) {
const element = testUserPage.getByTestId(testId);
await expect(element).not.toBeVisible();
}
}
});
test.afterAll('Cleanup domain', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await domain.delete(apiContext);
await EntityDataClass.postRequisitesForTests(apiContext);
await afterAction();
});
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.delete(apiContext);
await testUser.delete(apiContext);
await afterAction();
});

View File

@ -0,0 +1,175 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page, test as base } from '@playwright/test';
import { EntityClass } from '../../../support/entity/EntityClass';
import { EntityDataClass } from '../../../support/entity/EntityDataClass';
import { UserClass } from '../../../support/user/UserClass';
import { performAdminLogin } from '../../../utils/admin';
import { getApiContext, uuid } from '../../../utils/common';
import {
ALL_OPERATIONS,
createCustomPropertyForEntity,
entityConfig,
runCommonPermissionTests,
runEntitySpecificPermissionTests,
} from '../../../utils/entityPermissionUtils';
import {
assignRoleToUser,
cleanupPermissions,
initializePermissions,
} from '../../../utils/permission';
const adminUser = new UserClass();
const testUser = new UserClass();
const test = base.extend<{
page: Page;
testUserPage: Page;
}>({
page: async ({ browser }, use) => {
const adminPage = await browser.newPage();
await adminUser.login(adminPage);
await use(adminPage);
},
testUserPage: async ({ browser }, use) => {
const page = await browser.newPage();
await testUser.login(page);
await use(page);
},
});
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
await testUser.create(apiContext);
await afterAction();
});
Object.entries(entityConfig).forEach(([, config]) => {
const entity = new config.class();
const entityType = entity.getType();
test.describe(`${entityType} Permissions`, () => {
const customPropertyName = `pw${entityType}CustomProperty${uuid()}`;
test.beforeAll('Setup entity', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await EntityDataClass.preRequisitesForTests(apiContext);
await entity.create(apiContext);
// Create custom property for this entity type
await createCustomPropertyForEntity(
browser,
entityType,
customPropertyName,
adminUser
);
await afterAction();
});
// Allow permissions tests
test.describe('Allow permissions', () => {
test.beforeAll('Initialize allow permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'allow', ALL_OPERATIONS);
await assignRoleToUser(page, testUser);
});
test(`${entityType} allow common operations permissions`, async ({
testUserPage,
}) => {
test.slow(true);
await runCommonPermissionTests(testUserPage, entity, 'allow');
});
// Entity-specific tests
if ('specificTest' in config && config.specificTest) {
test(`${entityType} allow entity-specific permission operations`, async ({
testUserPage,
}) => {
test.slow(true);
await runEntitySpecificPermissionTests(
testUserPage,
entity,
'allow',
config.specificTest as (
page: Page,
entity: EntityClass,
effect: 'allow' | 'deny'
) => Promise<void>
);
});
}
test.afterAll('Cleanup allow permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
const { apiContext } = await getApiContext(page);
await cleanupPermissions(apiContext);
await page.close();
});
});
// Deny permissions tests
test.describe('Deny permissions', () => {
test.beforeAll('Initialize deny permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'deny', ALL_OPERATIONS);
await assignRoleToUser(page, testUser);
});
test(`${entityType} deny common operations permissions`, async ({
testUserPage,
}) => {
test.slow(true);
await runCommonPermissionTests(testUserPage, entity, 'deny');
});
// Entity-specific tests
if ('specificTest' in config && config.specificTest) {
test(`${entityType} deny entity-specific permission operations`, async ({
testUserPage,
}) => {
test.slow(true);
await runEntitySpecificPermissionTests(
testUserPage,
entity,
'deny',
config.specificTest as (
page: Page,
entity: EntityClass,
effect: 'allow' | 'deny'
) => Promise<void>
);
});
}
test.afterAll('Cleanup deny permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
const { apiContext } = await getApiContext(page);
await cleanupPermissions(apiContext);
await page.close();
});
});
});
});

View File

@ -0,0 +1,203 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page, test as base } from '@playwright/test';
import { SidebarItem } from '../../../constant/sidebar';
import { EntityDataClass } from '../../../support/entity/EntityDataClass';
import { Glossary } from '../../../support/glossary/Glossary';
import { UserClass } from '../../../support/user/UserClass';
import { performAdminLogin } from '../../../utils/admin';
import { redirectToHomePage } from '../../../utils/common';
import {
assignRoleToUser,
initializePermissions,
} from '../../../utils/permission';
import { sidebarClick } from '../../../utils/sidebar';
const adminUser = new UserClass();
const testUser = new UserClass();
const test = base.extend<{
page: Page;
testUserPage: Page;
}>({
page: async ({ browser }, use) => {
const adminPage = await browser.newPage();
await adminUser.login(adminPage);
await use(adminPage);
await adminPage.close();
},
testUserPage: async ({ browser }, use) => {
const page = await browser.newPage();
await testUser.login(page);
await use(page);
await page.close();
},
});
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
await testUser.create(apiContext);
await afterAction();
});
const glossary = new Glossary();
test.beforeAll('Setup glossary', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await EntityDataClass.preRequisitesForTests(apiContext);
await glossary.create(apiContext);
await afterAction();
});
test('Glossary allow operations', async ({ testUserPage, browser }) => {
test.slow(true);
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'allow', [
'EditDescription',
'EditOwners',
'EditTags',
'Delete',
'EditDisplayName',
'Create',
'Delete',
'EditReviewers',
]);
await assignRoleToUser(page, testUser);
await page.close();
await redirectToHomePage(testUserPage);
await sidebarClick(testUserPage, SidebarItem.GLOSSARY);
await glossary.visitEntityPage(testUserPage);
// Test that glossary operation elements are visible
const directElements = [
'edit-description',
'add-owner',
'add-tag',
'Add',
'add-glossary',
];
const manageButtonElements = ['delete-button', 'rename-button'];
await testUserPage.waitForLoadState('networkidle');
for (const testId of directElements) {
let element;
if (testId === 'add-tag') {
element = testUserPage
.getByTestId('tags-container')
.getByTestId('add-tag');
} else {
element = testUserPage.getByTestId(testId).first();
}
await expect(element).toBeVisible();
}
const manageButton = testUserPage.getByTestId('manage-button');
if (await manageButton.isVisible()) {
await manageButton.click();
for (const testId of manageButtonElements) {
const element = testUserPage.getByTestId(testId);
await expect(element).toBeVisible();
}
}
});
test('Glossary deny operations', async ({ testUserPage, browser }) => {
test.slow(true);
// Setup deny permissions
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'deny', [
'EditDescription',
'EditOwners',
'EditTags',
'Delete',
'EditDisplayName',
'Create',
'Delete',
'EditReviewers',
]);
await assignRoleToUser(page, testUser);
await page.close();
// Navigate to glossary page
await redirectToHomePage(testUserPage);
await sidebarClick(testUserPage, SidebarItem.GLOSSARY);
await glossary.visitEntityPage(testUserPage);
// Test that glossary operation elements are visible
const directElements = [
'edit-description',
'add-owner',
'add-tag',
'add-glossary',
];
const manageButtonElements = ['delete-button', 'rename-button'];
await testUserPage.waitForLoadState('networkidle');
for (const testId of directElements) {
let element;
if (testId === 'add-tag') {
// For add-tag, target the button within tags-container
element = testUserPage
.getByTestId('tags-container')
.getByTestId('add-tag');
} else {
element = testUserPage.getByTestId(testId).first();
}
await expect(element).not.toBeVisible();
}
// Click manage button once and test elements inside it
const manageButton = testUserPage.getByTestId('manage-button');
if (await manageButton.isVisible()) {
await manageButton.click();
for (const testId of manageButtonElements) {
const element = testUserPage.getByTestId(testId);
await expect(element).not.toBeVisible();
}
}
});
test.afterAll('Cleanup glossary', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await glossary.delete(apiContext);
await EntityDataClass.postRequisitesForTests(apiContext);
await afterAction();
});
test.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.delete(apiContext);
await testUser.delete(apiContext);
await afterAction();
});

View File

@ -0,0 +1,142 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Page, test as base } from '@playwright/test';
import { EntityDataClass } from '../../../support/entity/EntityDataClass';
import { ApiServiceClass } from '../../../support/entity/service/ApiServiceClass';
import { DashboardServiceClass } from '../../../support/entity/service/DashboardServiceClass';
import { DatabaseServiceClass } from '../../../support/entity/service/DatabaseServiceClass';
import { MessagingServiceClass } from '../../../support/entity/service/MessagingServiceClass';
import { MlmodelServiceClass } from '../../../support/entity/service/MlmodelServiceClass';
import { PipelineServiceClass } from '../../../support/entity/service/PipelineServiceClass';
import { SearchIndexServiceClass } from '../../../support/entity/service/SearchIndexServiceClass';
import { StorageServiceClass } from '../../../support/entity/service/StorageServiceClass';
import { UserClass } from '../../../support/user/UserClass';
import { performAdminLogin } from '../../../utils/admin';
import { getApiContext } from '../../../utils/common';
import {
ALL_OPERATIONS,
runCommonPermissionTests,
} from '../../../utils/entityPermissionUtils';
import {
assignRoleToUser,
cleanupPermissions,
initializePermissions,
} from '../../../utils/permission';
const adminUser = new UserClass();
const testUser = new UserClass();
// Service entity classes
const serviceEntities = [
ApiServiceClass,
DashboardServiceClass,
DatabaseServiceClass,
MessagingServiceClass,
MlmodelServiceClass,
PipelineServiceClass,
SearchIndexServiceClass,
StorageServiceClass,
] as const;
const test = base.extend<{
page: Page;
testUserPage: Page;
}>({
page: async ({ browser }, use) => {
const adminPage = await browser.newPage();
await adminUser.login(adminPage);
await use(adminPage);
await adminPage.close();
},
testUserPage: async ({ browser }, use) => {
const page = await browser.newPage();
await testUser.login(page);
await use(page);
await page.close();
},
});
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
await testUser.create(apiContext);
await afterAction();
});
serviceEntities.forEach((EntityClass) => {
const entity = new EntityClass();
const entityType = entity.getType();
test.describe(`${entityType} Permissions`, () => {
test.beforeAll('Setup entity', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await EntityDataClass.preRequisitesForTests(apiContext);
await entity.create(apiContext);
await afterAction();
});
test.describe('Allow permissions', () => {
test.beforeAll('Initialize allow permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'allow', ALL_OPERATIONS);
await assignRoleToUser(page, testUser);
await page.close();
});
test(`${entityType} allow common operations permissions`, async ({
testUserPage,
}) => {
test.slow(true);
await runCommonPermissionTests(testUserPage, entity, 'allow');
});
test.afterAll('Cleanup allow permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
const { apiContext } = await getApiContext(page);
await cleanupPermissions(apiContext);
await page.close();
});
});
test.describe('Deny permissions', () => {
test.beforeAll('Initialize deny permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
await initializePermissions(page, 'deny', ALL_OPERATIONS);
await assignRoleToUser(page, testUser);
await page.close();
});
test(`${entityType} deny common operations permissions`, async ({
testUserPage,
}) => {
test.slow(true);
await runCommonPermissionTests(testUserPage, entity, 'deny');
});
test.afterAll('Cleanup deny permissions', async ({ browser }) => {
const page = await browser.newPage();
await adminUser.login(page);
const { apiContext } = await getApiContext(page);
await cleanupPermissions(apiContext);
await page.close();
});
});
});
});

View File

@ -244,7 +244,7 @@ entities.forEach((EntityClass) => {
); );
}); });
if (['Dashboard', 'Dashboard Data Model'].includes(entityName)) { if (['Dashboard', 'DashboardDataModel'].includes(entityName)) {
test(`${entityName} page should show the project name`, async ({ test(`${entityName} page should show the project name`, async ({
page, page,
}) => { }) => {
@ -369,7 +369,7 @@ entities.forEach((EntityClass) => {
}); });
}); });
if (['Table', 'Dashboard Data Model'].includes(entity.type)) { if (['Table', 'DashboardDataModel'].includes(entity.type)) {
test('DisplayName Add, Update and Remove for child entities', async ({ test('DisplayName Add, Update and Remove for child entities', async ({
page, page,
}) => { }) => {

View File

@ -140,7 +140,7 @@ entities.forEach((EntityClass) => {
}); });
}); });
if (['Table', 'Dashboard Data Model'].includes(entity.type)) { if (['Table', 'DashboardDataModel'].includes(entity.type)) {
test('DisplayName edit for child entities should not be allowed', async ({ test('DisplayName edit for child entities should not be allowed', async ({
page, page,
}) => { }) => {

View File

@ -173,7 +173,7 @@ entities.forEach((EntityClass) => {
}); });
}); });
if (['Table', 'Dashboard Data Model'].includes(entity.type)) { if (['Table', 'DashboardDataModel'].includes(entity.type)) {
test('DisplayName Add, Update and Remove for child entities', async ({ test('DisplayName Add, Update and Remove for child entities', async ({
page, page,
}) => { }) => {

View File

@ -69,7 +69,7 @@ export class DashboardDataModelClass extends EntityClass {
constructor(name?: string) { constructor(name?: string) {
super(EntityTypeEndpoint.DataModel); super(EntityTypeEndpoint.DataModel);
this.service.name = name ?? this.service.name; this.service.name = name ?? this.service.name;
this.type = 'Dashboard Data Model'; this.type = 'DashboardDataModel';
this.childrenTabId = 'model'; this.childrenTabId = 'model';
this.childrenSelectorId = this.children[0].name; this.childrenSelectorId = this.children[0].name;
this.serviceCategory = SERVICE_TYPE.Dashboard; this.serviceCategory = SERVICE_TYPE.Dashboard;

View File

@ -107,6 +107,8 @@ export const getApiContext = async (page: Page) => {
return { apiContext, afterAction }; return { apiContext, afterAction };
}; };
const DASHBOARD_DATA_MODEL = 'DashboardDataModel';
export const getEntityTypeSearchIndexMapping = (entityType: string) => { export const getEntityTypeSearchIndexMapping = (entityType: string) => {
const entityMapping = { const entityMapping = {
Table: 'table_search_index', Table: 'table_search_index',
@ -118,7 +120,7 @@ export const getEntityTypeSearchIndexMapping = (entityType: string) => {
SearchIndex: 'search_entity_search_index', SearchIndex: 'search_entity_search_index',
ApiEndpoint: 'api_endpoint_search_index', ApiEndpoint: 'api_endpoint_search_index',
Metric: 'metric_search_index', Metric: 'metric_search_index',
'Dashboard Data Model': 'dashboard_data_model_search_index', [DASHBOARD_DATA_MODEL]: 'dashboard_data_model_search_index',
}; };
return entityMapping[entityType as keyof typeof entityMapping]; return entityMapping[entityType as keyof typeof entityMapping];

View File

@ -1790,7 +1790,7 @@ export const checkExploreSearchFilter = async (
export const getEntityDataTypeDisplayPatch = (entity: EntityClass) => { export const getEntityDataTypeDisplayPatch = (entity: EntityClass) => {
switch (entity.getType()) { switch (entity.getType()) {
case 'Table': case 'Table':
case 'Dashboard Data Model': case 'DashboardDataModel':
return '/columns/0/dataTypeDisplay'; return '/columns/0/dataTypeDisplay';
case 'ApiEndpoint': case 'ApiEndpoint':
return '/requestSchema/schemaFields/0/dataTypeDisplay'; return '/requestSchema/schemaFields/0/dataTypeDisplay';

View File

@ -0,0 +1,594 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import { ContainerClass } from '../support/entity/ContainerClass';
import { DashboardClass } from '../support/entity/DashboardClass';
import { DashboardDataModelClass } from '../support/entity/DashboardDataModelClass';
import { EntityClass } from '../support/entity/EntityClass';
import { MetricClass } from '../support/entity/MetricClass';
import { MlModelClass } from '../support/entity/MlModelClass';
import { PipelineClass } from '../support/entity/PipelineClass';
import { SearchIndexClass } from '../support/entity/SearchIndexClass';
import { TableClass } from '../support/entity/TableClass';
import { TopicClass } from '../support/entity/TopicClass';
import { UserClass } from '../support/user/UserClass';
import { redirectToHomePage } from './common';
import { addCustomPropertiesForEntity } from './customProperty';
import { settingClick, SettingOptionsType } from './sidebar';
// All operations across all entities
export const ALL_OPERATIONS = [
// Common operations
'EditDescription',
'EditOwners',
'EditTier',
'EditDisplayName',
'EditTags',
'EditGlossaryTerms',
'EditCustomFields',
'Delete',
// Entity specific operations
'ViewQueries',
'ViewSampleData',
'ViewDataProfile',
'ViewTests',
'ViewUsage',
'EditQueries',
'EditDataProfile',
'EditSampleData',
'EditTests',
'EditStatus',
'EditLineage',
];
// Helper function to check element visibility based on configuration
const checkElementVisibility = async (
testUserPage: Page,
config: {
testId: string;
type: string;
containers?: string[];
},
effect: 'allow' | 'deny'
) => {
const { testId, type } = config;
if (effect === 'allow') {
switch (type) {
case 'direct': {
await expect(
testUserPage.locator(`[data-testid="${testId}"]`).first()
).toBeVisible();
break;
}
case 'multiple-containers': {
// Handle elements that exist in multiple containers
const containerLocators =
config.containers?.map((container) =>
testUserPage
.locator(`[data-testid="${container}"]`)
.locator(`button[data-testid="${testId}"]`)
) || [];
const containerVisibilityChecks = await Promise.all(
containerLocators.map((locator) => locator.isVisible())
);
// In allow case: any one of the containers should have the element visible
expect(
containerVisibilityChecks.some((visible) => visible)
).toBeTruthy();
break;
}
case 'with-manage-button': {
const manageButton = testUserPage.locator(
'[data-testid="manage-button"]'
);
if (await manageButton.isVisible()) {
await manageButton.click();
await expect(
testUserPage.locator(`[data-testid="${testId}"]`)
).toBeVisible();
}
break;
}
case 'label': {
await expect(testUserPage.getByText(testId).first()).toBeVisible();
break;
}
default: {
await expect(
testUserPage.locator(`[data-testid="${testId}"]`)
).toBeVisible();
}
}
} else {
// Deny effect
switch (type) {
case 'direct': {
await expect(
testUserPage.locator(`[data-testid="${testId}"]`).first()
).not.toBeVisible();
break;
}
case 'multiple-containers': {
// Handle elements that exist in multiple containers for deny case
const containerLocators =
config.containers?.map((container) =>
testUserPage
.locator(`[data-testid="${container}"]`)
.locator(`button[data-testid="${testId}"]`)
) || [];
const containerVisibilityChecks = await Promise.all(
containerLocators.map((locator) => locator.isVisible())
);
// In deny case: none of the containers should have the element visible
expect(
containerVisibilityChecks.every((visible) => !visible)
).toBeTruthy();
break;
}
case 'with-manage-button': {
const manageButton = testUserPage.locator(
'[data-testid="manage-button"]'
);
if (await manageButton.isVisible()) {
await manageButton.click();
await expect(
testUserPage.locator(`[data-testid="${testId}"]`)
).not.toBeVisible();
}
break;
}
case 'label': {
await expect(testUserPage.getByText(testId).first()).not.toBeVisible();
break;
}
default: {
await expect(
testUserPage.locator(`[data-testid="${testId}"]`)
).not.toBeVisible();
}
}
}
};
// Test common operations for any entity
export const testCommonOperations = async (
testUserPage: Page,
entity: EntityClass,
effect: 'allow' | 'deny'
) => {
// Navigate to entity page
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Define test configurations with special handling
const testIdsConfigs = [
{ testId: 'edit-description', type: 'direct' },
{
testId: 'add-tag',
type: 'multiple-containers',
containers: ['tags-container', 'glossary-container'],
},
{ testId: 'edit-tier', type: 'direct' },
{ testId: 'edit-owner', type: 'direct' },
{ testId: 'rename-button', type: 'with-manage-button' },
{ testId: 'delete-button', type: 'with-manage-button' },
];
await expect(
testUserPage.locator('[data-testid="entity-header-title"]')
).toBeVisible();
for (const config of testIdsConfigs) {
await checkElementVisibility(testUserPage, config, effect);
}
// Check custom properties
const customPropertiesLocator = testUserPage.locator(
'[data-testid="custom_properties"]'
);
if (await customPropertiesLocator.isVisible()) {
await customPropertiesLocator.click();
if (effect === 'allow') {
await expect(
testUserPage
.locator('[data-testid="custom-properties-card"]')
.first()
.getByTestId('edit-icon')
.first()
).toBeVisible();
} else {
await expect(
testUserPage
.locator('[data-testid="custom-properties-card"]')
.first()
.getByTestId('edit-icon')
.first()
).not.toBeVisible();
}
}
};
// Helper function to test permission error visibility
export const testPermissionErrorVisibility = async (
testUserPage: Page,
testId: string,
effect: 'allow' | 'deny',
expectedErrorMessage?: string
) => {
await testUserPage.locator(`[data-testid="${testId}"]`).click();
if (effect === 'deny') {
await expect(
testUserPage
.locator('[data-testid="permission-error-placeholder"]')
.getByText(
expectedErrorMessage || "You don't have necessary permissions."
)
).toBeVisible();
} else {
await expect(
testUserPage
.locator('[data-testid="permission-error-placeholder"]')
.getByText(
expectedErrorMessage || "You don't have necessary permissions."
)
).not.toBeVisible();
}
};
// Helper function to test profiler tab permissions
export const testProfilerTabPermission = async (
testUserPage: Page,
tabName: string,
effect: 'allow' | 'deny',
expectedErrorMessage?: string
) => {
await testUserPage
.locator('[data-testid="profiler-tab-left-panel"]')
.getByText(tabName)
.click();
if (effect === 'deny') {
await expect(
testUserPage
.locator('[data-testid="permission-error-placeholder"]')
.getByText(
expectedErrorMessage || "You don't have necessary permissions."
)
).toBeVisible();
} else {
await expect(
testUserPage.locator('[data-testid="permission-error-placeholder"]')
).not.toBeVisible();
}
};
// Entity-specific test functions
export const testTableSpecificOperations = async (
testUserPage: Page,
entity: TableClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Test ViewQueries
await testPermissionErrorVisibility(
testUserPage,
'table_queries',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Queries permission."
);
// Test ViewSampleData
await testPermissionErrorVisibility(
testUserPage,
'sample_data',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Sample Data permission."
);
// Test ViewDataProfile
await testUserPage.locator('[data-testid="profiler"]').click();
// Test Table Profile
await testProfilerTabPermission(
testUserPage,
'Table Profile',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Data Observability permission."
);
// Test Column Profile
await testProfilerTabPermission(
testUserPage,
'Column Profile',
effect,
"You don't have necessary permissions. Please check with the admin to get the ViewDataProfile permission."
);
// Test Data Quality
await testProfilerTabPermission(
testUserPage,
'Data Quality',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Data Observability permission."
);
await checkElementVisibility(
testUserPage,
{
testId: 'Usage',
type: 'label',
},
effect
);
};
export const testTopicSpecificOperations = async (
testUserPage: Page,
entity: TopicClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Test ViewSampleData for Topic
await testPermissionErrorVisibility(
testUserPage,
'sample_data',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Sample Data permission."
);
};
export const testPipelineSpecificOperations = async (
testUserPage: Page,
entity: PipelineClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Test Edit Lineage for Pipeline
await testUserPage.getByRole('tab', { name: 'Lineage' }).click();
await testUserPage.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
if (effect === 'allow') {
await expect(testUserPage.getByTestId('edit-lineage')).toBeVisible();
} else {
await expect(testUserPage.getByTestId('edit-lineage')).toBeDisabled();
}
};
export const testSearchIndexSpecificOperations = async (
testUserPage: Page,
entity: SearchIndexClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Test ViewUsage for Search Index
await testPermissionErrorVisibility(
testUserPage,
'sample_data',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Sample Data permission."
);
};
export const testStoredProcedureSpecificOperations = async (
testUserPage: Page,
entity: TableClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Test ViewUsage for Stored Procedure
await testPermissionErrorVisibility(
testUserPage,
'usage',
effect,
"You don't have necessary permissions. Please check with the admin to get the View Usage permission."
);
};
export const testDashboardDataModelSpecificOperations = async (
testUserPage: Page,
entity: DashboardDataModelClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
// Test Edit Lineage for Dashboard Data Model
await testUserPage.getByRole('tab', { name: 'Lineage' }).click();
await testUserPage.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
if (effect === 'allow') {
await expect(testUserPage.getByTestId('edit-lineage')).toBeVisible();
} else {
await expect(testUserPage.getByTestId('edit-lineage')).toBeDisabled();
}
};
export const testDashboardSpecificOperations = async (
testUserPage: Page,
entity: DashboardClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
await checkElementVisibility(
testUserPage,
{
testId: 'Usage',
type: 'label',
},
effect
);
};
export const testMlModelSpecificOperations = async (
testUserPage: Page,
entity: MlModelClass,
effect: 'allow' | 'deny'
) => {
await redirectToHomePage(testUserPage);
await entity.visitEntityPage(testUserPage);
await checkElementVisibility(
testUserPage,
{
testId: 'Usage',
type: 'label',
},
effect
);
};
// Helper function to run common permission tests
export const runCommonPermissionTests = async (
testUserPage: Page,
entity: EntityClass,
effect: 'allow' | 'deny'
) => {
await testCommonOperations(testUserPage, entity, effect);
};
export const runEntitySpecificPermissionTests = async (
testUserPage: Page,
entity: EntityClass,
effect: 'allow' | 'deny',
specificTest: (
page: Page,
entity: EntityClass,
effect: 'allow' | 'deny'
) => Promise<void>
) => {
await specificTest(testUserPage, entity, effect);
};
// Entity configuration with their specific test functions
export const entityConfig = {
Table: {
class: TableClass,
specificTest: testTableSpecificOperations,
},
Dashboard: {
class: DashboardClass,
specificTest: testDashboardSpecificOperations,
},
Pipeline: {
class: PipelineClass,
specificTest: testPipelineSpecificOperations,
},
Topic: {
class: TopicClass,
specificTest: testTopicSpecificOperations,
},
MlModel: {
class: MlModelClass,
specificTest: testMlModelSpecificOperations,
},
Container: {
class: ContainerClass,
},
SearchIndex: {
class: SearchIndexClass,
specificTest: testSearchIndexSpecificOperations,
},
DashboardDataModel: {
class: DashboardDataModelClass,
specificTest: testDashboardDataModelSpecificOperations,
},
Metric: {
class: MetricClass,
},
} as const;
// Function to create custom properties for different entity types
export const createCustomPropertyForEntity = async (
browser: any,
entityType: string,
customPropertyName: string,
adminUser: UserClass
) => {
const page = await browser.newPage();
await adminUser.login(page);
// Map entity types to their correct API types (same as used in working tests)
const entityTypeMapping: Record<string, string> = {
Table: 'tables',
Dashboard: 'dashboards',
Pipeline: 'pipelines',
Topic: 'topics',
MlModel: 'mlmodels',
Container: 'containers',
SearchIndex: 'searchIndexes',
DashboardDataModel: 'dashboardDataModels',
Metric: 'metrics',
Database: 'databases',
DatabaseSchema: 'databaseSchemas',
StoredProcedure: 'storedProcedures',
GlossaryTerm: 'glossaryTerm',
Domain: 'domains',
ApiCollection: 'apiCollections',
ApiEndpoint: 'apiEndpoints',
DataProduct: 'dataProducts',
};
const entityApiType =
entityTypeMapping[entityType] || entityType.toLowerCase();
await settingClick(page, entityApiType as SettingOptionsType, true);
await addCustomPropertiesForEntity({
page,
propertyName: customPropertyName,
customPropertyData: { description: `Test ${entityType} custom property` },
customType: 'String',
});
await page.close();
};

View File

@ -14,7 +14,64 @@ import { APIRequestContext, expect, Page } from '@playwright/test';
import { import {
DATA_CONSUMER_RULES, DATA_CONSUMER_RULES,
ORGANIZATION_POLICY_RULES, ORGANIZATION_POLICY_RULES,
VIEW_ALL_RULE,
} from '../constant/permission'; } from '../constant/permission';
import { PolicyClass } from '../support/access-control/PoliciesClass';
import { RolesClass } from '../support/access-control/RolesClass';
import { UserClass } from '../support/user/UserClass';
import { getApiContext, redirectToHomePage } from './common';
let policy: PolicyClass;
let role: RolesClass;
export const initializePermissions = async (
page: Page,
effect: 'allow' | 'deny',
operations: string[]
) => {
await redirectToHomePage(page);
const { apiContext } = await getApiContext(page);
policy = new PolicyClass();
const policyRules = [
...VIEW_ALL_RULE,
{
name: `Global${effect}AllOperationsPolicy`,
resources: ['All'],
operations,
effect,
},
];
await policy.create(apiContext, policyRules);
role = new RolesClass();
await role.create(apiContext, [policy.responseData.name]);
return { apiContext, policy, role };
};
export const assignRoleToUser = async (page: Page, testUser: UserClass) => {
const { apiContext } = await getApiContext(page);
await testUser.patch({
apiContext,
patchData: [
{
op: 'replace',
path: '/roles',
value: [
{
id: role.responseData.id,
type: 'role',
name: role.responseData.name,
},
],
},
],
});
};
export const checkNoPermissionPlaceholder = async ( export const checkNoPermissionPlaceholder = async (
page: Page, page: Page,
@ -169,3 +226,12 @@ export const updateDefaultOrganizationPolicy = async (
}, },
}); });
}; };
export const cleanupPermissions = async (apiContext: APIRequestContext) => {
if (role && role.responseData?.id) {
await role.delete(apiContext);
}
if (policy && policy.responseData?.id) {
await policy.delete(apiContext);
}
};