diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 06a5ba74e0..29c551db06 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -415,7 +415,7 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/validate-entity') - .send(entity); + .send({ entity, location: 'validate:entity' }); expect(response.status).toEqual(200); expect(orchestrator.process).toHaveBeenCalledTimes(1); @@ -436,7 +436,7 @@ describe('createRouter readonly disabled', () => { }); describe('invalid entity', () => { - it('returns 422', async () => { + it('returns 400', async () => { const entity: Entity = { apiVersion: 'a', kind: 'b', @@ -450,10 +450,10 @@ describe('createRouter readonly disabled', () => { const response = await request(app) .post('/validate-entity') - .send(entity); + .send({ entity, location: 'validate:entity' }); - expect(response.status).toEqual(422); - expect(response.body).toEqual(['Invalid entity name']); + expect(response.status).toEqual(400); + expect(response.body.error.message).toContain('Invalid entity name'); expect(orchestrator.process).toHaveBeenCalledTimes(1); expect(orchestrator.process).toHaveBeenCalledWith({ entity: { @@ -470,6 +470,48 @@ describe('createRouter readonly disabled', () => { }); }); }); + + describe('no location', () => { + it('returns 400', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { name: 'n' }, + }; + + const response = await request(app) + .post('/validate-entity') + .send({ entity, location: null }); + + expect(response.status).toEqual(400); + expect(response.body.error.message).toContain('Malformed request:'); + expect(orchestrator.process).toHaveBeenCalledTimes(0); + }); + }); + + describe('no entity', () => { + it('returns 400', async () => { + orchestrator.process.mockResolvedValueOnce({ + ok: false, + errors: [ + new Error( + 'Entity envelope failed validation before processing; caused by TypeError: must be object - type: object', + ), + ], + }); + + const response = await request(app) + .post('/validate-entity') + .send({ entity: null, location: 'validate:entity' }); + + expect(response.status).toEqual(400); + expect(response.body.error.message).toContain( + 'Entity envelope failed validation', + ); + // We expect this to be called because of an open issue with zod where unknown values are optional https://github.com/colinhacks/zod/issues/493 + expect(orchestrator.process).toHaveBeenCalledTimes(1); + }); + }); }); }); diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 221b5413c2..0bcdd6e6d6 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -17,7 +17,7 @@ import { errorHandler } from '@backstage/backend-common'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { NotFoundError } from '@backstage/errors'; +import { InputError, NotFoundError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; @@ -34,7 +34,6 @@ import { disallowReadonlyMode, locationInput, validateRequestBody, - requireRequestBody, setRequiredEntityLocationAnnotations, } from './util'; import { z } from 'zod'; @@ -234,15 +233,22 @@ export async function createRouter( if (orchestrator) { router.post('/validate-entity', async (req, res) => { - const input = (await requireRequestBody(req)) as Entity; - const entity = setRequiredEntityLocationAnnotations(input); + const bodySchema = z.object({ + entity: z.unknown(), + location: z.string(), + }); + + const body = await validateRequestBody(req, bodySchema); + + const entity = setRequiredEntityLocationAnnotations( + body.entity as Entity, + body.location, + ); const processingResult = await orchestrator.process({ entity, }); if (!processingResult.ok) - return res - .status(422) - .json(processingResult.errors.map(e => e.message)); + throw new InputError(processingResult.errors[0].message); return res.status(200).send(); }); } diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 15ffdaa013..7bf6cd8619 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -53,17 +53,20 @@ export const locationInput = z }) .strict(); // no unknown keys; -export function setRequiredEntityLocationAnnotations(entity: Entity): Entity { +export function setRequiredEntityLocationAnnotations( + entity: Entity, + location: string, +): Entity { const clonedEntity = lodash.cloneDeep(entity); lodash.set( clonedEntity, `metadata.annotations.['${ANNOTATION_SOURCE_LOCATION}']`, - 'validate:entity', + location, ); lodash.set( clonedEntity, `metadata.annotations.['${ANNOTATION_ORIGIN_LOCATION}']`, - 'validate:entity', + location, ); return clonedEntity;