diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 7bc7b8acfe..292c4c1b7a 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -42,6 +42,7 @@ import { decodeCursor, encodeCursor } from './util'; import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils'; import { Server } from 'http'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { LocationAnalyzer } from '../ingestion'; describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; @@ -49,6 +50,7 @@ describe('createRouter readonly disabled', () => { let orchestrator: jest.Mocked; let app: express.Express | Server; let refreshService: RefreshService; + let locationAnalyzer: jest.Mocked; beforeAll(async () => { entitiesCatalog = { @@ -66,6 +68,10 @@ describe('createRouter readonly disabled', () => { deleteLocation: jest.fn(), getLocationByEntity: jest.fn(), }; + + locationAnalyzer = { + analyzeLocation: jest.fn(), + }; refreshService = { refresh: jest.fn() }; orchestrator = { process: jest.fn() }; const router = await createRouter({ @@ -78,6 +84,7 @@ describe('createRouter readonly disabled', () => { permissionIntegrationRouter: express.Router(), auth: mockServices.auth(), httpAuth: mockServices.httpAuth(), + locationAnalyzer, }); app = wrapInOpenApiTestServer(express().use(router)); }); @@ -821,6 +828,21 @@ describe('createRouter readonly disabled', () => { }); }); }); + + describe('POST /analyze-location', () => { + it('handles invalid URLs', async () => { + const parseUrlError = new Error(); + (parseUrlError as any).subject_url = 'not a url'; + locationAnalyzer.analyzeLocation.mockRejectedValue(parseUrlError); + const response = await request(app) + .post('/analyze-location') + .send({ location: { type: 'url', target: 'not a url' } }); + expect(response.status).toEqual(400); + expect(response.body.error.message).toMatch( + /The given location.target is not a URL/, + ); + }); + }); }); describe('createRouter readonly enabled', () => { diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 834e0953bb..4bfed783a6 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -23,7 +23,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { NotFoundError, serializeError } from '@backstage/errors'; +import { InputError, NotFoundError, serializeError } from '@backstage/errors'; import express from 'express'; import { Logger } from 'winston'; import yn from 'yn'; @@ -299,8 +299,19 @@ export async function createRouter( catalogFilename: z.string().optional(), }); const parsedBody = schema.parse(body); - const output = await locationAnalyzer.analyzeLocation(parsedBody); - res.status(200).json(output); + try { + const output = await locationAnalyzer.analyzeLocation(parsedBody); + res.status(200).json(output); + } catch (err) { + if ( + // Catch errors from parse-url library. + err.name === 'Error' && + 'subject_url' in err + ) { + throw new InputError('The given location.target is not a URL'); + } + throw err; + } }); } diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 6ad53c9050..4b041d1a70 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -50,7 +50,7 @@ export async function requireRequestBody(req: Request): Promise { export const locationInput = z .object({ type: z.string(), - target: z.string().url(), + target: z.string(), presence: z.literal('required').or(z.literal('optional')).optional(), }) .strict(); // no unknown keys;