diff --git a/frontend/src/__tests__/cypress/cypress/fixtures/e2e/dashboardNavigation/testManifestLinks.yaml b/frontend/src/__tests__/cypress/cypress/fixtures/e2e/dashboardNavigation/testManifestLinks.yaml index 19a2d56cfb..573e1691e2 100644 --- a/frontend/src/__tests__/cypress/cypress/fixtures/e2e/dashboardNavigation/testManifestLinks.yaml +++ b/frontend/src/__tests__/cypress/cypress/fixtures/e2e/dashboardNavigation/testManifestLinks.yaml @@ -10,4 +10,6 @@ excludedSubstrings: - localhost - console-openshift-console.apps.test-cluster.example.com/ - console-openshift-console.apps.test-cluster.example.com - - repo.anaconda.cloud/repo/t/$ \ No newline at end of file + - repo.anaconda.cloud/repo/t/$ + - figma.com/figma/ns + - scikit-learn.org/stable/getting_started.html \ No newline at end of file diff --git a/frontend/src/__tests__/cypress/cypress/pages/workbench.ts b/frontend/src/__tests__/cypress/cypress/pages/workbench.ts index 44972d670d..bbb6ccd44a 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/workbench.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/workbench.ts @@ -202,6 +202,19 @@ class AttachExistingStorageModal extends Modal { cy.findByTestId('persistent-storage-typeahead').contains(name).click(); } + verifyPSDropdownIsDisabled(): void { + cy.get('[data-testid="persistent-storage-group"] .pf-v6-c-menu-toggle') + .should('have.class', 'pf-m-disabled') + .and('have.attr', 'disabled'); + } + + verifyPSDropdownText(expectedText: string): void { + cy.get('[data-testid="persistent-storage-group"] .pf-v6-c-text-input-group__text-input').should( + 'have.value', + expectedText, + ); + } + findStandardPathInput() { return cy.findByTestId('mount-path-folder-value'); } diff --git a/frontend/src/__tests__/cypress/cypress/support/commands/application.ts b/frontend/src/__tests__/cypress/cypress/support/commands/application.ts index 247550ec2e..d9f2ee1137 100644 --- a/frontend/src/__tests__/cypress/cypress/support/commands/application.ts +++ b/frontend/src/__tests__/cypress/cypress/support/commands/application.ts @@ -5,6 +5,7 @@ import { HTPASSWD_CLUSTER_ADMIN_USER } from '~/__tests__/cypress/cypress/utils/e import { getDashboardConfig, getNotebookControllerConfig, + getNotebookControllerCullerConfig, } from '~/__tests__/cypress/cypress/utils/oc_commands/project'; /* eslint-disable @typescript-eslint/no-namespace */ @@ -155,6 +156,20 @@ declare global { * @returns A Cypress.Chainable that resolves to the requested config value or the full config object. */ getNotebookControllerConfig: (key?: string) => Cypress.Chainable; + + /** + * Retrieves the Notebook Controller Culler Config from OpenShift and returns either the full config or a specific value. + * + * When no key is provided, returns the entire Notebook Controller Culler Config object. + * When a key is provided, returns the specific value for that key. + * + * @param key Optional. The specific config key to retrieve. Use dot notation for nested properties. + * + * @returns A Cypress.Chainable that resolves to the requested config value or the full config object. + */ + getNotebookControllerCullerConfig: ( + key?: string, + ) => Cypress.Chainable; } } } @@ -290,6 +305,7 @@ Cypress.Commands.overwriteQuery('findAllByTestId', function findAllByTestId(...a }); Cypress.Commands.add('getNotebookControllerConfig', getNotebookControllerConfig); Cypress.Commands.add('getDashboardConfig', getDashboardConfig); +Cypress.Commands.add('getNotebookControllerCullerConfig', getNotebookControllerCullerConfig); const enhancedFindByTestId = ( command: Cypress.Command, diff --git a/frontend/src/__tests__/cypress/cypress/tests/e2e/dataScienceProjects/workbenches/workbenches.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/e2e/dataScienceProjects/workbenches/workbenches.cy.ts index 2ba00f9d7f..a03bd26abd 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/e2e/dataScienceProjects/workbenches/workbenches.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/e2e/dataScienceProjects/workbenches/workbenches.cy.ts @@ -72,9 +72,10 @@ describe('Workbench and PVSs tests', () => { cy.step(`Create Workbench ${projectName} using storage ${PVCDisplayName}`); workbenchPage.findCreateButton().click(); createSpawnerPage.getNameInput().fill(workbenchName); - createSpawnerPage.findNotebookImage('jupyter-minimal-notebook').click(); + createSpawnerPage.findNotebookImage('code-server-notebook').click(); createSpawnerPage.findAttachExistingStorageButton().click(); - attachExistingStorageModal.selectExistingPersistentStorage(PVCDisplayName); + attachExistingStorageModal.verifyPSDropdownIsDisabled(); + attachExistingStorageModal.verifyPSDropdownText(PVCDisplayName); attachExistingStorageModal.findStandardPathInput().fill(workbenchName); attachExistingStorageModal.findAttachButton().click(); createSpawnerPage.findSubmitButton().click(); @@ -82,7 +83,7 @@ describe('Workbench and PVSs tests', () => { cy.step(`Wait for Workbench ${workbenchName} to display a "Running" status`); const notebookRow = workbenchPage.getNotebookRow(workbenchName); notebookRow.expectStatusLabelToBe('Running', 120000); - notebookRow.shouldHaveNotebookImageName('Minimal Python'); + notebookRow.shouldHaveNotebookImageName('code-server'); notebookRow.shouldHaveContainerSize('Small'); cy.step(`Check the cluster storage ${PVCDisplayName} is now connected to ${workbenchName}`); diff --git a/frontend/src/__tests__/cypress/cypress/tests/e2e/settings/clusterSettings/testAdminClusterSettings.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/e2e/settings/clusterSettings/testAdminClusterSettings.cy.ts index e67e82746f..d15b41823c 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/e2e/settings/clusterSettings/testAdminClusterSettings.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/e2e/settings/clusterSettings/testAdminClusterSettings.cy.ts @@ -4,7 +4,11 @@ import { } from '~/__tests__/cypress/cypress/utils/e2eUsers'; import { clusterSettings } from '~/__tests__/cypress/cypress/pages/clusterSettings'; import { pageNotfound } from '~/__tests__/cypress/cypress/pages/pageNotFound'; -import type { DashboardConfig, NotebookControllerConfig } from '~/__tests__/cypress/cypress/types'; +import type { + DashboardConfig, + NotebookControllerConfig, + NotebookControllerCullerConfig, +} from '~/__tests__/cypress/cypress/types'; import { validateModelServingPlatforms, validatePVCSize, @@ -15,6 +19,7 @@ import { describe('Verify that only the Cluster Admin can access Cluster Settings', () => { let dashboardConfig: DashboardConfig; let notebookControllerConfig: NotebookControllerConfig; + let notebookControllerCullerConfig: NotebookControllerCullerConfig | string; before(() => { // Retrieve the dashboard configuration @@ -27,6 +32,21 @@ describe('Verify that only the Cluster Admin can access Cluster Settings', () => notebookControllerConfig = config as NotebookControllerConfig; cy.log('Controller Config:', JSON.stringify(notebookControllerConfig, null, 2)); }); + // Retrieve the Notebook controller culler configuration + cy.getNotebookControllerCullerConfig().then((config) => { + cy.log('Raw Controller Culler Config Response:', JSON.stringify(config)); + + if (typeof config === 'object' && config !== null) { + notebookControllerCullerConfig = config as NotebookControllerCullerConfig; + cy.log( + 'Controller Culler Config:', + JSON.stringify(notebookControllerCullerConfig, null, 2), + ); + } else { + notebookControllerCullerConfig = config as string; + cy.log('Controller Culler Config (Error):', config); + } + }); }); it('Admin should access Cluster Settings and see UI fields matching OpenShift configurations', () => { @@ -47,12 +67,14 @@ describe('Verify that only the Cluster Admin can access Cluster Settings', () => // Validate Stop idle notebooks based on OpenShift command to 'notebook-controller' to validate configuration cy.step('Validate Stop idle notebooks displays and fields are enabled/disabled'); - validateStopIdleNotebooks(notebookControllerConfig); + cy.log('Notebook Controller Culler Config:', JSON.stringify(notebookControllerCullerConfig)); + validateStopIdleNotebooks(notebookControllerCullerConfig); // Validate notebook pod tolerations displays based on OpenShift command to 'get OdhDashboardConfig' to validate configuration cy.step('Validate Notebook pod tolerations displays and fields are enabled/disabled'); validateNotebookPodTolerations(dashboardConfig); }); + it('Test User - should not have access rights to view the Cluster Settings tab', () => { cy.step('Log into the application'); cy.visitWithLogin('/', LDAP_CONTRIBUTOR_USER); diff --git a/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/clusterStorage.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/clusterStorage.cy.ts index ba3a6a4d5e..5fe7425f95 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/clusterStorage.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/clusterStorage.cy.ts @@ -22,10 +22,13 @@ describe('Regular Users can make use of the Storage Classes in the Cluster Stora // TODO: This test is failing due to https://issues.redhat.com/browse/RHOAIENG-16609 it('If all SC are disabled except one, the SC dropdown should be disabled', () => { + // Authentication and navigation cy.visitWithLogin('/projects', LDAP_CONTRIBUTOR_USER); // Open the project + cy.step(`Navigate to the Project list tab and search for ${dspName}`); projectListPage.filterProjectByName(dspName); projectListPage.findProjectLink(dspName).click(); + cy.step('Navigate to the Cluster Storage tab and disable all non-default storage classes'); // Go to cluster storage tab projectDetails.findSectionTab('cluster-storages').click(); // Disable all non-default storage classes @@ -33,6 +36,9 @@ describe('Regular Users can make use of the Storage Classes in the Cluster Stora // Open the Create cluster storage Modal findAddClusterStorageButton().click(); + cy.step( + 'Checking that Storage Classes Dropdown is disabled - 🐛 RHOAIENG-16609 will fail this test in RHOAI', + ); // Check that the SC Dropdown is disabled addClusterStorageModal.findStorageClassSelect().should('be.disabled'); }); diff --git a/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/storageClasses.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/storageClasses.cy.ts index f09a759a38..3d2e4e3366 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/storageClasses.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/e2e/storageClasses/storageClasses.cy.ts @@ -26,24 +26,32 @@ describe('An admin user can manage Storage Classes from Settings -> Storage clas }); it('An admin user can enable a disabled Storage Class', () => { + cy.step('Navigate to Storage Classes view'); cy.visitWithLogin('/', HTPASSWD_CLUSTER_ADMIN_USER); storageClassesPage.navigate(); const scDisabledName = `${scName}-disabled-non-default`; + cy.step('Check SC row exists'); // SC row exist storageClassesTable.findRowByName(scDisabledName).should('be.visible'); const scDisabledRow = storageClassesTable.getRowByConfigName(scDisabledName); + cy.step("Check there's no Default label"); // There's no Default label scDisabledRow.findOpenshiftDefaultLabel().should('not.exist'); + cy.step('Check the Enable switch is set to disabled'); // The Enable switch is set to disabled scDisabledRow.findEnableSwitchInput().should('have.attr', 'aria-checked', 'false'); + cy.step('Check the Default radio button is disabled'); // The Default radio button is disabled scDisabledRow.findDefaultRadioInput().should('be.disabled'); + cy.step('Enable the Storage Class'); // Enable the SC scDisabledRow.findEnableSwitchInput().click({ force: true }); + cy.step('Check the Enable switch is set to Enabled'); // The Enable switch is set to enabled scDisabledRow.findEnableSwitchInput().should('have.attr', 'aria-checked', 'true'); + cy.step('Check the Default radio button is disabled'); // The Default radio button is enabled but not checked scDisabledRow.findDefaultRadioInput().should('be.enabled'); scDisabledRow.findDefaultRadioInput().should('not.have.attr', 'checked'); @@ -51,47 +59,53 @@ describe('An admin user can manage Storage Classes from Settings -> Storage clas }); it('An admin user can disable an enabled Storage Class', () => { + cy.step('Navigate to Storage Classes view'); cy.visitWithLogin('/', HTPASSWD_CLUSTER_ADMIN_USER); storageClassesPage.navigate(); + const scEnabledName = `${scName}-enabled-non-default`; - // SC row exist + + cy.step('Check SC row exists'); storageClassesTable.findRowByName(scEnabledName).should('be.visible'); const scEnabledRow = storageClassesTable.getRowByConfigName(scEnabledName); - // There's no Default label + cy.step("Check there's no Default label"); scEnabledRow.findOpenshiftDefaultLabel().should('not.exist'); - // The Enable switch is set to enabled + cy.step('Check the Enable switch is set to enabled'); scEnabledRow.findEnableSwitchInput().should('have.attr', 'aria-checked', 'true'); - // The Default radio button is enabled but not checked + cy.step('Check the Default radio button is enabled but not checked'); scEnabledRow.findDefaultRadioInput().should('be.enabled'); scEnabledRow.findDefaultRadioInput().should('not.have.attr', 'checked'); - // Enable the SC + cy.step('Enable the Storage Class'); scEnabledRow.findEnableSwitchInput().click({ force: true }); - // The Enable switch is set to disabled + cy.step('Check the Enable switch is set to disabled'); scEnabledRow.findEnableSwitchInput().should('have.attr', 'aria-checked', 'false'); - // The Default radio button is disabled + cy.step('Check the Default radio button is disabled'); scEnabledRow.findDefaultRadioInput().should('be.disabled'); verifyStorageClassConfig(scEnabledName, false, false); }); it('An admin user can set an enabled Storage Class as the default one', () => { + cy.step('Navigate to Storage Classes view'); cy.visitWithLogin('/', HTPASSWD_CLUSTER_ADMIN_USER); storageClassesPage.navigate(); + const scToDefaultName = `${scName}-enabled-to-default`; const scToDefaultRow = storageClassesTable.getRowByConfigName(scToDefaultName); - // There's no Default label + + cy.step("Check there's no Default label"); scToDefaultRow.findOpenshiftDefaultLabel().should('not.exist'); - // The Default radio button is enabled but not checked + cy.step('Check the Default radio button is enabled but not checked'); scToDefaultRow.findDefaultRadioInput().should('be.enabled'); scToDefaultRow.findDefaultRadioInput().should('not.have.attr', 'checked'); - // Set the SC to be the default one + cy.step('Set the SC to be the default one'); scToDefaultRow.findDefaultRadioInput().click(); - // The Default radio button is enabled + cy.step('Check the Default radio button is enabled'); scToDefaultRow.findDefaultRadioInput().should('be.enabled'); - // The Enable switch is disabled + cy.step('Check the Enable switch is disabled'); scToDefaultRow.findEnableSwitchInput().should('be.disabled'); verifyStorageClassConfig(scToDefaultName, true, true); }); diff --git a/frontend/src/__tests__/cypress/cypress/types.ts b/frontend/src/__tests__/cypress/cypress/types.ts index f516f66ff0..82306f2001 100644 --- a/frontend/src/__tests__/cypress/cypress/types.ts +++ b/frontend/src/__tests__/cypress/cypress/types.ts @@ -129,6 +129,8 @@ export type NotebookController = { export type DashboardConfig = { dashboardConfig: { disableModelServing: boolean; + disableModelMesh: boolean; + disableKServe: boolean; }; notebookController: NotebookController; [key: string]: unknown; @@ -145,6 +147,12 @@ export type NotebookControllerConfig = { USE_ISTIO: string; }; +export type NotebookControllerCullerConfig = { + CULL_IDLE_TIME: string; + ENABLE_CULLING: string; + IDLENESS_CHECK_PERIOD: string; +}; + export type ResourceData = { kind: string; labelSelector: string; diff --git a/frontend/src/__tests__/cypress/cypress/utils/clusterSettingsUtils.ts b/frontend/src/__tests__/cypress/cypress/utils/clusterSettingsUtils.ts index a94b4bb89c..9b93ef6f98 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/clusterSettingsUtils.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/clusterSettingsUtils.ts @@ -4,24 +4,60 @@ import { cullerSettings, notebookTolerationSettings, } from '~/__tests__/cypress/cypress/pages/clusterSettings'; -import type { DashboardConfig, NotebookControllerConfig } from '~/__tests__/cypress/cypress/types'; +import type { + DashboardConfig, + NotebookControllerCullerConfig, +} from '~/__tests__/cypress/cypress/types'; /** - * Validates the Model Serving Platform checkboxes display in the Cluster Settings. - * @param dashboardConfig The Model Serving Platform configuration object. + * Validates the visibility and state of Model Serving Platform checkboxes + * in the Cluster Settings based on the provided dashboard configuration. + * + * This function checks whether the Model Serving feature is enabled or disabled, + * and subsequently verifies the state of the Multi-Platform and Single-Platform + * checkboxes based on their respective enable/disable flags. + * + * - If Model Serving is disabled, both checkboxes should not be visible. + * - If Model Serving is enabled: + * - The Multi-Platform Checkbox will be checked if Model Mesh is enabled; + * otherwise, it will not be checked. + * - The Single-Platform Checkbox will be checked if KServe is enabled; + * otherwise, it will not be checked. + * + * @param dashboardConfig The Model Serving Platform configuration object containing + * settings related to model serving, model mesh, and KServe. */ export const validateModelServingPlatforms = (dashboardConfig: DashboardConfig): void => { const isModelServingEnabled = dashboardConfig.dashboardConfig.disableModelServing; - cy.log(`Value of isModelServingDisabled: ${String(isModelServingEnabled)}`); + const isModelMeshEnabled = dashboardConfig.dashboardConfig.disableModelMesh; + const isKServeEnabled = dashboardConfig.dashboardConfig.disableKServe; + + cy.log(`Value of isModelServingEnabled: ${String(isModelServingEnabled)}`); + cy.log(`Value of isModelMeshEnabled: ${String(isModelMeshEnabled)}`); + cy.log(`Value of isKServeEnabled: ${String(isKServeEnabled)}`); if (isModelServingEnabled) { modelServingSettings.findSinglePlatformCheckbox().should('not.exist'); modelServingSettings.findMultiPlatformCheckbox().should('not.exist'); cy.log('Model Serving is disabled, checkboxes should not be visible'); } else { - modelServingSettings.findSinglePlatformCheckbox().should('be.checked'); - modelServingSettings.findMultiPlatformCheckbox().should('be.checked'); - cy.log('Model Serving is enabled, checkboxes should be checked'); + // Validate Multi-Platform Checkbox based on disableModelMesh + if (isModelMeshEnabled) { + modelServingSettings.findMultiPlatformCheckbox().should('not.be.checked'); + cy.log('Multi-Platform Checkbox is disabled, it should not be checked'); + } else { + modelServingSettings.findMultiPlatformCheckbox().should('be.checked'); + cy.log('Multi-Platform Checkbox is enabled, it should be checked'); + } + + // Validate Single-Platform Checkbox based on disableKServe + if (isKServeEnabled) { + modelServingSettings.findSinglePlatformCheckbox().should('not.be.checked'); + cy.log('Single-Platform Checkbox is disabled, it should not be checked'); + } else { + modelServingSettings.findSinglePlatformCheckbox().should('be.checked'); + cy.log('Single-Platform Checkbox is enabled, it should be checked'); + } } }; @@ -53,20 +89,25 @@ export const validatePVCSize = (dashboardConfig: DashboardConfig): void => { /** * Validates the Stop Idle Notebooks displays in the Cluster Settings. - * @param notebookControllerConfig The notebook controller configuration object. + * @param notebookControllerCullerConfig The notebook controller culler configuration object or error message. */ export const validateStopIdleNotebooks = ( - notebookControllerConfig: NotebookControllerConfig, + notebookControllerCullerConfig: NotebookControllerCullerConfig | string, ): void => { - const isCullingEnabled = notebookControllerConfig.ENABLE_CULLING; - cy.log(`Value of ENABLE_CULLING: ${isCullingEnabled}`); - - if (isCullingEnabled) { - cullerSettings.findStopIdleNotebooks().should('exist'); - cy.log('Culling is enabled; Stop Idle Notebooks setting should exist in the UI.'); + if (typeof notebookControllerCullerConfig === 'string') { + cy.log('Culler config not found or error occurred:', notebookControllerCullerConfig); + cullerSettings.findUnlimitedOption().should('be.checked'); + cullerSettings.findLimitedOption().should('not.be.checked'); + cy.log('Do not stop idle notebooks option should be checked when culler config is not found'); } else { - cullerSettings.findStopIdleNotebooks().should('not.exist'); - cy.log('Culling is disabled; Stop Idle Notebooks setting should not exist in the UI.'); + const isCullingEnabled = 'ENABLE_CULLING' in notebookControllerCullerConfig; + cy.log(`Value of ENABLE_CULLING: ${isCullingEnabled}`); + + if (isCullingEnabled) { + cullerSettings.findLimitedOption().should('be.checked'); + cullerSettings.findUnlimitedOption().should('not.be.checked'); + cy.log('Stop idle notebooks after should be checked when culling is enabled'); + } } }; diff --git a/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts index f9b1812f54..53d664e961 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts @@ -161,6 +161,43 @@ export const getNotebookControllerConfig = (key?: string): Cypress.Chainable => { + const applicationNamespace = Cypress.env('TEST_NAMESPACE'); + const command = `oc get configmap -n ${applicationNamespace} notebook-controller-culler-config -o jsonpath='{.data}' | jq .`; + + // Log the command being executed + cy.log('Executing command:', command); + + return cy.exec(command).then((result) => { + // Log the std error + cy.log('Command stderr:', result.stderr); + + if (result.code !== 0) { + return Cypress.Promise.resolve(`Error: ${result.stderr.trim()}`); + } + + const trimmedOutput = result.stdout.trim(); + if (!trimmedOutput) { + return Cypress.Promise.resolve('Error: Empty configuration'); + } + + try { + const config = JSON.parse(trimmedOutput) as Record; + return key + ? Cypress.Promise.resolve(getNestedProperty(config, key)) + : Cypress.Promise.resolve(config); + } catch (error) { + return Cypress.Promise.resolve(`Error: Invalid JSON - ${trimmedOutput}`); + } + }); +}; + // Helper function to safely get nested properties function getNestedProperty(obj: Record, path: string): unknown { return path.split('.').reduce((current: unknown, key: string) => {