diff --git a/frontend/src/__mocks__/mockGroupConfig.ts b/frontend/src/__mocks__/mockGroupConfig.ts new file mode 100644 index 0000000000..aa29a91add --- /dev/null +++ b/frontend/src/__mocks__/mockGroupConfig.ts @@ -0,0 +1,33 @@ +import { GroupsConfig } from '~/pages/groupSettings/groupTypes'; + +export const mockGroupSettings = (): GroupsConfig => ({ + adminGroups: [ + { + id: 0, + name: 'odh-admins', + enabled: true, + }, + { + id: 1, + name: 'odh-admins-1', + enabled: false, + }, + ], + allowedGroups: [ + { + id: 0, + name: 'odh-admins', + enabled: false, + }, + { + id: 1, + name: 'odh-admins-1', + enabled: false, + }, + { + id: 2, + name: 'system:authenticated', + enabled: true, + }, + ], +}); diff --git a/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts index ec9ab2e2d8..c2c55a9d7d 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/projects/projectDetails.cy.ts @@ -315,9 +315,11 @@ describe('Project Details', () => { }); projectDetails.visitSection('test-project', 'workbenches'); const notebookRow = projectDetails.getNotebookRow('test-notebook'); - notebookRow.findOutdatedElyraInfo().should('not.exist'); - projectDetails.findElyraInvalidVersionAlert().should('not.exist'); + notebookRow.findOutdatedElyraInfo().should('be.visible'); + projectDetails.findElyraInvalidVersionAlert().should('be.visible'); + projectDetails.findUnsupportedPipelineVersionAlert().should('not.exist'); }); + it('Notebook with updated Elyra image and no pipeline server', () => { initIntercepts({ imageStreamPythonDependencies: '[{"name":"odh-elyra","version":"3.16"}]', diff --git a/frontend/src/__tests__/cypress/cypress/e2e/userManagement/userManagement.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/userManagement/userManagement.cy.ts new file mode 100644 index 0000000000..b728392c8b --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/e2e/userManagement/userManagement.cy.ts @@ -0,0 +1,67 @@ +import { mockGroupSettings } from '~/__mocks__/mockGroupConfig'; +import { userManagement } from '~/__tests__/cypress/cypress/pages/userManagement'; +import { asProductAdminUser, asProjectAdminUser } from '~/__tests__/cypress/cypress/utils/users'; +import { pageNotfound } from '~/__tests__/cypress/cypress/pages/pageNotFound'; + +it('Cluster settings should not be available for non product admins', () => { + asProjectAdminUser(); + userManagement.visit(false); + pageNotfound.findPage().should('exist'); + userManagement.findNavItem().should('not.exist'); +}); + +describe('User Management', () => { + beforeEach(() => { + asProductAdminUser(); + cy.interceptOdh('GET /api/groups-config', mockGroupSettings()); + userManagement.visit(); + }); + + it('Administrator group setting', () => { + const administratorGroupSection = userManagement.getAdministratorGroupSection(); + userManagement.findSubmitButton().should('be.disabled'); + administratorGroupSection.findChipItem(/^odh-admins$/).should('exist'); + administratorGroupSection.shouldHaveAdministratorGroupInfo(); + administratorGroupSection.clearMultiChipItem(); + administratorGroupSection.selectMultiGroup('odh-admins'); + administratorGroupSection.findMultiGroupInput().type('odh-admin'); + administratorGroupSection.findMultiGroupOptions('odh-admins-1').click(); + administratorGroupSection.removeChipItem('odh-admins'); + administratorGroupSection.findChipItem(/^odh-admins$/).should('not.exist'); + administratorGroupSection.removeChipItem('odh-admins-1'); + administratorGroupSection.findErrorText().should('exist'); + administratorGroupSection.findMultiGroupOptions('odh-admins').click(); + administratorGroupSection.findErrorText().should('not.exist'); + userManagement.findSubmitButton().should('be.enabled'); + }); + + it('User group setting', () => { + const userGroupSection = userManagement.getUserGroupSection(); + userManagement.findSubmitButton().should('be.disabled'); + userGroupSection.findChipItem('system:authenticated').should('exist'); + userGroupSection.clearMultiChipItem(); + userGroupSection.findErrorText().should('exist'); + userGroupSection.selectMultiGroup('odh-admins'); + userGroupSection.findChipItem(/^odh-admins$/).should('exist'); + userGroupSection.findMultiGroupSelectButton().click(); + userManagement.findSubmitButton().should('be.enabled'); + + cy.interceptOdh('PUT /api/groups-config', mockGroupSettings()).as('saveGroupSetting'); + + userManagement.findSubmitButton().click(); + cy.wait('@saveGroupSetting').then((interception) => { + expect(interception.request.body).to.eql({ + adminGroups: [ + { id: 0, name: 'odh-admins', enabled: true }, + { id: 1, name: 'odh-admins-1', enabled: false }, + ], + allowedGroups: [ + { id: 0, name: 'odh-admins', enabled: true }, + { id: 1, name: 'odh-admins-1', enabled: false }, + { id: 2, name: 'system:authenticated', enabled: false }, + ], + }); + }); + userManagement.shouldHaveSuccessAlertMessage(); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts b/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts new file mode 100644 index 0000000000..d4c2fabbf0 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/pages/userManagement.ts @@ -0,0 +1,91 @@ +import { appChrome } from './appChrome'; +import { Contextual } from './components/Contextual'; + +class GroupSettingSection extends Contextual { + shouldHaveAdministratorGroupInfo() { + this.find().findByTestId('data-science-administrator-info'); + return this; + } + + clearMultiChipItem() { + this.find().findByRole('button', { name: 'Clear all' }).click(); + } + + findMultiGroupInput() { + return this.find().find('input'); + } + + findMultiGroupOptions(name: string) { + return this.find().findByTestId('multi-group-selection').findByRole('option', { name }); + } + + private findChipGroup() { + return this.find().findByRole('list', { name: 'Chip group category' }); + } + + findChipItem(name: string | RegExp) { + return this.findChipGroup().find('li').contains('span', name); + } + + removeChipItem(name: string) { + this.findChipGroup() + .findByRole('button', { name: `Remove ${name}` }) + .click(); + } + + findErrorText() { + return this.find().findByTestId('group-selection-error-text'); + } + + findMultiGroupSelectButton() { + return this.find().findByRole('button', { name: 'Options menu' }); + } + + selectMultiGroup(name: string) { + this.findMultiGroupSelectButton().click(); + this.findMultiGroupOptions(name).click(); + } +} +class UserManagement { + visit(wait = true) { + cy.visit('/groupSettings'); + if (wait) { + this.wait(); + } + } + + private wait() { + cy.findByTestId('app-page-title').should('have.text', 'User management'); + cy.testA11y(); + } + + navigate() { + this.findNavItem().click(); + this.wait(); + } + + findNavItem() { + return appChrome.findNavItem('User management', 'Settings'); + } + + findSubmitButton() { + return cy.findByTestId('save-button'); + } + + shouldHaveSuccessAlertMessage() { + cy.findByRole('heading', { name: 'Success alert: Group settings changes saved' }).should( + 'exist', + ); + return this; + } + + getAdministratorGroupSection() { + return new GroupSettingSection(() => cy.findByTestId('data-science-administrator-groups')); + } + + getUserGroupSection() { + return new GroupSettingSection(() => cy.findByTestId('data-science-user-groups')); + } +} + +export const userManagement = new UserManagement(); diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts index c4b955772d..30d06ce43d 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/odh.ts @@ -12,6 +12,7 @@ import type { TemplateKind, } from '~/k8sTypes'; import { AllowedUser } from '~/pages/notebookController/screens/admin/types'; +import { GroupsConfig } from '~/pages/groupSettings/groupTypes'; import type { StatusResponse } from '~/redux/types'; import type { BYONImage, @@ -56,7 +57,17 @@ declare global { interceptOdh( type: 'PUT /api/accelerator-profiles/:name', options: { path: { name: string } }, - response?: OdhResponse, + response: OdhResponse, + ): Cypress.Chainable; + + interceptOdh( + type: 'GET /api/groups-config', + response: OdhResponse, + ): Cypress.Chainable; + + interceptOdh( + type: 'PUT /api/groups-config', + response: OdhResponse, ): Cypress.Chainable; interceptOdh( diff --git a/frontend/src/components/MultiSelection.tsx b/frontend/src/components/MultiSelection.tsx index c3bcbe120c..123c38652e 100644 --- a/frontend/src/components/MultiSelection.tsx +++ b/frontend/src/components/MultiSelection.tsx @@ -26,6 +26,7 @@ export const MultiSelection: React.FC = ({ value, setValue, return ( <> {noSelectedItems && ( - + One or more group must be selected diff --git a/frontend/src/components/SettingSection.tsx b/frontend/src/components/SettingSection.tsx index c608576dcb..14de874e9c 100644 --- a/frontend/src/components/SettingSection.tsx +++ b/frontend/src/components/SettingSection.tsx @@ -4,6 +4,7 @@ import { Card, CardBody, CardFooter, CardTitle, Stack, StackItem } from '@patter type SettingSectionProps = { children: React.ReactNode; title: string; + testId?: string; description?: React.ReactNode; footer?: React.ReactNode; }; @@ -12,9 +13,10 @@ const SettingSection: React.FC = ({ title, children, footer, + testId, description, }) => ( - + {title} diff --git a/frontend/src/pages/groupSettings/GroupSettings.tsx b/frontend/src/pages/groupSettings/GroupSettings.tsx index 9e6b2f97ee..362d4fa61b 100644 --- a/frontend/src/pages/groupSettings/GroupSettings.tsx +++ b/frontend/src/pages/groupSettings/GroupSettings.tsx @@ -64,9 +64,11 @@ const GroupSettings: React.FC = () => { { - + {