chore(ui): added unit and cypress tests (#12728)

* fixed duplicate API call on data insight and search settings pages

* Added unit tests for IngestionListTable component

* Added cypress for DataInsightSettings page

* fixed failing unit tests

* fixed flaky teams cypress test

* removed unnecessary checks

* worked on comments

* added missing localization for kill ingestion modal button

* fixed failing cypress test

---------

Co-authored-by: Shailesh Parmar <shailesh.parmar.webdev@gmail.com>
Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Aniket Katkar 2023-08-08 10:42:34 +05:30 committed by GitHub
parent 3c5651221d
commit 59f5fc09b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 563 additions and 14 deletions

View File

@ -0,0 +1,249 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { interceptURL, verifyResponseStatusCode } from '../../common/common';
import { BASE_URL } from '../../constants/constants';
const PIPELINE_NAME = 'cypress_dataInsight_pipeline';
const REGION_NAME = 'US';
describe('Data Insight settings page should work properly', () => {
beforeEach(() => {
cy.login();
interceptURL('GET', '/api/v1/teams/name/*', 'settingsPage');
cy.get('[data-testid="appbar-item-settings"]').click();
verifyResponseStatusCode('@settingsPage', 200);
cy.get('[data-testid="settings-left-panel"]').should('be.visible');
interceptURL(
'GET',
'api/v1/services/ingestionPipelines?fields=pipelineStatuses&service=OpenMetadata&pipelineType=dataInsight',
'ingestionPipelines'
);
interceptURL(
'GET',
'/api/v1/services/ingestionPipelines/OpenMetadata.OpenMetadata_dataInsight/pipelineStatus?startTs=*',
'pipelineStatus'
);
cy.get(`[data-menu-id*="openMetadata.dataInsight"]`)
.scrollIntoView()
.click();
verifyResponseStatusCode('@ingestionPipelines', 200);
verifyResponseStatusCode('@pipelineStatus', 200);
});
it('Add data insight pipeline should work properly', () => {
interceptURL(
'GET',
'/api/v1/services/metadataServices/name/OpenMetadata',
'serviceDetails'
);
cy.get('[data-testid="add-new-ingestion-button"]').click();
verifyResponseStatusCode('@serviceDetails', 200);
cy.get('[data-testid="name"]').clear().type(PIPELINE_NAME);
cy.get('[data-testid="next-button"]').click();
cy.get('#root\\/regionName').type(REGION_NAME);
cy.get('#root\\/useAwsCredentials').click({ waitForAnimations: true });
cy.get('#root\\/useAwsCredentials')
.invoke('attr', 'aria-checked')
.should('eq', 'true');
cy.get('[data-testid="next-button"]').click();
interceptURL(
'POST',
'/api/v1/services/ingestionPipelines',
'postIngestionPipeline'
);
cy.get('[data-testid="deploy-button"]').click();
cy.wait('@postIngestionPipeline').then(({ request, response }) => {
expect(request.body.sourceConfig.config).to.deep.equal({
regionName: 'US',
useAwsCredentials: true,
useSSL: false,
verifyCerts: false,
type: 'MetadataToElasticSearch',
});
expect(request.body.loggerLevel).to.equal('INFO');
expect(response.statusCode).to.equal(201);
});
cy.get('[data-testid="view-service-button"]').click();
verifyResponseStatusCode('@ingestionPipelines', 200);
verifyResponseStatusCode('@pipelineStatus', 200);
cy.get(`[data-row-key="${PIPELINE_NAME}"]`).should('be.visible');
});
it('Edit data insight pipeline should work properly', () => {
interceptURL(
'GET',
'/api/v1/services/metadataServices/name/OpenMetadata',
'serviceDetails'
);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="edit"]`).click();
verifyResponseStatusCode('@serviceDetails', 200);
cy.get('#root\\/loggerLevel').click({ waitForAnimations: true });
cy.get('#root\\/loggerLevel')
.invoke('attr', 'aria-checked')
.should('eq', 'true');
cy.get('[data-testid="next-button"]').click();
cy.get('#root\\/useSSL').click({ waitForAnimations: true });
cy.get('#root\\/useSSL')
.invoke('attr', 'aria-checked')
.should('eq', 'true');
cy.get('[data-testid="next-button"]').click();
interceptURL(
'PUT',
'/api/v1/services/ingestionPipelines',
'putIngestionPipeline'
);
cy.get('[data-testid="deploy-button"]').click();
cy.wait('@putIngestionPipeline').then(({ request, response }) => {
expect(request.body.sourceConfig.config).to.deep.equal({
regionName: 'US',
useAwsCredentials: true,
useSSL: true,
verifyCerts: false,
type: 'MetadataToElasticSearch',
});
expect(request.body.loggerLevel).to.equal('DEBUG');
expect(response.statusCode).to.equal(200);
});
cy.get('[data-testid="view-service-button"]').click();
verifyResponseStatusCode('@ingestionPipelines', 200);
verifyResponseStatusCode('@pipelineStatus', 200);
cy.get(`[data-row-key="${PIPELINE_NAME}"]`).should('be.visible');
});
it('Run and kill data insight pipeline should work properly', () => {
interceptURL(
'POST',
'/api/v1/services/ingestionPipelines/trigger/*',
'runPipelineDag'
);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="run"]`).click();
verifyResponseStatusCode('@runPipelineDag', 200);
interceptURL(
'POST',
'/api/v1/services/ingestionPipelines/kill/*',
'killPipelineDag'
);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="kill"]`).click();
cy.get('[data-testid="kill-modal"]').contains('Confirm').click();
verifyResponseStatusCode('@killPipelineDag', 200);
});
it('Re deploy data insight pipeline should work properly', () => {
interceptURL(
'POST',
'/api/v1/services/ingestionPipelines/deploy/*',
'reDeployPipelineDag'
);
cy.get(
`[data-row-key="${PIPELINE_NAME}"] [data-testid="re-deploy-btn"]`
).click();
verifyResponseStatusCode('@reDeployPipelineDag', 200);
});
it('Pause and unpause data insight pipeline should work properly', () => {
interceptURL(
'POST',
'/api/v1/services/ingestionPipelines/toggleIngestion/*',
'togglePipelineDag'
);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="pause"]`).click();
verifyResponseStatusCode('@togglePipelineDag', 200);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="unpause"]`).click();
verifyResponseStatusCode('@togglePipelineDag', 200);
});
it('Logs action button for the data insight pipeline should redirect to the logs page', () => {
interceptURL(
'GET',
`/api/v1/services/ingestionPipelines/name/OpenMetadata.${PIPELINE_NAME}?fields=owner,pipelineStatuses`,
'getServiceDetails'
);
interceptURL(
'GET',
'/api/v1/services/ingestionPipelines/logs/*/*last?after=',
'getLogs'
);
interceptURL(
'GET',
`/api/v1/services/ingestionPipelines/OpenMetadata.cypress_dataInsight_pipeline/pipelineStatus?*`,
'getPipelineStatus'
);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="logs"]`).click();
verifyResponseStatusCode('@getServiceDetails', 200);
verifyResponseStatusCode('@getLogs', 200);
verifyResponseStatusCode('@getPipelineStatus', 200);
cy.url().should(
'eq',
`${BASE_URL}/metadataServices/OpenMetadata.${PIPELINE_NAME}/logs`
);
});
it('Delete data insight pipeline should work properly', () => {
interceptURL(
'DELETE',
'/api/v1/services/ingestionPipelines/*?hardDelete=true',
'deletePipelineDag'
);
cy.get(`[data-row-key="${PIPELINE_NAME}"] [data-testid="delete"]`).click();
cy.get('[data-testid="confirmation-text-input"]').type('DELETE');
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@deletePipelineDag', 200);
});
});

