diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index eec60f7f4f..ca59338c16 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -23,12 +23,12 @@ import { SchemaValidEntityPolicy, } from './entity'; import { - ApiEntityV1alpha1Policy, - ComponentEntityV1alpha1Policy, - GroupEntityV1alpha1Policy, - LocationEntityV1alpha1Policy, - TemplateEntityV1alpha1Policy, - UserEntityV1alpha1Policy, + apiEntityV1alpha1Policy, + componentEntityV1alpha1Policy, + groupEntityV1alpha1Policy, + locationEntityV1alpha1Policy, + templateEntityV1alpha1Policy, + userEntityV1alpha1Policy, } from './kinds'; import { EntityPolicy } from './types'; @@ -78,12 +78,12 @@ export class EntityPolicies implements EntityPolicy { new ReservedFieldsEntityPolicy(), ]), EntityPolicies.anyOf([ - new ComponentEntityV1alpha1Policy(), - new GroupEntityV1alpha1Policy(), - new UserEntityV1alpha1Policy(), - new LocationEntityV1alpha1Policy(), - new TemplateEntityV1alpha1Policy(), - new ApiEntityV1alpha1Policy(), + componentEntityV1alpha1Policy, + groupEntityV1alpha1Policy, + userEntityV1alpha1Policy, + locationEntityV1alpha1Policy, + templateEntityV1alpha1Policy, + apiEntityV1alpha1Policy, ]), ]); } diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts index 51b87734f9..142a8d8160 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.test.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { EntityPolicy } from '../types'; import { ApiEntityV1alpha1, - ApiEntityV1alpha1Policy, + apiEntityV1alpha1Policy as policy, } from './ApiEntityV1alpha1'; describe('ApiV1alpha1Policy', () => { let entity: ApiEntityV1alpha1; - let policy: EntityPolicy; beforeEach(() => { entity = { @@ -74,7 +72,6 @@ components: `, }, }; - policy = new ApiEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 2936b98531..052172d0a1 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -16,11 +16,24 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import type { EntityPolicy } from '../types'; +import { schemaPolicy } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'API' as const; +const schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + lifecycle: yup.string().required().min(1), + owner: yup.string().required().min(1), + definition: yup.string().required().min(1), + }) + .required(), +}); + export interface ApiEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -32,31 +45,4 @@ export interface ApiEntityV1alpha1 extends Entity { }; } -export class ApiEntityV1alpha1Policy implements EntityPolicy { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - lifecycle: yup.string().required().min(1), - owner: yup.string().required().min(1), - definition: yup.string().required().min(1), - }) - .required(), - }); - } - - async enforce(envelope: Entity): Promise { - if ( - KIND !== envelope.kind || - !API_VERSION.includes(envelope.apiVersion as any) - ) { - return undefined; - } - return await this.schema.validate(envelope, { strict: true }); - } -} +export const apiEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts index a5651b6501..57d6d47388 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.test.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { EntityPolicy } from '../types'; import { ComponentEntityV1alpha1, - ComponentEntityV1alpha1Policy, + componentEntityV1alpha1Policy as policy, } from './ComponentEntityV1alpha1'; describe('ComponentV1alpha1Policy', () => { let entity: ComponentEntityV1alpha1; - let policy: EntityPolicy; beforeEach(() => { entity = { @@ -38,7 +36,6 @@ describe('ComponentV1alpha1Policy', () => { implementsApis: ['api-0'], }, }; - policy = new ComponentEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index 3bd6b8250b..9efc491ca9 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -16,11 +16,24 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import type { EntityPolicy } from '../types'; +import { schemaPolicy } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Component' as const; +const schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + lifecycle: yup.string().required().min(1), + owner: yup.string().required().min(1), + implementsApis: yup.array(yup.string()).notRequired(), + }) + .required(), +}); + export interface ComponentEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -32,31 +45,8 @@ export interface ComponentEntityV1alpha1 extends Entity { }; } -export class ComponentEntityV1alpha1Policy implements EntityPolicy { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - lifecycle: yup.string().required().min(1), - owner: yup.string().required().min(1), - implementsApis: yup.array(yup.string()).notRequired(), - }) - .required(), - }); - } - - async enforce(envelope: Entity): Promise { - if ( - KIND !== envelope.kind || - !API_VERSION.includes(envelope.apiVersion as any) - ) { - return undefined; - } - return await this.schema.validate(envelope, { strict: true }); - } -} +export const componentEntityV1alpha1Policy = schemaPolicy( + KIND, + API_VERSION, + schema, +); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts index dcf62e07f5..dc0ecd3fa3 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.test.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { EntityPolicy } from '../types'; import { GroupEntityV1alpha1, - GroupEntityV1alpha1Policy, + groupEntityV1alpha1Policy as policy, } from './GroupEntityV1alpha1'; describe('GroupV1alpha1Policy', () => { let entity: GroupEntityV1alpha1; - let policy: EntityPolicy; beforeEach(() => { entity = { @@ -41,7 +39,6 @@ describe('GroupV1alpha1Policy', () => { descendants: ['desc-a', 'desc-b'], }, }; - policy = new GroupEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 9733a81a66..073332d707 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -16,11 +16,39 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import type { EntityPolicy } from '../types'; +import { schemaPolicy } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Group' as const; +const schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + parent: yup.string().notRequired().min(1), + // Use these manual tests because yup .required() requires at least + // one element and there is no simple workaround -_- + ancestors: yup.array(yup.string()).test({ + name: 'isDefined', + message: 'ancestors must be defined', + test: v => Boolean(v), + }), + children: yup.array(yup.string()).test({ + name: 'isDefined', + message: 'children must be defined', + test: v => Boolean(v), + }), + descendants: yup.array(yup.string()).test({ + name: 'isDefined', + message: 'descendants must be defined', + test: v => Boolean(v), + }), + }) + .required(), +}); + export interface GroupEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -33,46 +61,8 @@ export interface GroupEntityV1alpha1 extends Entity { }; } -export class GroupEntityV1alpha1Policy implements EntityPolicy { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - parent: yup.string().notRequired().min(1), - // Use these manual tests because yup .required() requires at least - // one element and there is no simple workaround -_- - ancestors: yup.array(yup.string()).test({ - name: 'isDefined', - message: 'ancestors must be defined', - test: v => Boolean(v), - }), - children: yup.array(yup.string()).test({ - name: 'isDefined', - message: 'children must be defined', - test: v => Boolean(v), - }), - descendants: yup.array(yup.string()).test({ - name: 'isDefined', - message: 'descendants must be defined', - test: v => Boolean(v), - }), - }) - .required(), - }); - } - - async enforce(envelope: Entity): Promise { - if ( - KIND !== envelope.kind || - !API_VERSION.includes(envelope.apiVersion as any) - ) { - return undefined; - } - return await this.schema.validate(envelope, { strict: true }); - } -} +export const groupEntityV1alpha1Policy = schemaPolicy( + KIND, + API_VERSION, + schema, +); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts index aed22aca05..a259eaeca8 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { EntityPolicy } from '../types'; import { LocationEntityV1alpha1, - LocationEntityV1alpha1Policy, + locationEntityV1alpha1Policy as policy, } from './LocationEntityV1alpha1'; describe('LocationV1alpha1Policy', () => { let entity: LocationEntityV1alpha1; - let policy: EntityPolicy; beforeEach(() => { entity = { @@ -35,7 +33,6 @@ describe('LocationV1alpha1Policy', () => { type: 'github', }, }; - policy = new LocationEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index f53fa9624c..6d1f1f5515 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -16,11 +16,23 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import type { EntityPolicy } from '../types'; +import { schemaPolicy } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Location' as const; +const schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + target: yup.string().notRequired().min(1), + targets: yup.array(yup.string()).notRequired(), + }) + .required(), +}); + export interface LocationEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -31,30 +43,8 @@ export interface LocationEntityV1alpha1 extends Entity { }; } -export class LocationEntityV1alpha1Policy implements EntityPolicy { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - target: yup.string().notRequired().min(1), - targets: yup.array(yup.string()).notRequired(), - }) - .required(), - }); - } - - async enforce(envelope: Entity): Promise { - if ( - KIND !== envelope.kind || - !API_VERSION.includes(envelope.apiVersion as any) - ) { - return undefined; - } - return await this.schema.validate(envelope, { strict: true }); - } -} +export const locationEntityV1alpha1Policy = schemaPolicy( + KIND, + API_VERSION, + schema, +); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts index cd3a4cfb71..86351b41aa 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.test.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { EntityPolicy } from '../types'; import { TemplateEntityV1alpha1, - TemplateEntityV1alpha1Policy, + templateEntityV1alpha1Policy as policy, } from './TemplateEntityV1alpha1'; -describe('TemplateEntityV1alpah1', () => { +describe('templateEntityV1alpha1', () => { let entity: TemplateEntityV1alpha1; - let policy: EntityPolicy; beforeEach(() => { entity = { @@ -52,7 +50,6 @@ describe('TemplateEntityV1alpah1', () => { }, }, }; - policy = new TemplateEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts index 1986638051..a26540fda2 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts @@ -16,11 +16,25 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import type { EntityPolicy, JSONSchema } from '../types'; +import type { JSONSchema } from '../types'; +import { schemaPolicy } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Template' as const; +const schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + type: yup.string().required().min(1), + path: yup.string(), + schema: yup.object().required(), + templater: yup.string().required(), + }) + .required(), +}); + export interface TemplateEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -32,31 +46,8 @@ export interface TemplateEntityV1alpha1 extends Entity { }; } -export class TemplateEntityV1alpha1Policy implements EntityPolicy { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - path: yup.string(), - schema: yup.object().required(), - templater: yup.string().required(), - }) - .required(), - }); - } - - async enforce(envelope: Entity): Promise { - if ( - KIND !== envelope.kind || - !API_VERSION.includes(envelope.apiVersion as any) - ) { - return undefined; - } - return await this.schema.validate(envelope, { strict: true }); - } -} +export const templateEntityV1alpha1Policy = schemaPolicy( + KIND, + API_VERSION, + schema, +); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts index 62f529ff7c..97fe0888f1 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { EntityPolicy } from '../types'; import { UserEntityV1alpha1, - UserEntityV1alpha1Policy, + userEntityV1alpha1Policy as policy, } from './UserEntityV1alpha1'; -describe('UserV1alpha1Policy', () => { +describe('userEntityV1alpha1Policy', () => { let entity: UserEntityV1alpha1; - let policy: EntityPolicy; beforeEach(() => { entity = { @@ -40,7 +38,6 @@ describe('UserV1alpha1Policy', () => { memberOf: ['team-a', 'developers'], }, }; - policy = new UserEntityV1alpha1Policy(); }); it('happy path: accepts valid data', async () => { diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index bfab45f202..ff4edcc8a2 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -16,11 +16,34 @@ import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import type { EntityPolicy } from '../types'; +import { schemaPolicy } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'User' as const; +const schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + profile: yup + .object({ + displayName: yup.string().min(1).notRequired(), + email: yup.string().min(1).notRequired(), + picture: yup.string().min(1).notRequired(), + }) + .notRequired(), + // Use this manual test because yup .required() requires at least one + // element and there is no simple workaround -_- + memberOf: yup.array(yup.string()).test({ + name: 'isDefined', + message: 'memberOf must be defined', + test: v => Boolean(v), + }), + }) + .required(), +}); + export interface UserEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -34,41 +57,4 @@ export interface UserEntityV1alpha1 extends Entity { }; } -export class UserEntityV1alpha1Policy implements EntityPolicy { - private schema: yup.Schema; - - constructor() { - this.schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - profile: yup - .object({ - displayName: yup.string().min(1).notRequired(), - email: yup.string().min(1).notRequired(), - picture: yup.string().min(1).notRequired(), - }) - .notRequired(), - // Use this manual test because yup .required() requires at least one - // element and there is no simple workaround -_- - memberOf: yup.array(yup.string()).test({ - name: 'isDefined', - message: 'memberOf must be defined', - test: v => Boolean(v), - }), - }) - .required(), - }); - } - - async enforce(envelope: Entity): Promise { - if ( - KIND !== envelope.kind || - !API_VERSION.includes(envelope.apiVersion as any) - ) { - return undefined; - } - return await this.schema.validate(envelope, { strict: true }); - } -} +export const userEntityV1alpha1Policy = schemaPolicy(KIND, API_VERSION, schema); diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 1ac93b426d..6df4f1212a 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -14,32 +14,32 @@ * limitations under the License. */ -export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1'; +export { apiEntityV1alpha1Policy } from './ApiEntityV1alpha1'; export type { ApiEntityV1alpha1 as ApiEntity, ApiEntityV1alpha1, } from './ApiEntityV1alpha1'; -export { ComponentEntityV1alpha1Policy } from './ComponentEntityV1alpha1'; +export { componentEntityV1alpha1Policy } from './ComponentEntityV1alpha1'; export type { ComponentEntityV1alpha1 as ComponentEntity, ComponentEntityV1alpha1, } from './ComponentEntityV1alpha1'; -export { GroupEntityV1alpha1Policy } from './GroupEntityV1alpha1'; +export { groupEntityV1alpha1Policy } from './GroupEntityV1alpha1'; export type { GroupEntityV1alpha1 as GroupEntity, GroupEntityV1alpha1, } from './GroupEntityV1alpha1'; -export { LocationEntityV1alpha1Policy } from './LocationEntityV1alpha1'; +export { locationEntityV1alpha1Policy } from './LocationEntityV1alpha1'; export type { LocationEntityV1alpha1 as LocationEntity, LocationEntityV1alpha1, } from './LocationEntityV1alpha1'; -export { TemplateEntityV1alpha1Policy } from './TemplateEntityV1alpha1'; +export { templateEntityV1alpha1Policy } from './TemplateEntityV1alpha1'; export type { TemplateEntityV1alpha1 as TemplateEntity, TemplateEntityV1alpha1, } from './TemplateEntityV1alpha1'; -export { UserEntityV1alpha1Policy } from './UserEntityV1alpha1'; +export { userEntityV1alpha1Policy } from './UserEntityV1alpha1'; export type { UserEntityV1alpha1 as UserEntity, UserEntityV1alpha1, diff --git a/packages/catalog-model/src/kinds/util.ts b/packages/catalog-model/src/kinds/util.ts new file mode 100644 index 0000000000..491b2c5e2f --- /dev/null +++ b/packages/catalog-model/src/kinds/util.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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 * as yup from 'yup'; +import { Entity } from '../entity'; +import { EntityPolicy } from '../types'; + +export function schemaPolicy( + kind: string, + apiVersion: readonly string[], + schema: yup.Schema, +): EntityPolicy { + return { + async enforce(envelope: Entity): Promise { + if ( + kind !== envelope.kind || + !apiVersion.includes(envelope.apiVersion as any) + ) { + return undefined; + } + return await schema.validate(envelope, { strict: true }); + }, + }; +} diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 1069790218..fdb39444d8 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -10,7 +10,7 @@ import { setupServer } from 'msw/node'; describe('ExampleComponent', () => { const server = setupServer(); // Enable API mocking before tests. - beforeAll(() => server.listen()) + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) // Reset any runtime request handlers we may add during the tests. afterEach(() => server.resetHandlers()) @@ -32,4 +32,3 @@ describe('ExampleComponent', () => { expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); }); }); - \ No newline at end of file diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index c584289077..ca1990b4bc 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -7,7 +7,7 @@ import { setupServer } from 'msw/node'; describe('ExampleFetchComponent', () => { const server = setupServer(); // Enable API mocking before tests. - beforeAll(() => server.listen()) + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) // Reset any runtime request handlers we may add during the tests. afterEach(() => server.resetHandlers()) diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index f612094a2d..8219c8e513 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -179,12 +179,14 @@ export class LocationReaders implements LocationReader { } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while reading location ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); + this.logger.warn(message); } } } const message = `No processor was able to read location ${item.location.type} ${item.location.target}`; emit(result.inputError(item.location, message)); + this.logger.warn(message); } private async handleData( @@ -204,6 +206,7 @@ export class LocationReaders implements LocationReader { } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while parsing ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); + this.logger.warn(message); } } } @@ -232,13 +235,13 @@ export class LocationReaders implements LocationReader { this.readLocation.bind(this), ); } catch (e) { - const message = `Processor ${ - processor.constructor.name - } threw an error while processing entity ${current.kind}:${ - current.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE - }/${current.metadata.name} at ${item.location.type} ${ - item.location.target - }, ${e}`; + // Construct the name carefully, if we got validation errors we do + // not want to crash here due to missing metadata or so + const namespace = !current.metadata + ? '' + : current.metadata.namespace ?? ENTITY_DEFAULT_NAMESPACE; + const name = !current.metadata ? '' : current.metadata.name; + const message = `Processor ${processor.constructor.name} threw an error while processing entity ${current.kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); this.logger.warn(message); } @@ -263,6 +266,7 @@ export class LocationReaders implements LocationReader { } catch (e) { const message = `Processor ${processor.constructor.name} threw an error while handling another error at ${item.location.type} ${item.location.target}, ${e}`; emit(result.generalError(item.location, message)); + this.logger.warn(message); } } } diff --git a/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts index 0a6f2a11c0..67da9a6705 100644 --- a/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/EntityPolicyProcessor.ts @@ -27,7 +27,7 @@ export class EntityPolicyProcessor implements LocationProcessor { async processEntity(entity: Entity): Promise { const output = await this.policy.enforce(entity); if (!output) { - throw new Error(`No policy applied to entity`); + throw new Error(`Entity did not match any known schema`); } return output; } diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index 324fc7f627..5084d4c16f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -152,19 +152,9 @@ export function parseUrl(urlString: string): { org: string } { const path = new URL(urlString).pathname.substr(1).split('/'); // /spotify - if (path.length === 1) { - return { org: path[0] }; + if (path.length === 1 && path[0].length) { + return { org: decodeURIComponent(path[0]) }; } - // /orgs/spotify[/] - if ( - path.length >= 2 && - path.length <= 3 && - path[0] === 'orgs' && - [undefined, 'teams', 'people'].includes(path[2]) - ) { - return { org: path[1] }; - } - - throw new Error(`Expected a URL pointing to / or /orgs/`); + throw new Error(`Expected a URL pointing to /`); } diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index e30b320bf4..1ab4cb09b8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -27,7 +27,7 @@ describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost:23000'; const server = setupServer(); - beforeAll(() => server.listen()); + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); diff --git a/plugins/catalog-backend/src/ingestion/processors/util/github.test.ts b/plugins/catalog-backend/src/ingestion/processors/util/github.test.ts new file mode 100644 index 0000000000..25a37e0546 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/util/github.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2020 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 { graphql } from '@octokit/graphql'; +import { graphql as graphqlMsw } from 'msw'; +import { setupServer } from 'msw/node'; +import { + getOrganizationTeams, + getOrganizationUsers, + getTeamMembers, + QueryResponse, +} from './github'; + +describe('github', () => { + const server = setupServer(); + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + describe('getOrganizationUsers', () => { + it('reads members', async () => { + const input: QueryResponse = { + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + }, + ], + }, + }, + }; + + const output = { + users: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'a', description: 'c' }), + spec: { + profile: { displayName: 'b', email: 'd', picture: 'e' }, + memberOf: [], + }, + }), + ], + }; + + server.use( + graphqlMsw.query('users', (_req, res, ctx) => res(ctx.data(input))), + ); + + await expect(getOrganizationUsers(graphql, 'a')).resolves.toEqual(output); + }); + }); + + describe('getOrganizationTeams', () => { + it('reads teams', async () => { + const input: QueryResponse = { + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'blah/team', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: { pageInfo: { hasNextPage: false }, nodes: [] }, + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'user' }], + }, + }, + ], + }, + }, + }; + + const output = { + groups: [ + expect.objectContaining({ + metadata: expect.objectContaining({ name: 'team' }), + spec: { + type: 'team', + parent: 'parent', + ancestors: [], + children: [], + descendants: [], + }, + }), + ], + groupMemberUsers: new Map([['team', ['user']]]), + }; + + server.use( + graphqlMsw.query('teams', (_req, res, ctx) => res(ctx.data(input))), + ); + + await expect(getOrganizationTeams(graphql, 'a')).resolves.toEqual(output); + }); + }); + + describe('getTeamMembers', () => { + it('reads team members', async () => { + const input: QueryResponse = { + organization: { + team: { + slug: '', + combinedSlug: '', + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'user' }], + }, + }, + }, + }; + + const output = { + members: ['user'], + }; + + server.use( + graphqlMsw.query('members', (_req, res, ctx) => res(ctx.data(input))), + ); + + await expect(getTeamMembers(graphql, 'a', 'b')).resolves.toEqual(output); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/util/github.ts b/plugins/catalog-backend/src/ingestion/processors/util/github.ts index 5cb963c797..96cf4a7668 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/github.ts +++ b/plugins/catalog-backend/src/ingestion/processors/util/github.ts @@ -19,22 +19,22 @@ import { graphql } from '@octokit/graphql'; // Graphql types -type QueryResponse = { +export type QueryResponse = { organization: Organization; }; -type Organization = { - membersWithRole: Connection; - team: Team; - teams: Connection; +export type Organization = { + membersWithRole?: Connection; + team?: Team; + teams?: Connection; }; -type PageInfo = { +export type PageInfo = { hasNextPage: boolean; endCursor?: string; }; -type User = { +export type User = { login: string; bio?: string; avatarUrl?: string; @@ -42,7 +42,7 @@ type User = { name?: string; }; -type Team = { +export type Team = { slug: string; combinedSlug: string; description?: string; @@ -50,7 +50,7 @@ type Team = { members: Connection; }; -type Connection = { +export type Connection = { pageInfo: PageInfo; nodes: T[]; }; @@ -67,63 +67,49 @@ export async function getOrganizationUsers( client: typeof graphql, org: string, ): Promise<{ users: UserEntity[] }> { - const users: UserEntity[] = []; + const query = ` + query users($org: String!, $cursor: String) { + organization(login: $org) { + membersWithRole(first: 100, after: $cursor) { + pageInfo { hasNextPage, endCursor } + nodes { avatarUrl, bio, email, login, name } + } + } + }`; // There is no user -> teams edge, so we leave the memberships empty for // now and let the team iteration handle it instead - let cursor: string | undefined = undefined; - const query = ` - query users($org: String!, $cursor: String) { - organization(login: $org) { - membersWithRole(first: 100, after: $cursor) { - pageInfo { hasNextPage, endCursor } - nodes { avatarUrl, bio, email, login, name } - } - } - }`; - - for (let i = 0; i < 100 /* just for sanity */; ++i) { - const response: QueryResponse = await client(query, { - org, - cursor, - }); - - const connection = response.organization?.membersWithRole; - if (!connection) { - throw new Error(`Found no organization named ${org}`); - } - - for (const user of connection.nodes) { - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: user.login, - annotations: { - 'github.com/user-login': user.login, - }, + const mapper = (user: User) => { + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: user.login, + annotations: { + 'github.com/user-login': user.login, }, - spec: { - profile: {}, - memberOf: [], - }, - }; + }, + spec: { + profile: {}, + memberOf: [], + }, + }; - if (user.bio) entity.metadata.description = user.bio; - if (user.name) entity.spec.profile!.displayName = user.name; - if (user.email) entity.spec.profile!.email = user.email; - if (user.avatarUrl) entity.spec.profile!.picture = user.avatarUrl; + if (user.bio) entity.metadata.description = user.bio; + if (user.name) entity.spec.profile!.displayName = user.name; + if (user.email) entity.spec.profile!.email = user.email; + if (user.avatarUrl) entity.spec.profile!.picture = user.avatarUrl; - users.push(entity); - } + return entity; + }; - const { hasNextPage, endCursor } = connection.pageInfo; - if (!hasNextPage) { - break; - } else { - cursor = endCursor; - } - } + const users = await queryWithPaging( + client, + query, + r => r.organization?.membersWithRole, + mapper, + { org }, + ); return { users }; } @@ -143,10 +129,6 @@ export async function getOrganizationTeams( groups: GroupEntity[]; groupMemberUsers: Map; }> { - const groups: GroupEntity[] = []; - const groupMemberUsers = new Map(); - - let cursor: string | undefined = undefined; const query = ` query teams($org: String!, $cursor: String) { organization(login: $org) { @@ -157,7 +139,7 @@ export async function getOrganizationTeams( combinedSlug parentTeam { slug } members(first: 100, membership: IMMEDIATE) { - pageInfo { hasNextPage, endCursor } + pageInfo { hasNextPage } nodes { login } } } @@ -165,65 +147,57 @@ export async function getOrganizationTeams( } }`; - for (let i = 0; i < 100 /* just for sanity */; ++i) { - const response: QueryResponse = await client(query, { - org, - cursor, - }); + // Gets populated inside the mapper below + const groupMemberUsers = new Map(); - const connection = response.organization?.teams; - if (!connection) { - throw new Error(`Found no organization named ${org}`); - } - - for (const team of connection.nodes) { - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: team.slug, - annotations: { - 'github.com/team-slug': team.combinedSlug, - }, + const mapper = async (team: Team) => { + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: team.slug, + annotations: { + 'github.com/team-slug': team.combinedSlug, }, - spec: { - type: 'team', - ancestors: [], - children: [], - descendants: [], - }, - }; + }, + spec: { + type: 'team', + ancestors: [], + children: [], + descendants: [], + }, + }; - if (team.description) entity.metadata.description = team.description; - if (team.parentTeam) entity.spec.parent = team.parentTeam.slug; + if (team.description) entity.metadata.description = team.description; + if (team.parentTeam) entity.spec.parent = team.parentTeam.slug; - groups.push(entity); + const memberNames: string[] = []; + groupMemberUsers.set(team.slug, memberNames); - const memberNames: string[] = []; - groupMemberUsers.set(team.slug, memberNames); - - if (!team.members.pageInfo.hasNextPage) { - // We got all the members in one go, run the fast path - for (const user of team.members.nodes) { - memberNames.push(user.login); - } - } else { - // There were more than a hundred immediate members - run the slow - // path of fetching them explicitly - const { members } = await getTeamMembers(client, org, team.slug); - for (const userLogin of members) { - memberNames.push(userLogin); - } + if (!team.members.pageInfo.hasNextPage) { + // We got all the members in one go, run the fast path + for (const user of team.members.nodes) { + memberNames.push(user.login); + } + } else { + // There were more than a hundred immediate members - run the slow + // path of fetching them explicitly + const { members } = await getTeamMembers(client, org, team.slug); + for (const userLogin of members) { + memberNames.push(userLogin); } } - const { hasNextPage, endCursor } = connection.pageInfo; - if (!hasNextPage) { - break; - } else { - cursor = endCursor; - } - } + return entity; + }; + + const groups = await queryWithPaging( + client, + query, + r => r.organization?.teams, + mapper, + { org }, + ); return { groups, groupMemberUsers }; } @@ -242,9 +216,6 @@ export async function getTeamMembers( org: string, teamSlug: string, ): Promise<{ members: string[] }> { - const members: string[] = []; - - let cursor: string | undefined = undefined; const query = ` query members($org: String!, $teamSlug: String!, $cursor: String) { organization(login: $org) { @@ -257,29 +228,70 @@ export async function getTeamMembers( } }`; - for (let j = 0; j < 100 /* just for sanity */; ++j) { - const response: QueryResponse = await client(query, { - org, - teamSlug, - cursor, - }); - - const connection = response.organization?.team?.members; - if (!connection) { - throw new Error(`Found no team named ${teamSlug} in named ${org}`); - } - - for (const user of connection.nodes) { - members.push(user.login); - } - - const { hasNextPage, endCursor } = connection.pageInfo; - if (!hasNextPage) { - break; - } else { - cursor = endCursor; - } - } + const members = await queryWithPaging( + client, + query, + r => r.organization?.team?.members, + user => user.login, + { org, teamSlug }, + ); return { members }; } + +// +// Helpers +// + +/** + * Assists in repeatedly executing a query with a paged response. + * + * Requires that the query accepts a $cursor variable. + * + * @param client The octokit client + * @param query The query to execute + * @param connection A function that, given the response, picks out the actual + * Connection object that's being iterated + * @param mapper A function that, given one of the nodes in the Connection, + * returns the model mapped form of it + * @param variables The variable values that the query needs, minus the cursor + */ +export async function queryWithPaging< + GraphqlType, + OutputType, + Variables extends {}, + Response = QueryResponse +>( + client: typeof graphql, + query: string, + connection: (response: Response) => Connection | undefined, + mapper: (item: GraphqlType) => Promise | OutputType, + variables: Variables, +): Promise { + const result: OutputType[] = []; + + let cursor: string | undefined = undefined; + for (let j = 0; j < 1000 /* just for sanity */; ++j) { + const response: Response = await client(query, { + ...variables, + cursor, + }); + + const conn = connection(response); + if (!conn) { + throw new Error(`Found no match for ${JSON.stringify(variables)}`); + } + + for (const node of conn.nodes) { + result.push(await mapper(node)); + } + + if (!conn.pageInfo.hasNextPage) { + break; + } else { + cursor = conn.pageInfo.endCursor; + } + } + + return result; +} diff --git a/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts b/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts new file mode 100644 index 0000000000..e0120735fe --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/util/org.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2020 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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { buildOrgHierarchy } from './org'; + +function u(name: string): UserEntity { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name }, + spec: { memberOf: [] }, + }; +} + +function g( + name: string, + parent: string | undefined, + children: string[], +): GroupEntity { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name }, + spec: { type: 'team', parent, children, ancestors: [], descendants: [] }, + }; +} + +describe('buildOrgHierarchy', () => { + it('puts users in the respective groups', () => { + const a = g('a', undefined, []); + const b = g('b', undefined, []); + const x = u('x'); + const y = u('y'); + const groupMemberUsers: Map = new Map([ + ['a', ['x', 'y']], + ['b', ['y']], + ]); + buildOrgHierarchy([a, b], [x, y], groupMemberUsers); + expect(x.spec.memberOf).toEqual(['a']); + expect(y.spec.memberOf).toEqual(['a', 'b']); + }); + + it('adds groups to their parent.children', () => { + const a = g('a', undefined, []); + const b = g('b', 'a', []); + const c = g('c', 'b', []); + const d = g('d', 'a', []); + buildOrgHierarchy([a, b, c, d], [], new Map()); + expect(a.spec.children).toEqual(expect.arrayContaining(['b', 'd'])); + expect(b.spec.children).toEqual(expect.arrayContaining(['c'])); + expect(c.spec.children).toEqual([]); + expect(d.spec.children).toEqual([]); + }); + + it('fills out descendants', () => { + const a = g('a', undefined, []); + const b = g('b', 'a', []); + const c = g('c', 'b', []); + const d = g('d', 'a', []); + buildOrgHierarchy([a, b, c, d], [], new Map()); + expect(a.spec.descendants).toEqual(expect.arrayContaining(['b', 'c', 'd'])); + expect(b.spec.descendants).toEqual(expect.arrayContaining(['c'])); + expect(c.spec.descendants).toEqual([]); + expect(d.spec.descendants).toEqual([]); + }); + + it('fills out ancestors', () => { + const a = g('a', undefined, []); + const b = g('b', 'a', []); + const c = g('c', 'b', []); + const d = g('d', 'a', []); + buildOrgHierarchy([a, b, c, d], [], new Map()); + expect(a.spec.ancestors).toEqual([]); + expect(b.spec.ancestors).toEqual(expect.arrayContaining(['a'])); + expect(c.spec.ancestors).toEqual(expect.arrayContaining(['a', 'b'])); + expect(d.spec.ancestors).toEqual(expect.arrayContaining(['a'])); + }); +}); diff --git a/plugins/catalog-graphql/src/graphql/module.test.ts b/plugins/catalog-graphql/src/graphql/module.test.ts index 09708f68f4..e3e03a9b4a 100644 --- a/plugins/catalog-graphql/src/graphql/module.test.ts +++ b/plugins/catalog-graphql/src/graphql/module.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createModule } from './module'; import { execute } from 'graphql'; import { rest } from 'msw'; @@ -36,9 +37,8 @@ describe('Catalog Module', () => { }, ]); - beforeAll(() => worker.listen()); + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); afterAll(() => worker.close()); - afterEach(() => worker.resetHandlers()); describe('Default Entity', () => { diff --git a/plugins/catalog-graphql/src/service/client.test.ts b/plugins/catalog-graphql/src/service/client.test.ts index 876059802b..d8cc59d1a8 100644 --- a/plugins/catalog-graphql/src/service/client.test.ts +++ b/plugins/catalog-graphql/src/service/client.test.ts @@ -20,9 +20,8 @@ import { setupServer } from 'msw/node'; describe('Catalog GraphQL Module', () => { const worker = setupServer(); - beforeAll(() => worker.listen()); + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); afterAll(() => worker.close()); - afterEach(() => worker.resetHandlers()); const baseUrl = 'http://localhost:1234'; diff --git a/plugins/catalog/src/api/CatalogClient.test.ts b/plugins/catalog/src/api/CatalogClient.test.ts index 18f062db77..832a114276 100644 --- a/plugins/catalog/src/api/CatalogClient.test.ts +++ b/plugins/catalog/src/api/CatalogClient.test.ts @@ -25,7 +25,7 @@ const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); describe('CatalogClient', () => { - beforeAll(() => server.listen()); + beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterEach(() => server.resetHandlers()); afterAll(() => server.close());