diff --git a/frontend/src/__mocks__/mockRoleBindingK8sResource.ts b/frontend/src/__mocks__/mockRoleBindingK8sResource.ts index e3be0ffd91..83cf71dbef 100644 --- a/frontend/src/__mocks__/mockRoleBindingK8sResource.ts +++ b/frontend/src/__mocks__/mockRoleBindingK8sResource.ts @@ -1,14 +1,25 @@ import { genUID } from '~/__mocks__/mockUtils'; import { KnownLabels, RoleBindingKind } from '~/k8sTypes'; +import { RoleBindingSubject } from '~/types'; type MockResourceConfigType = { name?: string; namespace?: string; + subjects?: RoleBindingSubject[]; + roleRefName?: string; }; export const mockRoleBindingK8sResource = ({ name = 'test-name-view', namespace = 'test-project', + subjects = [ + { + kind: 'ServiceAccount', + apiGroup: 'rbac.authorization.k8s.io', + name: 'test-name-sa', + }, + ], + roleRefName = 'view', }: MockResourceConfigType): RoleBindingKind => ({ kind: 'RoleBinding', apiVersion: 'rbac.authorization.k8s.io/v1', @@ -21,15 +32,10 @@ export const mockRoleBindingK8sResource = ({ [KnownLabels.DASHBOARD_RESOURCE]: 'true', }, }, - subjects: [ - { - kind: 'ServiceAccount', - name: 'test-name-sa', - }, - ], + subjects, roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', - name: 'view', + name: roleRefName, }, }); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/projects/Permissions.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/projects/Permissions.cy.ts new file mode 100644 index 0000000000..f18ef2e142 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/e2e/projects/Permissions.cy.ts @@ -0,0 +1,313 @@ +import { + mockDashboardConfig, + mockDscStatus, + mockK8sResourceList, + mockProjectK8sResource, + mockStatus, +} from '~/__mocks__'; +import { mockRoleBindingK8sResource } from '~/__mocks__/mockRoleBindingK8sResource'; +import { permissions } from '~/__tests__/cypress/cypress/pages/permissions'; +import { be } from '~/__tests__/cypress/cypress/utils/should'; +import { RoleBindingSubject } from '~/types'; + +const userSubjects: RoleBindingSubject[] = [ + { + kind: 'User', + apiGroup: 'rbac.authorization.k8s.io', + name: 'user-1', + }, +]; + +const groupSubjects: RoleBindingSubject[] = [ + { + kind: 'Group', + apiGroup: 'rbac.authorization.k8s.io', + name: 'group-1', + }, +]; + +type HandlersProps = { + isEmpty?: boolean; +}; + +const initIntercepts = ({ isEmpty = false }: HandlersProps) => { + cy.intercept('/api/status', mockStatus()); + cy.intercept('/api/config', mockDashboardConfig({})); + cy.intercept('/api/dsc/status', mockDscStatus({})); + cy.intercept( + { pathname: '/api/k8s/apis/project.openshift.io/v1/projects' }, + mockK8sResourceList([mockProjectK8sResource({ k8sName: 'test-project' })]), + ); + cy.intercept( + { + method: 'GET', + pathname: '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings', + }, + { + statusCode: 200, + body: mockK8sResourceList( + isEmpty + ? [] + : [ + mockRoleBindingK8sResource({ + name: 'user-1', + subjects: userSubjects, + roleRefName: 'edit', + }), + mockRoleBindingK8sResource({ + name: 'group-1', + subjects: groupSubjects, + roleRefName: 'edit', + }), + ], + ), + }, + ); +}; + +describe('Permissions tab', () => { + const userTable = permissions.getUserTable(); + const groupTable = permissions.getGroupTable(); + it('Empty table for groups and users', () => { + initIntercepts({ isEmpty: true }); + permissions.visit('test-project'); + + //User table + userTable.findRows().should('have.length', 0); + permissions.findAddUserButton().should('be.enabled'); + + //Group table + groupTable.findRows().should('have.length', 0); + permissions.findAddGroupButton().should('be.enabled'); + }); + + describe('Users table', () => { + it('Table sorting for users table', () => { + initIntercepts({ isEmpty: false }); + permissions.visit('test-project'); + + // by name + userTable.findTableHeaderButton('Name').click(); + userTable.findTableHeaderButton('Name').should(be.sortDescending); + userTable.findTableHeaderButton('Name').click(); + userTable.findTableHeaderButton('Name').should(be.sortAscending); + + //by permissions + userTable.findTableHeaderButton('Permission').click(); + userTable.findTableHeaderButton('Permission').should(be.sortAscending); + userTable.findTableHeaderButton('Permission').click(); + userTable.findTableHeaderButton('Permission').should(be.sortDescending); + + //by date added + userTable.findTableHeaderButton('Date added').click(); + userTable.findTableHeaderButton('Date added').should(be.sortAscending); + userTable.findTableHeaderButton('Date added').click(); + userTable.findTableHeaderButton('Date added').should(be.sortDescending); + }); + + it('Add user', () => { + initIntercepts({ isEmpty: true }); + cy.intercept( + { + method: 'POST', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings', + }, + mockRoleBindingK8sResource({}), + ).as('addUser'); + permissions.visit('test-project'); + + permissions.findAddUserButton().click(); + + userTable.findAddInput().fill('user-1'); + userTable.selectPermission('user-1', 'Admin Edit the project and manage user access'); + userTable.findSaveButton('User').click(); + + cy.wait('@addUser').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + 'opendatahub.io/dashboard': 'true', + 'opendatahub.io/project-sharing': 'true', + }, + }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'admin' }, + subjects: [{ apiGroup: 'rbac.authorization.k8s.io', kind: 'User', name: 'user-1' }], + }); + }); + }); + + it('Edit user', () => { + initIntercepts({ isEmpty: false }); + cy.intercept( + { + method: 'POST', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings', + }, + mockRoleBindingK8sResource({}), + ).as('editUser'); + cy.intercept( + { + method: 'DELETE', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings/user-1', + }, + { kind: 'Status', apiVersion: 'v1', metadata: {}, status: 'Success' }, + ).as('deleteUser'); + + permissions.visit('test-project'); + + userTable.getTableRow('user-1').findKebabAction('Edit').click(); + userTable.findEditInput('user-1').clear().type('user-3'); + userTable.selectPermission('user-3', 'Admin Edit the project and manage user access'); + userTable.findSaveButton('user-3').click(); + + cy.wait('@editUser').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + 'opendatahub.io/dashboard': 'true', + 'opendatahub.io/project-sharing': 'true', + }, + }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'admin' }, + subjects: [{ apiGroup: 'rbac.authorization.k8s.io', kind: 'User', name: 'user-3' }], + }); + }); + cy.wait('@deleteUser'); + }); + + it('Delete user', () => { + initIntercepts({ isEmpty: false }); + + cy.intercept( + { + method: 'DELETE', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings/user-1', + }, + { kind: 'Status', apiVersion: 'v1', metadata: {}, status: 'Success' }, + ).as('deleteUser'); + permissions.visit('test-project'); + + userTable.getTableRow('user-1').findKebabAction('Delete').click(); + + cy.wait('@deleteUser'); + }); + }); + + describe('Groups table', () => { + it('Table sorting for groups table', () => { + initIntercepts({ isEmpty: false }); + + permissions.visit('test-project'); + + groupTable.findTableHeaderButton('Name').click(); + groupTable.findTableHeaderButton('Name').should(be.sortDescending); + groupTable.findTableHeaderButton('Name').click(); + groupTable.findTableHeaderButton('Name').should(be.sortAscending); + + groupTable.findTableHeaderButton('Permission').click(); + groupTable.findTableHeaderButton('Permission').should(be.sortAscending); + groupTable.findTableHeaderButton('Permission').click(); + groupTable.findTableHeaderButton('Permission').should(be.sortDescending); + + groupTable.findTableHeaderButton('Date added').click(); + groupTable.findTableHeaderButton('Date added').should(be.sortAscending); + groupTable.findTableHeaderButton('Date added').click(); + groupTable.findTableHeaderButton('Date added').should(be.sortDescending); + }); + + it('Add group', () => { + initIntercepts({ isEmpty: true }); + cy.intercept( + { + method: 'POST', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings', + }, + mockRoleBindingK8sResource({}), + ).as('addGroup'); + permissions.visit('test-project'); + + permissions.findAddGroupButton().click(); + groupTable.findAddInput().fill('group-1'); + groupTable.selectPermission('group-1', 'Admin Edit the project and manage user access'); + groupTable.findSaveButton('Group').click(); + + cy.wait('@addGroup').then((inerception) => { + expect(inerception.request.body).to.containSubset({ + metadata: { + labels: { + 'opendatahub.io/dashboard': 'true', + 'opendatahub.io/project-sharing': 'true', + }, + }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'admin' }, + subjects: [{ apiGroup: 'rbac.authorization.k8s.io', kind: 'Group', name: 'group-1' }], + }); + }); + }); + + it('Edit group', () => { + initIntercepts({ isEmpty: false }); + + cy.intercept( + { + method: 'POST', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings', + }, + mockRoleBindingK8sResource({}), + ).as('editGroup'); + cy.intercept( + { + method: 'DELETE', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings/group-1', + }, + { kind: 'Status', apiVersion: 'v1', metadata: {}, status: 'Success' }, + ).as('deleteGroup'); + + permissions.visit('test-project'); + + groupTable.getTableRow('group-1').findKebabAction('Edit').click(); + groupTable.findEditInput('group-1').clear().type('group-3'); + groupTable.selectPermission('group-3', 'Admin Edit the project and manage user access'); + groupTable.findSaveButton('group-3').click(); + + cy.wait('@editGroup').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { + labels: { + 'opendatahub.io/dashboard': 'true', + 'opendatahub.io/project-sharing': 'true', + }, + }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'admin' }, + subjects: [{ apiGroup: 'rbac.authorization.k8s.io', kind: 'Group', name: 'group-3' }], + }); + }); + cy.wait('@deleteGroup'); + }); + + it('Delete group', () => { + initIntercepts({ isEmpty: false }); + + cy.intercept( + { + method: 'DELETE', + pathname: + '/api/k8s/apis/rbac.authorization.k8s.io/v1/namespaces/test-project/rolebindings/group-1', + }, + { kind: 'Status', apiVersion: 'v1', metadata: {}, status: 'Success' }, + ).as('deleteGroup'); + + permissions.visit('test-project'); + groupTable.getTableRow('group-1').findKebabAction('Delete').click(); + + cy.wait('@deleteGroup'); + }); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/pages/permissions.ts b/frontend/src/__tests__/cypress/cypress/pages/permissions.ts new file mode 100644 index 0000000000..2b529a977b --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/permissions.ts @@ -0,0 +1,71 @@ +import { Contextual } from './components/Contextual'; +import { TableRow } from './components/table'; + +class PermissionsTableRow extends TableRow {} + +class PermissionsTab { + visit(projectName: string) { + cy.visit(`/projects/${projectName}?section=permissions`); + this.wait(); + } + + private wait() { + cy.findByTestId('app-page-title'); + cy.testA11y(); + } + + findAddUserButton() { + return cy.findByTestId('add-button User'); + } + + findAddGroupButton() { + return cy.findByTestId('add-button Group'); + } + + getUserTable() { + return new PermissionTable(() => cy.findByTestId('project-sharing-table User')); + } + + getGroupTable() { + return new PermissionTable(() => cy.findByTestId('project-sharing-table Group')); + } +} + +class PermissionTable extends Contextual { + findRows() { + return this.find().find(`[data-label=Username]`); + } + + findAddInput() { + return this.find().findByTestId('project-sharing-name-input'); + } + + findEditInput(id: string) { + return this.find().findByTestId(['project-sharing-name-input', id]); + } + + getTableRow(name: string) { + return new PermissionsTableRow(() => + this.find().find(`[data-label=Username]`).contains(name).parents('tr'), + ); + } + + findTableHeaderButton(name: string) { + return this.find().find('thead').findByRole('button', { name }); + } + + selectPermission(id: string, name: string) { + return this.find() + .findByTestId(['project-sharing-name-input', id]) + .parents('tr') + .findByRole('button', { name: 'Options menu' }) + .findSelectOption(name) + .click(); + } + + findSaveButton(id: string) { + return this.find().findByTestId(['save-button', id]); + } +} + +export const permissions = new PermissionsTab(); diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx b/frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx index 561efd98f2..0c6c6d1fe5 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx +++ b/frontend/src/pages/projects/projectSharing/ProjectSharingNameInput.tsx @@ -25,6 +25,7 @@ const ProjectSharingNameInput: React.FC = ({ if (!typeAhead) { return ( = ({ diff --git a/frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx b/frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx index def20d1235..e000cba1a0 100644 --- a/frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx +++ b/frontend/src/pages/projects/projectSharing/ProjectSharingTableRow.tsx @@ -92,6 +92,7 @@ const ProjectSharingTableRow: React.FC = ({