From 7600c15f9f63196fe81fdd39dc0ec08395e5d7a3 Mon Sep 17 00:00:00 2001 From: Ashique Ansari Date: Tue, 7 May 2024 14:04:37 +0530 Subject: [PATCH] Cypress test for Pipeline list page[Update] --- .../cypress/e2e/pipelines/Pipelines.cy.ts | 283 ++---------- .../cypress/e2e/pipelines/PipelinesList.cy.ts | 424 +++++++++++++++++- .../pages/pipelines/pipelineImportModal.ts | 193 +++++++- .../pages/pipelines/pipelinesGlobal.ts | 71 +++ .../cypress/pages/pipelines/pipelinesTable.ts | 15 + 5 files changed, 725 insertions(+), 261 deletions(-) diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts index bf658375ca..bb1fcfd132 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/Pipelines.cy.ts @@ -28,14 +28,35 @@ import { import { asProductAdminUser } from '~/__tests__/cypress/cypress/utils/users'; import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; -import { be } from '~/__tests__/cypress/cypress/utils/should'; const projectName = 'test-project-name'; const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' }); const initialMockPipelineVersion = buildMockPipelineVersionV2({ pipeline_id: initialMockPipeline.pipeline_id, }); -const pipelineYamlPath = './cypress/e2e/pipelines/mock-upload-pipeline.yaml'; + +const uploadVersionParams = { + display_name: 'New pipeline version', + description: 'New pipeline version description', + pipeline_id: 'test-pipeline', +}; + +const uploadPipelineParams = { + display_name: 'New pipeline', + description: 'New pipeline description', +}; + +const createPipelineAndVersionParams = { + pipeline: { + display_name: 'New pipeline', + }, + pipeline_version: { + display_name: 'New pipeline', + package_url: { + pipeline_url: 'https://example.com/pipeline.yaml', + }, + }, +}; describe('Pipelines', () => { it('Empty state', () => { @@ -160,59 +181,23 @@ describe('Pipelines', () => { }), ).as('createDSPA'); - pipelinesGlobal.findConfigurePipelineServerButton().should('be.enabled'); - pipelinesGlobal.findConfigurePipelineServerButton().click(); - configurePipelineServerModal.findAwsKeyInput().type('test-aws-key'); - configurePipelineServerModal.findAwsSecretKeyInput().type('test-secret-key'); - configurePipelineServerModal.findEndpointInput().type('https://s3.amazonaws.com/'); - configurePipelineServerModal.findRegionInput().should('have.value', 'us-east-1'); - configurePipelineServerModal.findBucketInput().type('test-bucket'); - configurePipelineServerModal.findSubmitButton().should('be.enabled'); - configurePipelineServerModal.findSubmitButton().click(); - - cy.wait('@createSecret').then((interception) => { - expect(interception.request.url).to.include('?dryRun=All'); - expect(interception.request.body).to.containSubset({ - metadata: { - name: 'dashboard-dspa-secret', - namespace: projectName, - annotations: {}, - labels: { 'opendatahub.io/dashboard': 'true' }, - }, - stringData: { AWS_ACCESS_KEY_ID: 'test-aws-key', AWS_SECRET_ACCESS_KEY: 'test-secret-key' }, - }); - }); - - cy.wait('@createSecret').then((interception) => { - expect(interception.request.url).not.to.include('?dryRun=All'); - }); + configurePipelineServerModal.configurePipelineServer(projectName); + }); - cy.get('@createSecret.all').then((interceptions) => { - expect(interceptions).to.have.length(2); // 1 dry-run request and 1 actual request - }); + it('imports a new pipeline', () => { + initIntercepts({}); + pipelinesGlobal.visit(projectName); + pipelineImportModal.importNewPipeline(projectName, initialMockPipeline, uploadPipelineParams); + }); - cy.wait('@createDSPA').then((interception) => { - expect(interception.request.body).to.containSubset({ - metadata: { name: 'dspa', namespace: 'test-project-name' }, - spec: { - apiServer: { enableSamplePipeline: false }, - dspVersion: 'v2', - objectStorage: { - externalStorage: { - host: 's3.us-east-1.amazonaws.com', - scheme: 'https', - bucket: 'test-bucket', - region: 'us-east-1', - s3CredentialsSecret: { - accessKey: 'AWS_ACCESS_KEY_ID', - secretKey: 'AWS_SECRET_ACCESS_KEY', - secretName: 'test-secret', - }, - }, - }, - }, - }); - }); + it('imports a new pipeline by url', () => { + initIntercepts({}); + pipelinesGlobal.visit(projectName); + pipelineImportModal.importPipelineFromUrl( + projectName, + initialMockPipeline, + createPipelineAndVersionParams, + ); }); it('Connect external database while configuring pipeline server', () => { @@ -375,18 +360,7 @@ describe('Pipelines', () => { ); pipelinesGlobal.visit(projectName); - - pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); - viewPipelineServerModal.findCloseButton().click(); - - pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); - viewPipelineServerModal.shouldHaveAccessKey('sdsd'); - viewPipelineServerModal.findPasswordHiddenButton().click(); - viewPipelineServerModal.shouldHaveSecretKey('sdsd'); - viewPipelineServerModal.shouldHaveEndPoint('https://s3.amazonaws.com'); - viewPipelineServerModal.shouldHaveBucketName('test-pipelines-bucket'); - - viewPipelineServerModal.findDoneButton().click(); + viewPipelineServerModal.viewPipelineServerDetails(); }); it('renders the page with pipelines table data', () => { @@ -480,18 +454,7 @@ describe('Pipelines', () => { it('Table sort', () => { initIntercepts({}); pipelinesGlobal.visit(projectName); - - // by Pipeline - pipelinesTable.findTableHeaderButton('Pipeline').click(); - pipelinesTable.findTableHeaderButton('Pipeline').should(be.sortAscending); - pipelinesTable.findTableHeaderButton('Pipeline').click(); - pipelinesTable.findTableHeaderButton('Pipeline').should(be.sortDescending); - - // by Created - pipelinesTable.findTableHeaderButton('Created').click(); - pipelinesTable.findTableHeaderButton('Created').should(be.sortAscending); - pipelinesTable.findTableHeaderButton('Created').click(); - pipelinesTable.findTableHeaderButton('Created').should(be.sortDescending); + pipelinesTable.sortTable(); }); }); @@ -545,171 +508,15 @@ describe('Pipelines', () => { verifyRelativeURL('/pipelines/test-project-name-2'); }); - it('imports a new pipeline', () => { - initIntercepts({}); - pipelinesGlobal.visit(projectName); - const uploadPipelineParams = { - display_name: 'New pipeline', - description: 'New pipeline description', - }; - const uploadedMockPipeline = buildMockPipelineV2(uploadPipelineParams); - - // Intercept upload/re-fetch of pipelines - pipelineImportModal.mockUploadPipeline(uploadPipelineParams, projectName).as('uploadPipeline'); - pipelinesTable - .mockGetPipelines([initialMockPipeline, uploadedMockPipeline], projectName) - .as('refreshPipelines'); - - // Wait for the pipelines table to load - pipelinesTable.find(); - - // Open the "Import pipeline" modal - pipelinesGlobal.findImportPipelineButton().click(); - - // Fill out the "Import pipeline" modal and submit - pipelineImportModal.shouldBeOpen(); - pipelineImportModal.fillPipelineName('New pipeline'); - pipelineImportModal.fillPipelineDescription('New pipeline description'); - pipelineImportModal.uploadPipelineYaml(pipelineYamlPath); - pipelineImportModal.submit(); - - // Wait for upload/fetch requests - cy.wait('@uploadPipeline').then((interception) => { - // Note: contain is used instead of equals as different browser engines will add a different boundary - // to the body - the aim is to not limit these tests to working with one specific engine. - expect(interception.request.body).to.contain( - 'Content-Disposition: form-data; name="uploadfile"; filename="uploadedFile.yml"', - ); - expect(interception.request.body).to.contain('Content-Type: application/x-yaml'); - expect(interception.request.body).to.contain('test-yaml-pipeline-content'); - - expect(interception.request.query).to.eql({ - name: 'New pipeline', - description: 'New pipeline description', - }); - }); - - cy.wait('@refreshPipelines').then((interception) => { - expect(interception.request.query).to.eql({ sort_by: 'created_at desc', page_size: '10' }); - }); - - // Verify the uploaded pipeline is in the table - pipelinesTable.getRowByName('New pipeline').find().should('exist'); - }); - - it('imports a new pipeline by url', () => { + it('uploads a new pipeline version', () => { initIntercepts({}); pipelinesGlobal.visit(projectName); - const createPipelineAndVersionParams = { - pipeline: { - display_name: 'New pipeline', - }, - pipeline_version: { - display_name: 'New pipeline', - package_url: { - pipeline_url: 'https://example.com/pipeline.yaml', - }, - }, - }; - const createdMockPipeline = buildMockPipelineV2(createPipelineAndVersionParams.pipeline); - - // Intercept upload/re-fetch of pipelines - pipelineImportModal - .mockCreatePipelineAndVersion(createPipelineAndVersionParams, projectName) - .as('createPipelineAndVersion'); - pipelinesTable - .mockGetPipelines([initialMockPipeline, createdMockPipeline], projectName) - .as('refreshPipelines'); - pipelinesTable.mockGetPipelineVersions( - [buildMockPipelineVersionV2(createPipelineAndVersionParams.pipeline_version)], - 'new-pipeline', + pipelineImportModal.uploadPipelineVersion( projectName, + initialMockPipeline, + initialMockPipelineVersion, + uploadVersionParams, ); - - // Wait for the pipelines table to load - pipelinesTable.find(); - - // Open the "Import pipeline" modal - pipelinesGlobal.findImportPipelineButton().click(); - - // Fill out the "Import pipeline" modal and submit - pipelineImportModal.shouldBeOpen(); - pipelineImportModal.fillPipelineName('New pipeline'); - pipelineImportModal.findImportPipelineRadio().check(); - pipelineImportModal.findPipelineUrlInput().type('https://example.com/pipeline.yaml'); - pipelineImportModal.submit(); - - // Wait for upload/fetch requests - cy.wait('@createPipelineAndVersion'); - cy.wait('@refreshPipelines'); - - // Verify the uploaded pipeline is in the table - pipelinesTable.getRowByName('New pipeline').find().should('exist'); - }); - - it('uploads a new pipeline version', () => { - initIntercepts({}); - pipelinesGlobal.visit(projectName); - const uploadVersionParams = { - display_name: 'New pipeline version', - description: 'New pipeline version description', - pipeline_id: 'test-pipeline', - }; - - // Wait for the pipelines table to load - pipelinesTable.find(); - - // Open the "Upload new version" modal - pipelinesGlobal.findUploadVersionButton().click(); - - // Intercept upload/re-fetch of pipeline versions - pipelineVersionImportModal - .mockUploadVersion(uploadVersionParams, projectName) - .as('uploadVersion'); - pipelinesTable - .mockGetPipelineVersions( - [initialMockPipelineVersion, buildMockPipelineVersionV2(uploadVersionParams)], - initialMockPipeline.pipeline_id, - projectName, - ) - .as('refreshVersions'); - - // Fill out the "Upload new version" modal and submit - pipelineVersionImportModal.shouldBeOpen(); - pipelineVersionImportModal.selectPipelineByName('Test pipeline'); - pipelineVersionImportModal.fillVersionName('New pipeline version'); - pipelineVersionImportModal.fillVersionDescription('New pipeline version description'); - pipelineVersionImportModal.uploadPipelineYaml(pipelineYamlPath); - pipelineVersionImportModal.submit(); - - // Wait for upload/fetch requests - cy.wait('@uploadVersion').then((interception) => { - // Note: contain is used instead of equals as different browser engines will add a different boundary - // to the body - the aim is to not limit these tests to working with one specific engine. - expect(interception.request.body).to.contain( - 'Content-Disposition: form-data; name="uploadfile"; filename="uploadedFile.yml"', - ); - expect(interception.request.body).to.contain('Content-Type: application/x-yaml'); - expect(interception.request.body).to.contain('test-yaml-pipeline-content'); - - expect(interception.request.query).to.eql({ - name: 'New pipeline version', - description: 'New pipeline version description', - pipelineid: 'test-pipeline', - }); - }); - - cy.wait('@refreshVersions').then((interception) => { - expect(interception.request.query).to.eql({ - sort_by: 'created_at desc', - page_size: '1', - pipeline_id: 'test-pipeline', - }); - }); - - // Verify the uploaded pipeline version is in the table - pipelinesTable.getRowByName('Test pipeline').toggleExpandByIndex(0); - pipelinesTable.getRowByName('New pipeline version').find().should('exist'); }); it('imports a new pipeline version by url', () => { diff --git a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts index fb3b0984b0..61d7e71581 100644 --- a/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/e2e/pipelines/PipelinesList.cy.ts @@ -12,10 +12,17 @@ import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; import { mockRouteK8sResource } from '~/__mocks__/mockRouteK8sResource'; import { mockSecretK8sResource } from '~/__mocks__/mockSecretK8sResource'; -import { pipelinesTable } from '~/__tests__/cypress/cypress/pages/pipelines'; +import { + pipelinesTable, + pipelinesGlobal, + configurePipelineServerModal, + viewPipelineServerModal, + pipelineImportModal, +} from '~/__tests__/cypress/cypress/pages/pipelines'; import { pipelinesSection } from '~/__tests__/cypress/cypress/pages/pipelines/pipelinesSection'; import { projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; import { verifyRelativeURL } from '~/__tests__/cypress/cypress/utils/url'; +import { deleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; import { DataSciencePipelineApplicationModel, PVCModel, @@ -24,13 +31,54 @@ import { RouteModel, SecretModel, } from '~/__tests__/cypress/cypress/utils/models'; +import { PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; + +type HandlersProps = { + isEmpty?: boolean; +}; const initialMockPipeline = buildMockPipelineV2({ display_name: 'Test pipeline' }); const initialMockPipelineVersion = buildMockPipelineVersionV2({ pipeline_id: initialMockPipeline.pipeline_id, }); +const projectName = 'test-project'; + +const uploadPipelineParams = { + display_name: 'New pipeline', + description: 'New pipeline description', +}; + +const uploadVersionParams = { + display_name: 'New pipeline version', + description: 'New pipeline version description', + pipeline_id: 'test-pipeline', +}; + +const mockPipelines: PipelineKFv2[] = [ + buildMockPipelineV2({ + display_name: 'Test pipeline', + pipeline_id: 'test-pipeline', + }), + + buildMockPipelineV2({ + display_name: 'Test pipeline 2', + pipeline_id: 'test-pipeline-2', + }), +]; + +const createPipelineAndVersionParams = { + pipeline: { + display_name: 'New pipeline', + }, + pipeline_version: { + display_name: 'New pipeline', + package_url: { + pipeline_url: 'https://example.com/pipeline.yaml', + }, + }, +}; -const initIntercepts = () => { +const initIntercepts = ({ isEmpty = false }: HandlersProps) => { cy.interceptOdh( 'GET /api/dsc/status', mockDscStatus({ installedComponents: { 'data-science-pipelines-operator': true } }), @@ -49,15 +97,40 @@ const initIntercepts = () => { notebookName: 'ds-pipeline-dspa', }), ); + cy.interceptK8sList( + DataSciencePipelineApplicationModel, + mockK8sResourceList(isEmpty ? [] : [mockDataSciencePipelineApplicationK8sResource({})]), + ); + cy.interceptK8s( + DataSciencePipelineApplicationModel, + mockDataSciencePipelineApplicationK8sResource({ dspaSecretName: 'aws-connection-test' }), + ); }; describe('PipelinesList', () => { + it('Empty state', () => { + initIntercepts({ isEmpty: true }); + cy.interceptK8s( + { + model: DataSciencePipelineApplicationModel, + ns: projectName, + name: 'pipelines-definition', + }, + { + statusCode: 404, + body: mock404Error({}), + }, + ); + projectDetails.visitSection(projectName, 'pipelines-projects'); + pipelinesGlobal.findEmptyState().should('exist'); + }); + it('should show the configure pipeline server button when the server is not configured', () => { - initIntercepts(); + initIntercepts({ isEmpty: true }); cy.interceptK8s( { model: DataSciencePipelineApplicationModel, - ns: 'test-project', + ns: projectName, name: 'pipelines-definition', }, { @@ -66,17 +139,327 @@ describe('PipelinesList', () => { }, ); - projectDetails.visitSection('test-project', 'pipelines-projects'); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesSection.findCreatePipelineButton().should('be.enabled'); }); - it('should disable the upload version button when the list is empty', () => { - initIntercepts(); - cy.interceptK8sList( + it('should configure pipeline server', () => { + initIntercepts({ isEmpty: true }); + projectDetails.visitSection(projectName, 'pipelines-projects'); + + cy.interceptK8s( DataSciencePipelineApplicationModel, - mockK8sResourceList([mockDataSciencePipelineApplicationK8sResource({})]), + mockDataSciencePipelineApplicationK8sResource({}), ); + + cy.interceptK8s( + 'POST', + { + model: SecretModel, + ns: projectName, + }, + mockSecretK8sResource({ namespace: projectName }), + ).as('createSecret'); + + cy.interceptK8s( + 'POST', + DataSciencePipelineApplicationModel, + mockDataSciencePipelineApplicationK8sResource({ + namespace: projectName, + }), + ).as('createDSPA'); + + configurePipelineServerModal.configurePipelineServer(projectName); + }); + + it('should configuring pipeline server to connect external DB', () => { + initIntercepts({ isEmpty: true }); + projectDetails.visitSection(projectName, 'pipelines-projects'); + + cy.interceptK8s( + DataSciencePipelineApplicationModel, + mockDataSciencePipelineApplicationK8sResource({}), + ); + + cy.interceptK8s( + 'POST', + { + model: SecretModel, + ns: projectName, + }, + mockSecretK8sResource({ namespace: projectName }), + ).as('createSecret'); + + cy.interceptK8s( + 'POST', + DataSciencePipelineApplicationModel, + mockDataSciencePipelineApplicationK8sResource({ + namespace: projectName, + }), + ).as('createDSPA'); + + pipelinesSection.findCreatePipelineButton().should('be.enabled'); + pipelinesSection.findCreatePipelineButton().click(); + + configurePipelineServerModal.findAwsKeyInput().type('test-aws-key'); + configurePipelineServerModal.findAwsSecretKeyInput().type('test-secret-key'); + configurePipelineServerModal.findEndpointInput().type('https://s3.amazonaws.com/'); + configurePipelineServerModal.findRegionInput().should('have.value', 'us-east-1'); + configurePipelineServerModal.findBucketInput().type('test-bucket'); + + configurePipelineServerModal.findToggleButton().click(); + configurePipelineServerModal.findExternalMYSQLDatabaseRadio().click(); + configurePipelineServerModal.findSubmitButton().should('be.disabled'); + + configurePipelineServerModal.findHostInput().type('mysql'); + configurePipelineServerModal.findPortInput().type('3306'); + configurePipelineServerModal.findUsernameInput().type('test-user'); + configurePipelineServerModal.findPasswordInput().type('password'); + configurePipelineServerModal.findDatabaseInput().type('mlpipelines'); + + configurePipelineServerModal.findSubmitButton().should('be.enabled'); + configurePipelineServerModal.findSubmitButton().click(); + + cy.wait('@createSecret').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body).to.containSubset({ + metadata: { + name: 'pipelines-db-password', + namespace: projectName, + annotations: {}, + labels: { 'opendatahub.io/dashboard': 'true' }, + }, + stringData: { 'db-password': 'password' }, + }); + }); + + cy.wait('@createSecret').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body).to.containSubset({ + metadata: { + name: 'dashboard-dspa-secret', + namespace: projectName, + annotations: {}, + labels: { 'opendatahub.io/dashboard': 'true' }, + }, + stringData: { AWS_ACCESS_KEY_ID: 'test-aws-key', AWS_SECRET_ACCESS_KEY: 'test-secret-key' }, + }); + }); + + cy.wait('@createSecret').then((interception) => { + expect(interception.request.url).not.to.include('?dryRun=All'); + }); + + cy.get('@createSecret.all').then((interceptions) => { + expect(interceptions).to.have.length(4); // 2 dry-run request and 2 actual request + }); + + cy.wait('@createDSPA').then((interception) => { + expect(interception.request.body).to.containSubset({ + spec: { + apiServer: { enableSamplePipeline: false }, + dspVersion: 'v2', + objectStorage: { + externalStorage: { + host: 's3.us-east-1.amazonaws.com', + scheme: 'https', + bucket: 'test-bucket', + region: 'us-east-1', + s3CredentialsSecret: { + accessKey: 'AWS_ACCESS_KEY_ID', + secretKey: 'AWS_SECRET_ACCESS_KEY', + secretName: 'test-secret', + }, + }, + }, + database: { + externalDB: { + host: 'mysql', + passwordSecret: { key: 'db-password', name: 'test-secret' }, + pipelineDBName: 'mlpipelines', + port: '3306', + username: 'test-user', + }, + }, + }, + }); + }); + }); + + it('should view pipeline server', () => { + initIntercepts({}); + + cy.interceptK8s( + { + model: SecretModel, + ns: projectName, + }, + mockSecretK8sResource({ + s3Bucket: 'c2RzZA==', + namespace: projectName, + name: 'aws-connection-test', + }), + ); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + + viewPipelineServerModal.viewPipelineServerDetails(); + }); + + it('should delete pipeline server', () => { + initIntercepts({}); + + cy.interceptK8s( + 'DELETE', + SecretModel, + mockSecretK8sResource({ name: 'ds-pipeline-config', namespace: projectName }), + ).as('deletePipelineConfig'); + cy.interceptK8s( + 'DELETE', + SecretModel, + mockSecretK8sResource({ name: 'pipelines-db-password', namespace: projectName }), + ).as('deletePipelineDBPassword'); + cy.interceptK8s( + 'DELETE', + SecretModel, + mockSecretK8sResource({ name: 'dashboard-dspa-secret', namespace: projectName }), + ).as('deleteDSPASecret'); + cy.interceptK8s( + 'DELETE', + DataSciencePipelineApplicationModel, + mockDataSciencePipelineApplicationK8sResource({ namespace: projectName }), + ).as('deleteDSPA'); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + + pipelinesGlobal.selectPipelineServerAction('Delete pipeline server'); + deleteModal.findSubmitButton().should('be.disabled'); + deleteModal.findInput().fill('Test Project pipeline server'); + deleteModal.findSubmitButton().should('be.enabled').click(); + + cy.wait('@deletePipelineDBPassword'); + cy.wait('@deletePipelineConfig'); + cy.wait('@deleteDSPASecret'); + cy.wait('@deleteDSPA'); + }); + + it('imports a new pipeline', () => { + initIntercepts({}); + + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines([initialMockPipelineVersion]), + ); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + + pipelineImportModal.importNewPipeline(projectName, initialMockPipeline, uploadPipelineParams); + }); + + it('imports a new pipeline by url', () => { + initIntercepts({}); + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines([initialMockPipelineVersion]), + ); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + pipelineImportModal.importPipelineFromUrl( + projectName, + initialMockPipeline, + createPipelineAndVersionParams, + ); + }); + + it('uploads a new pipeline version', () => { + initIntercepts({}); + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines([initialMockPipelineVersion]), + ); + projectDetails.visitSection(projectName, 'pipelines-projects'); + + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines(mockPipelines), + ); + + pipelineImportModal.uploadPipelineVersion( + projectName, + initialMockPipeline, + initialMockPipelineVersion, + uploadVersionParams, + ); + }); + + it('should sort the table', () => { + initIntercepts({}); + + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines(mockPipelines), + ); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + + pipelinesTable.sortTable(); + }); + + it('navigate to create run page from pipeline row', () => { + initIntercepts({}); + + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines(mockPipelines), + ); + + projectDetails.visitSection(projectName, 'pipelines-projects'); + + // Wait for the pipelines table to load + pipelinesTable.find(); + pipelinesTable + .getRowByName(initialMockPipeline.display_name) + .findKebabAction('Create run') + .click(); + verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create`); + }); + + it('navigates to "Schedule run" page from pipeline row', () => { + initIntercepts({}); + + cy.intercept( + { + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, + }, + buildMockPipelines(mockPipelines), + ); + projectDetails.visitSection(projectName, 'pipelines-projects'); + + pipelinesTable.find(); + pipelinesTable + .getRowByName(initialMockPipeline.display_name) + .findKebabAction('Schedule run') + .click(); + + verifyRelativeURL(`/pipelines/${projectName}/pipelineRun/create?runType=scheduled`); + }); + + it('should disable the upload version button when the list is empty', () => { + initIntercepts({}); + cy.interceptK8s( DataSciencePipelineApplicationModel, mockDataSciencePipelineApplicationK8sResource({}), @@ -84,11 +467,11 @@ describe('PipelinesList', () => { cy.intercept( { method: 'GET', - pathname: '/api/service/pipelines/test-project/dspa/apis/v2beta1/pipelines', + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, }, buildMockPipelines([]), ).as('pipelines'); - projectDetails.visitSection('test-project', 'pipelines-projects'); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesSection.findImportPipelineSplitButton().should('be.enabled').click(); @@ -103,7 +486,7 @@ describe('PipelinesList', () => { }); it('should show the ability to delete the pipeline server kebab option', () => { - initIntercepts(); + initIntercepts({}); cy.interceptK8sList( DataSciencePipelineApplicationModel, mockK8sResourceList([mockDataSciencePipelineApplicationK8sResource({ dspVersion: 'v1' })]), @@ -112,7 +495,7 @@ describe('PipelinesList', () => { DataSciencePipelineApplicationModel, mockDataSciencePipelineApplicationK8sResource({ dspVersion: 'v1' }), ); - projectDetails.visitSection('test-project', 'pipelines-projects'); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesSection.findAllActions().should('have.length', 1); pipelinesSection.findImportPipelineSplitButton().should('not.exist'); @@ -121,18 +504,15 @@ describe('PipelinesList', () => { }); it('should navigate to details page when clicking on the version name', () => { - initIntercepts(); - cy.interceptK8sList( - DataSciencePipelineApplicationModel, - mockK8sResourceList([mockDataSciencePipelineApplicationK8sResource({})]), - ); + initIntercepts({}); + cy.interceptK8s( DataSciencePipelineApplicationModel, mockDataSciencePipelineApplicationK8sResource({}), ); cy.intercept( { - pathname: '/api/service/pipelines/test-project/dspa/apis/v2beta1/pipelines', + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines`, }, buildMockPipelines([initialMockPipeline]), ); @@ -140,11 +520,11 @@ describe('PipelinesList', () => { cy.intercept( { method: 'GET', - pathname: `/api/service/pipelines/test-project/dspa/apis/v2beta1/pipelines/${initialMockPipeline.pipeline_id}/versions`, + pathname: `/api/service/pipelines/${projectName}/dspa/apis/v2beta1/pipelines/${initialMockPipeline.pipeline_id}/versions`, }, buildMockPipelineVersionsV2([initialMockPipelineVersion]), ); - projectDetails.visitSection('test-project', 'pipelines-projects'); + projectDetails.visitSection(projectName, 'pipelines-projects'); pipelinesTable.find(); pipelinesTable.getRowByName(initialMockPipeline.display_name).toggleExpandByIndex(0); @@ -153,7 +533,7 @@ describe('PipelinesList', () => { .findPipelineName(initialMockPipelineVersion.display_name) .click(); verifyRelativeURL( - `/projects/test-project/pipeline/view/${initialMockPipeline.pipeline_id}/${initialMockPipelineVersion.pipeline_version_id}`, + `/projects/${projectName}/pipeline/view/${initialMockPipeline.pipeline_id}/${initialMockPipelineVersion.pipeline_version_id}`, ); }); }); diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts index 4f7bdacebe..72a0aa58d8 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal.ts @@ -1,12 +1,203 @@ -import { CreatePipelineAndVersionKFData, PipelineKFv2 } from '~/concepts/pipelines/kfTypes'; +import { + CreatePipelineAndVersionKFData, + PipelineKFv2, + PipelineVersionKFv2, +} from '~/concepts/pipelines/kfTypes'; +import { + pipelinesTable, + pipelinesGlobal, + pipelineVersionImportModal, +} from '~/__tests__/cypress/cypress/pages/pipelines'; +import { pipelinesSection } from '~/__tests__/cypress/cypress/pages/pipelines/pipelinesSection'; import { buildMockPipelineV2 } from '~/__mocks__/mockPipelinesProxy'; +import { buildMockPipelineVersionV2 } from '~/__mocks__'; import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal'; +const pipelineYamlPath = './cypress/e2e/pipelines/mock-upload-pipeline.yaml'; + +type UploadPipelineParams = { + display_name: string; + description: string; +}; + +type UploadVersionParams = { + display_name: string; + description: string; + pipeline_id: string; +}; + +type CreatePipelineAndVersionParams = { + pipeline: { + display_name: string; + }; + pipeline_version: { + display_name: string; + package_url: { + pipeline_url: string; + }; + }; +}; + class PipelineImportModal extends Modal { constructor() { super('Import pipeline'); } + importNewPipeline( + projectName: string, + initialMockPipeline: PipelineKFv2, + uploadPipelineParams: UploadPipelineParams, + ) { + const uploadedMockPipeline = buildMockPipelineV2(uploadPipelineParams); + + // Intercept upload/re-fetch of pipelines + pipelineImportModal.mockUploadPipeline(uploadPipelineParams, projectName).as('uploadPipeline'); + pipelinesTable + .mockGetPipelines([initialMockPipeline, uploadedMockPipeline], projectName) + .as('refreshPipelines'); + + // Wait for the pipelines table to load + pipelinesTable.find().should('exist'); + + // Open the "Import pipeline" modal + pipelinesSection.findImportPipelineButton().click(); + + // Fill out the "Import pipeline" modal and submit + pipelineImportModal.shouldBeOpen(); + pipelineImportModal.fillPipelineName(uploadPipelineParams.display_name); + pipelineImportModal.fillPipelineDescription(uploadPipelineParams.description); + pipelineImportModal.uploadPipelineYaml(pipelineYamlPath); + pipelineImportModal.submit(); + + // Wait for upload/fetch requests + cy.wait('@uploadPipeline').then((interception) => { + // Note: contain is used instead of equals as different browser engines will add a different boundary + // to the body - the aim is to not limit these tests to working with one specific engine. + expect(interception.request.body).to.contain( + 'Content-Disposition: form-data; name="uploadfile"; filename="uploadedFile.yml"', + ); + expect(interception.request.body).to.contain('Content-Type: application/x-yaml'); + expect(interception.request.body).to.contain('test-yaml-pipeline-content'); + + expect(interception.request.query).to.eql({ + name: 'New pipeline', + description: 'New pipeline description', + }); + }); + + cy.wait('@refreshPipelines').then((interception) => { + /* eslint-disable camelcase */ + expect(interception.request.query).to.containSubset({ sort_by: 'created_at desc' }); + }); + + // Verify the uploaded pipeline is in the table + pipelinesTable.getRowByName('New pipeline').find().should('exist'); + } + + importPipelineFromUrl( + projectName: string, + initialMockPipeline: PipelineKFv2, + createPipelineAndVersionParams: CreatePipelineAndVersionParams, + ) { + const createdMockPipeline = buildMockPipelineV2(createPipelineAndVersionParams.pipeline); + + // Intercept upload/re-fetch of pipelines + pipelineImportModal + .mockCreatePipelineAndVersion(createPipelineAndVersionParams, projectName) + .as('createPipelineAndVersion'); + pipelinesTable + .mockGetPipelines([initialMockPipeline, createdMockPipeline], projectName) + .as('refreshPipelines'); + pipelinesTable.mockGetPipelineVersions( + [buildMockPipelineVersionV2(createPipelineAndVersionParams.pipeline_version)], + 'new-pipeline', + projectName, + ); + + // Wait for the pipelines table to load + pipelinesTable.find().should('exist'); + + // Open the "Import pipeline" modal + pipelinesGlobal.findImportPipelineButton().click(); + + // Fill out the "Import pipeline" modal and submit + pipelineImportModal.shouldBeOpen(); + pipelineImportModal.fillPipelineName('New pipeline'); + pipelineImportModal.findImportPipelineRadio().check(); + pipelineImportModal.findPipelineUrlInput().type('https://example.com/pipeline.yaml'); + pipelineImportModal.submit(); + + // Wait for upload/fetch requests + cy.wait('@createPipelineAndVersion'); + cy.wait('@refreshPipelines'); + + // Verify the uploaded pipeline is in the table + pipelinesTable.getRowByName('New pipeline').find().should('exist'); + } + + uploadPipelineVersion( + projectName: string, + initialMockPipeline: PipelineKFv2, + initialMockPipelineVersion: PipelineVersionKFv2, + uploadVersionParams: UploadVersionParams, + ) { + // Wait for the pipelines table to load + pipelinesTable.find().should('exist'); + + // Open the "Upload new version" modal + pipelinesGlobal.findUploadVersionButton().click(); + + // Intercept upload/re-fetch of pipeline versions + pipelineVersionImportModal + .mockUploadVersion(uploadVersionParams, projectName) + .as('uploadVersion'); + pipelinesTable + .mockGetPipelineVersions( + [initialMockPipelineVersion, buildMockPipelineVersionV2(uploadVersionParams)], + initialMockPipeline.pipeline_id, + projectName, + ) + .as('refreshVersions'); + + // Fill out the "Upload new version" modal and submit + pipelineVersionImportModal.shouldBeOpen(); + pipelineVersionImportModal.selectPipelineByName('Test pipeline'); + pipelineVersionImportModal.fillVersionName('New pipeline version'); + pipelineVersionImportModal.fillVersionDescription('New pipeline version description'); + pipelineVersionImportModal.uploadPipelineYaml(pipelineYamlPath); + pipelineVersionImportModal.submit(); + + // Wait for upload/fetch requests + cy.wait('@uploadVersion').then((interception) => { + // Note: contain is used instead of equals as different browser engines will add a different boundary + // to the body - the aim is to not limit these tests to working with one specific engine. + expect(interception.request.body).to.contain( + 'Content-Disposition: form-data; name="uploadfile"; filename="uploadedFile.yml"', + ); + expect(interception.request.body).to.contain('Content-Type: application/x-yaml'); + expect(interception.request.body).to.contain('test-yaml-pipeline-content'); + + expect(interception.request.query).to.eql({ + name: 'New pipeline version', + description: 'New pipeline version description', + pipelineid: 'test-pipeline', + }); + }); + + cy.wait('@refreshVersions').then((interception) => { + expect(interception.request.query).to.eql({ + /* eslint-disable camelcase */ + sort_by: 'created_at desc', + page_size: '1', + pipeline_id: 'test-pipeline', + }); + }); + + // Verify the uploaded pipeline version is in the table + pipelinesTable.getRowByName('Test pipeline').toggleExpandByIndex(0); + pipelinesTable.getRowByName('New pipeline version').find().should('exist'); + } + find() { return cy.findByTestId('import-pipeline-modal').parents('div[role="dialog"]'); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts index 412ed6dbf6..e6b5a13bd2 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts @@ -132,6 +132,63 @@ class ConfigurePipelineServerModal extends Modal { findDatabaseInput() { return this.find().findByTestId('field Database'); } + + configurePipelineServer(projectName: string) { + pipelinesGlobal.findConfigurePipelineServerButton().should('be.enabled'); + pipelinesGlobal.findConfigurePipelineServerButton().click(); + + this.findAwsKeyInput().type('test-aws-key'); + this.findAwsSecretKeyInput().type('test-secret-key'); + this.findEndpointInput().type('https://s3.amazonaws.com/'); + this.findRegionInput().should('have.value', 'us-east-1'); + this.findBucketInput().type('test-bucket'); + this.findSubmitButton().should('be.enabled'); + this.findSubmitButton().click(); + + cy.wait('@createSecret').then((interception) => { + expect(interception.request.url).to.include('?dryRun=All'); + expect(interception.request.body).to.containSubset({ + metadata: { + name: 'dashboard-dspa-secret', + namespace: projectName, + annotations: {}, + labels: { 'opendatahub.io/dashboard': 'true' }, + }, + stringData: { AWS_ACCESS_KEY_ID: 'test-aws-key', AWS_SECRET_ACCESS_KEY: 'test-secret-key' }, + }); + }); + + cy.wait('@createSecret').then((interception) => { + expect(interception.request.url).not.to.include('?dryRun=All'); + }); + + cy.get('@createSecret.all').then((interceptions) => { + expect(interceptions).to.have.length(2); // 1 dry-run request and 1 actual request + }); + + cy.wait('@createDSPA').then((interception) => { + expect(interception.request.body).to.containSubset({ + metadata: { name: 'dspa', namespace: projectName }, + spec: { + apiServer: { enableSamplePipeline: false }, + dspVersion: 'v2', + objectStorage: { + externalStorage: { + host: 's3.us-east-1.amazonaws.com', + scheme: 'https', + bucket: 'test-bucket', + region: 'us-east-1', + s3CredentialsSecret: { + accessKey: 'AWS_ACCESS_KEY_ID', + secretKey: 'AWS_SECRET_ACCESS_KEY', + secretName: 'test-secret', + }, + }, + }, + }, + }); + }); + } } class ViewPipelineServerModal extends Modal { @@ -139,6 +196,20 @@ class ViewPipelineServerModal extends Modal { super('View pipeline server'); } + viewPipelineServerDetails() { + pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); + this.findCloseButton().click(); + + pipelinesGlobal.selectPipelineServerAction('View pipeline server configuration'); + this.shouldHaveAccessKey('sdsd'); + this.findPasswordHiddenButton().click(); + this.shouldHaveSecretKey('sdsd'); + this.shouldHaveEndPoint('https://s3.amazonaws.com'); + this.shouldHaveBucketName('test-pipelines-bucket'); + + this.findDoneButton().click(); + } + findDoneButton() { return this.find().findByTestId('view-pipeline-server-done-button'); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts index 45a2ab7078..71045a7d8b 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesTable.ts @@ -3,6 +3,7 @@ import { PipelineKFv2, PipelineVersionKFv2 } from '~/concepts/pipelines/kfTypes' import { buildMockPipelines } from '~/__mocks__/mockPipelinesProxy'; import { buildMockPipelineVersionsV2 } from '~/__mocks__/mockPipelineVersionsProxy'; import { TableRow } from '~/__tests__/cypress/cypress/pages/components/table'; +import { be } from '~/__tests__/cypress/cypress/utils/should'; class PipelinesTableRow extends TableRow { findPipelineName(name: string) { @@ -26,6 +27,20 @@ class PipelinesTableRow extends TableRow { class PipelinesTable { private testId = 'pipelines-table'; + sortTable() { + // by Pipeline + this.findTableHeaderButton('Pipeline').click(); + this.findTableHeaderButton('Pipeline').should(be.sortAscending); + this.findTableHeaderButton('Pipeline').click(); + this.findTableHeaderButton('Pipeline').should(be.sortDescending); + + // by Created + this.findTableHeaderButton('Created').click(); + this.findTableHeaderButton('Created').should(be.sortAscending); + this.findTableHeaderButton('Created').click(); + this.findTableHeaderButton('Created').should(be.sortDescending); + } + find() { return cy.findByTestId(this.testId); }