diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts deleted file mode 100644 index 5e6891c7bdd..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts +++ /dev/null @@ -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 don’t have access, please check with the admin to get permissions' - ); - } -}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.ts deleted file mode 100644 index f798e8d1d52..00000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Users.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts index 81ddb6f7962..ca25fa42536 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts @@ -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?*', + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts index 9be3a9c6142..110ec2b9104 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/service.ts @@ -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 = { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts index 0dc0c97c2e7..e0fbfc864ca 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ActivityFeed.spec.ts @@ -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(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts index 5e5988f2a68..481d2b3b505 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Users.spec.ts @@ -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 user.create(apiContext); + 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(); + 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); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 3e919437981..6174d901dbc 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -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` ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 7634787aa0a..8b30549011d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -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', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts index 52c1bfd3a36..f9cbeb05656 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customizeLandingPage.ts @@ -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( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index 9d613422974..36c2f20c0c7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -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 | 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); + } + } +};