From 183f2644b59d6edc089a16ffddfc83b67ff02667 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Wed, 13 Sep 2023 10:38:39 +0530 Subject: [PATCH] supported unit test for stored database schema page (#13131) --- .../DatabaseSchemaPage.component.tsx | 8 +- .../DatabaseSchemaPage.test.tsx | 391 ++++++++++-------- .../mocks/DatabaseSchemaPage.mock.ts | 106 +++++ .../ui/src/rest/storedProceduresAPI.ts | 2 +- 4 files changed, 323 insertions(+), 184 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx index 097e77af120..5787c3ae929 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.component.tsx @@ -51,14 +51,16 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { ListDataModelParams } from 'rest/dashboardAPI'; import { getDatabaseSchemaDetailsByFQN, patchDatabaseSchemaDetails, restoreDatabaseSchema, } from 'rest/databaseAPI'; import { getFeedCount, postThread } from 'rest/feedsAPI'; -import { getStoredProceduresList } from 'rest/storedProceduresAPI'; +import { + getStoredProceduresList, + ListStoredProcedureParams, +} from 'rest/storedProceduresAPI'; import { getTableList, TableListParams } from 'rest/tableAPI'; import { getEntityMissingError } from 'utils/CommonUtils'; import { getDatabaseSchemaVersionPath } from 'utils/RouterUtils'; @@ -218,7 +220,7 @@ const DatabaseSchemaPage: FunctionComponent = () => { }, [databaseSchemaFQN]); const fetchStoreProcedureDetails = useCallback( - async (params?: ListDataModelParams) => { + async (params?: ListStoredProcedureParams) => { try { setStoredProcedure((prev) => ({ ...prev, isLoading: true })); const { data, paging } = await getStoredProceduresList({ diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx index e0c9ea270c7..16938a6cdac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/DatabaseSchemaPage.test.tsx @@ -11,23 +11,126 @@ * 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 React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { getDatabaseSchemaDetailsByFQN } from 'rest/databaseAPI'; +import { getStoredProceduresList } from 'rest/storedProceduresAPI'; +import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils'; import DatabaseSchemaPageComponent from './DatabaseSchemaPage.component'; import { - mockEntityPermissions, - mockGetAllFeedsData, mockGetDatabaseSchemaDetailsByFQNData, mockGetFeedCountData, mockPatchDatabaseSchemaDetailsData, - mockPostFeedByIdData, mockPostThreadData, - mockSearchQueryData, } 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(() =>

testActivityThreadPanel

); + } +); + +jest.mock( + 'components/DataAssets/DataAssetsHeader/DataAssetsHeader.component', + () => ({ + DataAssetsHeader: jest + .fn() + .mockImplementation(() =>

testDataAssetsHeader

), + }) +); + +jest.mock('components/TabsLabel/TabsLabel.component', () => + jest.fn().mockImplementation(({ name }) =>
{name}
) +); + +jest.mock('components/Tag/TagsContainerV2/TagsContainerV2', () => { + return jest.fn().mockImplementation(() =>

testTagsContainerV2

); +}); + +jest.mock('./SchemaTablesTab', () => { + return jest.fn().mockReturnValue(

testSchemaTablesTab

); +}); + +jest.mock('pages/StoredProcedure/StoredProcedureTab', () => { + return jest.fn().mockImplementation(() =>
testStoredProcedureTab
); +}); + +jest.mock('components/containers/PageLayoutV1', () => { + return jest.fn().mockImplementation(({ children }) =>

{children}

); +}); + +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', () => ({ showErrorToast: jest .fn() @@ -35,79 +138,23 @@ jest.mock('../../utils/ToastUtils', () => ({ })); jest.mock('components/Loader/Loader', () => - jest.fn().mockImplementation(() =>
Loader
) + jest.fn().mockImplementation(() =>
testLoader
) ); -jest.mock('components/common/rich-text-editor/RichTextEditorPreviewer', () => - jest.fn().mockImplementation(() =>
RichTextEditorPreviewer
) -); - -jest.mock('components/common/next-previous/NextPrevious', () => - jest.fn().mockImplementation(() =>
NextPrevious
) -); - -jest.mock('components/FeedEditor/FeedEditor', () => { - return jest.fn().mockReturnValue(

ActivityFeedEditor

); -}); - jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () => - jest - .fn() - .mockImplementation(({ children }) => ( -
{children}
- )) -); - -jest.mock('components/common/description/Description', () => - jest - .fn() - .mockImplementation( - ({ onThreadLinkSelect, onDescriptionEdit, onDescriptionUpdate }) => ( -
{ - onThreadLinkSelect('threadLink'); - onDescriptionEdit(); - onDescriptionUpdate('Updated Description'); - }}> - Description -
- ) - ) -); - -jest.mock( - 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel', - () => jest.fn().mockImplementation(() =>
ActivityThreadPanel
) + jest.fn().mockImplementation(() =>

ErrorPlaceHolder

) ); jest.mock('components/PermissionProvider/PermissionProvider', () => ({ usePermissionProvider: jest.fn().mockImplementation(() => ({ - getEntityPermissionByFqn: jest - .fn() - .mockImplementation(() => Promise.resolve(mockEntityPermissions)), + getEntityPermissionByFqn: mockEntityPermissionByFqn, })), })); -jest.mock('rest/searchAPI', () => ({ - searchQuery: jest - .fn() - .mockImplementation(() => Promise.resolve(mockSearchQueryData)), -})); - -jest.mock('components/MyData/LeftSidebar/LeftSidebar.component', () => - jest.fn().mockReturnValue(

Sidebar

) -); - jest.mock('rest/feedsAPI', () => ({ - getAllFeeds: jest - .fn() - .mockImplementation(() => Promise.resolve(mockGetAllFeedsData)), getFeedCount: jest .fn() .mockImplementation(() => Promise.resolve(mockGetFeedCountData)), - postFeedById: jest - .fn() - .mockImplementation(() => Promise.resolve(mockPostFeedByIdData)), postThread: jest .fn() .mockImplementation(() => Promise.resolve(mockPostThreadData)), @@ -124,6 +171,11 @@ jest.mock('rest/databaseAPI', () => ({ .mockImplementation(() => Promise.resolve(mockPatchDatabaseSchemaDetailsData) ), + restoreDatabaseSchema: jest + .fn() + .mockImplementation(() => + Promise.resolve(mockPatchDatabaseSchemaDetailsData) + ), })); jest.mock('../../AppState', () => ({ @@ -136,11 +188,6 @@ const mockParams = { }; jest.mock('react-router-dom', () => ({ - Link: jest - .fn() - .mockImplementation(({ children }) => ( -
{children}
- )), useHistory: jest.fn().mockImplementation(() => ({ history: { push: jest.fn(), @@ -149,129 +196,113 @@ jest.mock('react-router-dom', () => ({ useParams: jest.fn().mockImplementation(() => mockParams), })); -jest.mock('components/containers/PageLayoutV1', () => { - return jest.fn().mockImplementation(({ children }) => children); -}); +describe('Tests for DatabaseSchemaPage', () => { + it('DatabaseSchemaPage should fetch permissions', () => { + render(); -describe.skip('Tests for DatabaseSchemaPage', () => { - it('Page should render properly for "Tables" tab', async () => { - act(() => { - render(, { - wrapper: MemoryRouter, - }); - }); - - const entityPageInfo = await screen.findByTestId('entityPageInfo'); - const tabsPane = await screen.findByTestId('tabs'); - const richTextEditorPreviewer = await screen.findAllByText( - 'RichTextEditorPreviewer' + expect(mockEntityPermissionByFqn).toHaveBeenCalledWith( + 'databaseSchema', + mockParams.databaseSchemaFQN ); - 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 () => { - await act(async () => { - render(, { - wrapper: MemoryRouter, - }); + it('DatabaseSchemaPage should not fetch details if permission is there', () => { + render(); - const loader = screen.getByText('Loader'); - const errorPlaceHolder = screen.queryByText('error-placeHolder'); - - expect(loader).toBeInTheDocument(); - expect(errorPlaceHolder).toBeNull(); - }); - - const entityPageInfo = await screen.findByTestId('entityPageInfo'); - const tabsPane = await screen.findByTestId('tabs'); - - expect(entityPageInfo).toBeInTheDocument(); - expect(tabsPane).toBeInTheDocument(); + expect(getDatabaseSchemaDetailsByFQN).not.toHaveBeenCalled(); + expect(getStoredProceduresList).not.toHaveBeenCalled(); }); - it('Activity Feed List should render properly for "Activity Feeds" tab', async () => { - mockParams.tab = 'activity_feed'; - - await act(async () => { - render(, { - 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(, { - 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(, { - 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 () => { + it('DatabaseSchemaPage should render permission placeholder if not have required permission', async () => { (usePermissionProvider as jest.Mock).mockImplementationOnce(() => ({ - getEntityPermissionByFqn: jest.fn().mockImplementation(() => - Promise.resolve({ - ...mockEntityPermissions, - ViewAll: false, - ViewBasic: false, - }) - ), + getEntityPermissionByFqn: jest.fn().mockImplementationOnce(() => ({ + ViewBasic: false, + })), })); await act(async () => { - render(, { - wrapper: MemoryRouter, - }); + render(); }); - 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(); + }); + + 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(); + }); + + 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(); + }); + + 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(); + }); + + expect(getDatabaseSchemaDetailsByFQN).toHaveBeenCalledWith( + mockParams.databaseSchemaFQN, + ['owner', 'usageSummary', 'tags'], + 'include=all' + ); + + expect(await screen.findByText('testSchemaTablesTab')).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts index 3afec746f42..82f5701c81e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaPage/mocks/DatabaseSchemaPage.mock.ts @@ -254,3 +254,109 @@ export const mockEntityPermissions = { EditDisplayName: 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, + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts index 5a3682617fa..ea7cb229b93 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/storedProceduresAPI.ts @@ -21,7 +21,7 @@ import { ServicePageData } from 'pages/ServiceDetailsPage/ServiceDetailsPage'; import { getURLWithQueryFields } from 'utils/APIUtils'; import APIClient from './index'; -interface ListStoredProcedureParams { +export interface ListStoredProcedureParams { databaseSchema?: string; fields?: string; after?: string;