From 6ba478138daf15064d63987becd81a12ab3872ed Mon Sep 17 00:00:00 2001 From: Joon Park Date: Wed, 19 Jan 2022 12:53:38 +0000 Subject: [PATCH] Refactor useEntityPermission Signed-off-by: Joon Park --- plugins/catalog-react/api-report.md | 9 +- .../src/hooks/useEntityPermission.test.tsx | 105 ++++++++++++------ .../src/hooks/useEntityPermission.ts | 30 ++++- 3 files changed, 99 insertions(+), 45 deletions(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 8550b6a5c6..129f7b36af 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -6,7 +6,6 @@ /// import { ApiRef } from '@backstage/core-plugin-api'; -import { AsyncPermissionResult } from '@backstage/plugin-permission-react'; import { AsyncState } from 'react-use/lib/useAsync'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; @@ -882,9 +881,11 @@ export function useEntityOwnership(): { }; // @public -export function useEntityPermission( - permission: Permission, -): AsyncPermissionResult; +export function useEntityPermission(permission: Permission): { + loading: boolean; + allowed: boolean; + error?: Error; +}; // Warning: (ae-forgotten-export) The symbol "EntityTypeReturn" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "useEntityTypeFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx b/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx index db7202de20..553d999b61 100644 --- a/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityPermission.test.tsx @@ -14,51 +14,84 @@ * limitations under the License. */ -import React, { PropsWithChildren } from 'react'; import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common'; import { renderHook } from '@testing-library/react-hooks'; import { useEntityPermission } from './useEntityPermission'; -import { MockPermissionApi, TestApiProvider } from '@backstage/test-utils'; -import { permissionApiRef } from '@backstage/plugin-permission-react'; -import { Entity } from '@backstage/catalog-model'; -import { EntityProvider } from './useEntity'; +import { useEntity } from './useEntity'; +import { usePermission } from '@backstage/plugin-permission-react'; -const mockPermissionApi = new MockPermissionApi(); - -function createWrapper(entity?: Entity) { - return ({ children }: PropsWithChildren<{}>) => ( - - - - ); -} +jest.mock('./useEntity', () => ({ + ...jest.requireActual('./useEntity'), + useEntity: jest.fn(), +})); +jest.mock('@backstage/plugin-permission-react', () => ({ + ...jest.requireActual('@backstage/plugin-permission-react'), + usePermission: jest.fn(), +})); +const useEntityMock = useEntity as jest.Mock; +const usePermissionMock = usePermission as jest.Mock; describe('useEntityPermission', () => { - it('returns authorization result', async () => { - const { result, waitForValueToChange } = renderHook( - () => useEntityPermission(catalogEntityDeletePermission), - { - wrapper: createWrapper({ - apiVersion: 'a', - kind: 'b', - metadata: { name: 'c' }, - }), - }, + afterEach(() => { + jest.resetAllMocks(); + }); + + it('returns loading when entity is loading', () => { + useEntityMock.mockReturnValue({ loading: true, entity: undefined }); + usePermissionMock.mockReturnValue({ loading: false, allowed: false }); + const { result } = renderHook(() => + useEntityPermission(catalogEntityDeletePermission), ); - await waitForValueToChange(() => result.current); + expect(result.current.loading).toBe(true); + }); + + it('returns loading when permission is loading', () => { + useEntityMock.mockReturnValue({ + loading: false, + entity: { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'c' }, + }, + }); + usePermissionMock.mockReturnValue({ loading: true, allowed: false }); + const { result } = renderHook(() => + useEntityPermission(catalogEntityDeletePermission), + ); + + expect(result.current.loading).toBe(true); + }); + + it('does not authorize when there is an entity error', () => { + useEntityMock.mockReturnValue({ + loading: false, + entity: undefined, + error: new Error(), + }); + usePermissionMock.mockReturnValue({ loading: false, allowed: false }); + const { result } = renderHook(() => + useEntityPermission(catalogEntityDeletePermission), + ); + + expect(result.current.error).toBeInstanceOf(Error); + expect(result.current.allowed).toBe(false); + }); + + it('returns authorization result', () => { + useEntityMock.mockReturnValue({ + loading: false, + entity: { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'c' }, + }, + }); + usePermissionMock.mockReturnValue({ loading: false, allowed: true }); + const { result } = renderHook(() => + useEntityPermission(catalogEntityDeletePermission), + ); expect(result.current.allowed).toBe(true); }); - - it('throws error if no entity is found', async () => { - const { waitForNextUpdate } = renderHook( - () => useEntityPermission(catalogEntityDeletePermission), - { - wrapper: createWrapper(), - }, - ); - - await expect(() => waitForNextUpdate()).rejects.toThrowError(); - }); }); diff --git a/plugins/catalog-react/src/hooks/useEntityPermission.ts b/plugins/catalog-react/src/hooks/useEntityPermission.ts index 1783526e34..c1f62cfc39 100644 --- a/plugins/catalog-react/src/hooks/useEntityPermission.ts +++ b/plugins/catalog-react/src/hooks/useEntityPermission.ts @@ -24,12 +24,32 @@ import { useEntity } from './useEntity'; * {@link @backstage/plugin-permission-react#usePermission} hook which uses the * current entity in context to make an authorization request for the given * permission. + * + * Note: this hook blocks the permission request until the entity has loaded in + * context. If you have the entityRef and need concurrent requests, use the + * `usePermission` hook directly. * @public */ -export function useEntityPermission(permission: Permission) { - const { entity } = useEntity(); - if (!entity) { - throw new Error('No entity in current context.'); +export function useEntityPermission(permission: Permission): { + loading: boolean; + allowed: boolean; + error?: Error; +} { + const { entity, loading: loadingEntity, error: entityError } = useEntity(); + const { + allowed, + loading: loadingPermission, + error: permissionError, + } = usePermission( + permission, + entity ? stringifyEntityRef(entity) : undefined, + ); + + if (loadingEntity || loadingPermission) { + return { loading: true, allowed: false }; } - return usePermission(permission, stringifyEntityRef(entity)); + if (entityError) { + return { loading: false, allowed: false, error: entityError }; + } + return { loading: false, allowed, error: permissionError }; }