Merge pull request #13583 from taras/tm/add-verify-entity-to-catalog-client

Add validateEntity to catalog client
This commit is contained in:
Fredrik Adelöw
2022-09-08 19:21:36 +02:00
committed by GitHub
19 changed files with 184 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': minor
---
Adding `validateEntity` method that calls `/validate-entity` endpoint.
+21
View File
@@ -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<void>;
validateEntity(
entity: Entity,
location: string,
options?: CatalogRequestOptions,
): Promise<ValidateEntityResponse>;
}
// @public
@@ -122,6 +128,11 @@ export class CatalogClient implements CatalogApi {
id: string,
options?: CatalogRequestOptions,
): Promise<void>;
validateEntity(
entity: Entity,
location: string,
options?: CatalogRequestOptions,
): Promise<ValidateEntityResponse>;
}
// @public
@@ -196,4 +207,14 @@ type Location_2 = {
target: string;
};
export { Location_2 as Location };
// @public
export type ValidateEntityResponse =
| {
valid: true;
}
| {
valid: false;
errors: SerializedError[];
};
```
@@ -295,4 +295,87 @@ 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,
});
});
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/);
});
});
});
@@ -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,44 @@ export class CatalogClient implements CatalogApi {
);
}
/**
* {@inheritdoc CatalogApi.validateEntity}
*/
async validateEntity(
entity: Entity,
location: string,
options?: CatalogRequestOptions,
): Promise<ValidateEntityResponse> {
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,
};
}
if (response.status !== 400) {
throw await ResponseError.fromResponse(response);
}
const { errors = [] } = await response.json();
return {
valid: false,
errors,
};
}
//
// Private methods
//
+22
View File
@@ -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,15 @@ export type AddLocationResponse = {
exists?: boolean;
};
/**
* The response type for {@link CatalogClient.validateEntity}
*
* @public
*/
export type ValidateEntityResponse =
| { valid: true }
| { valid: false; errors: SerializedError[] };
/**
* A client for interacting with the Backstage software catalog through its API.
*
@@ -390,4 +400,16 @@ export interface CatalogApi {
id: string,
options?: CatalogRequestOptions,
): Promise<void>;
/**
* 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<ValidateEntityResponse>;
}
@@ -27,5 +27,6 @@ export type {
Location,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
ValidateEntityResponse,
} from './api';
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
@@ -35,6 +35,7 @@ describe('CatalogIdentityClient', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const tokenManager: jest.Mocked<TokenManager> = {
getToken: jest.fn(),
@@ -68,6 +68,7 @@ describe('createRouter', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
config = new ConfigReader({
@@ -59,6 +59,7 @@ describe('<CatalogGraphCard/>', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
apis = TestApiRegistry.from([catalogApiRef, catalog]);
@@ -92,6 +92,7 @@ describe('<CatalogGraphPage/>', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
wrapper = (
@@ -119,6 +119,7 @@ describe('<EntityRelationsGraph/>', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
Wrapper = ({ children }) => (
@@ -38,6 +38,7 @@ describe('useEntityStore', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
useApi.mockReturnValue(catalogApi);
@@ -100,6 +100,7 @@ describe('CatalogImportClient', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
let catalogImportClient: CatalogImportClient;
@@ -46,6 +46,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const errorApi: jest.Mocked<typeof errorApiRef.T> = {
@@ -32,6 +32,7 @@ describe('<DefaultExplorePage />', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
@@ -33,6 +33,7 @@ describe('<DomainExplorerContent />', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
@@ -33,6 +33,7 @@ describe('<GroupsExplorerContent />', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
@@ -37,6 +37,7 @@ describe('<FossaPage />', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const fossaApi: jest.Mocked<FossaApi> = {
getFindingSummary: jest.fn(),
@@ -53,6 +53,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked<CatalogApi> {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
if (entity) {
mock.getEntityByRef.mockReturnValue(entity);