From d207f94b6eb8726ea16240d7af59746e9f828e63 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 19 Mar 2021 11:42:24 +0100 Subject: [PATCH 1/2] badges: improve test coverage. Signed-off-by: Andreas Stenius --- plugins/badges-backend/src/badges.test.ts | 68 +++++++++++++++++++ plugins/badges-backend/src/badges.ts | 2 +- .../BadgeBuilder/DefaultBadgeBuilder.test.ts | 35 +++++++++- .../badges-backend/src/service/router.test.ts | 36 ++++++++++ plugins/badges-backend/src/service/router.ts | 4 +- 5 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 plugins/badges-backend/src/badges.test.ts diff --git a/plugins/badges-backend/src/badges.test.ts b/plugins/badges-backend/src/badges.test.ts new file mode 100644 index 0000000000..15be96154e --- /dev/null +++ b/plugins/badges-backend/src/badges.test.ts @@ -0,0 +1,68 @@ +/* + * 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 type { Entity } from '@backstage/catalog-model'; +import { InputError } from '@backstage/errors'; +import { Config, ConfigReader } from '@backstage/config'; +import { BadgeContext, BadgeFactories } from './types'; +import { createDefaultBadgeFactories } from './badges'; + +describe('BadgeFactories', () => { + let badgeFactories: BadgeFactories; + let config: Config; + + beforeAll(() => { + badgeFactories = createDefaultBadgeFactories(); + config = new ConfigReader({ + app: { + baseUrl: 'http://localhost', + }, + }); + }); + + it('throws when missing entity', () => { + const context: BadgeContext = { + badgeUrl: '/dummy/url', + config, + }; + expect.assertions(Object.keys(badgeFactories).length); + for (const badgeFactory of Object.values(badgeFactories)) { + expect(() => badgeFactory.createBadge(context)).toThrow(InputError); + } + }); + + it('returns valid badge for entity', () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'service', + metadata: { + name: 'test', + }, + }; + + const context: BadgeContext = { + badgeUrl: '/dummy/url', + config, + entity, + }; + + expect.assertions(Object.keys(badgeFactories).length); + for (const badgeFactory of Object.values(badgeFactories)) { + const badge = badgeFactory.createBadge(context); + expect(badge.kind).toEqual('entity'); + } + }); +}); diff --git a/plugins/badges-backend/src/badges.ts b/plugins/badges-backend/src/badges.ts index 2e2848a8af..f8637dfe88 100644 --- a/plugins/badges-backend/src/badges.ts +++ b/plugins/badges-backend/src/badges.ts @@ -19,7 +19,7 @@ import { InputError } from '@backstage/errors'; import { Badge, BadgeContext, BadgeFactories } from './types'; function appTitle(context: BadgeContext): string { - return context.config.getString('app.title') || 'Backstage'; + return context.config.getOptionalString('app.title') || 'Backstage'; } function entityUrl(context: BadgeContext): string { diff --git a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts index a36587f241..9eceb49e50 100644 --- a/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts +++ b/plugins/badges-backend/src/lib/BadgeBuilder/DefaultBadgeBuilder.test.ts @@ -15,9 +15,10 @@ */ import { Config, ConfigReader } from '@backstage/config'; +import { InputError } from '@backstage/errors'; import { DefaultBadgeBuilder } from './DefaultBadgeBuilder'; import { BadgeBuilder, BadgeOptions } from './types'; -import { BadgeContext, BadgeFactories } from '../../types'; +import { Badge, BadgeContext, BadgeFactories, BadgeStyle } from '../../types'; describe('DefaultBadgeBuilder', () => { let builder: BadgeBuilder; @@ -40,6 +41,12 @@ describe('DefaultBadgeBuilder', () => { testbadge: { createBadge: () => badge, }, + failbadge: { + createBadge: () => (undefined as unknown) as Badge, // force a bad return value.. + }, + invalidbadge: { + createBadge: () => ({ style: 'wrong' as BadgeStyle, ...badge }), + }, }; }); @@ -49,7 +56,11 @@ describe('DefaultBadgeBuilder', () => { }); it('getBadges() returns all badge factory ids', async () => { - expect(await builder.getBadges()).toEqual([{ id: 'testbadge' }]); + expect(await builder.getBadges()).toEqual([ + { id: 'testbadge' }, + { id: 'failbadge' }, + { id: 'invalidbadge' }, + ]); }); describe('createBadge[Json|Svg]', () => { @@ -101,5 +112,25 @@ describe('DefaultBadgeBuilder', () => { markdown: `![unknown badge: other-id](${context.badgeUrl})`, }); }); + + it('throws InputError when the BadgeFactory.createBadge() fails', async () => { + expect.assertions(1); + await expect( + builder.createBadgeJson({ badgeInfo: { id: 'failbadge' }, context }), + ).rejects.toEqual( + new InputError( + 'The badge factory failed to produce a "failbadge" badge with the provided context', + ), + ); + }); + + it('returns "invalid badge" for bad parameters', async () => { + expect( + await builder.createBadgeSvg({ + badgeInfo: { id: 'invalidbadge' }, + context, + }), + ).toEqual(expect.stringMatching(/Error: Field `style` must be one of/)); + }); }); }); diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index e8de5dfd42..84fb8deef7 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -167,5 +167,41 @@ describe('createRouter', () => { }, }); }); + + it('returns badge spec for entity', async () => { + catalog.getEntityByName.mockResolvedValueOnce(entity); + badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge); + + const url = '/entity/default/service/test/badge/test-badge?format=json'; + const response = await request(app).get(url); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(badge); + }); + }); + + describe('Errors', () => { + it('returns 404 for unknown entities', async () => { + catalog.getEntityByName.mockResolvedValue(undefined); + async function testUrl(url: string) { + const response = await request(app).get(url); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ + error: { + message: 'No service entity in default named "missing"', + name: 'NotFoundError', + }, + request: { + method: 'GET', + url, + }, + response: { + statusCode: 404, + }, + }); + } + await testUrl('/entity/default/service/missing/badge-specs'); + await testUrl('/entity/default/service/missing/badge/test-badge'); + }); }); }); diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts index bfbb10fc94..49b0e36944 100644 --- a/plugins/badges-backend/src/service/router.ts +++ b/plugins/badges-backend/src/service/router.ts @@ -68,9 +68,7 @@ export async function createRouter( }; const badge = await badgeBuilder.createBadgeJson({ badgeInfo, context }); - if (badge) { - specs.push(badge); - } + specs.push(badge); } res.setHeader('Content-Type', 'application/json'); From 2be7cf060d62b530303fc0f879caad0bccc27ea6 Mon Sep 17 00:00:00 2001 From: Andreas Stenius Date: Fri, 19 Mar 2021 16:32:53 +0100 Subject: [PATCH 2/2] badges-backend: fix minor URL typo in readme. Signed-off-by: Andreas Stenius --- plugins/badges-backend/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md index e64360656d..73f72d32ac 100644 --- a/plugins/badges-backend/README.md +++ b/plugins/badges-backend/README.md @@ -88,9 +88,9 @@ The badges backend api exposes two main endpoints for entity badges. The [BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts) from the frontend plugin for a type declaration. -- `/badges/entity/:namespace/:kind/:name/:badgeId` Get the entity badge as an - SVG image. If the `accept` request header prefers `application/json` the badge - spec as JSON will be returned instead of the image. +- `/badges/entity/:namespace/:kind/:name/badge/:badgeId` Get the entity badge as + an SVG image. If the `accept` request header prefers `application/json` the + badge spec as JSON will be returned instead of the image. ## Links