migrated user spec to playwright (#17652)

* migrated user spec to playwright

* optimization around making dataStewardUser from userClass

* changes as per comments

(cherry picked from commit d74cbed6e6f59b56ec11238788df368229b5eb3f)
This commit is contained in:
Ashish Gupta 2024-09-02 10:43:33 +05:30
parent 4e7b8c7167
commit c61fe37220
10 changed files with 1206 additions and 755 deletions

View File

@ -1,391 +0,0 @@
/*
* 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 {
customFormatDateTime,
getEpochMillisForFutureDays,
} from '../../../src/utils/date-time/DateTimeUtils';
import {
descriptionBox,
interceptURL,
toastNotification,
verifyResponseStatusCode,
} from '../common';
export const addUser = ({
name,
email,
password,
role,
}: {
name: string;
email: string;
password: string;
role: string;
}) => {
cy.get('[data-testid="add-user"]').click();
cy.get('[data-testid="email"]')
.scrollIntoView()
.should('exist')
.should('be.visible')
.type(email);
cy.get('[data-testid="displayName"]')
.should('exist')
.should('be.visible')
.type(name);
cy.get(descriptionBox)
.should('exist')
.should('be.visible')
.type('Adding user');
cy.get(':nth-child(2) > .ant-radio > .ant-radio-input').click();
cy.get('#password').type(password);
cy.get('#confirmPassword').type(password);
cy.get('[data-testid="roles-dropdown"] > .ant-select-selector')
.click()
.type(role);
cy.get('.ant-select-item-option-content').click();
cy.get('[data-testid="roles-dropdown"] > .ant-select-selector').click();
interceptURL('POST', ' /api/v1/users', 'add-user');
cy.get('[data-testid="save-user"]').scrollIntoView().click();
verifyResponseStatusCode('@add-user', 201);
interceptURL('GET', '/api/v1/users?*', 'getUsers');
verifyResponseStatusCode('@getUsers', 200);
};
export const visitProfileSection = () => {
interceptURL('GET', '/api/v1/users?*', 'getUsers');
verifyResponseStatusCode('@getUsers', 200);
cy.get('[data-testid="dropdown-profile"]').click({ force: true });
cy.get('[data-testid="user-name"] > .ant-typography').click({
force: true,
});
cy.get('[data-testid="access-token"] > .ant-space-item').click();
};
export const softDeleteUser = (username: string, displayName: string) => {
cy.get('[data-testid="loader"]').should('not.exist');
// Search the created user
interceptURL(
'GET',
'/api/v1/search/query?q=**&from=0&size=*&index=*',
'searchUser'
);
cy.get('[data-testid="searchbar"]').type(username);
verifyResponseStatusCode('@searchUser', 200);
// Click on delete button
cy.get(`[data-testid="delete-user-btn-${username}"]`).click();
// Soft deleting the user
cy.get('[data-testid="soft-delete"]').click();
cy.get('[data-testid="confirmation-text-input"]').type('DELETE');
interceptURL(
'DELETE',
'/api/v1/users/*?hardDelete=false&recursive=false',
'softdeleteUser'
);
interceptURL('GET', '/api/v1/users*', 'userDeleted');
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@softdeleteUser', 200);
verifyResponseStatusCode('@userDeleted', 200);
toastNotification(`"${displayName}" deleted successfully!`);
interceptURL('GET', '/api/v1/search/query*', 'searchUser');
// Verifying the deleted user
cy.get('[data-testid="searchbar"]').scrollIntoView().clear().type(username);
verifyResponseStatusCode('@searchUser', 200);
};
export const restoreUser = (username: string, editedUserName: string) => {
interceptURL('GET', '/api/v1/users?*', 'getUsers');
verifyResponseStatusCode('@getUsers', 200);
cy.get('[data-testid="loader"]').should('not.exist');
// Click on deleted user toggle
cy.get('[data-testid="show-deleted"]').click();
interceptURL('GET', '/api/v1/search/query*', 'searchUser');
verifyResponseStatusCode('@getUsers', 200);
cy.get('[data-testid="searchbar"]').type(username);
verifyResponseStatusCode('@searchUser', 200);
cy.get(`[data-testid="restore-user-btn-${username}"]`).click();
cy.get('.ant-modal-body > p').should(
'contain',
`Are you sure you want to restore ${editedUserName}?`
);
interceptURL('PUT', '/api/v1/users/restore', 'restoreUser');
cy.get('.ant-modal-footer > .ant-btn-primary').click();
verifyResponseStatusCode('@restoreUser', 200);
toastNotification('User restored successfully');
};
export const permanentDeleteUser = (username: string, displayName: string) => {
interceptURL('GET', '/api/v1/users?*', 'getUsers');
interceptURL('GET', '/api/v1/users/name/*', 'getUser');
verifyResponseStatusCode('@getUsers', 200);
verifyResponseStatusCode('@getUser', 200);
cy.get('[data-testid="loader"]').should('not.exist');
interceptURL('GET', '/api/v1/search/query*', 'searchUser');
cy.get('[data-testid="searchbar"]').type(username);
verifyResponseStatusCode('@searchUser', 200);
cy.get(`[data-testid="delete-user-btn-${username}"]`).click();
cy.get('[data-testid="hard-delete"]').click();
cy.get('[data-testid="confirmation-text-input"]').type('DELETE');
interceptURL(
'DELETE',
'api/v1/users/*?hardDelete=true&recursive=false',
'hardDeleteUser'
);
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@hardDeleteUser', 200);
toastNotification(`"${displayName}" deleted successfully!`);
interceptURL(
'GET',
'api/v1/search/query?q=**&from=0&size=15&index=user_search_index',
'searchUser'
);
cy.get('[data-testid="searchbar"]').type(username);
verifyResponseStatusCode('@searchUser', 200);
cy.get('[data-testid="search-error-placeholder"]').should('be.exist');
};
export const generateToken = () => {
cy.get('[data-testid="no-token"]').should('be.visible');
cy.get('[data-testid="auth-mechanism"] > span').click();
cy.get('[data-testid="token-expiry"]').should('be.visible').click();
cy.contains('1 hr').should('exist').should('be.visible').click();
cy.get('[data-testid="token-expiry"]').should('be.visible');
cy.get('[data-testid="save-edit"]').should('be.visible').click();
};
export const revokeToken = () => {
cy.get('[data-testid="revoke-button"]').should('be.visible').click();
cy.get('[data-testid="body-text"]').should(
'contain',
'Are you sure you want to revoke access for Personal Access Token?'
);
cy.get('[data-testid="save-button"]').click();
cy.get('[data-testid="revoke-button"]').should('not.exist');
};
export const updateExpiration = (expiry: number | string) => {
cy.get('[data-testid="dropdown-profile"]').click();
cy.get('[data-testid="user-name"] > .ant-typography').click({
force: true,
});
cy.get('[data-testid="access-token"] > .ant-space-item').click();
cy.get('[data-testid="no-token"]').should('be.visible');
cy.get('[data-testid="auth-mechanism"] > span').click();
cy.get('[data-testid="token-expiry"]').click();
cy.contains(`${expiry} days`).click();
const expiryDate = customFormatDateTime(
getEpochMillisForFutureDays(expiry as number),
`ccc d'th' MMMM, yyyy`
);
cy.get('[data-testid="save-edit"]').click();
cy.get('[data-testid="center-panel"]')
.find('[data-testid="revoke-button"]')
.should('be.visible');
cy.get('[data-testid="token-expiry"]')
.invoke('text')
.should('contain', `Expires on ${expiryDate}`);
cy.get('[data-testid="token-expiry"]').click();
revokeToken();
};
export const editDisplayName = (editedUserName: string) => {
interceptURL(
'GET',
'/api/v1/feed?*type=Conversation*',
'ActivityFeedConversation'
);
cy.get('[data-testid="edit-displayName"]').should('be.visible');
verifyResponseStatusCode('@ActivityFeedConversation', 200); // wait for the feed to load
cy.get('[data-testid="edit-displayName"]').click();
cy.get('[data-testid="displayName"]').clear();
cy.get('[data-testid="displayName"]').type(editedUserName);
interceptURL('PATCH', '/api/v1/users/*', 'updateName');
cy.get('[data-testid="inline-save-btn"]').click();
cy.get('[data-testid="user-name"]').should('contain', editedUserName);
};
export const editDescription = (updatedDescription: string) => {
cy.get('[data-testid="edit-description"]').click();
cy.get(descriptionBox).clear().type(updatedDescription);
interceptURL('PATCH', '/api/v1/users/*', 'patchDescription');
cy.get('[data-testid="save"]').should('be.visible').click();
verifyResponseStatusCode('@patchDescription', 200);
cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView();
cy.get('.ant-collapse-expand-icon > .anticon > svg').click();
cy.get(
':nth-child(2) > :nth-child(1) > [data-testid="viewer-container"] > [data-testid="markdown-parser"] > :nth-child(1) > .toastui-editor-contents > p'
).should('contain', updatedDescription);
};
export const editTeams = (teamName: string) => {
cy.get('[data-testid="edit-teams-button"]').click();
cy.get('.ant-select-selection-item-remove > .anticon').click();
cy.get('[data-testid="team-select"]').click();
cy.get('[data-testid="team-select"]').type(teamName);
interceptURL('PATCH', '/api/v1/users/*', 'updateTeams');
cy.get('.filter-node > .ant-select-tree-node-content-wrapper').click();
cy.get('[data-testid="inline-save-btn"]').click({ timeout: 10000 });
verifyResponseStatusCode('@updateTeams', 200);
cy.get(`[data-testid="${teamName}-link"]`)
.scrollIntoView()
.should('be.visible');
};
export const handleUserUpdateDetails = (
editedUserName: string,
updatedDescription: string
) => {
cy.get('[data-testid="dropdown-profile"]').click({ force: true });
cy.get('[data-testid="user-name"] > .ant-typography').click({
force: true,
});
// edit displayName
editDisplayName(editedUserName);
// edit description
cy.wait(500);
cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView();
cy.get('.ant-collapse-expand-icon > .anticon > svg').click();
editDescription(updatedDescription);
cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView();
cy.get('.ant-collapse-expand-icon > .anticon > svg').click();
};
export const handleAdminUpdateDetails = (
editedUserName: string,
updatedDescription: string,
teamName: string,
role?: string
) => {
// edit displayName
cy.get('[data-testid="dropdown-profile"]').click({ force: true });
cy.get('[data-testid="user-name"] > .ant-typography').click({
force: true,
});
editDisplayName(editedUserName);
// edit teams
cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView().click();
editTeams(teamName);
// edit description
editDescription(updatedDescription);
// edit roles
cy.get(`[data-testid="chip-container"]`).should('contain', role);
};
export const updateDetails = ({
updatedDisplayName,
updatedDescription,
isAdmin,
teamName,
role,
}: {
email: string;
password: string;
updatedDisplayName: string;
updatedDescription: string;
teamName: string;
isAdmin?: boolean;
role?: string;
}) => {
isAdmin
? handleAdminUpdateDetails(
updatedDisplayName,
updatedDescription,
teamName,
role
)
: handleUserUpdateDetails(updatedDisplayName, updatedDescription);
};
export const resetPassword = (password: string, newPassword: string) => {
cy.get('[data-testid="dropdown-profile"]').click({ force: true });
cy.get('[data-testid="user-name"] > .ant-typography').click({
force: true,
});
cy.clickOutside();
cy.get('[data-testid="change-password-button"]').click();
cy.get('.ant-modal-wrap').should('be.visible');
cy.get('[data-testid="input-oldPassword"]').clear().type(password);
cy.get('[data-testid="input-newPassword"]').clear().type(newPassword);
cy.get('[data-testid="input-confirm-newPassword"]').clear().type(newPassword);
interceptURL('PUT', '/api/v1/users/changePassword', 'changePassword');
cy.get('.ant-modal-footer > .ant-btn-primary')
.contains('Update Password')
.click();
verifyResponseStatusCode('@changePassword', 200);
toastNotification('Password updated successfully.');
};
export const editRole = (username: string, role: string) => {
interceptURL('GET', '/api/v1/users?*', 'getUsers');
verifyResponseStatusCode('@getUsers', 200);
cy.get('[data-testid="loader"]').should('not.exist');
// Search the created user
interceptURL(
'GET',
'/api/v1/search/query?q=**&from=0&size=*&index=*',
'searchUser'
);
cy.get('[data-testid="searchbar"]').type(username);
verifyResponseStatusCode('@searchUser', 200);
cy.get(`[data-testid=${username}]`).click();
cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView();
cy.get('.ant-collapse-expand-icon > .anticon > svg').click();
cy.get('[data-testid="edit-roles-button"]').click();
cy.get('.ant-select-selection-item-remove > .anticon').click();
cy.get('[data-testid="inline-edit-container"] #select-role')
.click()
.type(role);
cy.get('.ant-select-item-option-content').contains(role).click();
interceptURL('PATCH', `/api/v1/users/*`, 'updateRole');
cy.get('[data-testid="inline-save-btn"]').click();
verifyResponseStatusCode('@updateRole', 200);
cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView();
cy.get(`[data-testid=chip-container]`).should('contain', role);
};
export const checkNoPermissionPlaceholder = (permission = false) => {
cy.get('[data-testid="permission-error-placeholder"]').should(
permission ? 'not.be.visible' : 'be.visible'
);
if (!permission) {
cy.get('[data-testid="permission-error-placeholder"]').should(
'contain',
'You dont have access, please check with the admin to get permissions'
);
}
};

View File

@ -1,336 +0,0 @@
/*
* 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.
*/
// eslint-disable-next-line spaced-comment
import { interceptURL, verifyResponseStatusCode } from '../../common/common';
import UsersTestClass from '../../common/Entities/UserClass';
import { visitEntityDetailsPage } from '../../common/Utils/Entity';
import { getToken } from '../../common/Utils/LocalStorage';
import {
addOwner,
generateRandomUser,
removeOwner,
} from '../../common/Utils/Owner';
import {
cleanupPolicies,
createRoleViaREST,
DATA_CONSUMER_ROLE,
DATA_STEWARD_ROLE,
} from '../../common/Utils/Policy';
import {
addUser,
editRole,
generateToken,
resetPassword,
revokeToken,
updateDetails,
updateExpiration,
} from '../../common/Utils/Users';
import {
BASE_URL,
DELETE_ENTITY,
GLOBAL_SETTING_PERMISSIONS,
ID,
uuid,
} from '../../constants/constants';
import { EntityType, SidebarItem } from '../../constants/Entity.interface';
import {
GlobalSettingOptions,
SETTINGS_OPTIONS_PATH,
SETTING_CUSTOM_PROPERTIES_PATH,
} from '../../constants/settings.constant';
const entity = new UsersTestClass();
const expirationTime = {
oneday: '1',
sevendays: '7',
onemonth: '30',
twomonths: '60',
threemonths: '90',
};
const name = `usercttest${uuid()}`;
const owner = generateRandomUser();
let userId = '';
const ownerName = `${owner.firstName}${owner.lastName}`;
const user = {
name: name,
email: `${name}@gmail.com`,
password: `User@${uuid()}`,
updatedDisplayName: `Edited${uuid()}`,
newPassword: `NewUser@${uuid()}`,
teamName: 'Applications',
updatedDescription: 'This is updated description',
newStewardPassword: `StewUser@${uuid()}`,
};
describe('User with different Roles', { tags: 'Settings' }, () => {
before(() => {
cy.login();
cy.getAllLocalStorage().then((data) => {
const token = getToken(data);
createRoleViaREST({ token });
// Create a new user
cy.request({
method: 'POST',
url: `/api/v1/users/signup`,
headers: { Authorization: `Bearer ${token}` },
body: owner,
}).then((response) => {
userId = response.body.id;
});
});
});
after(() => {
cy.login();
cy.getAllLocalStorage().then((data) => {
const token = getToken(data);
cleanupPolicies({ token });
// Delete created user
cy.request({
method: 'DELETE',
url: `/api/v1/users/${userId}?hardDelete=true&recursive=false`,
headers: { Authorization: `Bearer ${token}` },
});
});
});
it('Update own admin details', () => {
cy.login();
updateDetails({
...user,
isAdmin: true,
role: 'Admin',
});
});
it('Create Data Consumer User', () => {
cy.login();
entity.visitUserListPage();
addUser({ ...user, role: DATA_CONSUMER_ROLE.name });
cy.logout();
});
it('Reset Data Consumer Password', () => {
cy.login(user.email, user.password);
resetPassword(user.password, user.newPassword);
cy.logout();
cy.login(user.email, user.newPassword);
});
it('Token generation & revocation', () => {
cy.login(user.email, user.newPassword);
cy.get('[data-testid="dropdown-profile"]').click({ force: true });
cy.get('[data-testid="user-name"] > .ant-typography', {
timeout: 10000,
}).click();
cy.get('[data-testid="access-token"]').click();
generateToken();
revokeToken();
});
it(`Update token expiration`, () => {
cy.login(user.email, user.newPassword);
entity.visitUserListPage();
Object.values(expirationTime).forEach((expiry) => {
updateExpiration(expiry);
});
});
it('Data Consumer user should have only view permission for glossary and tags', () => {
Cypress.session.clearAllSavedSessions();
cy.storeSession(user.email, user.newPassword);
cy.goToHomePage();
cy.url().should('eq', `${BASE_URL}/my-data`);
// Check CRUD for Glossary
cy.sidebarClick(SidebarItem.GLOSSARY);
cy.clickOnLogo();
// Check CRUD for Tags
cy.sidebarClick(SidebarItem.TAGS);
cy.wait(200);
cy.get('[data-testid="add-new-tag-button"]').should('not.exist');
cy.get('[data-testid="manage-button"]').should('not.exist');
});
it('Data Consumer operations for settings page', () => {
cy.login(user.email, user.newPassword);
Object.values(ID).forEach((id) => {
if (id?.api) {
interceptURL('GET', id.api, 'getTabDetails');
}
// Navigate to settings and respective tab page
cy.settingClick(id.testid);
if (id?.api) {
verifyResponseStatusCode('@getTabDetails', 200);
}
cy.get(`[data-testid="${id.button}"]`).should('not.be.exist');
});
Object.values(GLOBAL_SETTING_PERMISSIONS).forEach((id) => {
if (id.testid === GlobalSettingOptions.METADATA) {
cy.settingClick(id.testid);
} else {
cy.sidebarClick(SidebarItem.SETTINGS);
let paths = SETTINGS_OPTIONS_PATH[id.testid];
if (id.isCustomProperty) {
paths = SETTING_CUSTOM_PROPERTIES_PATH[id.testid];
}
cy.get(`[data-testid="${paths[0]}"]`).should('not.be.exist');
}
});
});
it('Data Consumer permissions for table details page', () => {
cy.login();
visitEntityDetailsPage({
term: DELETE_ENTITY.table.term,
serviceName: DELETE_ENTITY.table.serviceName,
entity: EntityType.Table,
});
addOwner(ownerName);
cy.logout();
cy.login(user.email, user.newPassword);
visitEntityDetailsPage({
term: DELETE_ENTITY.table.term,
serviceName: DELETE_ENTITY.table.serviceName,
entity: EntityType.Table,
});
entity.checkConsumerPermissions();
});
it('Update Data Consumer details', () => {
cy.login(user.email, user.newPassword);
updateDetails({ ...user, isAdmin: false });
});
it('Update Data Steward details', () => {
// change role from consumer to steward
cy.login();
entity.visitUserListPage();
editRole(user.name, DATA_STEWARD_ROLE.name);
cy.logout();
// login to steward user
cy.login(user.email, user.newPassword);
updateDetails({ ...user, isAdmin: false });
cy.logout();
});
it('Reset Data Steward Password', () => {
cy.login(user.email, user.newPassword);
resetPassword(user.newPassword, user.newStewardPassword);
cy.logout();
cy.login(user.email, user.newStewardPassword);
});
it('Token generation & revocation for Data Steward', () => {
cy.login(user.email, user.newStewardPassword);
entity.visitUserListPage();
cy.get('[data-testid="dropdown-profile"]').click({ force: true });
cy.get('[data-testid="user-name"] > .ant-typography').click({
force: true,
});
cy.get('[data-testid="access-token"] > .ant-space-item').click();
generateToken();
revokeToken();
});
it(`Update token expiration for Data Steward`, () => {
cy.login(user.email, user.newStewardPassword);
entity.visitUserListPage();
Object.values(expirationTime).forEach((expiry) => {
updateExpiration(expiry);
});
});
it('Data Steward operations for settings page', () => {
cy.login(user.email, user.newStewardPassword);
Object.values(ID).forEach((id) => {
if (id?.api) {
interceptURL('GET', id.api, 'getTabDetails');
}
// Navigate to settings and respective tab page
cy.settingClick(id.testid);
if (id?.api) {
verifyResponseStatusCode('@getTabDetails', 200);
}
cy.get(`[data-testid="${id.button}"]`).should('not.be.exist');
});
Object.values(GLOBAL_SETTING_PERMISSIONS).forEach((id) => {
if (id.testid === GlobalSettingOptions.METADATA) {
cy.settingClick(id.testid);
} else {
cy.sidebarClick(SidebarItem.SETTINGS);
let paths = SETTINGS_OPTIONS_PATH[id.testid];
if (id.isCustomProperty) {
paths = SETTING_CUSTOM_PROPERTIES_PATH[id.testid];
}
cy.get(`[data-testid="${paths[0]}"]`).should('not.be.exist');
}
});
});
it('Check Data Steward permissions', () => {
cy.login(user.email, user.newStewardPassword);
entity.checkStewardServicesPermissions();
cy.goToHomePage();
visitEntityDetailsPage({
term: DELETE_ENTITY.table.term,
serviceName: DELETE_ENTITY.table.serviceName,
entity: EntityType.Table,
});
entity.checkStewardPermissions();
cy.logout();
});
it('Admin Soft delete user', () => {
cy.login();
entity.visitUserListPage();
entity.softDeleteUser(user.name, user.updatedDisplayName);
});
it('Admin Restore soft deleted user', () => {
cy.login();
entity.visitUserListPage();
entity.restoreSoftDeletedUser(user.name, user.updatedDisplayName);
});
it('Admin Permanent Delete User', () => {
cy.login();
entity.visitUserListPage();
entity.permanentDeleteUser(user.name, user.updatedDisplayName);
});
it('Restore Admin Details', () => {
cy.login();
entity.restoreAdminDetails();
cy.goToHomePage();
visitEntityDetailsPage({
term: DELETE_ENTITY.table.term,
serviceName: DELETE_ENTITY.table.serviceName,
entity: EntityType.Table,
});
removeOwner(ownerName);
});
});

View File

@ -10,7 +10,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { PolicyRulesType } from '../support/access-control/PoliciesClass';
import { uuid } from '../utils/common';
import { GlobalSettingOptions } from './settings';
export const DEFAULT_POLICIES = {
dataConsumerPolicy: 'Data Consumer Policy',
@ -43,3 +45,128 @@ export const NEW_RULE_NAME = `New / Rule-test-${uuid()}`;
export const NEW_RULE_DESCRIPTION = `This is ${NEW_RULE_NAME} description`;
export const UPDATED_RULE_NAME = `New-Rule-test-${uuid()}-updated`;
export const DATA_STEWARD_RULES: PolicyRulesType[] = [
{
name: 'DataStewardRole',
resources: ['All'],
operations: [
'EditDescription',
'EditDisplayName',
'EditLineage',
'EditOwners',
'EditTags',
'ViewAll',
],
effect: 'allow',
},
];
export const GLOBAL_SETTING_PERMISSIONS: Record<
string,
{ testid: GlobalSettingOptions; isCustomProperty?: boolean }
> = {
metadata: {
testid: GlobalSettingOptions.METADATA,
},
customAttributesDatabase: {
testid: GlobalSettingOptions.DATABASES,
isCustomProperty: true,
},
customAttributesDatabaseSchema: {
testid: GlobalSettingOptions.DATABASE_SCHEMA,
isCustomProperty: true,
},
customAttributesStoredProcedure: {
testid: GlobalSettingOptions.STORED_PROCEDURES,
isCustomProperty: true,
},
customAttributesTable: {
testid: GlobalSettingOptions.TABLES,
isCustomProperty: true,
},
customAttributesTopics: {
testid: GlobalSettingOptions.TOPICS,
isCustomProperty: true,
},
customAttributesDashboards: {
testid: GlobalSettingOptions.DASHBOARDS,
isCustomProperty: true,
},
customAttributesPipelines: {
testid: GlobalSettingOptions.PIPELINES,
isCustomProperty: true,
},
customAttributesMlModels: {
testid: GlobalSettingOptions.MLMODELS,
isCustomProperty: true,
},
customAttributesSearchIndex: {
testid: GlobalSettingOptions.SEARCH_INDEXES,
isCustomProperty: true,
},
customAttributesGlossaryTerm: {
testid: GlobalSettingOptions.GLOSSARY_TERM,
isCustomProperty: true,
},
customAttributesAPICollection: {
testid: GlobalSettingOptions.API_COLLECTIONS,
isCustomProperty: true,
},
customAttributesAPIEndpoint: {
testid: GlobalSettingOptions.API_ENDPOINTS,
isCustomProperty: true,
},
bots: {
testid: GlobalSettingOptions.BOTS,
},
};
export const SETTING_PAGE_ENTITY_PERMISSION: Record<
string,
{ testid: GlobalSettingOptions; button: string; api?: string }
> = {
teams: {
testid: GlobalSettingOptions.TEAMS,
button: 'add-team',
},
users: {
testid: GlobalSettingOptions.USERS,
button: 'add-user',
api: '/api/v1/users?*',
},
admins: {
testid: GlobalSettingOptions.ADMINS,
button: 'add-user',
api: '/api/v1/users?*',
},
databases: {
testid: GlobalSettingOptions.DATABASES,
button: 'add-service-button',
api: '/api/v1/services/databaseServices?*',
},
messaging: {
testid: GlobalSettingOptions.MESSAGING,
button: 'add-service-button',
api: '/api/v1/services/messagingServices?*',
},
dashboard: {
testid: GlobalSettingOptions.DASHBOARDS,
button: 'add-service-button',
api: '/api/v1/services/dashboardServices?*',
},
pipelines: {
testid: GlobalSettingOptions.PIPELINES,
button: 'add-service-button',
api: '/api/v1/services/pipelineServices?*',
},
mlmodels: {
testid: GlobalSettingOptions.MLMODELS,
button: 'add-service-button',
api: '/api/v1/services/mlmodelServices?*',
},
storage: {
testid: GlobalSettingOptions.STORAGES,
button: 'add-service-button',
api: '/api/v1/services/storageServices?*',
},
};

View File

@ -26,6 +26,52 @@ export const SERVICE_TYPE = {
ApiService: GlobalSettingOptions.APIS,
};
export const SERVICE_CATEGORIES = {
DATABASE_SERVICES: 'databaseServices',
MESSAGING_SERVICES: 'messagingServices',
PIPELINE_SERVICES: 'pipelineServices',
DASHBOARD_SERVICES: 'dashboardServices',
ML_MODEL_SERVICES: 'mlmodelServices',
STORAGE_SERVICES: 'storageServices',
METADATA_SERVICES: 'metadataServices',
SEARCH_SERVICES: 'searchServices',
};
export const VISIT_SERVICE_PAGE_DETAILS = {
[SERVICE_TYPE.Database]: {
settingsMenuId: GlobalSettingOptions.DATABASES,
serviceCategory: SERVICE_CATEGORIES.DATABASE_SERVICES,
},
[SERVICE_TYPE.Messaging]: {
settingsMenuId: GlobalSettingOptions.MESSAGING,
serviceCategory: SERVICE_CATEGORIES.MESSAGING_SERVICES,
},
[SERVICE_TYPE.Dashboard]: {
settingsMenuId: GlobalSettingOptions.DASHBOARDS,
serviceCategory: SERVICE_CATEGORIES.DASHBOARD_SERVICES,
},
[SERVICE_TYPE.Pipeline]: {
settingsMenuId: GlobalSettingOptions.PIPELINES,
serviceCategory: SERVICE_CATEGORIES.PIPELINE_SERVICES,
},
[SERVICE_TYPE.MLModels]: {
settingsMenuId: GlobalSettingOptions.MLMODELS,
serviceCategory: SERVICE_CATEGORIES.ML_MODEL_SERVICES,
},
[SERVICE_TYPE.Storage]: {
settingsMenuId: GlobalSettingOptions.STORAGES,
serviceCategory: SERVICE_CATEGORIES.STORAGE_SERVICES,
},
[SERVICE_TYPE.Search]: {
settingsMenuId: GlobalSettingOptions.SEARCH,
serviceCategory: SERVICE_CATEGORIES.SEARCH_SERVICES,
},
[SERVICE_TYPE.Metadata]: {
settingsMenuId: GlobalSettingOptions.METADATA,
serviceCategory: SERVICE_CATEGORIES.METADATA_SERVICES,
},
};
const uniqueID = uuid();
export const REDSHIFT = {

View File

@ -30,7 +30,7 @@ import {
redirectToHomePage,
toastNotification,
uuid,
visitUserProfilePage,
visitOwnProfilePage,
} from '../../utils/common';
import { addOwner, updateDescription } from '../../utils/entity';
import { clickOnLogo } from '../../utils/sidebar';
@ -169,7 +169,7 @@ test.describe('Activity feed', () => {
}) => {
await redirectToHomePage(page);
await visitUserProfilePage(page);
await visitOwnProfilePage(page);
const secondFeedConversation = page
.locator('#center-container [data-testid="message-container"]')
@ -479,7 +479,7 @@ test.describe('Activity feed', () => {
});
});
base.describe('Activity feed with Data Steward User', () => {
base.describe('Activity feed with Data Consumer User', () => {
base.slow(true);
const id = uuid();

View File

@ -10,46 +10,430 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import test from '@playwright/test';
import { expect, Page, test as base } from '@playwright/test';
import { DATA_STEWARD_RULES } from '../../constant/permission';
import { GlobalSettingOptions } from '../../constant/settings';
import { SidebarItem } from '../../constant/sidebar';
import { PolicyClass } from '../../support/access-control/PoliciesClass';
import { RolesClass } from '../../support/access-control/RolesClass';
import { EntityTypeEndpoint } from '../../support/entity/Entity.interface';
import { TableClass } from '../../support/entity/TableClass';
import { UserClass } from '../../support/user/UserClass';
import { createNewPage, redirectToHomePage } from '../../utils/common';
import { settingClick } from '../../utils/sidebar';
import { performAdminLogin } from '../../utils/admin';
import {
redirectToHomePage,
uuid,
visitOwnProfilePage,
} from '../../utils/common';
import { addOwner } from '../../utils/entity';
import { settingClick, sidebarClick } from '../../utils/sidebar';
import {
addUser,
checkDataConsumerPermissions,
checkStewardPermissions,
checkStewardServicesPermissions,
generateToken,
hardDeleteUserProfilePage,
permanentDeleteUser,
resetPassword,
restoreUser,
restoreUserProfilePage,
revokeToken,
settingPageOperationPermissionCheck,
softDeleteUser,
softDeleteUserProfilePage,
updateExpiration,
updateUserDetails,
visitUserListPage,
visitUserProfilePage,
} from '../../utils/user';
const userName = `pw-user-${uuid()}`;
const expirationTime = [1, 7, 30, 60, 90];
const updatedUserDetails = {
name: userName,
email: `${userName}@gmail.com`,
updatedDisplayName: `Edited${uuid()}`,
teamName: 'Applications',
updatedDescription: `This is updated description ${uuid()}`,
password: `User@${uuid()}`,
newPassword: `NewUser@${uuid()}`,
};
const adminUser = new UserClass();
const dataConsumerUser = new UserClass();
const dataStewardUser = new UserClass();
const user = new UserClass();
const user2 = new UserClass();
const tableEntity = new TableClass();
const tableEntity2 = new TableClass();
const policy = new PolicyClass();
const role = new RolesClass();
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
const test = base.extend<{
adminPage: Page;
dataConsumerPage: Page;
dataStewardPage: Page;
}>({
adminPage: async ({ browser }, use) => {
const adminPage = await browser.newPage();
await adminUser.login(adminPage);
await use(adminPage);
await adminPage.close();
},
dataConsumerPage: async ({ browser }, use) => {
const page = await browser.newPage();
await dataConsumerUser.login(page);
await use(page);
await page.close();
},
dataStewardPage: async ({ browser }, use) => {
const page = await browser.newPage();
await dataStewardUser.login(page);
await use(page);
await page.close();
},
});
test.describe('User with different Roles', () => {
test.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await createNewPage(browser);
base.beforeAll('Setup pre-requests', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.create(apiContext);
await adminUser.setAdminRole(apiContext);
await dataConsumerUser.create(apiContext);
await dataStewardUser.create(apiContext);
await dataStewardUser.setDataStewardRole(apiContext);
await user.create(apiContext);
await user2.create(apiContext);
await tableEntity.create(apiContext);
await tableEntity2.create(apiContext);
await policy.create(apiContext, DATA_STEWARD_RULES);
await role.create(apiContext, [policy.responseData.name]);
await afterAction();
});
base.afterAll('Cleanup', async ({ browser }) => {
const { apiContext, afterAction } = await performAdminLogin(browser);
await adminUser.delete(apiContext);
await dataConsumerUser.delete(apiContext);
await dataStewardUser.delete(apiContext);
await tableEntity.delete(apiContext);
await tableEntity2.delete(apiContext);
await policy.delete(apiContext);
await role.delete(apiContext);
await afterAction();
});
test.describe('User with Admin Roles', () => {
test.slow(true);
test('Update own admin details', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
await updateUserDetails(adminPage, {
...updatedUserDetails,
isAdmin: true,
role: 'Admin',
});
});
test.beforeEach('Visit user list page', async ({ page }) => {
await redirectToHomePage(page);
test('Create and Delete user', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
await visitUserListPage(adminPage);
await addUser(adminPage, {
...updatedUserDetails,
role: role.responseData.displayName,
});
await visitUserProfilePage(adminPage, updatedUserDetails.name);
await visitUserListPage(adminPage);
await permanentDeleteUser(
adminPage,
updatedUserDetails.name,
updatedUserDetails.name,
false
);
});
test('Admin soft & hard delete and restore user', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
await visitUserListPage(adminPage);
await softDeleteUser(
adminPage,
user2.responseData.name,
user2.responseData.displayName
);
await restoreUser(
adminPage,
user2.responseData.name,
user2.responseData.displayName
);
await permanentDeleteUser(
adminPage,
user2.responseData.name,
user2.responseData.displayName
);
});
test('Admin soft & hard delete and restore user from profile page', async ({
page,
adminPage,
}) => {
await settingClick(page, GlobalSettingOptions.USERS);
await redirectToHomePage(adminPage);
await settingClick(adminPage, GlobalSettingOptions.USERS);
await softDeleteUserProfilePage(
page,
adminPage,
user.responseData.name,
user.responseData.displayName
);
await restoreUserProfilePage(page, user.responseData.fullyQualifiedName);
await hardDeleteUserProfilePage(page, user.responseData.displayName);
await restoreUserProfilePage(
adminPage,
user.responseData.fullyQualifiedName
);
await hardDeleteUserProfilePage(adminPage, user.responseData.displayName);
});
});
test.describe('User with Data Consumer Roles', () => {
test.slow(true);
test('Token generation & revocation for Data Consumer', async ({
dataConsumerPage,
}) => {
await redirectToHomePage(dataConsumerPage);
await visitOwnProfilePage(dataConsumerPage);
await dataConsumerPage.getByTestId('access-token').click();
await generateToken(dataConsumerPage);
await revokeToken(dataConsumerPage);
});
test(`Update token expiration for Data Consumer`, async ({
dataConsumerPage,
}) => {
await redirectToHomePage(dataConsumerPage);
await visitOwnProfilePage(dataConsumerPage);
await dataConsumerPage.getByTestId('access-token').click();
await expect(
dataConsumerPage.locator('[data-testid="no-token"]')
).toBeVisible();
await dataConsumerPage.click('[data-testid="auth-mechanism"] > span');
for (const expiry of expirationTime) {
await updateExpiration(dataConsumerPage, expiry);
}
});
test('User should have only view permission for glossary and tags for Data Consumer', async ({
dataConsumerPage,
}) => {
await redirectToHomePage(dataConsumerPage);
// Check CRUD for Glossary
await sidebarClick(dataConsumerPage, SidebarItem.GLOSSARY);
await expect(
dataConsumerPage.locator('[data-testid="add-glossary"]')
).not.toBeVisible();
await expect(
dataConsumerPage.locator('[data-testid="add-new-tag-button-header"]')
).not.toBeVisible();
await expect(
dataConsumerPage.locator('[data-testid="manage-button"]')
).not.toBeVisible();
// Glossary Term Table Action column
await expect(dataConsumerPage.getByText('Actions')).not.toBeVisible();
// right panel
await expect(
dataConsumerPage.locator('[data-testid="add-domain"]')
).not.toBeVisible();
await expect(
dataConsumerPage.locator('[data-testid="edit-owner"]')
).not.toBeVisible();
await expect(
dataConsumerPage.locator('[data-testid="edit-review-button"]')
).not.toBeVisible();
// Check CRUD for Tags
await sidebarClick(dataConsumerPage, SidebarItem.TAGS);
await expect(
dataConsumerPage.locator('[data-testid="add-classification"]')
).not.toBeVisible();
await expect(
dataConsumerPage.locator('[data-testid="add-new-tag-button"]')
).not.toBeVisible();
await expect(
dataConsumerPage.locator('[data-testid="manage-button"]')
).not.toBeVisible();
});
test('Operations for settings page for Data Consumer', async ({
dataConsumerPage,
}) => {
await settingPageOperationPermissionCheck(dataConsumerPage);
});
test('Permissions for table details page for Data Consumer', async ({
adminPage,
dataConsumerPage,
}) => {
await redirectToHomePage(adminPage);
await tableEntity.visitEntityPage(adminPage);
await addOwner({
page: adminPage,
owner: user.responseData.displayName,
type: 'Users',
endpoint: EntityTypeEndpoint.Table,
dataTestId: 'data-assets-header',
});
await tableEntity.visitEntityPage(dataConsumerPage);
await checkDataConsumerPermissions(dataConsumerPage);
});
test('Update user details for Data Consumer', async ({
dataConsumerPage,
}) => {
await redirectToHomePage(dataConsumerPage);
await updateUserDetails(dataConsumerPage, {
...updatedUserDetails,
isAdmin: false,
});
});
test('Reset Password for Data Consumer', async ({ dataConsumerPage }) => {
await redirectToHomePage(dataConsumerPage);
await resetPassword(
dataConsumerPage,
dataConsumerUser.data.password,
updatedUserDetails.password,
updatedUserDetails.newPassword
);
await dataConsumerUser.logout(dataConsumerPage);
await dataConsumerUser.login(
dataConsumerPage,
dataConsumerUser.data.email,
updatedUserDetails.newPassword
);
await visitOwnProfilePage(dataConsumerPage);
});
});
test.describe('User with Data Steward Roles', () => {
test.slow(true);
test('Update user details for Data Steward', async ({ dataStewardPage }) => {
await redirectToHomePage(dataStewardPage);
await updateUserDetails(dataStewardPage, {
...updatedUserDetails,
isAdmin: false,
});
});
test('Token generation & revocation for Data Steward', async ({
dataStewardPage,
}) => {
await redirectToHomePage(dataStewardPage);
await visitOwnProfilePage(dataStewardPage);
await dataStewardPage.getByTestId('access-token').click();
await generateToken(dataStewardPage);
await revokeToken(dataStewardPage);
});
test('Update token expiration for Data Steward', async ({
dataStewardPage,
}) => {
await redirectToHomePage(dataStewardPage);
await visitOwnProfilePage(dataStewardPage);
await dataStewardPage.getByTestId('access-token').click();
await expect(
dataStewardPage.locator('[data-testid="no-token"]')
).toBeVisible();
await dataStewardPage.click('[data-testid="auth-mechanism"] > span');
for (const expiry of expirationTime) {
await updateExpiration(dataStewardPage, expiry);
}
});
test('Operations for settings page for Data Steward', async ({
dataStewardPage,
}) => {
await settingPageOperationPermissionCheck(dataStewardPage);
});
test('Check permissions for Data Steward', async ({
adminPage,
dataStewardPage,
}) => {
await redirectToHomePage(adminPage);
await checkStewardServicesPermissions(dataStewardPage);
await tableEntity2.visitEntityPage(adminPage);
await addOwner({
page: adminPage,
owner: user.responseData.displayName,
type: 'Users',
endpoint: EntityTypeEndpoint.Table,
dataTestId: 'data-assets-header',
});
await tableEntity2.visitEntityPage(dataStewardPage);
await checkStewardPermissions(dataStewardPage);
});
test('Reset Password for Data Steward', async ({ dataStewardPage }) => {
await redirectToHomePage(dataStewardPage);
await resetPassword(
dataStewardPage,
dataStewardUser.data.password,
updatedUserDetails.password,
updatedUserDetails.newPassword
);
await dataStewardUser.logout(dataStewardPage);
await dataStewardUser.login(
dataStewardPage,
dataStewardUser.data.email,
updatedUserDetails.newPassword
);
await visitOwnProfilePage(dataStewardPage);
});
});

View File

@ -12,7 +12,11 @@
*/
import { APIRequestContext, Page } from '@playwright/test';
import { Operation } from 'fast-json-patch';
import { generateRandomUsername } from '../../utils/common';
import { DATA_STEWARD_RULES } from '../../constant/permission';
import { generateRandomUsername, uuid } from '../../utils/common';
import { PolicyClass } from '../access-control/PoliciesClass';
import { RolesClass } from '../access-control/RolesClass';
import { TeamClass } from '../team/TeamClass';
type ResponseDataType = {
name: string;
@ -29,10 +33,15 @@ type UserData = {
password: string;
};
const dataStewardPolicy = new PolicyClass();
const dataStewardRoles = new RolesClass();
let dataStewardTeam: TeamClass;
export class UserClass {
data: UserData;
responseData: ResponseDataType;
isUserDataSteward = false;
constructor(data?: UserData) {
this.data = data ? data : generateRandomUsername();
@ -85,7 +94,33 @@ export class UserClass {
});
}
async setDataStewardRole(apiContext: APIRequestContext) {
this.isUserDataSteward = true;
const id = uuid();
await dataStewardPolicy.create(apiContext, DATA_STEWARD_RULES);
await dataStewardRoles.create(apiContext, [
dataStewardPolicy.responseData.name,
]);
dataStewardTeam = new TeamClass({
name: `PW%data_steward_team-${id}`,
displayName: `PW Data Steward Team ${id}`,
description: 'playwright data steward team description',
teamType: 'Group',
users: [this.responseData.id],
defaultRoles: dataStewardRoles.responseData.id
? [dataStewardRoles.responseData.id]
: [],
});
await dataStewardTeam.create(apiContext);
}
async delete(apiContext: APIRequestContext) {
if (this.isUserDataSteward) {
await dataStewardPolicy.delete(apiContext);
await dataStewardRoles.delete(apiContext);
await dataStewardTeam.delete(apiContext);
}
const response = await apiContext.delete(
`/api/v1/users/${this.responseData.id}?recursive=false&hardDelete=true`
);

View File

@ -127,7 +127,7 @@ export const clickOutside = async (page: Page) => {
await page.mouse.move(1280, 0); // moving out side left menu bar to avoid random failure due to left menu bar
};
export const visitUserProfilePage = async (page: Page) => {
export const visitOwnProfilePage = async (page: Page) => {
await page.locator('[data-testid="dropdown-profile"] svg').click();
await page.waitForSelector('[role="menu"].profile-dropdown', {
state: 'visible',

View File

@ -12,7 +12,7 @@
*/
import { expect, Page } from '@playwright/test';
import { GlobalSettingOptions } from '../constant/settings';
import { visitUserProfilePage } from './common';
import { visitOwnProfilePage } from './common';
import { settingClick } from './sidebar';
export const navigateToCustomizeLandingPage = async (
@ -95,7 +95,7 @@ export const setUserDefaultPersona = async (
page: Page,
personaName: string
) => {
await visitUserProfilePage(page);
await visitOwnProfilePage(page);
await page
.locator(

View File

@ -11,11 +11,38 @@
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import { GlobalSettingOptions } from '../constant/settings';
import { expect, Page, Response } from '@playwright/test';
import {
customFormatDateTime,
getEpochMillisForFutureDays,
} from '../../src/utils/date-time/DateTimeUtils';
import {
GLOBAL_SETTING_PERMISSIONS,
SETTING_PAGE_ENTITY_PERMISSION,
} from '../constant/permission';
import { VISIT_SERVICE_PAGE_DETAILS } from '../constant/service';
import {
GlobalSettingOptions,
SETTINGS_OPTIONS_PATH,
SETTING_CUSTOM_PROPERTIES_PATH,
} from '../constant/settings';
import { SidebarItem } from '../constant/sidebar';
import { UserClass } from '../support/user/UserClass';
import { getAuthContext, getToken, toastNotification } from './common';
import { settingClick } from './sidebar';
import {
descriptionBox,
getAuthContext,
getToken,
redirectToHomePage,
toastNotification,
visitOwnProfilePage,
} from './common';
import { settingClick, sidebarClick } from './sidebar';
export const visitUserListPage = async (page: Page) => {
const fetchUsers = page.waitForResponse('/api/v1/users?*');
await settingClick(page, GlobalSettingOptions.USERS);
await fetchUsers;
};
export const performUserLogin = async (browser, user: UserClass) => {
const page = await browser.newPage();
@ -168,3 +195,562 @@ export const hardDeleteUserProfilePage = async (
await toastNotification(page, /deleted successfully!/);
};
export const editDisplayName = async (page: Page, editedUserName: string) => {
await page.click('[data-testid="edit-displayName"]');
await page.fill('[data-testid="displayName"]', '');
await page.type('[data-testid="displayName"]', editedUserName);
const saveResponse = page.waitForResponse('/api/v1/users/*');
await page.click('[data-testid="inline-save-btn"]');
await saveResponse;
// Verify the updated display name
const userName = await page.textContent('[data-testid="user-name"]');
expect(userName).toContain(editedUserName);
};
export const editTeams = async (page: Page, teamName: string) => {
await page.click('[data-testid="edit-teams-button"]');
await page.click('.ant-select-selection-item-remove > .anticon');
await page.click('[data-testid="team-select"]');
await page.type('[data-testid="team-select"]', teamName);
// Click the team from the dropdown
await page.click('.filter-node > .ant-select-tree-node-content-wrapper');
const updateTeamResponse = page.waitForResponse('/api/v1/users/*');
await page.click('[data-testid="inline-save-btn"]');
await updateTeamResponse;
// Verify the new team link is visible
await expect(page.locator(`[data-testid="${teamName}-link"]`)).toBeVisible();
};
export const editDescription = async (
page: Page,
updatedDescription: string
) => {
await page.click('[data-testid="edit-description"]');
// Clear and type the new description
await page.locator(descriptionBox).fill(updatedDescription);
const updateDescription = page.waitForResponse('/api/v1/users/*');
await page.click('[data-testid="save"]');
await updateDescription;
await page.click('.ant-collapse-expand-icon > .anticon > svg');
// Verify the updated description
const description = page.locator(
'[data-testid="asset-description-container"] .toastui-editor-contents > p'
);
await expect(description).toContainText(updatedDescription);
};
export const handleAdminUpdateDetails = async (
page: Page,
editedUserName: string,
updatedDescription: string,
teamName: string,
role?: string
) => {
const feedResponse = page.waitForResponse('/api/v1/feed?type=Conversation');
await visitOwnProfilePage(page);
await feedResponse;
// edit displayName
await editDisplayName(page, editedUserName);
// edit teams
await page.click('.ant-collapse-expand-icon > .anticon > svg');
await editTeams(page, teamName);
// edit description
await editDescription(page, updatedDescription);
await page.click('.ant-collapse-expand-icon > .anticon > svg');
// verify role for the user
const chipContainer = page.locator(
'[data-testid="user-profile-roles"] [data-testid="chip-container"]'
);
await expect(chipContainer).toContainText(role ?? '');
};
export const handleUserUpdateDetails = async (
page: Page,
editedUserName: string,
updatedDescription: string
) => {
const feedResponse = page.waitForResponse(
'/api/v1/feed?type=Conversation&filterType=OWNER_OR_FOLLOWS&userId=*'
);
await visitOwnProfilePage(page);
await feedResponse;
// edit displayName
await editDisplayName(page, editedUserName);
// edit description
await page.click('.ant-collapse-expand-icon > .anticon > svg');
await editDescription(page, updatedDescription);
};
export const updateUserDetails = async (
page: Page,
{
updatedDisplayName,
updatedDescription,
isAdmin,
teamName,
role,
}: {
updatedDisplayName: string;
updatedDescription: string;
teamName: string;
isAdmin?: boolean;
role?: string;
}
) => {
if (isAdmin) {
await handleAdminUpdateDetails(
page,
updatedDisplayName,
updatedDescription,
teamName,
role
);
} else {
await handleUserUpdateDetails(page, updatedDisplayName, updatedDescription);
}
};
export const softDeleteUser = async (
page: Page,
username: string,
displayName: string
) => {
// Wait for the loader to disappear
await page.waitForSelector('[data-testid="loader"]', { state: 'hidden' });
const searchResponse = page.waitForResponse(
'/api/v1/search/query?q=**&from=0&size=*&index=*'
);
await page.fill('[data-testid="searchbar"]', username);
await searchResponse;
// Click on delete button
await page.click(`[data-testid="delete-user-btn-${username}"]`);
// Soft deleting the user
await page.click('[data-testid="soft-delete"]');
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
const fetchUpdatedUsers = page.waitForResponse('/api/v1/users/*');
const deleteResponse = page.waitForResponse(
'/api/v1/users/*?hardDelete=false&recursive=false'
);
await page.click('[data-testid="confirm-button"]');
await deleteResponse;
await fetchUpdatedUsers;
await toastNotification(page, `"${displayName}" deleted successfully!`);
// Search soft deleted user in non-deleted mode
const searchSoftDeletedUserResponse = page.waitForResponse(
'/api/v1/search/query*'
);
await page.fill('[data-testid="searchbar"]', username);
await searchSoftDeletedUserResponse;
// Verify the search error placeholder is visible
const searchErrorPlaceholder = page.locator(
'[data-testid="search-error-placeholder"]'
);
await expect(searchErrorPlaceholder).toBeVisible();
};
export const restoreUser = async (
page: Page,
username: string,
editedUserName: string
) => {
// Click on deleted user toggle
await page.click('[data-testid="show-deleted"]');
const searchUsers = page.waitForResponse('/api/v1/search/query*');
await page.fill('[data-testid="searchbar"]', username);
await searchUsers;
// Click on restore user button
await page.click(`[data-testid="restore-user-btn-${username}"]`);
// Verify the modal content
const modalContent = page.locator('.ant-modal-body > p');
await expect(modalContent).toContainText(
`Are you sure you want to restore ${editedUserName}?`
);
// Click the confirm button in the modal
const restoreUserResponse = page.waitForResponse('/api/v1/users/restore');
await page.click('.ant-modal-footer > .ant-btn-primary');
await restoreUserResponse;
await toastNotification(page, 'User restored successfully');
};
export const permanentDeleteUser = async (
page: Page,
username: string,
displayName: string,
isUserSoftDeleted = true
) => {
if (isUserSoftDeleted) {
// Click on deleted user toggle to off it
await page.click('[data-testid="show-deleted"]');
}
// Search the user
const searchUserResponse = page.waitForResponse('/api/v1/search/query*');
await page.fill('[data-testid="searchbar"]', username);
await searchUserResponse;
// Click on delete user button
await page.click(`[data-testid="delete-user-btn-${username}"]`);
// Click on hard delete
await page.click('[data-testid="hard-delete"]');
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
const hardDeleteUserResponse = page.waitForResponse(
'api/v1/users/*?hardDelete=true&recursive=false'
);
await page.click('[data-testid="confirm-button"]');
await hardDeleteUserResponse;
await toastNotification(page, `"${displayName}" deleted successfully!`);
// Search the user again
const searchUserAfterDeleteResponse = page.waitForResponse(
'/api/v1/search/query*'
);
await page.fill('[data-testid="searchbar"]', username);
await searchUserAfterDeleteResponse;
// Verify the search error placeholder is visible
const searchErrorPlaceholder = page.locator(
'[data-testid="search-error-placeholder"]'
);
await expect(searchErrorPlaceholder).toBeVisible();
};
export const generateToken = async (page: Page) => {
await expect(page.locator('[data-testid="no-token"]')).toBeVisible();
await page.click('[data-testid="auth-mechanism"] > span');
await page.click('[data-testid="token-expiry"]');
await page.locator('[title="1 hr"] div').click();
await expect(page.locator('[data-testid="token-expiry"]')).toBeVisible();
const generateToken = page.waitForResponse('/api/v1/users/security/token');
await page.click('[data-testid="save-edit"]');
await generateToken;
};
export const revokeToken = async (page: Page) => {
await page.click('[data-testid="revoke-button"]');
await expect(page.locator('[data-testid="body-text"]')).toContainText(
'Are you sure you want to revoke access for Personal Access Token?'
);
await page.click('[data-testid="save-button"]');
await expect(page.locator('[data-testid="revoke-button"]')).not.toBeVisible();
};
export const updateExpiration = async (page: Page, expiry: number | string) => {
await page.click('[data-testid="token-expiry"]');
await page.click(`text=${expiry} days`);
const expiryDate = customFormatDateTime(
getEpochMillisForFutureDays(expiry as number),
`ccc d'th' MMMM, yyyy`
);
await page.click('[data-testid="save-edit"]');
await expect(
page.locator('[data-testid="center-panel"] [data-testid="revoke-button"]')
).toBeVisible();
await expect(page.locator('[data-testid="token-expiry"]')).toContainText(
`Expires on ${expiryDate}`
);
await revokeToken(page);
};
export const checkDataConsumerPermissions = async (page: Page) => {
// check Add domain permission
await expect(page.locator('[data-testid="add-domain"]')).not.toBeVisible();
await expect(
page.locator('[data-testid="edit-displayName-button"]')
).not.toBeVisible();
// Check edit owner permission
await expect(page.locator('[data-testid="edit-owner"]')).not.toBeVisible();
// Check edit description permission
await expect(page.locator('[data-testid="edit-description"]')).toBeVisible();
// Check edit tier permission
await expect(page.locator('[data-testid="edit-tier"]')).toBeVisible();
// Check right panel add tags button
await expect(
page.locator(
'[data-testid="entity-right-panel"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button'
)
).toBeVisible();
// Check right panel add glossary term button
await expect(
page.locator(
'[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button'
)
).toBeVisible();
if (process.env.PLAYWRIGHT_IS_OSS) {
await expect(
page.locator('[data-testid="manage-button"]')
).not.toBeVisible();
} else {
await expect(page.locator('[data-testid="manage-button"]')).toBeVisible();
await page.click('[data-testid="manage-button"]');
await expect(page.locator('[data-testid="export-button"]')).toBeVisible();
await expect(
page.locator('[data-testid="import-button"]')
).not.toBeVisible();
await expect(
page.locator('[data-testid="announcement-button"]')
).not.toBeVisible();
await expect(
page.locator('[data-testid="delete-button"]')
).not.toBeVisible();
}
await page.click('[data-testid="lineage"] > .ant-space-item');
await expect(page.locator('[data-testid="edit-lineage"]')).toBeDisabled();
};
export const checkStewardServicesPermissions = async (page: Page) => {
// Click on the sidebar item for Explore
await sidebarClick(page, SidebarItem.EXPLORE);
// Iterate through the service page details and check for the add service button
for (const service of Object.values(VISIT_SERVICE_PAGE_DETAILS)) {
await settingClick(page, service.settingsMenuId);
await expect(
page.locator('[data-testid="add-service-button"] > span')
).not.toBeVisible();
}
// Click on the sidebar item for Explore again
await sidebarClick(page, SidebarItem.EXPLORE);
// Perform search actions
await page.click('[data-testid="search-dropdown-Data Assets"]');
await page.locator('[data-testid="table-checkbox"]').scrollIntoViewIfNeeded();
await page.click('[data-testid="table-checkbox"]');
const getSearchResultResponse = page.waitForResponse(
'/api/v1/search/query?q=*'
);
await page.click('[data-testid="update-btn"]');
await getSearchResultResponse;
// Click on the entity link in the drawer title
await page.click(
'.ant-drawer-title > [data-testid="entity-link"] > .ant-typography'
);
// Check if the edit tier button is visible
await expect(page.locator('[data-testid="edit-tier"]')).toBeVisible();
};
export const checkStewardPermissions = async (page: Page) => {
// Check Add domain permission
await expect(page.locator('[data-testid="add-domain"]')).not.toBeVisible();
await expect(
page
.getByRole('cell', { name: 'user_id' })
.getByTestId('edit-displayName-button')
).toBeVisible();
// Check edit owner permission
await expect(page.locator('[data-testid="edit-owner"]')).toBeVisible();
// Check edit description permission
await expect(page.locator('[data-testid="edit-description"]')).toBeVisible();
// Check edit tier permission
await expect(page.locator('[data-testid="edit-tier"]')).toBeVisible();
// Check right panel add tags button
await expect(
page.locator(
'[data-testid="entity-right-panel"] [data-testid="tags-container"] [data-testid="entity-tags"] .tag-chip-add-button'
)
).toBeVisible();
// Check right panel add glossary term button
await expect(
page.locator(
'[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="entity-tags"] .tag-chip-add-button'
)
).toBeVisible();
// Check manage button
await expect(page.locator('[data-testid="manage-button"]')).toBeVisible();
// Click on lineage item
await page.click('[data-testid="lineage"] > .ant-space-item');
// Check if edit lineage button is enabled
await expect(page.locator('[data-testid="edit-lineage"]')).toBeEnabled();
};
export const addUser = async (page: Page, { name, email, password, role }) => {
await page.click('[data-testid="add-user"]');
await page.fill('[data-testid="email"]', email);
await page.fill('[data-testid="displayName"]', name);
await page.fill(descriptionBox, 'Adding new user');
await page.click(':nth-child(2) > .ant-radio > .ant-radio-input');
await page.fill('#password', password);
await page.fill('#confirmPassword', password);
await page.click('[data-testid="roles-dropdown"] > .ant-select-selector');
await page.type(
'[data-testid="roles-dropdown"] > .ant-select-selector',
role
);
await page.click('.ant-select-item-option-content');
await page.click('[data-testid="roles-dropdown"] > .ant-select-selector');
const saveResponse = page.waitForResponse('/api/v1/users');
await page.click('[data-testid="save-user"]');
await saveResponse;
expect((await saveResponse).status()).toBe(201);
};
const resetPasswordModal = async (
page: Page,
oldPassword: string,
newPassword: string,
isOldPasswordCorrect = true
) => {
await page.fill('[data-testid="input-oldPassword"]', oldPassword);
await page.fill('[data-testid="input-newPassword"]', newPassword);
await page.fill('[data-testid="input-confirm-newPassword"]', newPassword);
const saveResetPasswordResponse = page.waitForResponse(
'/api/v1/users/changePassword'
);
await page.click(
'.ant-modal-footer > .ant-btn-primary:has-text("Update Password")'
);
await saveResetPasswordResponse;
await toastNotification(
page,
isOldPasswordCorrect
? 'Password updated successfully.'
: 'Old Password is not correct'
);
};
export const resetPassword = async (
page: Page,
oldCorrectPassword: string,
oldWrongPassword: string,
newPassword: string
) => {
await visitOwnProfilePage(page);
await page.click('[data-testid="change-password-button"]');
await expect(page.locator('.ant-modal-wrap')).toBeVisible();
// Try with the wrong old password should throw an error
await resetPasswordModal(page, oldWrongPassword, newPassword, false);
// Try with the Correct old password should reset the password
await resetPasswordModal(page, oldCorrectPassword, newPassword);
};
export const expectSettingEntityNotVisible = async (
page: Page,
path: string[]
) => {
await expect(page.getByTestId(path[0])).not.toBeVisible();
};
// Check the permissions for the settings page for DataSteward and DataConsumer
export const settingPageOperationPermissionCheck = async (page: Page) => {
await redirectToHomePage(page);
for (const id of Object.values(SETTING_PAGE_ENTITY_PERMISSION)) {
let apiResponse: Promise<Response> | undefined;
if (id?.api) {
apiResponse = page.waitForResponse(id.api);
}
// Navigate to settings and respective tab page
await settingClick(page, id.testid);
if (id?.api && apiResponse) {
await apiResponse;
}
await expect(page.locator('.ant-skeleton-button')).not.toBeVisible();
await expect(page.getByTestId(id.button)).not.toBeVisible();
}
for (const id of Object.values(GLOBAL_SETTING_PERMISSIONS)) {
if (id.testid === GlobalSettingOptions.METADATA) {
await settingClick(page, id.testid);
} else {
await sidebarClick(page, SidebarItem.SETTINGS);
let paths = SETTINGS_OPTIONS_PATH[id.testid];
if (id.isCustomProperty) {
paths = SETTING_CUSTOM_PROPERTIES_PATH[id.testid];
}
await expectSettingEntityNotVisible(page, paths);
}
}
};