View File

@ -357,12 +357,25 @@ describe('Teams flow should work properly', () => {
`/api/v1/teams/name/${TEAM_DETAILS.name}*`,
'getSelectedTeam'
);
interceptURL(
'GET',
`/api/v1/teams?limit=100000&parentTeam=${TEAM_DETAILS.name}&include=all`,
'getTeamParent'
);
interceptURL(
'GET',
`/api/v1/teams?fields=userCount%2CchildrenCount%2Cowns%2Cparents&limit=100000&parentTeam=${TEAM_DETAILS.name}&include=all`,
'getChildrenCount'
);
cy.get('table').should('contain', TEAM_DETAILS.name).click();
cy.get('table').find('.ant-table-row').contains(TEAM_DETAILS.name).click();
verifyResponseStatusCode('@getSelectedTeam', 200);
verifyResponseStatusCode('@getTeamParent', 200);
verifyResponseStatusCode('@getChildrenCount', 200);
cy.get('[data-testid="team-heading"]')
.should('be.visible')
.contains(TEAM_DETAILS.updatedname);

View File

@ -118,12 +118,12 @@ const DataInsightMetadataToESConfigForm = ({
<Divider />
<Row justify="end">
<Col>
<Button type="link" onClick={handlePrev}>
<Button data-testid="back-button" type="link" onClick={handlePrev}>
{t('label.back')}
</Button>
</Col>
<Col>
<Button htmlType="submit" type="primary">
<Button data-testid="next-button" htmlType="submit" type="primary">
{t('label.next')}
</Button>
</Col>

View File

@ -307,7 +307,7 @@ describe('Test Ingestion page', () => {
}
);
const viewButton = await findByTestId(container, 'airflow-tree-view');
const viewButton = await findByTestId(container, 'ingestion-dag-link');
expect(viewButton).toBeInTheDocument();
});

View File

@ -13,6 +13,7 @@
import { Table, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import NextPrevious from 'components/common/next-previous/NextPrevious';
import Loader from 'components/Loader/Loader';
import cronstrue from 'cronstrue';
import { Paging } from 'generated/type/paging';
@ -23,7 +24,6 @@ import { getEntityName } from 'utils/EntityUtils';
import { getErrorPlaceHolder } from 'utils/IngestionUtils';
import { PAGE_SIZE } from '../../constants/constants';
import { IngestionPipeline } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import NextPrevious from '../common/next-previous/NextPrevious';
import { IngestionListTableProps } from './IngestionListTable.interface';
import { IngestionRecentRuns } from './IngestionRecentRun/IngestionRecentRuns.component';
import PipelineActions from './PipelineActions.component';
@ -72,7 +72,7 @@ function IngestionListTable({
}>
<Typography.Link
className="tw-mr-2 overflow-wrap-anywhere"
data-testid="airflow-tree-view"
data-testid="ingestion-dag-link"
disabled={!(permissions.ViewAll || permissions.ViewBasic)}
href={`${airflowEndpoint}/tree?dag_id=${text}`}
rel="noopener noreferrer"
@ -185,7 +185,7 @@ function IngestionListTable({
<Table
bordered
columns={tableColumn}
data-testid="schema-table"
data-testid="ingestion-list-table"
dataSource={ingestionData}
loading={{
spinning: isLoading,

View File

@ -0,0 +1,120 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import { mockIngestionListTableProps } from 'mocks/IngestionListTable.mock';
import React from 'react';
import IngestionListTable from './IngestionListTable.component';
jest.mock('components/common/next-previous/NextPrevious', () =>
jest.fn().mockImplementation(() => <div>nextPrevious</div>)
);
jest.mock('components/Loader/Loader', () =>
jest.fn().mockImplementation(() => <div>loader</div>)
);
jest.mock('./PipelineActions.component', () =>
jest.fn().mockImplementation(() => <div>pipelineActions</div>)
);
jest.mock('./IngestionRecentRun/IngestionRecentRuns.component', () => ({
IngestionRecentRuns: jest
.fn()
.mockImplementation(() => <div>ingestionRecentRuns</div>),
}));
describe('IngestionListTable tests', () => {
it('Should display the loader if the isLoading is true', () => {
render(<IngestionListTable {...mockIngestionListTableProps} isLoading />);
const ingestionListTable = screen.getByTestId('ingestion-list-table');
const loader = screen.getByText('loader');
expect(ingestionListTable).toBeInTheDocument();
expect(loader).toBeInTheDocument();
});
it('Should not display the loader if the isLoading is false', () => {
render(
<IngestionListTable {...mockIngestionListTableProps} isLoading={false} />
);
const ingestionListTable = screen.getByTestId('ingestion-list-table');
const loader = screen.queryByText('loader');
expect(ingestionListTable).toBeInTheDocument();
expect(loader).toBeNull();
});
it('Should not display the loader if the isLoading is undefined', () => {
render(
<IngestionListTable
{...mockIngestionListTableProps}
isLoading={undefined}
/>
);
const ingestionListTable = screen.getByTestId('ingestion-list-table');
const loader = screen.queryByText('loader');
expect(ingestionListTable).toBeInTheDocument();
expect(loader).toBeNull();
});
it('Should display NexPrevious component for list size more than 10 and paging object has after field', () => {
render(
<IngestionListTable
{...mockIngestionListTableProps}
paging={{
total: 14,
after: 'after',
}}
/>
);
const nextPrevious = screen.getByText('nextPrevious');
expect(nextPrevious).toBeInTheDocument();
});
it('Should not display NexPrevious component for list size less than 10', () => {
render(
<IngestionListTable
{...mockIngestionListTableProps}
paging={{
total: 4,
}}
/>
);
const nextPrevious = screen.queryByText('nextPrevious');
expect(nextPrevious).toBeNull();
});
it('Should render the ingestion link if airflowEndpoint is provided', () => {
render(<IngestionListTable {...mockIngestionListTableProps} />);
const ingestionDagLink = screen.getByTestId('ingestion-dag-link');
expect(ingestionDagLink).toBeInTheDocument();
});
it('Should not render the ingestion link if airflowEndpoint is not provided', () => {
render(
<IngestionListTable {...mockIngestionListTableProps} airflowEndpoint="" />
);
const ingestionDagLink = screen.queryByTestId('ingestion-dag-link');
expect(ingestionDagLink).toBeNull();
});
});

View File

@ -43,8 +43,8 @@ describe('Test Kill Ingestion Modal component', () => {
const container = await screen.findByTestId('kill-modal');
const body = await screen.findByTestId('kill-modal-body');
const cancelButton = await screen.findByText('Cancel');
const confirmButton = await screen.findByText('Confirm');
const cancelButton = await screen.findByText('label.cancel');
const confirmButton = await screen.findByText('label.confirm');
expect(container).toBeInTheDocument();
expect(body).toBeInTheDocument();
@ -55,7 +55,7 @@ describe('Test Kill Ingestion Modal component', () => {
it('Should close modal on click of cancel button', async () => {
render(<KillIngestionModal {...mockProps} />);
const cancelButton = await screen.findByText('Cancel');
const cancelButton = await screen.findByText('label.cancel');
expect(cancelButton).toBeInTheDocument();
@ -68,7 +68,7 @@ describe('Test Kill Ingestion Modal component', () => {
await act(async () => {
render(<KillIngestionModal {...mockProps} />);
const confirmButton = await screen.findByText('Confirm');
const confirmButton = await screen.findByText('label.confirm');
expect(confirmButton).toBeInTheDocument();

View File

@ -57,11 +57,12 @@ const KillIngestionModal: FC<KillIngestionModalProps> = ({
return (
<Modal
destroyOnClose
cancelText={t('label.cancel')}
closable={false}
confirmLoading={isLoading}
data-testid="kill-modal"
maskClosable={false}
okText="Confirm"
okText={t('label.confirm')}
title={`${t('label.kill')} ${pipelinName} ?`}
visible={isModalOpen}
onCancel={onClose}

View File

@ -113,9 +113,6 @@ function SettingsIngestion({
);
} finally {
setIsLoading(false);
if (!airflowEndpoint) {
getAirflowEndpoint();
}
}
};
@ -257,6 +254,7 @@ function SettingsIngestion({
useEffect(() => {
if (isAirflowAvailable) {
getAllIngestionWorkflows();
getAirflowEndpoint();
}
}, [isAirflowAvailable]);

View File

@ -0,0 +1,168 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IngestionListTableProps } from 'components/Ingestion/IngestionListTable.interface';
import { ServiceCategory } from 'enums/service.enum';
import {
AuthProvider,
ConfigType,
IngestionPipeline,
LogLevels,
OpenmetadataType,
PipelineState,
PipelineType,
ProviderType,
SearchIndexMappingLanguage,
SecretsManagerClientLoader,
SecretsManagerProvider,
VerifySSL,
} from 'generated/entity/services/ingestionPipelines/ingestionPipeline';
import { ENTITY_PERMISSIONS } from './Permissions.mock';
const mockTriggerIngestion = jest.fn();
const mockDeployIngestion = jest.fn();
const mockHandleEnableDisableIngestion = jest.fn();
const mockOnIngestionWorkflowsUpdate = jest.fn();
const mockHandleDeleteSelection = jest.fn();
const mockHandleIsConfirmationModalOpen = jest.fn();
const mockESIngestionData: IngestionPipeline[] = [
{
id: '5ff66f1c-9809-4333-836e-ba4dadda11f2',
name: 'OpenMetadata_elasticSearchReindex',
displayName: 'OpenMetadata_elasticSearchReindex',
description: 'Elastic Search Reindexing Pipeline',
pipelineType: PipelineType.ElasticSearchReindex,
fullyQualifiedName: 'OpenMetadata.OpenMetadata_elasticSearchReindex',
sourceConfig: {
config: {
type: ConfigType.MetadataToElasticSearch,
useSSL: false,
timeout: 30,
batchSize: 1000,
verifyCerts: false,
recreateIndex: true,
useAwsCredentials: false,
searchIndexMappingLanguage: SearchIndexMappingLanguage.En,
},
},
openMetadataServerConnection: {
clusterName: 'sandbox-beta',
type: OpenmetadataType.OpenMetadata,
hostPort: 'http://openmetadata-server:8585/api',
authProvider: AuthProvider.Openmetadata,
verifySSL: VerifySSL.NoSSL,
securityConfig: {
jwtToken: 'eyJraWQiOiJHYjM4OWEtOWY3Ni1nZGpzLWE5MmotMDI0MmJrO',
},
secretsManagerProvider: SecretsManagerProvider.Noop,
secretsManagerLoader: SecretsManagerClientLoader.Noop,
apiVersion: 'v1',
includeTopics: true,
includeTables: true,
includeDashboards: true,
includePipelines: true,
includeMlModels: true,
includeUsers: true,
includeTeams: true,
includeGlossaryTerms: true,
includeTags: true,
includePolicy: true,
includeMessagingServices: true,
enableVersionValidation: true,
includeDatabaseServices: true,
includePipelineServices: true,
limitRecords: 1000,
forceEntityOverwriting: false,
supportsDataInsightExtraction: true,
supportsElasticSearchReindexingExtraction: true,
},
airflowConfig: {
pausePipeline: false,
concurrency: 1,
pipelineTimezone: 'UTC',
retries: 3,
retryDelay: 300,
pipelineCatchup: false,
scheduleInterval: '*/30 * * * *',
maxActiveRuns: 1,
workflowDefaultView: 'tree',
workflowDefaultViewOrientation: 'LR',
},
service: {
id: 'd520c9bb-a517-4f1e-8962-d8518de71279',
type: 'metadataService',
name: 'OpenMetadata',
fullyQualifiedName: 'OpenMetadata',
description:
'Service Used for creating OpenMetadata Ingestion Pipelines.',
displayName: 'OpenMetadata Service',
deleted: false,
href: 'http://sandbox-beta.open-metadata.org/api/v1/services/databaseServices/d520c9bb-a517-4f1e-8962-d8518de71279',
},
pipelineStatuses: {
runId: '8bd07fbd-a356-45c1-8621-7bb6a4dff5b2',
pipelineState: PipelineState.Success,
startDate: 1690885805006,
timestamp: 1690885805006,
endDate: 1690885844106,
},
loggerLevel: LogLevels.Info,
deployed: true,
enabled: false,
href: 'http://sandbox-beta.open-metadata.org/api/v1/services/ingestionPipelines/5ff66f1c-9809-4333-836e-ba4dadda11f2',
version: 0.5,
updatedAt: 1687854372726,
updatedBy: 'teddy',
changeDescription: {
fieldsAdded: [],
fieldsUpdated: [
{
name: 'enabled',
oldValue: true,
newValue: false,
},
],
fieldsDeleted: [],
previousVersion: 0.4,
},
deleted: false,
provider: ProviderType.User,
},
];
export const mockIngestionListTableProps: IngestionListTableProps = {
airflowEndpoint: 'http://localhost:8080',
triggerIngestion: mockTriggerIngestion,
deployIngestion: mockDeployIngestion,
isRequiredDetailsAvailable: true,
paging: { total: 2 },
handleEnableDisableIngestion: mockHandleEnableDisableIngestion,
onIngestionWorkflowsUpdate: mockOnIngestionWorkflowsUpdate,
ingestionPipelinesPermission: {
OpenMetadata_elasticSearchReindex: ENTITY_PERMISSIONS,
},
serviceCategory: ServiceCategory.METADATA_SERVICES,
serviceName: 'OpenMetadata',
handleDeleteSelection: mockHandleDeleteSelection,
handleIsConfirmationModalOpen: mockHandleIsConfirmationModalOpen,
ingestionData: mockESIngestionData,
deleteSelection: {
id: '',
name: '',
state: '',
},
permissions: ENTITY_PERMISSIONS,
pipelineType: PipelineType.ElasticSearchReindex,
isLoading: false,
};