supported unit test for stored database schema page (#13131)

This commit is contained in:
Ashish Gupta 2023-09-13 10:38:39 +05:30 committed by GitHub
parent 6d4e425f36
commit 183f2644b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 323 additions and 184 deletions

View File

@ -51,14 +51,16 @@ import React, {
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { ListDataModelParams } from 'rest/dashboardAPI';
import { import {
getDatabaseSchemaDetailsByFQN, getDatabaseSchemaDetailsByFQN,
patchDatabaseSchemaDetails, patchDatabaseSchemaDetails,
restoreDatabaseSchema, restoreDatabaseSchema,
} from 'rest/databaseAPI'; } from 'rest/databaseAPI';
import { getFeedCount, postThread } from 'rest/feedsAPI'; import { getFeedCount, postThread } from 'rest/feedsAPI';
import { getStoredProceduresList } from 'rest/storedProceduresAPI'; import {
getStoredProceduresList,
ListStoredProcedureParams,
} from 'rest/storedProceduresAPI';
import { getTableList, TableListParams } from 'rest/tableAPI'; import { getTableList, TableListParams } from 'rest/tableAPI';
import { getEntityMissingError } from 'utils/CommonUtils'; import { getEntityMissingError } from 'utils/CommonUtils';
import { getDatabaseSchemaVersionPath } from 'utils/RouterUtils'; import { getDatabaseSchemaVersionPath } from 'utils/RouterUtils';
@ -218,7 +220,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
}, [databaseSchemaFQN]); }, [databaseSchemaFQN]);
const fetchStoreProcedureDetails = useCallback( const fetchStoreProcedureDetails = useCallback(
async (params?: ListDataModelParams) => { async (params?: ListStoredProcedureParams) => {
try { try {
setStoredProcedure((prev) => ({ ...prev, isLoading: true })); setStoredProcedure((prev) => ({ ...prev, isLoading: true }));
const { data, paging } = await getStoredProceduresList({ const { data, paging } = await getStoredProceduresList({

View File

@ -11,23 +11,126 @@
* limitations under the License. * limitations under the License.
*/ */
import { act, fireEvent, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider'; import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import React from 'react'; import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { getDatabaseSchemaDetailsByFQN } from 'rest/databaseAPI'; import { getDatabaseSchemaDetailsByFQN } from 'rest/databaseAPI';
import { getStoredProceduresList } from 'rest/storedProceduresAPI';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component'; import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component';
import { import {
mockEntityPermissions,
mockGetAllFeedsData,
mockGetDatabaseSchemaDetailsByFQNData, mockGetDatabaseSchemaDetailsByFQNData,
mockGetFeedCountData, mockGetFeedCountData,
mockPatchDatabaseSchemaDetailsData, mockPatchDatabaseSchemaDetailsData,
mockPostFeedByIdData,
mockPostThreadData, mockPostThreadData,
mockSearchQueryData,
} from './mocks/DatabaseSchemaPage.mock'; } from './mocks/DatabaseSchemaPage.mock';
const mockEntityPermissionByFqn = jest
.fn()
.mockImplementation(() => DEFAULT_ENTITY_PERMISSION);
jest.mock('components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockImplementation(() => ({
getEntityPermissionByFqn: mockEntityPermissionByFqn,
})),
}));
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(
'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component',
() => ({
ActivityFeedTab: jest
.fn()
.mockImplementation(() => <>testActivityFeedTab</>),
})
);
jest.mock(
'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel',
() => {
return jest.fn().mockImplementation(() => <p>testActivityThreadPanel</p>);
}
);
jest.mock(
'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component',
() => ({
DataAssetsHeader: jest
.fn()
.mockImplementation(() => <p>testDataAssetsHeader</p>),
})
);
jest.mock('components/TabsLabel/TabsLabel.component', () =>
jest.fn().mockImplementation(({ name }) => <div>{name}</div>)
);
jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => {
return jest.fn().mockImplementation(() => <p>testTagsContainerV2</p>);
});
jest.mock('./SchemaTablesTab', () => {
return jest.fn().mockReturnValue(<p>testSchemaTablesTab</p>);
});
jest.mock('pages/StoredProcedure/StoredProcedureTab', () => {
return jest.fn().mockImplementation(() => <div>testStoredProcedureTab</div>);
});
jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => <p>{children}</p>);
});
jest.mock('utils/StringsUtils', () => ({
getDecodedFqn: jest.fn().mockImplementation((fqn) => fqn),
}));
jest.mock('rest/storedProceduresAPI', () => ({
getStoredProceduresList: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: [], paging: { total: 2 } })
),
}));
jest.mock('rest/tableAPI', () => ({
getTableList: jest
.fn()
.mockImplementation(() =>
Promise.resolve({ data: [], paging: { total: 0 } })
),
}));
jest.mock('utils/CommonUtils', () => ({
getEntityMissingError: jest.fn().mockImplementation((error) => error),
}));
jest.mock('utils/RouterUtils', () => ({
getDatabaseSchemaVersionPath: jest.fn().mockImplementation((path) => path),
}));
jest.mock('../../utils/EntityUtils', () => ({
getEntityFeedLink: jest.fn(),
getEntityName: jest.fn().mockImplementation((obj) => obj.name),
}));
jest.mock('../../utils/TableUtils', () => ({
getTierTags: jest.fn(),
getTagsWithoutTier: jest.fn(),
}));
jest.mock('../../utils/ToastUtils', () => ({ jest.mock('../../utils/ToastUtils', () => ({
showErrorToast: jest showErrorToast: jest
.fn() .fn()
@ -35,79 +138,23 @@ jest.mock('../../utils/ToastUtils', () => ({
})); }));
jest.mock('components/Loader/Loader', () => jest.mock('components/Loader/Loader', () =>
jest.fn().mockImplementation(() => <div>Loader</div>) jest.fn().mockImplementation(() => <div>testLoader</div>)
); );
jest.mock('components/common/rich-text-editor/RichTextEditorPreviewer', () =>
jest.fn().mockImplementation(() => <div>RichTextEditorPreviewer</div>)
);
jest.mock('components/common/next-previous/NextPrevious', () =>
jest.fn().mockImplementation(() => <div>NextPrevious</div>)
);
jest.mock('components/FeedEditor/FeedEditor', () => {
return jest.fn().mockReturnValue(<p>ActivityFeedEditor</p>);
});
jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () => jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () =>
jest jest.fn().mockImplementation(() => <p>ErrorPlaceHolder</p>)
.fn()
.mockImplementation(({ children }) => (
<div data-testid="error-placeHolder">{children}</div>
))
);
jest.mock('components/common/description/Description', () =>
jest
.fn()
.mockImplementation(
({ onThreadLinkSelect, onDescriptionEdit, onDescriptionUpdate }) => (
<div
onClick={() => {
onThreadLinkSelect('threadLink');
onDescriptionEdit();
onDescriptionUpdate('Updated Description');
}}>
Description
</div>
)
)
);
jest.mock(
'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel',
() => jest.fn().mockImplementation(() => <div>ActivityThreadPanel</div>)
); );
jest.mock('components/PermissionProvider/PermissionProvider', () => ({ jest.mock('components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockImplementation(() => ({ usePermissionProvider: jest.fn().mockImplementation(() => ({
getEntityPermissionByFqn: jest getEntityPermissionByFqn: mockEntityPermissionByFqn,
.fn()
.mockImplementation(() => Promise.resolve(mockEntityPermissions)),
})), })),
})); }));
jest.mock('rest/searchAPI', () => ({
searchQuery: jest
.fn()
.mockImplementation(() => Promise.resolve(mockSearchQueryData)),
}));
jest.mock('components/MyData/LeftSidebar/LeftSidebar.component', () =>
jest.fn().mockReturnValue(<p>Sidebar</p>)
);
jest.mock('rest/feedsAPI', () => ({ jest.mock('rest/feedsAPI', () => ({
getAllFeeds: jest
.fn()
.mockImplementation(() => Promise.resolve(mockGetAllFeedsData)),
getFeedCount: jest getFeedCount: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve(mockGetFeedCountData)), .mockImplementation(() => Promise.resolve(mockGetFeedCountData)),
postFeedById: jest
.fn()
.mockImplementation(() => Promise.resolve(mockPostFeedByIdData)),
postThread: jest postThread: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve(mockPostThreadData)), .mockImplementation(() => Promise.resolve(mockPostThreadData)),
@ -124,6 +171,11 @@ jest.mock('rest/databaseAPI', () => ({
.mockImplementation(() => .mockImplementation(() =>
Promise.resolve(mockPatchDatabaseSchemaDetailsData) Promise.resolve(mockPatchDatabaseSchemaDetailsData)
), ),
restoreDatabaseSchema: jest
.fn()
.mockImplementation(() =>
Promise.resolve(mockPatchDatabaseSchemaDetailsData)
),
})); }));
jest.mock('../../AppState', () => ({ jest.mock('../../AppState', () => ({
@ -136,11 +188,6 @@ const mockParams = {
}; };
jest.mock('react-router-dom', () => ({ jest.mock('react-router-dom', () => ({
Link: jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="link">{children}</div>
)),
useHistory: jest.fn().mockImplementation(() => ({ useHistory: jest.fn().mockImplementation(() => ({
history: { history: {
push: jest.fn(), push: jest.fn(),
@ -149,129 +196,113 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => mockParams), useParams: jest.fn().mockImplementation(() => mockParams),
})); }));
jest.mock('components/containers/PageLayoutV1', () => { describe('Tests for DatabaseSchemaPage', () => {
return jest.fn().mockImplementation(({ children }) => children); it('DatabaseSchemaPage should fetch permissions', () => {
}); render(<DatabaseSchemaPageComponent />);
describe.skip('Tests for DatabaseSchemaPage', () => { expect(mockEntityPermissionByFqn).toHaveBeenCalledWith(
it('Page should render properly for "Tables" tab', async () => { 'databaseSchema',
act(() => { mockParams.databaseSchemaFQN
render(<DatabaseSchemaPageComponent />, {
wrapper: MemoryRouter,
});
});
const entityPageInfo = await screen.findByTestId('entityPageInfo');
const tabsPane = await screen.findByTestId('tabs');
const richTextEditorPreviewer = await screen.findAllByText(
'RichTextEditorPreviewer'
); );
const description = await screen.findByText('Description');
const nextPrevious = await screen.findByText('NextPrevious');
const databaseSchemaTable = await screen.findByTestId(
'databaseSchema-tables'
);
expect(entityPageInfo).toBeInTheDocument();
expect(tabsPane).toBeInTheDocument();
expect(richTextEditorPreviewer).toHaveLength(10);
expect(description).toBeInTheDocument();
expect(nextPrevious).toBeInTheDocument();
expect(databaseSchemaTable).toBeInTheDocument();
}); });
it('Loader should be visible if the permissions are being fetched', async () => { it('DatabaseSchemaPage should not fetch details if permission is there', () => {
await act(async () => { render(<DatabaseSchemaPageComponent />);
render(<DatabaseSchemaPageComponent />, {
wrapper: MemoryRouter,
});
const loader = screen.getByText('Loader'); expect(getDatabaseSchemaDetailsByFQN).not.toHaveBeenCalled();
const errorPlaceHolder = screen.queryByText('error-placeHolder'); expect(getStoredProceduresList).not.toHaveBeenCalled();
expect(loader).toBeInTheDocument();
expect(errorPlaceHolder).toBeNull();
});
const entityPageInfo = await screen.findByTestId('entityPageInfo');
const tabsPane = await screen.findByTestId('tabs');
expect(entityPageInfo).toBeInTheDocument();
expect(tabsPane).toBeInTheDocument();
}); });
it('Activity Feed List should render properly for "Activity Feeds" tab', async () => { it('DatabaseSchemaPage should render permission placeholder if not have required permission', async () => {
mockParams.tab = 'activity_feed';
await act(async () => {
render(<DatabaseSchemaPageComponent />, {
wrapper: MemoryRouter,
});
});
const activityFeedList = await screen.findByTestId('ActivityFeedList');
expect(activityFeedList).toBeInTheDocument();
});
it('ActivityThreadPanel should render properly after clicked on thread panel button', async () => {
mockParams.tab = 'table';
await act(async () => {
render(<DatabaseSchemaPageComponent />, {
wrapper: MemoryRouter,
});
});
const description = await screen.findByText('Description');
expect(description).toBeInTheDocument();
act(() => {
fireEvent.click(description);
});
const activityThreadPanel = await screen.findByText('ActivityThreadPanel');
expect(activityThreadPanel).toBeInTheDocument();
});
it('ErrorPlaceholder should be displayed in case error occurs while fetching database schema details', async () => {
(getDatabaseSchemaDetailsByFQN as jest.Mock).mockImplementationOnce(() =>
Promise.reject('An error occurred')
);
await act(async () => {
render(<DatabaseSchemaPageComponent />, {
wrapper: MemoryRouter,
});
});
const errorPlaceHolder = await screen.findByTestId('error-placeHolder');
const errorMessage = await screen.findByTestId('error-message');
expect(errorPlaceHolder).toBeInTheDocument();
expect(errorMessage).toHaveTextContent('An error occurred');
});
it('ErrorPlaceholder should be shown in case of not viewing permissions', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({ (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementation(() => getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
Promise.resolve({ ViewBasic: false,
...mockEntityPermissions, })),
ViewAll: false,
ViewBasic: false,
})
),
})); }));
await act(async () => { await act(async () => {
render(<DatabaseSchemaPageComponent />, { render(<DatabaseSchemaPageComponent />);
wrapper: MemoryRouter,
});
}); });
const errorPlaceHolder = await screen.findByTestId('error-placeHolder'); expect(await screen.findByText('ErrorPlaceHolder')).toBeInTheDocument();
});
expect(errorPlaceHolder).toBeInTheDocument(); it('DatabaseSchemaPage should fetch details with basic fields', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<DatabaseSchemaPageComponent />);
});
expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
mockParams.databaseSchemaFQN,
['owner', 'usageSummary', 'tags'],
'include=all'
);
});
it('DatabaseSchemaPage should fetch storedProcedure with basic fields', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<DatabaseSchemaPageComponent />);
});
expect(getStoredProceduresList).toHaveBeenCalledWith({
databaseSchema: mockParams.databaseSchemaFQN,
fields: 'owner,tags,followers',
include: 'non-deleted',
limit: 0,
});
});
it('DatabaseSchemaPage should render page for ViewBasic permissions', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<DatabaseSchemaPageComponent />);
});
expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
mockParams.databaseSchemaFQN,
['owner', 'usageSummary', 'tags'],
'include=all'
);
expect(await screen.findByText('testDataAssetsHeader')).toBeInTheDocument();
expect(await screen.findByTestId('tabs')).toBeInTheDocument();
expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument();
});
it('DatabaseSchemaPage should render tables by default', async () => {
(usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({
getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({
ViewBasic: true,
})),
}));
await act(async () => {
render(<DatabaseSchemaPageComponent />);
});
expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith(
mockParams.databaseSchemaFQN,
['owner', 'usageSummary', 'tags'],
'include=all'
);
expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument();
}); });
}); });

