|
| 1 | +import { projectListPage, projectDetails } from '#~/__tests__/cypress/cypress/pages/projects'; |
| 2 | +import { |
| 3 | + modelServingGlobal, |
| 4 | + inferenceServiceModal, |
| 5 | + modelServingSection, |
| 6 | + kserveModalEdit, |
| 7 | +} from '#~/__tests__/cypress/cypress/pages/modelServing'; |
| 8 | +import type { DataScienceProjectData } from '#~/__tests__/cypress/cypress/types'; |
| 9 | +import { loadDSPFixture } from '#~/__tests__/cypress/cypress/utils/dataLoader'; |
| 10 | +import { retryableBefore } from '#~/__tests__/cypress/cypress/utils/retryableHooks'; |
| 11 | +import { generateTestUUID } from '#~/__tests__/cypress/cypress/utils/uuidGenerator'; |
| 12 | +import { |
| 13 | + checkInferenceServiceState, |
| 14 | + modelExternalTester, |
| 15 | + provisionProjectForModelServing, |
| 16 | + verifyModelExternalToken, |
| 17 | +} from '#~/__tests__/cypress/cypress/utils/oc_commands/modelServing'; |
| 18 | +import { deleteOpenShiftProject } from '#~/__tests__/cypress/cypress/utils/oc_commands/project'; |
| 19 | +import { HTPASSWD_CLUSTER_ADMIN_USER } from '#~/__tests__/cypress/cypress/utils/e2eUsers'; |
| 20 | + |
| 21 | +let testData: DataScienceProjectData; |
| 22 | +let projectName: string; |
| 23 | +let modelName: string; |
| 24 | +let modelFilePath: string; |
| 25 | +const awsBucket = 'BUCKET_1' as const; |
| 26 | +const uuid = generateTestUUID(); |
| 27 | + |
| 28 | +describe('A model can be deployed with token auth', () => { |
| 29 | + retryableBefore(() => { |
| 30 | + cy.log('Loading test data'); |
| 31 | + return loadDSPFixture('e2e/dataScienceProjects/testModelTokenAuth.yaml').then( |
| 32 | + (fixtureData: DataScienceProjectData) => { |
| 33 | + testData = fixtureData; |
| 34 | + projectName = `${testData.projectResourceName}-${uuid}`; |
| 35 | + modelName = testData.singleModelName; |
| 36 | + modelFilePath = testData.modelOpenVinoExamplePath; |
| 37 | + |
| 38 | + if (!projectName) { |
| 39 | + throw new Error('Project name is undefined or empty in the loaded fixture'); |
| 40 | + } |
| 41 | + cy.log(`Loaded project name: ${projectName}`); |
| 42 | + // Create a Project |
| 43 | + provisionProjectForModelServing( |
| 44 | + projectName, |
| 45 | + awsBucket, |
| 46 | + 'resources/yaml/data_connection_model_serving.yaml', |
| 47 | + ); |
| 48 | + }, |
| 49 | + ); |
| 50 | + }); |
| 51 | + after(() => { |
| 52 | + // Delete provisioned Project - wait for completion due to RHOAIENG-19969 to support test retries, 5 minute timeout |
| 53 | + // TODO: Review this timeout once RHOAIENG-19969 is resolved |
| 54 | + deleteOpenShiftProject(projectName, { wait: true, ignoreNotFound: true, timeout: 300000 }); |
| 55 | + }); |
| 56 | + |
| 57 | + it( |
| 58 | + 'Verify that a model can be deployed with token auth', |
| 59 | + { tags: ['@Smoke', '@SmokeSet3', '@Dashboard', '@ModelServing'] }, |
| 60 | + () => { |
| 61 | + cy.log('Model Name:', modelName); |
| 62 | + cy.step(`Log into the application with ${HTPASSWD_CLUSTER_ADMIN_USER.USERNAME}`); |
| 63 | + cy.visitWithLogin('/', HTPASSWD_CLUSTER_ADMIN_USER); |
| 64 | + |
| 65 | + // Project navigation |
| 66 | + cy.step(`Navigate to the Project list tab and search for ${projectName}`); |
| 67 | + projectListPage.navigate(); |
| 68 | + projectListPage.filterProjectByName(projectName); |
| 69 | + projectListPage.findProjectLink(projectName).click(); |
| 70 | + |
| 71 | + // Navigate to Model Serving tab and Deploy a model |
| 72 | + cy.step('Navigate to Model Serving and click to Deploy a model'); |
| 73 | + projectDetails.findSectionTab('model-server').click(); |
| 74 | + modelServingGlobal.findSingleServingModelButton().click(); |
| 75 | + modelServingGlobal.findDeployModelButton().click(); |
| 76 | + |
| 77 | + // Launch a model |
| 78 | + cy.step('Launch a Single Serving Model using Openvino'); |
| 79 | + inferenceServiceModal.findModelNameInput().type(testData.singleModelName); |
| 80 | + inferenceServiceModal.findServingRuntimeTemplateSearchSelector().click(); |
| 81 | + inferenceServiceModal.findGlobalScopedTemplateOption('OpenVINO Model Server').click(); |
| 82 | + inferenceServiceModal.findModelFrameworkSelect().click(); |
| 83 | + inferenceServiceModal.findOpenVinoIROpSet13().click(); |
| 84 | + |
| 85 | + // Enable Model access through an external route |
| 86 | + cy.step('Enable Model access through an external route'); |
| 87 | + inferenceServiceModal.findDeployedModelRouteCheckbox().click(); |
| 88 | + inferenceServiceModal.findDeployedModelRouteCheckbox().should('be.checked'); |
| 89 | + inferenceServiceModal.findServiceAccountIndex(0).clear(); |
| 90 | + inferenceServiceModal.findServiceAccountIndex(0).type('secret'); |
| 91 | + inferenceServiceModal.findAddServiceAccountButton().click(); |
| 92 | + inferenceServiceModal.findServiceAccountIndex(1).clear(); |
| 93 | + inferenceServiceModal.findServiceAccountIndex(1).type('secret2'); |
| 94 | + inferenceServiceModal.findLocationPathInput().type(modelFilePath); |
| 95 | + inferenceServiceModal.findSubmitButton().click(); |
| 96 | + inferenceServiceModal.shouldBeOpen(false); |
| 97 | + |
| 98 | + // Verify the model created |
| 99 | + cy.step('Verify that the Model is running'); |
| 100 | + checkInferenceServiceState(testData.singleModelName, projectName, { |
| 101 | + checkReady: true, |
| 102 | + checkLatestDeploymentReady: true, |
| 103 | + }); |
| 104 | + |
| 105 | + // Verify the model is not accessible without a token |
| 106 | + cy.step('Verify the model is not accessible without a token'); |
| 107 | + modelExternalTester(modelName, projectName).then(({ response }) => { |
| 108 | + expect(response.status).to.equal(401); |
| 109 | + }); |
| 110 | + |
| 111 | + // Get the tokens from the UI |
| 112 | + cy.step('Get the tokens from the UI'); |
| 113 | + const kserveRow = modelServingSection.getKServeRow(testData.singleModelName); |
| 114 | + kserveRow.findToggleButton().click(); |
| 115 | + |
| 116 | + cy.window().then((win) => { |
| 117 | + const copied: string[] = []; |
| 118 | + cy.wrap(copied).as('copiedTokens'); |
| 119 | + |
| 120 | + cy.stub(win.navigator.clipboard, 'writeText').callsFake((text: string) => { |
| 121 | + copied.push(text); |
| 122 | + return Promise.resolve(); |
| 123 | + }); |
| 124 | + }); |
| 125 | + |
| 126 | + // Click the two copy buttons |
| 127 | + modelServingGlobal.findTokenCopyButton(0).click(); |
| 128 | + modelServingGlobal.findTokenCopyButton(1).click(); |
| 129 | + |
| 130 | + // Use the copied tokens |
| 131 | + cy.get<string[]>('@copiedTokens') |
| 132 | + .should('have.length.at.least', 2) |
| 133 | + .then((tokens) => { |
| 134 | + const [token1, token2] = tokens; |
| 135 | + verifyModelExternalToken(modelName, projectName, token1).then((r) => |
| 136 | + expect(r.status).to.equal(200), |
| 137 | + ); |
| 138 | + verifyModelExternalToken(modelName, projectName, token2).then((r) => |
| 139 | + expect(r.status).to.equal(200), |
| 140 | + ); |
| 141 | + }); |
| 142 | + |
| 143 | + // Remove the token |
| 144 | + cy.step('Remove the token'); |
| 145 | + modelServingSection |
| 146 | + .getKServeRow(testData.singleModelName) |
| 147 | + .find() |
| 148 | + .findKebabAction('Edit') |
| 149 | + .click(); |
| 150 | + // Check the service accounts are showing up in the UI |
| 151 | + kserveModalEdit.findServiceAccountIndex(0).should('have.value', 'secret'); |
| 152 | + kserveModalEdit.findServiceAccountIndex(1).should('have.value', 'secret2'); |
| 153 | + kserveModalEdit.findTokenAuthenticationCheckbox().click(); |
| 154 | + kserveModalEdit.findTokenAuthenticationCheckbox().should('not.be.checked'); |
| 155 | + kserveModalEdit.findSubmitButton().click(); |
| 156 | + kserveModalEdit.shouldBeOpen(false); |
| 157 | + |
| 158 | + // Verify the model is accessible without a token |
| 159 | + cy.step('Verify the model is accessible without a token'); |
| 160 | + verifyModelExternalToken(modelName, projectName).then((response) => { |
| 161 | + expect(response.status).to.equal(200); |
| 162 | + }); |
| 163 | + }, |
| 164 | + ); |
| 165 | +}); |
0 commit comments