From 54f14d5b88eee65693896124e1013776e377781b Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 7 Sep 2022 17:32:06 -0400 Subject: [PATCH 1/7] Added /validate-entity to the catalog client Signed-off-by: Taras --- .../catalog-client/src/CatalogClient.test.ts | 63 +++++++++++++++++++ packages/catalog-client/src/CatalogClient.ts | 38 ++++++++++- packages/catalog-client/src/types/api.ts | 23 +++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 0b838eb4c2..3fc70e25ae 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -295,4 +295,67 @@ describe('CatalogClient', () => { await client.getLocationById('42'); }); }); + + describe('validateEntity', () => { + it('returns valid false when validation fails', async () => { + server.use( + rest.post(`${mockBaseUrl}/validate-entity`, (req, res, ctx) => { + return res( + ctx.status(400), + ctx.json({ + errors: [ + { + message: 'Missing name', + }, + ], + }), + ); + }), + ); + + expect( + await client.validateEntity( + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: '', + }, + }, + 'http://example.com', + ), + ).toMatchObject({ + valid: false, + errors: [ + { + message: 'Missing name', + }, + ], + }); + }); + + it('returns valid true when validation fails', async () => { + server.use( + rest.post(`${mockBaseUrl}/validate-entity`, (req, res, ctx) => { + return res(ctx.status(200), ctx.text('')); + }), + ); + + expect( + await client.validateEntity( + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'good', + }, + }, + 'http://example.com', + ), + ).toMatchObject({ + valid: true, + errors: undefined, + }); + }); + }); }); diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 8ddc3eb36d..bbc9b0b62d 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -21,7 +21,7 @@ import { stringifyEntityRef, stringifyLocationRef, } from '@backstage/catalog-model'; -import { ResponseError } from '@backstage/errors'; +import { ResponseError, SerializedError } from '@backstage/errors'; import crossFetch from 'cross-fetch'; import { CATALOG_FILTER_EXISTS, @@ -36,6 +36,7 @@ import { Location, GetEntityFacetsRequest, GetEntityFacetsResponse, + ValidateEntityResponse, } from './types/api'; import { DiscoveryApi } from './types/discovery'; import { FetchApi } from './types/fetch'; @@ -353,6 +354,41 @@ export class CatalogClient implements CatalogApi { ); } + /** + * {@inheritdoc CatalogApi.validateEntity} + */ + async validateEntity( + entity: Entity, + location: string, + options?: CatalogRequestOptions, + ): Promise { + const response = await this.fetchApi.fetch( + `${await this.discoveryApi.getBaseUrl('catalog')}/validate-entity`, + { + headers: { + 'Content-Type': 'application/json', + ...(options?.token && { Authorization: `Bearer ${options?.token}` }), + }, + method: 'POST', + body: JSON.stringify({ entity, location }), + }, + ); + + if (response.ok) { + return { + valid: true, + errors: undefined, + }; + } + + const { errors } = (await response.json()) as { errors: SerializedError[] }; + + return { + valid: false, + errors, + }; + } + // // Private methods // diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 389b82c081..d66e2dfaf9 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -15,6 +15,7 @@ */ import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { SerializedError } from '@backstage/errors'; /** * This symbol can be used in place of a value when passed to filters in e.g. @@ -269,6 +270,16 @@ export type AddLocationResponse = { exists?: boolean; }; +/** + * The response type for {@link CatalogClient.validateEntity} + * + * @public + */ +export type ValidateEntityResponse = { + valid: boolean; + errors?: SerializedError[]; +}; + /** * A client for interacting with the Backstage software catalog through its API. * @@ -390,4 +401,16 @@ export interface CatalogApi { id: string, options?: CatalogRequestOptions, ): Promise; + + /** + * Validate entity and its location. + * + * @param entity - Entity to validate + * @param location - URL location of the entity + */ + validateEntity( + entity: Entity, + location: string, + options?: CatalogRequestOptions, + ): Promise; } From 93a1072a0a6d51815bbca6fa84e055ee157fd885 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 7 Sep 2022 18:12:01 -0400 Subject: [PATCH 2/7] Updated tests to include validateEntity in mocks Signed-off-by: Taras --- packages/catalog-client/src/CatalogClient.test.ts | 4 ++-- .../src/lib/catalog/CatalogIdentityClient.test.ts | 1 + plugins/badges-backend/src/service/router.test.ts | 1 + .../src/components/CatalogGraphCard/CatalogGraphCard.test.tsx | 1 + .../src/components/CatalogGraphPage/CatalogGraphPage.test.tsx | 1 + .../EntityRelationsGraph/EntityRelationsGraph.test.tsx | 1 + .../components/EntityRelationsGraph/useEntityStore.test.ts | 1 + plugins/catalog-import/src/api/CatalogImportClient.test.ts | 1 + .../StepPrepareCreatePullRequest.test.tsx | 1 + .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 1 + .../DomainExplorerContent/DomainExplorerContent.test.tsx | 1 + .../GroupsExplorerContent/GroupsExplorerContent.test.tsx | 1 + plugins/fossa/src/components/FossaPage/FossaPage.test.tsx | 1 + plugins/todo-backend/src/service/TodoReaderService.test.ts | 1 + 14 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 3fc70e25ae..9427d78f5c 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -299,7 +299,7 @@ describe('CatalogClient', () => { describe('validateEntity', () => { it('returns valid false when validation fails', async () => { server.use( - rest.post(`${mockBaseUrl}/validate-entity`, (req, res, ctx) => { + rest.post(`${mockBaseUrl}/validate-entity`, (_req, res, ctx) => { return res( ctx.status(400), ctx.json({ @@ -336,7 +336,7 @@ describe('CatalogClient', () => { it('returns valid true when validation fails', async () => { server.use( - rest.post(`${mockBaseUrl}/validate-entity`, (req, res, ctx) => { + rest.post(`${mockBaseUrl}/validate-entity`, (_req, res, ctx) => { return res(ctx.status(200), ctx.text('')); }), ); diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 587b433ca1..e4d9304892 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -35,6 +35,7 @@ describe('CatalogIdentityClient', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; const tokenManager: jest.Mocked = { getToken: jest.fn(), diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 1750a55be7..5fc809906f 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -68,6 +68,7 @@ describe('createRouter', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; config = new ConfigReader({ diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 9b354b5f38..74fcd4e501 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -59,6 +59,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; apis = TestApiRegistry.from([catalogApiRef, catalog]); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index d8b7ede2c2..9cca6ef9be 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -92,6 +92,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; wrapper = ( diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index db6711a0a5..c79f975789 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -119,6 +119,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; Wrapper = ({ children }) => ( diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index 74d7423eea..e76b1001c6 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -38,6 +38,7 @@ describe('useEntityStore', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; useApi.mockReturnValue(catalogApi); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index e26d6c5d6a..30b9f47d99 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -100,6 +100,7 @@ describe('CatalogImportClient', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; let catalogImportClient: CatalogImportClient; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index f3dc1f2144..5ad7e27c4b 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -46,6 +46,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; const errorApi: jest.Mocked = { diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index a79d02a2a0..399112fac6 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -32,6 +32,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index c30e845112..c84363f5c6 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -33,6 +33,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 4488a4d0d9..a4769e8f4f 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -33,6 +33,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 7ef6c799df..676a3cff3c 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -37,6 +37,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; const fossaApi: jest.Mocked = { getFindingSummary: jest.fn(), diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index 6dd14afcb4..8077e2ddb9 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -53,6 +53,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), getEntityFacets: jest.fn(), + validateEntity: jest.fn(), }; if (entity) { mock.getEntityByRef.mockReturnValue(entity); From 184a610d12de1bbabef326d2443c649c2e1cf105 Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 7 Sep 2022 18:20:10 -0400 Subject: [PATCH 3/7] Add missing export and generate api report Signed-off-by: Taras --- packages/catalog-client/api-report.md | 17 +++++++++++++++++ packages/catalog-client/src/types/index.ts | 1 + 2 files changed, 18 insertions(+) diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 964a3c1018..1d66b469a9 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -5,6 +5,7 @@ ```ts import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; +import { SerializedError } from '@backstage/errors'; // @public export type AddLocationRequest = { @@ -65,6 +66,11 @@ export interface CatalogApi { id: string, options?: CatalogRequestOptions, ): Promise; + validateEntity( + entity: Entity, + location: string, + options?: CatalogRequestOptions, + ): Promise; } // @public @@ -122,6 +128,11 @@ export class CatalogClient implements CatalogApi { id: string, options?: CatalogRequestOptions, ): Promise; + validateEntity( + entity: Entity, + location: string, + options?: CatalogRequestOptions, + ): Promise; } // @public @@ -196,4 +207,10 @@ type Location_2 = { target: string; }; export { Location_2 as Location }; + +// @public +export type ValidateEntityResponse = { + valid: boolean; + errors?: SerializedError[]; +}; ``` diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 8bf4e34da2..86f7b18ab0 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -27,5 +27,6 @@ export type { Location, GetEntityFacetsRequest, GetEntityFacetsResponse, + ValidateEntityResponse, } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; From 65d1d4343ff6cf56b8cf28879d6d2891400b572b Mon Sep 17 00:00:00 2001 From: Taras Date: Wed, 7 Sep 2022 18:22:07 -0400 Subject: [PATCH 4/7] Added changeset Signed-off-by: Taras --- .changeset/large-books-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/large-books-deny.md diff --git a/.changeset/large-books-deny.md b/.changeset/large-books-deny.md new file mode 100644 index 0000000000..51d2ce38c0 --- /dev/null +++ b/.changeset/large-books-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +Adding validateEntity method that calls /validate-entity endpoint From 2a46e3701e291ac499997ecae9bc2d6fbf81290d Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 8 Sep 2022 08:42:07 -0400 Subject: [PATCH 5/7] Integrated feedback from @freben Signed-off-by: Taras --- .changeset/large-books-deny.md | 2 +- packages/catalog-client/api-report.md | 12 ++++++++---- packages/catalog-client/src/CatalogClient.ts | 9 ++++++--- packages/catalog-client/src/types/api.ts | 7 +++---- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.changeset/large-books-deny.md b/.changeset/large-books-deny.md index 51d2ce38c0..c88d391639 100644 --- a/.changeset/large-books-deny.md +++ b/.changeset/large-books-deny.md @@ -2,4 +2,4 @@ '@backstage/catalog-client': minor --- -Adding validateEntity method that calls /validate-entity endpoint +Adding `validateEntity` method that calls `/validate-entity` endpoint. diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 1d66b469a9..0ad77ce39c 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -209,8 +209,12 @@ type Location_2 = { export { Location_2 as Location }; // @public -export type ValidateEntityResponse = { - valid: boolean; - errors?: SerializedError[]; -}; +export type ValidateEntityResponse = + | { + valid: true; + } + | { + valid: false; + errors: SerializedError[]; + }; ``` diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index bbc9b0b62d..f2b4186bfb 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -21,7 +21,7 @@ import { stringifyEntityRef, stringifyLocationRef, } from '@backstage/catalog-model'; -import { ResponseError, SerializedError } from '@backstage/errors'; +import { ResponseError } from '@backstage/errors'; import crossFetch from 'cross-fetch'; import { CATALOG_FILTER_EXISTS, @@ -377,11 +377,14 @@ export class CatalogClient implements CatalogApi { if (response.ok) { return { valid: true, - errors: undefined, }; } - const { errors } = (await response.json()) as { errors: SerializedError[] }; + if (response.status !== 400) { + throw await ResponseError.fromResponse(response); + } + + const { errors = [] } = await response.json(); return { valid: false, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index d66e2dfaf9..f483c3454c 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -275,10 +275,9 @@ export type AddLocationResponse = { * * @public */ -export type ValidateEntityResponse = { - valid: boolean; - errors?: SerializedError[]; -}; +export type ValidateEntityResponse = + | { valid: true } + | { valid: false; errors: SerializedError[] }; /** * A client for interacting with the Backstage software catalog through its API. From afcafda62064363d13897f9839009de065df1df8 Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 8 Sep 2022 11:20:43 -0400 Subject: [PATCH 6/7] Fix the test Signed-off-by: Taras --- packages/catalog-client/src/CatalogClient.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 9427d78f5c..263bb8971d 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -354,7 +354,6 @@ describe('CatalogClient', () => { ), ).toMatchObject({ valid: true, - errors: undefined, }); }); }); From c5cf37026ec31e83364e3c655e2594588892733e Mon Sep 17 00:00:00 2001 From: Taras Date: Thu, 8 Sep 2022 11:55:08 -0400 Subject: [PATCH 7/7] Add test for unexpected error Signed-off-by: Taras --- .../catalog-client/src/CatalogClient.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 263bb8971d..6c9c68efa2 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -356,5 +356,26 @@ describe('CatalogClient', () => { valid: true, }); }); + + it('throws unexpected error', async () => { + server.use( + rest.post(`${mockBaseUrl}/validate-entity`, (_req, res, ctx) => { + return res(ctx.status(500), ctx.json({})); + }), + ); + + await expect(() => + client.validateEntity( + { + apiVersion: '1', + kind: 'Component', + metadata: { + name: 'good', + }, + }, + 'http://example.com', + ), + ).rejects.toThrow(/Request failed with 500 Error/); + }); }); });