View File

@ -254,3 +254,109 @@ export const mockEntityPermissions = {
EditDisplayName: true, EditDisplayName: true,
EditCustomFields: true, EditCustomFields: true,
}; };
export const mockStoredProcedure = {
data: [
{
id: '5c225b39-0a20-4157-8085-5b22d0beea95',
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: 0.1,
updatedAt: 1694001765000,
updatedBy: 'admin',
href: 'http://localhost:8585/api/v1/storedProcedures/5c225b39-0a20-4157-8085-5b22d0beea95',
databaseSchema: {
id: '6745cd0c-e9d7-423e-80cc-accc3629811c',
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/6745cd0c-e9d7-423e-80cc-accc3629811c',
},
database: {
id: 'a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
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/a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
},
service: {
id: '7ac73a36-67f0-451e-a042-e91fb24ee6f1',
type: 'databaseService',
name: 'sample_data',
fullyQualifiedName: 'sample_data',
deleted: false,
href: 'http://localhost:8585/api/v1/services/databaseServices/7ac73a36-67f0-451e-a042-e91fb24ee6f1',
},
serviceType: 'CustomDatabase',
deleted: false,
followers: [],
tags: [],
},
{
id: '4651cccf-6c8a-49fc-804f-41a81bf6898b',
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.1,
updatedAt: 1694001765116,
updatedBy: 'admin',
href: 'http://localhost:8585/api/v1/storedProcedures/4651cccf-6c8a-49fc-804f-41a81bf6898b',
databaseSchema: {
id: '6745cd0c-e9d7-423e-80cc-accc3629811c',
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/6745cd0c-e9d7-423e-80cc-accc3629811c',
},
database: {
id: 'a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
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/a5f8e3b5-a23d-4d3c-9fca-ce7449493c5b',
},
service: {
id: '7ac73a36-67f0-451e-a042-e91fb24ee6f1',
type: 'databaseService',
name: 'sample_data',
fullyQualifiedName: 'sample_data',
deleted: false,
href: 'http://localhost:8585/api/v1/services/databaseServices/7ac73a36-67f0-451e-a042-e91fb24ee6f1',
},
serviceType: 'CustomDatabase',
deleted: false,
followers: [],
tags: [],
},
],
paging: {
total: 2,
},
};

View File

@ -21,7 +21,7 @@ import { ServicePageData } from 'pages/ServiceDetailsPage/ServiceDetailsPage';
import { getURLWithQueryFields } from 'utils/APIUtils'; import { getURLWithQueryFields } from 'utils/APIUtils';
import APIClient from './index'; import APIClient from './index';
interface ListStoredProcedureParams { export interface ListStoredProcedureParams {
databaseSchema?: string; databaseSchema?: string;
fields?: string; fields?: string;
after?: string; after?: string;