diff --git a/.changeset/good-baboons-taste.md b/.changeset/good-baboons-taste.md new file mode 100644 index 0000000000..820c286635 --- /dev/null +++ b/.changeset/good-baboons-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Add tests for kubernetes custom hook diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 2e9d12bfb9..c21ac7e18a 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -128,7 +128,7 @@ regions. Configures which [custom resources][3] to look for when returning an entity's Kubernetes resources. -Defaults to empty array. router.ts Example: +Defaults to empty array. Example: ```yaml --- diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 76bd427d34..560e8a24a2 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -55,6 +55,7 @@ "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", + "@testing-library/react-hooks": "^3.4.2", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", diff --git a/plugins/kubernetes/src/components/Pods/PodsTable.tsx b/plugins/kubernetes/src/components/Pods/PodsTable.tsx index fb2b26cd39..0a219bef89 100644 --- a/plugins/kubernetes/src/components/Pods/PodsTable.tsx +++ b/plugins/kubernetes/src/components/Pods/PodsTable.tsx @@ -57,7 +57,7 @@ type DeploymentTablesProps = { }; export const PodsTable = ({ pods }: DeploymentTablesProps) => { - const tableStyle: React.CSSProperties = { + const tableStyle = { minWidth: '0', width: '100%', }; diff --git a/plugins/kubernetes/src/hooks/useKubernetesObjects.test.ts b/plugins/kubernetes/src/hooks/useKubernetesObjects.test.ts new file mode 100644 index 0000000000..0e9de6e4c0 --- /dev/null +++ b/plugins/kubernetes/src/hooks/useKubernetesObjects.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { useKubernetesObjects } from './useKubernetesObjects'; +import { Entity } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core'; +import { renderHook } from '@testing-library/react-hooks'; + +jest.mock('@backstage/core'); + +const entity = { + metadata: { + name: 'some-entity', + }, +} as Entity; + +const entityWithAuthToken = { + auth: { + google: 'some-token', + }, + entity, +}; + +const mockResponse = { + items: [ + { + cluster: { name: 'some-cluster' }, + resources: [ + { + type: 'pods', + resources: [ + { + metadata: { + name: 'some-pod', + }, + }, + ], + }, + ], + errors: [], + }, + ], +}; + +const getClustersResponse = [ + { + name: 'cluster-a', + authProvider: 'google', + }, + { + name: 'cluster-b', + authProvider: 'authprovider2', + }, +]; + +describe('useKubernetesObjects', () => { + const mockGetClusters = jest.fn(); + const mockGetObjectsByEntity = jest.fn(); + const mockDecorateRequestBodyForAuth = jest.fn(); + + const expectMocksCalledCorrectly = () => { + expect(mockGetClusters).toBeCalledTimes(1); + expect(mockGetClusters).toHaveBeenLastCalledWith(); + expect(mockDecorateRequestBodyForAuth).toBeCalledTimes(2); + expect(mockDecorateRequestBodyForAuth).toHaveBeenCalledWith('google', { + entity, + }); + expect(mockDecorateRequestBodyForAuth).toHaveBeenCalledWith( + 'authprovider2', + entityWithAuthToken, + ); + expect(mockGetObjectsByEntity).toBeCalledTimes(1); + expect(mockGetObjectsByEntity).toHaveBeenLastCalledWith( + entityWithAuthToken, + ); + }; + + afterEach(() => { + jest.resetAllMocks(); + }); + it('should return objects', async () => { + (useApi as any).mockReturnValue({ + getClusters: mockGetClusters.mockResolvedValue(getClustersResponse), + getObjectsByEntity: mockGetObjectsByEntity.mockResolvedValue( + mockResponse, + ), + decorateRequestBodyForAuth: mockDecorateRequestBodyForAuth.mockResolvedValue( + entityWithAuthToken, + ), + }); + const { result, waitForNextUpdate } = renderHook(() => + useKubernetesObjects(entity), + ); + + await waitForNextUpdate(); + + expect(result.current.error).toBeUndefined(); + expect(result.current.kubernetesObjects).toStrictEqual(mockResponse); + + expectMocksCalledCorrectly(); + }); + it('should return error when getObjectsByEntity throws', async () => { + (useApi as any).mockReturnValue({ + getClusters: mockGetClusters.mockResolvedValue(getClustersResponse), + getObjectsByEntity: mockGetObjectsByEntity.mockRejectedValue({ + message: 'some error', + }), + decorateRequestBodyForAuth: mockDecorateRequestBodyForAuth.mockResolvedValue( + entityWithAuthToken, + ), + }); + const { result, waitForNextUpdate } = renderHook(() => + useKubernetesObjects(entity), + ); + + await waitForNextUpdate(); + + expect(result.current.error).toBe('some error'); + expect(result.current.kubernetesObjects).toBeUndefined(); + + expectMocksCalledCorrectly(); + }); + + it('should return error when getClusters throws', async () => { + (useApi as any).mockReturnValue({ + getClusters: mockGetClusters.mockRejectedValue({ message: 'some-error' }), + getObjectsByEntity: mockGetObjectsByEntity, + decorateRequestBodyForAuth: mockDecorateRequestBodyForAuth, + }); + const { result, waitForNextUpdate } = renderHook(() => + useKubernetesObjects(entity), + ); + + await waitForNextUpdate(); + + expect(result.current.error).toBe('some-error'); + expect(result.current.kubernetesObjects).toBeUndefined(); + + expect(mockGetClusters).toBeCalledTimes(1); + expect(mockGetClusters).toHaveBeenLastCalledWith(); + expect(mockDecorateRequestBodyForAuth).toBeCalledTimes(0); + expect(mockGetObjectsByEntity).toBeCalledTimes(0); + }); + it('should return error when decorateRequestBodyForAuth throws', async () => { + (useApi as any).mockReturnValue({ + getClusters: mockGetClusters.mockResolvedValue(getClustersResponse), + decorateRequestBodyForAuth: mockDecorateRequestBodyForAuth.mockRejectedValue( + { message: 'some-error' }, + ), + getObjectsByEntity: mockGetObjectsByEntity, + }); + const { result, waitForNextUpdate } = renderHook(() => + useKubernetesObjects(entity), + ); + + await waitForNextUpdate(); + + expect(result.current.error).toBe('some-error'); + expect(result.current.kubernetesObjects).toBeUndefined(); + + expect(mockGetClusters).toBeCalledTimes(1); + expect(mockGetClusters).toHaveBeenLastCalledWith(); + expect(mockDecorateRequestBodyForAuth).toBeCalledTimes(1); + expect(mockDecorateRequestBodyForAuth).toHaveBeenCalledWith('google', { + entity, + }); + expect(mockGetObjectsByEntity).toBeCalledTimes(0); + }); +}); diff --git a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts index ade7717f9e..7c4de9ff1a 100644 --- a/plugins/kubernetes/src/hooks/useKubernetesObjects.ts +++ b/plugins/kubernetes/src/hooks/useKubernetesObjects.ts @@ -40,7 +40,15 @@ export const useKubernetesObjects = (entity: Entity): KubernetesObjects => { useEffect(() => { (async () => { - const clusters = await kubernetesApi.getClusters(); + let clusters = []; + + try { + clusters = await kubernetesApi.getClusters(); + } catch (e) { + setError(e.message); + return; + } + const authProviders: string[] = [ ...new Set(clusters.map(c => c.authProvider)), ]; @@ -50,21 +58,25 @@ export const useKubernetesObjects = (entity: Entity): KubernetesObjects => { }; for (const authProviderStr of authProviders) { // Multiple asyncs done sequentially instead of all at once to prevent same requestBody from being modified simultaneously - requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth( - authProviderStr, - requestBody, - ); + try { + requestBody = await kubernetesAuthProvidersApi.decorateRequestBodyForAuth( + authProviderStr, + requestBody, + ); + } catch (e) { + setError(e.message); + return; + } } - // TODO: Add validation on contents/format of requestBody - kubernetesApi - .getObjectsByEntity(requestBody) - .then(result => { - setKubernetesObjects(result); - }) - .catch(e => { - setError(e.message); - }); + try { + setKubernetesObjects( + await kubernetesApi.getObjectsByEntity(requestBody), + ); + } catch (e) { + setError(e.message); + return; + } })(); /* eslint-disable react-hooks/exhaustive-deps */ }, [entity.metadata.name, kubernetesApi, kubernetesAuthProvidersApi]);