supported unit test for stored procedure (#13095)

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Ashish Gupta 2023-09-08 10:39:05 +05:30 committed by GitHub
parent ba09f874df
commit 478837fa85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 654 additions and 2 deletions

View File

@ -0,0 +1,214 @@
/*
* 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 {
DatabaseServiceType,
LabelType,
State,
TagSource,
} from 'generated/entity/data/storedProcedure';
export const mockStoredProcedureData = [
{
id: 'de9c83b5-c37a-4d0a-a7aa-6bab1835bc1b',
name: 'update_dim_address_table',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify.update_dim_address_table',
description: 'This stored procedure updates dim_address table',
storedProcedureCode: {
code: 'CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\nRETURNS VARCHAR NOT NULL\nLANGUAGE SQL\nAS\n$$\nBEGIN\n RETURN message;\nEND;\n$$\n;',
},
version: 3.4,
updatedAt: 1693892749147,
updatedBy: 'admin',
href: 'http://localhost:8585/api/v1/storedProcedures/de9c83b5-c37a-4d0a-a7aa-6bab1835bc1b',
changeDescription: {
fieldsAdded: [],
fieldsUpdated: [
{
name: 'description',
oldValue: 'This stored procedure updates dim_address table.',
newValue: 'This stored procedure updates dim_address table',
},
{
name: 'storedProcedureCode',
oldValue: {
code: 'CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\nRETURNS VARCHAR NOT NULL\nLANGUAGE SQL\nAS\n$$\nBEGIN\n RETURN message;\nEND;\n$$\n RETURN message;\nEND;\n$$\n;',
},
newValue: {
code: 'CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\nRETURNS VARCHAR NOT NULL\nLANGUAGE SQL\nAS\n$$\nBEGIN\n RETURN message;\nEND;\n$$\n;',
},
},
],
fieldsDeleted: [],
previousVersion: 3.3,
},
databaseSchema: {
id: '48261b8c-4c99-4c5d-9ec7-cb758cc9f9c1',
type: 'databaseSchema',
name: 'shopify',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify',
description:
'This **mock** database contains schema related to shopify sales and orders with related dimension tables.',
deleted: false,
href: 'http://localhost:8585/api/v1/databaseSchemas/48261b8c-4c99-4c5d-9ec7-cb758cc9f9c1',
},
database: {
id: 'd500add1-f101-4d1a-a9b8-01c72eb81904',
type: 'database',
name: 'ecommerce_db',
fullyQualifiedName: 'sample_data.ecommerce_db',
description:
'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.',
deleted: false,
href: 'http://localhost:8585/api/v1/databases/d500add1-f101-4d1a-a9b8-01c72eb81904',
},
service: {
id: 'd610e9be-3a1d-4fb9-bc01-8bc95ef96170',
type: 'databaseService',
name: 'sample_data',
fullyQualifiedName: 'sample_data',
deleted: false,
href: 'http://localhost:8585/api/v1/services/databaseServices/d610e9be-3a1d-4fb9-bc01-8bc95ef96170',
},
serviceType: DatabaseServiceType.CustomDatabase,
deleted: false,
followers: [],
tags: [
{
tagFQN: 'PersonalData.Personal',
description:
'Data that can be used to directly or indirectly identify a person.',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'PII.NonSensitive',
description:
'PII which is easily accessible from public sources and can include zip code, race, gender, and date of birth.',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'Tier.Tier3',
description: `**Department/group level datasets that are typically non-business and general internal
system**\n\n- Used in product metrics, and dashboards to drive product decisions\n\n- Used
to track operational metrics of internal systems\n\n- Source used to derive other critical Tier-3 datasets`,
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
],
},
{
id: 'b6ca035b-7786-41dc-83b9-d75a7de20199',
name: 'update_orders_table',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.update_orders_table',
description:
'This stored procedure is written java script to update the orders table',
storedProcedureCode: {
code: `create or replace procedure read_result_set()\n returns float not null\n language
javascript\n as \n $$ \n var my_sql_command = "select * from table1";\n
var statement1 = snowflake.createStatement( {sqlText: my_sql_command} );\n var result_set1 =
statement1.execute();\n // Loop through the results, processing one row at a time... \n
while (result_set1.next()) {\n var column1 = result_set1.getColumnValue(1);\n
var column2 = result_set1.getColumnValue(2);\n // Do something with the retrieved values...\n
}\n return 0.0; // Replace with something more useful.\n $$\n ;`,
},
version: 0.8,
updatedAt: 1693577191456,
updatedBy: 'admin',
href: 'http://localhost:8585/api/v1/storedProcedures/b6ca035b-7786-41dc-83b9-d75a7de20199',
changeDescription: {
fieldsAdded: [
{
name: 'tags',
newValue:
'[{"tagFQN":"PII.Sensitive","source":"Classification","labelType":"Manual","state":"Confirmed"}]',
},
],
fieldsUpdated: [],
fieldsDeleted: [],
previousVersion: 0.7,
},
databaseSchema: {
id: '48261b8c-4c99-4c5d-9ec7-cb758cc9f9c1',
type: 'databaseSchema',
name: 'shopify',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify',
description:
'This **mock** database contains schema related to shopify sales and orders with related dimension tables.',
deleted: false,
href: 'http://localhost:8585/api/v1/databaseSchemas/48261b8c-4c99-4c5d-9ec7-cb758cc9f9c1',
},
database: {
id: 'd500add1-f101-4d1a-a9b8-01c72eb81904',
type: 'database',
name: 'ecommerce_db',
fullyQualifiedName: 'sample_data.ecommerce_db',
description:
'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.',
deleted: false,
href: 'http://localhost:8585/api/v1/databases/d500add1-f101-4d1a-a9b8-01c72eb81904',
},
service: {
id: 'd610e9be-3a1d-4fb9-bc01-8bc95ef96170',
type: 'databaseService',
name: 'sample_data',
fullyQualifiedName: 'sample_data',
deleted: false,
href: 'http://localhost:8585/api/v1/services/databaseServices/d610e9be-3a1d-4fb9-bc01-8bc95ef96170',
},
serviceType: DatabaseServiceType.CustomDatabase,
deleted: false,
owner: {
id: '306ac549-7804-4695-9b1a-f0730a1fb809',
type: 'team',
name: 'Compute',
fullyQualifiedName: 'Compute',
deleted: false,
href: 'http://localhost:8585/api/v1/teams/306ac549-7804-4695-9b1a-f0730a1fb809',
},
followers: [
{
id: 'bdd9b364-7905-48d8-8a06-50bab8d372ef',
type: 'user',
name: 'admin',
fullyQualifiedName: 'admin',
deleted: false,
href: 'http://localhost:8585/api/v1/users/bdd9b364-7905-48d8-8a06-50bab8d372ef',
},
],
tags: [
{
tagFQN: 'PII.Sensitive',
description:
'PII which if lost, compromised, or disclosed without authorization, could result in substantial harm, embarrassment, inconvenience, or unfairness to an individual.',
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
{
tagFQN: 'Tier.Tier3',
description: `**Department/group level datasets that are typically non-business and general internal
system**\n\n- Used in product metrics, and dashboards to drive product decisions\n\n-
Used to track operational metrics of internal systems\n\n- Source used to derive other critical Tier-3 datasets`,
source: TagSource.Classification,
labelType: LabelType.Manual,
state: State.Confirmed,
},
],
},
];

View File

@ -0,0 +1,244 @@
/*
* 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 { act, render, screen } from '@testing-library/react';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import React from 'react';
import { getStoredProceduresDetailsByFQN } from 'rest/storedProceduresAPI';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import StoredProcedurePage from './StoredProcedurePage';
const mockEntityPermissionByFqn = jest
.fn()
.mockImplementation(() => DEFAULT_ENTITY_PERMISSION);
jest.mock('components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockImplementation(() => ({
getEntityPermissionByFqn: mockEntityPermissionByFqn,
})),
}));
jest.mock('rest/storedProceduresAPI', () => ({
getStoredProceduresDetailsByFQN: jest.fn().mockImplementation(() =>
Promise.resolve({
name: 'test',
id: '123',
})
),
addStoredProceduresFollower: jest.fn(),
patchStoredProceduresDetails: jest.fn(),
removeStoredProceduresFollower: jest.fn(),
restoreStoredProcedures: jest.fn(),
}));
jest.mock('utils/CommonUtils', () => ({
getCurrentUserId: jest.fn(),
getFeedCounts: jest.fn(),
sortTagsCaseInsensitive: jest.fn(),
}));
jest.mock(
'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component',
() => ({
ActivityFeedTab: jest
.fn()
.mockImplementation(() => <p>testActivityFeedTab</p>),
})
);
jest.mock(
'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel',
() => {
return jest.fn().mockImplementation(() => <p>testActivityThreadPanel</p>);
}
);
jest.mock('components/common/description/DescriptionV1', () => {
return jest.fn().mockImplementation(() => <p>testDescriptionV1</p>);
});
jest.mock('components/Entity/EntityLineage/EntityLineage.component', () => {
return jest.fn().mockImplementation(() => <p>testEntityLineageComponent</p>);
});
jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () => {
return jest.fn().mockImplementation(() => <p>testErrorPlaceHolder</p>);
});
jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => <p>{children}</p>);
});
jest.mock(
'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component',
() => ({
DataAssetsHeader: jest
.fn()
.mockImplementation(() => <p>testDataAssetsHeader</p>),
})
);
jest.mock('components/Entity/EntityLineage/EntityLineage.component', () => {
return jest.fn().mockImplementation(() => <p>testEntityLineage</p>);
});
jest.mock('components/schema-editor/SchemaEditor', () => {
return jest.fn().mockImplementation(() => <p>testSchemaEditor</p>);
});
jest.mock('components/TabsLabel/TabsLabel.component', () => {
return jest.fn().mockImplementation(({ name }) => <p>{name}</p>);
});
jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
return jest.fn().mockImplementation(() => <p>testTagsContainerV2</p>);
});
jest.mock(
'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider',
() => ({
useActivityFeedProvider: jest.fn().mockImplementation(() => ({
postFeed: jest.fn(),
deleteFeed: jest.fn(),
updateFeed: jest.fn(),
})),
__esModule: true,
default: 'ActivityFeedProvider',
})
);
jest.mock('react-router-dom', () => ({
useParams: jest
.fn()
.mockImplementation(() => ({ storedProcedureFQN: 'fqn', tab: 'code' })),
useHistory: jest.fn().mockImplementation(() => ({})),
}));
jest.mock('components/Loader/Loader', () => {
return jest.fn().mockImplementation(() => <>testLoader</>);
});
jest.useFakeTimers();
describe('StoredProcedure component', () => {
it('StoredProcedurePage should fetch permissions', () => {
render(<StoredProcedurePage />);
expect(mockEntityPermissionByFqn).toHaveBeenCalledWith(
'storedProcedure',
'fqn'
);
});
it('StoredProcedurePage should not fetch details if permission is there', () => {
render(<StoredProcedurePage />);
expect(getStoredProceduresDetailsByFQN).not.toHaveBeenCalled();
});
it('StoredProcedurePage should fetch details with basic fields', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<StoredProcedurePage />);
});
expect(getStoredProceduresDetailsByFQN).toHaveBeenCalledWith(
'fqn',
'owner, followers, tags, extension'
);
});
it('StoredProcedurePage should fetch details with all the permitted fields', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewAll: true,
ViewBasic: true,
ViewUsage: true,
})),
}));
await act(async () => {
render(<StoredProcedurePage />);
});
expect(getStoredProceduresDetailsByFQN).toHaveBeenCalledWith(
'fqn',
'owner, followers, tags, extension'
);
});
it('StoredProcedurePage should render permission placeholder if not have required permission', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: false,
})),
}));
await act(async () => {
render(<StoredProcedurePage />);
});
expect(await screen.findByText('testErrorPlaceHolder')).toBeInTheDocument();
});
it('StoredProcedurePage should render page for ViewBasic permissions', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<StoredProcedurePage />);
});
expect(getStoredProceduresDetailsByFQN).toHaveBeenCalledWith(
'fqn',
'owner, followers, tags, extension'
);
expect(await screen.findByText('testDataAssetsHeader')).toBeInTheDocument();
expect(await screen.findByText('label.code')).toBeInTheDocument();
expect(
await screen.findByText('label.activity-feed-and-task-plural')
).toBeInTheDocument();
expect(await screen.findByText('label.lineage')).toBeInTheDocument();
expect(
await screen.findByText('label.custom-property-plural')
).toBeInTheDocument();
});
it('StoredProcedurePage should render codeTab by default', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<StoredProcedurePage />);
});
expect(getStoredProceduresDetailsByFQN).toHaveBeenCalledWith(
'fqn',
'owner, followers, tags, extension'
);
expect(await screen.findByText('testSchemaEditor')).toBeInTheDocument();
});
});

View File

@ -478,7 +478,7 @@ const StoredProcedurePage = () => {
onThreadLinkSelect={onThreadLinkSelect}
/>
<Card className="m-b-md">
<Card className="m-b-md" data-testid="code-component">
<SchemaEditor
editorClass="custom-code-mirror-theme full-screen-editor-height"
mode={{ name: CSMode.SQL }}

View File

@ -0,0 +1,195 @@
/*
* 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 { fireEvent, render, screen } from '@testing-library/react';
import { INITIAL_PAGING_VALUE, pagingObject } from 'constants/constants';
import { mockStoredProcedureData } from 'mocks/StoredProcedure.mock';
import React from 'react';
import { StoredProcedureTabProps } from './storedProcedure.interface';
import StoredProcedureTab from './StoredProcedureTab';
const mockPagingHandler = jest.fn();
const mockShowDeletedHandler = jest.fn();
const mockFetchHandler = jest.fn();
const mockProps: StoredProcedureTabProps = {
storedProcedure: {
data: mockStoredProcedureData,
isLoading: false,
deleted: false,
paging: pagingObject,
currentPage: INITIAL_PAGING_VALUE,
},
pagingHandler: mockPagingHandler,
fetchStoredProcedure: mockFetchHandler,
onShowDeletedStoreProcedureChange: mockShowDeletedHandler,
};
jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () => {
return jest.fn().mockImplementation(() => <p>testErrorPlaceHolder</p>);
});
jest.mock('components/common/next-previous/NextPrevious', () => {
return jest.fn().mockImplementation(({ pagingHandler }) => (
<p data-testid="next-previous" onClick={pagingHandler}>
testNextPrevious
</p>
));
});
jest.mock('components/common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockImplementation(() => <p>testRichTextEditorPreviewer</p>);
});
jest.mock('components/Loader/Loader', () => {
return jest.fn().mockImplementation(() => <p>testLoader</p>);
});
// mock library imports
jest.mock('react-router-dom', () => ({
Link: jest
.fn()
.mockImplementation(({ children }) => <a href="#">{children}</a>),
}));
jest.mock('utils/EntityUtils', () => ({
getEntityName: jest.fn().mockImplementation(() => 'displayName'),
}));
jest.mock('utils/StringsUtils', () => ({
getEncodedFqn: jest.fn().mockImplementation((fqn) => fqn),
}));
jest.mock('utils/TableUtils', () => ({
getEntityLink: jest.fn().mockImplementation((link) => link),
}));
describe('StoredProcedureTab component', () => {
it('StoredProcedureTab should fetch details', () => {
render(<StoredProcedureTab {...mockProps} />);
expect(mockFetchHandler).toHaveBeenCalled();
});
it('StoredProcedureTab should render components', () => {
render(<StoredProcedureTab {...mockProps} />);
expect(mockFetchHandler).toHaveBeenCalled();
expect(screen.getByTestId('stored-procedure-table')).toBeInTheDocument();
expect(
screen.getByTestId('show-deleted-stored-procedure')
).toBeInTheDocument();
expect(screen.queryByText('testNextPrevious')).not.toBeInTheDocument();
});
it('StoredProcedureTab should show loader till api is not resolved', () => {
render(
<StoredProcedureTab
{...mockProps}
storedProcedure={{ ...mockProps.storedProcedure, isLoading: true }}
/>
);
expect(mockFetchHandler).toHaveBeenCalled();
expect(screen.queryByText('testLoader')).toBeInTheDocument();
});
it('StoredProcedureTab should show empty placeholder within table when data is empty', () => {
render(
<StoredProcedureTab
{...mockProps}
storedProcedure={{
...mockProps.storedProcedure,
data: [],
}}
/>
);
expect(mockFetchHandler).toHaveBeenCalled();
expect(screen.queryByText('testErrorPlaceHolder')).toBeInTheDocument();
});
it('StoredProcedureTab should show table along with data', () => {
render(<StoredProcedureTab {...mockProps} />);
expect(mockFetchHandler).toHaveBeenCalled();
const container = screen.getByTestId('stored-procedure-table');
expect(screen.getAllByText('testRichTextEditorPreviewer')).toHaveLength(2);
screen.debug(container);
});
it('show deleted switch handler show properly', () => {
render(<StoredProcedureTab {...mockProps} />);
expect(mockFetchHandler).toHaveBeenCalled();
const showDeletedHandler = screen.getByTestId(
'show-deleted-stored-procedure'
);
expect(showDeletedHandler).toBeInTheDocument();
fireEvent.click(showDeletedHandler);
expect(mockShowDeletedHandler).toHaveBeenCalled();
});
it('show render next_previous component', () => {
render(
<StoredProcedureTab
{...mockProps}
storedProcedure={{
data: [],
isLoading: false,
deleted: false,
paging: { ...pagingObject, total: 20 },
currentPage: INITIAL_PAGING_VALUE,
}}
/>
);
expect(mockFetchHandler).toHaveBeenCalled();
expect(screen.queryByText('testNextPrevious')).toBeInTheDocument();
});
it('next_previous handler should work properly', () => {
render(
<StoredProcedureTab
{...mockProps}
storedProcedure={{
data: [],
isLoading: false,
deleted: false,
paging: { ...pagingObject, total: 20 },
currentPage: INITIAL_PAGING_VALUE,
}}
/>
);
expect(mockFetchHandler).toHaveBeenCalled();
const nextComponent = screen.getByTestId('next-previous');
expect(nextComponent).toBeInTheDocument();
fireEvent.click(nextComponent);
expect(mockPagingHandler).toHaveBeenCalled();
});
});

View File

@ -91,7 +91,6 @@ const StoredProcedureTab = ({
<Table
bordered
columns={tableColumn}
data-testid="data-models-table"
dataSource={data}
loading={{
spinning: isLoading,