Refactor useEntityPermission

Signed-off-by: Joon Park <joonp@spotify.com>
This commit is contained in:
Joon Park
2022-01-19 12:53:38 +00:00
parent 6be405bde4
commit 6ba478138d
3 changed files with 99 additions and 45 deletions
+5 -4
View File
@@ -6,7 +6,6 @@
/// <reference types="react" />
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)
@@ -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<{}>) => (
<TestApiProvider apis={[[permissionApiRef, mockPermissionApi]]}>
<EntityProvider entity={entity} children={children} />
</TestApiProvider>
);
}
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();
});
});
@@ -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 };
}