From c40aa8798c437fd8b89ac84da0a4b916bd5d4bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 10 Sep 2024 17:36:29 +0200 Subject: [PATCH 01/11] empty setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-client/api-report-testUtils.md | 101 +++++++++++++ packages/catalog-client/package.json | 20 ++- packages/catalog-client/src/testUtils.ts | 17 +++ .../src/testUtils/InMemoryCatalogClient.ts | 138 ++++++++++++++++++ .../catalog-client/src/testUtils/index.ts | 17 +++ plugins/catalog-node/api-report-testUtils.md | 7 + plugins/catalog-node/package.json | 4 + plugins/catalog-node/src/testUtils.ts | 17 +++ .../src/testUtils/catalogServiceMock.ts | 32 ++++ plugins/catalog-node/src/testUtils/index.ts | 17 +++ 10 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 packages/catalog-client/api-report-testUtils.md create mode 100644 packages/catalog-client/src/testUtils.ts create mode 100644 packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts create mode 100644 packages/catalog-client/src/testUtils/index.ts create mode 100644 plugins/catalog-node/api-report-testUtils.md create mode 100644 plugins/catalog-node/src/testUtils.ts create mode 100644 plugins/catalog-node/src/testUtils/catalogServiceMock.ts create mode 100644 plugins/catalog-node/src/testUtils/index.ts diff --git a/packages/catalog-client/api-report-testUtils.md b/packages/catalog-client/api-report-testUtils.md new file mode 100644 index 0000000000..f78c6de5c9 --- /dev/null +++ b/packages/catalog-client/api-report-testUtils.md @@ -0,0 +1,101 @@ +## API Report File for "@backstage/catalog-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AddLocationRequest } from '@backstage/catalog-client'; +import { AddLocationResponse } from '@backstage/catalog-client'; +import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogRequestOptions } from '@backstage/catalog-client'; +import { CompoundEntityRef } from '@backstage/catalog-model'; +import { Entity } from '@backstage/catalog-model'; +import { GetEntitiesByRefsRequest } from '@backstage/catalog-client'; +import { GetEntitiesByRefsResponse } from '@backstage/catalog-client'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { GetEntityAncestorsRequest } from '@backstage/catalog-client'; +import { GetEntityAncestorsResponse } from '@backstage/catalog-client'; +import { GetEntityFacetsRequest } from '@backstage/catalog-client'; +import { GetEntityFacetsResponse } from '@backstage/catalog-client'; +import { Location as Location_2 } from '@backstage/catalog-client'; +import { QueryEntitiesRequest } from '@backstage/catalog-client'; +import { QueryEntitiesResponse } from '@backstage/catalog-client'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; + +// @public (undocumented) +export class InMemoryCatalogClient implements CatalogApi { + // (undocumented) + addLocation( + _location: AddLocationRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getEntities( + _request?: GetEntitiesRequest | undefined, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getEntitiesByRefs( + _request: GetEntitiesByRefsRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getEntityAncestors( + _request: GetEntityAncestorsRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getEntityByRef( + _entityRef: string | CompoundEntityRef, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getEntityFacets( + _request: GetEntityFacetsRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getLocationByEntity( + _entityRef: string | CompoundEntityRef, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getLocationById( + _id: string, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + getLocationByRef( + _locationRef: string, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + queryEntities( + _request?: QueryEntitiesRequest | undefined, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + refreshEntity( + _entityRef: string, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + removeEntityByUid( + _uid: string, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + removeLocationById( + _id: string, + _options?: CatalogRequestOptions | undefined, + ): Promise; + // (undocumented) + validateEntity( + _entity: Entity, + _locationRef: string, + _options?: CatalogRequestOptions | undefined, + ): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index d09a85efbc..4119614503 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -6,10 +6,7 @@ "role": "common-library" }, "publishConfig": { - "access": "public", - "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage" @@ -22,8 +19,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./testUtils": "./src/testUtils.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "testUtils": [ + "src/testUtils.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], diff --git a/packages/catalog-client/src/testUtils.ts b/packages/catalog-client/src/testUtils.ts new file mode 100644 index 0000000000..15017c7fa0 --- /dev/null +++ b/packages/catalog-client/src/testUtils.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +export { InMemoryCatalogClient } from './testUtils/InMemoryCatalogClient'; diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts new file mode 100644 index 0000000000..c5ceafcd46 --- /dev/null +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -0,0 +1,138 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { + AddLocationRequest, + AddLocationResponse, + CatalogApi, + CatalogRequestOptions, + GetEntitiesByRefsRequest, + GetEntitiesByRefsResponse, + GetEntitiesRequest, + GetEntitiesResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, + GetEntityFacetsRequest, + GetEntityFacetsResponse, + Location, + QueryEntitiesRequest, + QueryEntitiesResponse, + ValidateEntityResponse, +} from '@backstage/catalog-client'; +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { NotImplementedError } from '@backstage/errors'; + +/** @public */ +export class InMemoryCatalogClient implements CatalogApi { + getEntities( + _request?: GetEntitiesRequest | undefined, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getEntitiesByRefs( + _request: GetEntitiesByRefsRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + queryEntities( + _request?: QueryEntitiesRequest | undefined, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getEntityAncestors( + _request: GetEntityAncestorsRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getEntityByRef( + _entityRef: string | CompoundEntityRef, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + removeEntityByUid( + _uid: string, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + refreshEntity( + _entityRef: string, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getEntityFacets( + _request: GetEntityFacetsRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getLocationById( + _id: string, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getLocationByRef( + _locationRef: string, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + addLocation( + _location: AddLocationRequest, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + removeLocationById( + _id: string, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + getLocationByEntity( + _entityRef: string | CompoundEntityRef, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } + + validateEntity( + _entity: Entity, + _locationRef: string, + _options?: CatalogRequestOptions | undefined, + ): Promise { + throw new NotImplementedError('Method not implemented.'); + } +} diff --git a/packages/catalog-client/src/testUtils/index.ts b/packages/catalog-client/src/testUtils/index.ts new file mode 100644 index 0000000000..a28adac13d --- /dev/null +++ b/packages/catalog-client/src/testUtils/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +export { InMemoryCatalogClient } from './InMemoryCatalogClient'; diff --git a/plugins/catalog-node/api-report-testUtils.md b/plugins/catalog-node/api-report-testUtils.md new file mode 100644 index 0000000000..23c759fa9a --- /dev/null +++ b/plugins/catalog-node/api-report-testUtils.md @@ -0,0 +1,7 @@ +## API Report File for "@backstage/plugin-catalog-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 32cb210f97..eb920bff93 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -26,6 +26,7 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", + "./testUtils": "./src/testUtils.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -35,6 +36,9 @@ "alpha": [ "src/alpha.ts" ], + "testUtils": [ + "src/testUtils.ts" + ], "package.json": [ "package.json" ] diff --git a/plugins/catalog-node/src/testUtils.ts b/plugins/catalog-node/src/testUtils.ts new file mode 100644 index 0000000000..96551ee7b7 --- /dev/null +++ b/plugins/catalog-node/src/testUtils.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +export * from './testUtils'; diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts new file mode 100644 index 0000000000..080609986c --- /dev/null +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { createServiceFactory } from '@backstage/backend-plugin-api'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils'; + +/** @public */ +export function catalogServiceMock() {} + +/** @public */ +export namespace catalogServiceMock { + export const factory = () => + createServiceFactory({ + service: catalogServiceRef, + deps: {}, + factory: () => new InMemoryCatalogClient(), + }); +} diff --git a/plugins/catalog-node/src/testUtils/index.ts b/plugins/catalog-node/src/testUtils/index.ts new file mode 100644 index 0000000000..71c20fa7f4 --- /dev/null +++ b/plugins/catalog-node/src/testUtils/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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. + */ + +export { type catalogServiceMock } from './catalogServiceMock'; From f19f84807774df897b1916498e570c074d9aa9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 10 Sep 2024 22:21:54 +0200 Subject: [PATCH 02/11] flesh out the mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-client/api-report-testUtils.md | 52 ++------- .../src/testUtils/InMemoryCatalogClient.ts | 108 +++++++++--------- .../src/testUtils/catalogServiceMock.ts | 60 +++++++++- 3 files changed, 123 insertions(+), 97 deletions(-) diff --git a/packages/catalog-client/api-report-testUtils.md b/packages/catalog-client/api-report-testUtils.md index f78c6de5c9..a33470e35d 100644 --- a/packages/catalog-client/api-report-testUtils.md +++ b/packages/catalog-client/api-report-testUtils.md @@ -6,7 +6,6 @@ import { AddLocationRequest } from '@backstage/catalog-client'; import { AddLocationResponse } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; -import { CatalogRequestOptions } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; import { Entity } from '@backstage/catalog-model'; import { GetEntitiesByRefsRequest } from '@backstage/catalog-client'; @@ -24,76 +23,49 @@ import { ValidateEntityResponse } from '@backstage/catalog-client'; // @public (undocumented) export class InMemoryCatalogClient implements CatalogApi { + constructor(options?: { entities?: Entity[] }); // (undocumented) - addLocation( - _location: AddLocationRequest, - _options?: CatalogRequestOptions | undefined, - ): Promise; + addLocation(_location: AddLocationRequest): Promise; // (undocumented) - getEntities( - _request?: GetEntitiesRequest | undefined, - _options?: CatalogRequestOptions | undefined, - ): Promise; + getEntities(_request?: GetEntitiesRequest): Promise; // (undocumented) getEntitiesByRefs( - _request: GetEntitiesByRefsRequest, - _options?: CatalogRequestOptions | undefined, + request: GetEntitiesByRefsRequest, ): Promise; // (undocumented) getEntityAncestors( - _request: GetEntityAncestorsRequest, - _options?: CatalogRequestOptions | undefined, + request: GetEntityAncestorsRequest, ): Promise; // (undocumented) getEntityByRef( - _entityRef: string | CompoundEntityRef, - _options?: CatalogRequestOptions | undefined, + entityRef: string | CompoundEntityRef, ): Promise; // (undocumented) getEntityFacets( _request: GetEntityFacetsRequest, - _options?: CatalogRequestOptions | undefined, ): Promise; // (undocumented) getLocationByEntity( _entityRef: string | CompoundEntityRef, - _options?: CatalogRequestOptions | undefined, ): Promise; // (undocumented) - getLocationById( - _id: string, - _options?: CatalogRequestOptions | undefined, - ): Promise; + getLocationById(_id: string): Promise; // (undocumented) - getLocationByRef( - _locationRef: string, - _options?: CatalogRequestOptions | undefined, - ): Promise; + getLocationByRef(_locationRef: string): Promise; // (undocumented) queryEntities( - _request?: QueryEntitiesRequest | undefined, - _options?: CatalogRequestOptions | undefined, + _request?: QueryEntitiesRequest, ): Promise; // (undocumented) - refreshEntity( - _entityRef: string, - _options?: CatalogRequestOptions | undefined, - ): Promise; + refreshEntity(_entityRef: string): Promise; // (undocumented) - removeEntityByUid( - _uid: string, - _options?: CatalogRequestOptions | undefined, - ): Promise; + removeEntityByUid(uid: string): Promise; // (undocumented) - removeLocationById( - _id: string, - _options?: CatalogRequestOptions | undefined, - ): Promise; + removeLocationById(_id: string): Promise; // (undocumented) validateEntity( _entity: Entity, _locationRef: string, - _options?: CatalogRequestOptions | undefined, ): Promise; } diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index c5ceafcd46..ce65290e04 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -18,7 +18,6 @@ import { AddLocationRequest, AddLocationResponse, CatalogApi, - CatalogRequestOptions, GetEntitiesByRefsRequest, GetEntitiesByRefsResponse, GetEntitiesRequest, @@ -32,106 +31,111 @@ import { QueryEntitiesResponse, ValidateEntityResponse, } from '@backstage/catalog-client'; -import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; -import { NotImplementedError } from '@backstage/errors'; +import { + CompoundEntityRef, + Entity, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { NotFoundError, NotImplementedError } from '@backstage/errors'; /** @public */ export class InMemoryCatalogClient implements CatalogApi { - getEntities( - _request?: GetEntitiesRequest | undefined, - _options?: CatalogRequestOptions | undefined, + #entities: Entity[]; + + constructor(options?: { entities?: Entity[] }) { + this.#entities = options?.entities ?? []; + } + + async getEntities( + _request?: GetEntitiesRequest, ): Promise { throw new NotImplementedError('Method not implemented.'); } - getEntitiesByRefs( - _request: GetEntitiesByRefsRequest, - _options?: CatalogRequestOptions | undefined, + async getEntitiesByRefs( + request: GetEntitiesByRefsRequest, ): Promise { - throw new NotImplementedError('Method not implemented.'); + const entitiesByRef = new Map(); + for (const entity of this.#entities) { + entitiesByRef.set(stringifyEntityRef(entity), entity); + } + + const items = request.entityRefs.map(candidateRef => + entitiesByRef.get(candidateRef), + ); + + // TODO(freben): Fields, filters etc + return { items }; } - queryEntities( - _request?: QueryEntitiesRequest | undefined, - _options?: CatalogRequestOptions | undefined, + async queryEntities( + _request?: QueryEntitiesRequest, ): Promise { throw new NotImplementedError('Method not implemented.'); } - getEntityAncestors( - _request: GetEntityAncestorsRequest, - _options?: CatalogRequestOptions | undefined, + async getEntityAncestors( + request: GetEntityAncestorsRequest, ): Promise { - throw new NotImplementedError('Method not implemented.'); + const entity = this.#entities.find( + e => stringifyEntityRef(e) === request.entityRef, + ); + if (!entity) { + throw new NotFoundError(`Entity with ref ${request.entityRef} not found`); + } + return { + rootEntityRef: request.entityRef, + items: [{ entity, parentEntityRefs: [] }], + }; } - getEntityByRef( - _entityRef: string | CompoundEntityRef, - _options?: CatalogRequestOptions | undefined, + async getEntityByRef( + entityRef: string | CompoundEntityRef, ): Promise { - throw new NotImplementedError('Method not implemented.'); + const ref = + typeof entityRef === 'string' ? entityRef : stringifyEntityRef(entityRef); + return this.#entities.find(e => stringifyEntityRef(e) === ref); } - removeEntityByUid( - _uid: string, - _options?: CatalogRequestOptions | undefined, - ): Promise { - throw new NotImplementedError('Method not implemented.'); + async removeEntityByUid(uid: string): Promise { + this.#entities = this.#entities.filter(e => e.metadata.uid !== uid); } - refreshEntity( - _entityRef: string, - _options?: CatalogRequestOptions | undefined, - ): Promise { - throw new NotImplementedError('Method not implemented.'); - } + async refreshEntity(_entityRef: string): Promise {} - getEntityFacets( + async getEntityFacets( _request: GetEntityFacetsRequest, - _options?: CatalogRequestOptions | undefined, ): Promise { throw new NotImplementedError('Method not implemented.'); } - getLocationById( - _id: string, - _options?: CatalogRequestOptions | undefined, - ): Promise { + async getLocationById(_id: string): Promise { throw new NotImplementedError('Method not implemented.'); } - getLocationByRef( - _locationRef: string, - _options?: CatalogRequestOptions | undefined, - ): Promise { + async getLocationByRef(_locationRef: string): Promise { throw new NotImplementedError('Method not implemented.'); } - addLocation( + async addLocation( _location: AddLocationRequest, - _options?: CatalogRequestOptions | undefined, ): Promise { throw new NotImplementedError('Method not implemented.'); } - removeLocationById( - _id: string, - _options?: CatalogRequestOptions | undefined, - ): Promise { + async removeLocationById(_id: string): Promise { throw new NotImplementedError('Method not implemented.'); } - getLocationByEntity( + async getLocationByEntity( _entityRef: string | CompoundEntityRef, - _options?: CatalogRequestOptions | undefined, ): Promise { throw new NotImplementedError('Method not implemented.'); } - validateEntity( + async validateEntity( _entity: Entity, _locationRef: string, - _options?: CatalogRequestOptions | undefined, ): Promise { throw new NotImplementedError('Method not implemented.'); } diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts index 080609986c..dc73176a84 100644 --- a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts @@ -14,19 +14,69 @@ * limitations under the License. */ -import { createServiceFactory } from '@backstage/backend-plugin-api'; -import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { + ServiceRef, + createServiceFactory, +} from '@backstage/backend-plugin-api'; import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils'; +import { Entity } from '@backstage/catalog-model'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +// eslint-disable-next-line @backstage/no-undeclared-imports +import { ServiceMock } from '@backstage/backend-test-utils'; + +/** @internal */ +function simpleMock( + ref: ServiceRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ServiceMock { + return partialImpl => { + const mock = mockFactory(); + if (partialImpl) { + for (const [key, impl] of Object.entries(partialImpl)) { + if (typeof impl === 'function') { + (mock as any)[key].mockImplementation(impl); + } else { + (mock as any)[key] = impl; + } + } + } + return Object.assign(mock, { + factory: createServiceFactory({ + service: ref, + deps: {}, + factory: () => mock, + }), + }) as ServiceMock; + }; +} /** @public */ -export function catalogServiceMock() {} +export function catalogServiceMock(options?: { entities?: Entity[] }) { + return new InMemoryCatalogClient(options); +} /** @public */ export namespace catalogServiceMock { - export const factory = () => + export const factory = (options?: { entities?: Entity[] }) => createServiceFactory({ service: catalogServiceRef, deps: {}, - factory: () => new InMemoryCatalogClient(), + factory: () => new InMemoryCatalogClient(options), }); + export const mock = simpleMock(catalogServiceRef, () => ({ + getEntities: jest.fn(), + getEntitiesByRefs: jest.fn(), + queryEntities: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityByRef: jest.fn(), + removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), + getEntityFacets: jest.fn(), + getLocationById: jest.fn(), + getLocationByRef: jest.fn(), + addLocation: jest.fn(), + removeLocationById: jest.fn(), + getLocationByEntity: jest.fn(), + validateEntity: jest.fn(), + })); } From 0e77776b51009ee7e414edd3291d6d3810bece30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Sep 2024 10:41:02 +0200 Subject: [PATCH 03/11] implement in some tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../src/testUtils/InMemoryCatalogClient.ts | 10 +- .../catalog-client/src/testUtils/index.ts | 17 --- .../lib/catalog/CatalogIdentityClient.test.ts | 29 ++--- .../BitbucketCloudEntityProvider.test.ts | 32 ++--- plugins/catalog-node/api-report-testUtils.md | 23 ++++ plugins/catalog-node/src/testUtils.ts | 2 +- plugins/catalog-node/src/testUtils/index.ts | 17 --- .../CatalogClusterLocator.test.ts | 116 ++++++++---------- .../src/cluster-locator/index.test.ts | 30 +---- .../NotificationsEmailProcessor.test.ts | 45 +++---- .../src/service/getUsersForEntityRef.test.ts | 72 +++++------ .../src/service/router.test.ts | 7 +- .../builtin/catalog/fetch.examples.test.ts | 22 ++-- .../actions/builtin/catalog/fetch.test.ts | 72 ++++++----- .../builtin/catalog/register.examples.test.ts | 17 ++- .../actions/builtin/catalog/register.test.ts | 46 ++++--- .../src/service/router.test.ts | 54 ++++---- .../src/service/CachedEntityLoader.test.ts | 30 +++-- 18 files changed, 285 insertions(+), 356 deletions(-) delete mode 100644 packages/catalog-client/src/testUtils/index.ts delete mode 100644 plugins/catalog-node/src/testUtils/index.ts diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index ce65290e04..ff01c99532 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -49,7 +49,8 @@ export class InMemoryCatalogClient implements CatalogApi { async getEntities( _request?: GetEntitiesRequest, ): Promise { - throw new NotImplementedError('Method not implemented.'); + // TODO(freben): Fields, filters etc + return { items: this.#entities }; } async getEntitiesByRefs( @@ -71,7 +72,12 @@ export class InMemoryCatalogClient implements CatalogApi { async queryEntities( _request?: QueryEntitiesRequest, ): Promise { - throw new NotImplementedError('Method not implemented.'); + // TODO(freben): Fields, filters etc + return { + items: this.#entities, + totalItems: this.#entities.length, + pageInfo: {}, + }; } async getEntityAncestors( diff --git a/packages/catalog-client/src/testUtils/index.ts b/packages/catalog-client/src/testUtils/index.ts deleted file mode 100644 index a28adac13d..0000000000 --- a/packages/catalog-client/src/testUtils/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * 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. - */ - -export { InMemoryCatalogClient } from './InMemoryCatalogClient'; diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 3439578d5e..2cd58d692f 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -15,28 +15,15 @@ */ import { TokenManager } from '@backstage/backend-common'; -import { CatalogApi } from '@backstage/catalog-client'; import { RELATION_MEMBER_OF, UserEntity, UserEntityV1alpha1, } from '@backstage/catalog-model'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; import { CatalogIdentityClient } from './CatalogIdentityClient'; describe('CatalogIdentityClient', () => { - const catalogApi = { - getLocationById: jest.fn(), - getEntityByRef: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - getLocationByRef: jest.fn(), - removeEntityByUid: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityFacets: jest.fn(), - validateEntity: jest.fn(), - }; const tokenManager: jest.Mocked = { getToken: jest.fn(), authenticate: jest.fn(), @@ -45,11 +32,15 @@ describe('CatalogIdentityClient', () => { afterEach(() => jest.resetAllMocks()); it('findUser passes through the correct search params', async () => { - catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] }); + const catalogApi = catalogServiceMock.mock({ + getEntities: jest + .fn() + .mockResolvedValueOnce({ items: [{} as UserEntity] }), + }); tokenManager.getToken.mockResolvedValue({ token: 'my-token' }); const client = new CatalogIdentityClient({ discovery: {} as any, - catalogApi: catalogApi as Partial as CatalogApi, + catalogApi, tokenManager, }); @@ -103,12 +94,14 @@ describe('CatalogIdentityClient', () => { ], }, ]; - catalogApi.getEntities.mockResolvedValueOnce({ items: mockUsers }); + const catalogApi = catalogServiceMock.mock({ + getEntities: jest.fn().mockResolvedValueOnce({ items: mockUsers }), + }); tokenManager.getToken.mockResolvedValue({ token: 'my-token' }); const client = new CatalogIdentityClient({ discovery: {} as any, - catalogApi: catalogApi as Partial as CatalogApi, + catalogApi, tokenManager, }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 4fbbb67c7d..262582b99a 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -16,7 +16,6 @@ import { TokenManager } from '@backstage/backend-common'; import { - SchedulerService, SchedulerServiceTaskInvocationDefinition, SchedulerServiceTaskRunner, } from '@backstage/backend-plugin-api'; @@ -24,7 +23,6 @@ import { mockServices, registerMswTestHooks, } from '@backstage/backend-test-utils'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { @@ -39,6 +37,7 @@ import { ANNOTATION_BITBUCKET_CLOUD_REPO_URL, BitbucketCloudEntityProvider, } from './BitbucketCloudEntityProvider'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; class PersistingTaskRunner implements SchedulerServiceTaskRunner { private tasks: SchedulerServiceTaskInvocationDefinition[] = []; @@ -60,10 +59,9 @@ class PersistingTaskRunner implements SchedulerServiceTaskRunner { const logger = mockServices.logger.mock(); const server = setupServer(); +registerMswTestHooks(server); describe('BitbucketCloudEntityProvider', () => { - registerMswTestHooks(server); - const simpleConfig = new ConfigReader({ catalog: { providers: { @@ -190,7 +188,7 @@ describe('BitbucketCloudEntityProvider', () => { }); it('fail with scheduler but no schedule config', () => { - const scheduler = jest.fn() as unknown as SchedulerService; + const scheduler = mockServices.scheduler.mock(); const config = new ConfigReader({ catalog: { providers: { @@ -212,9 +210,7 @@ describe('BitbucketCloudEntityProvider', () => { }); it('single simple provider config with schedule in config', () => { - const scheduler = { - createScheduledTaskRunner: (_: any) => jest.fn(), - } as unknown as SchedulerService; + const scheduler = mockServices.scheduler.mock(); const config = new ConfigReader({ catalog: { providers: { @@ -441,7 +437,7 @@ describe('BitbucketCloudEntityProvider', () => { ); const events = DefaultEventsService.create({ logger }); - const catalogApi = { + const catalogApi = catalogServiceMock.mock({ getEntities: async ( request: { filter: Record }, options: { token: string }, @@ -459,9 +455,9 @@ describe('BitbucketCloudEntityProvider', () => { items: [keptModule, removedModule], }; }, - }; + }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi, events, logger, schedule, @@ -573,13 +569,10 @@ describe('BitbucketCloudEntityProvider', () => { }); it('no onRepoPush update on non-matching workspace slug', async () => { - const catalogApi = { - getEntities: jest.fn(), - refreshEntity: jest.fn(), - }; + const catalogApi = catalogServiceMock.mock(); const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi, events, logger, schedule, @@ -606,13 +599,10 @@ describe('BitbucketCloudEntityProvider', () => { }); it('no onRepoPush update on non-matching repo slug', async () => { - const catalogApi = { - getEntities: jest.fn(), - refreshEntity: jest.fn(), - }; + const catalogApi = catalogServiceMock.mock(); const events = DefaultEventsService.create({ logger }); const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { - catalogApi: catalogApi as any as CatalogApi, + catalogApi, events, logger, schedule, diff --git a/plugins/catalog-node/api-report-testUtils.md b/plugins/catalog-node/api-report-testUtils.md index 23c759fa9a..f3e465c7ac 100644 --- a/plugins/catalog-node/api-report-testUtils.md +++ b/plugins/catalog-node/api-report-testUtils.md @@ -3,5 +3,28 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils'; +import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceMock } from '@backstage/backend-test-utils'; + +// @public (undocumented) +export function catalogServiceMock(options?: { + entities?: Entity[]; +}): InMemoryCatalogClient; + +// @public (undocumented) +export namespace catalogServiceMock { + const // (undocumented) + factory: (options?: { + entities?: Entity[]; + }) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-node/src/testUtils.ts b/plugins/catalog-node/src/testUtils.ts index 96551ee7b7..de60594701 100644 --- a/plugins/catalog-node/src/testUtils.ts +++ b/plugins/catalog-node/src/testUtils.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './testUtils'; +export { catalogServiceMock } from './testUtils/catalogServiceMock'; diff --git a/plugins/catalog-node/src/testUtils/index.ts b/plugins/catalog-node/src/testUtils/index.ts deleted file mode 100644 index 71c20fa7f4..0000000000 --- a/plugins/catalog-node/src/testUtils/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * 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. - */ - -export { type catalogServiceMock } from './catalogServiceMock'; diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index 5ce316099e..baa26df91f 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -22,103 +22,83 @@ import { ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER, } from '@backstage/plugin-kubernetes-common'; import { CatalogClusterLocator } from './CatalogClusterLocator'; -import { CatalogApi } from '@backstage/catalog-client'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { Entity } from '@backstage/catalog-model'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; -const mockCatalogApi = { - getEntityByRef: jest.fn(), - getEntities: async () => ({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Resource', - metadata: { - annotations: { - 'kubernetes.io/api-server': 'https://apiserver.com', - 'kubernetes.io/api-server-certificate-authority': 'caData', - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', - [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', - 'kubernetes.io/skip-metrics-lookup': 'true', - 'kubernetes.io/skip-tls-verify': 'true', - 'kubernetes.io/dashboard-url': 'my-url', - 'kubernetes.io/dashboard-app': 'my-app', - }, - name: 'owned', - title: 'title', - namespace: 'default', - }, +const entities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', + metadata: { + annotations: { + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'oidc', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', + 'kubernetes.io/skip-metrics-lookup': 'true', + 'kubernetes.io/skip-tls-verify': 'true', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Resource', - metadata: { - annotations: { - 'kubernetes.io/api-server': 'https://apiserver.com', - 'kubernetes.io/api-server-certificate-authority': 'caData', - [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'aws', - [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role', - [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id', - [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', - 'kubernetes.io/dashboard-url': 'my-url', - 'kubernetes.io/dashboard-app': 'my-app', - }, - name: 'owned', - namespace: 'default', - }, + name: 'owned', + title: 'title', + namespace: 'default', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Resource', + metadata: { + annotations: { + 'kubernetes.io/api-server': 'https://apiserver.com', + 'kubernetes.io/api-server-certificate-authority': 'caData', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'aws', + [ANNOTATION_KUBERNETES_AWS_ASSUME_ROLE]: 'my-role', + [ANNOTATION_KUBERNETES_AWS_EXTERNAL_ID]: 'my-id', + [ANNOTATION_KUBERNETES_OIDC_TOKEN_PROVIDER]: 'google', + 'kubernetes.io/dashboard-url': 'my-url', + 'kubernetes.io/dashboard-app': 'my-app', }, - ], - }), -} as unknown as CatalogApi; + name: 'owned', + namespace: 'default', + }, + }, +]; describe('CatalogClusterLocator', () => { it('returns empty cluster details when the cluster is empty', async () => { - const emptyMockCatalogApi = { - getEntityByRef: jest.fn(), - getEntities: async () => ({ - items: [], - }), - } as Partial as CatalogApi; - const auth = mockServices.auth(); - + const credentials = mockCredentials.user(); const clusterSupplier = CatalogClusterLocator.fromConfig( - emptyMockCatalogApi, - auth, + catalogServiceMock({ entities: [] }), + mockServices.auth(), ); - const credentials = mockCredentials.user(); - const result = await clusterSupplier.getClusters({ credentials }); - expect(result).toHaveLength(0); expect(result).toStrictEqual([]); }); it('returns the cluster details provided by annotations', async () => { - const auth = mockServices.auth(); + const credentials = mockCredentials.user(); const clusterSupplier = CatalogClusterLocator.fromConfig( - mockCatalogApi, - auth, + catalogServiceMock({ entities }), + mockServices.auth(), ); - const credentials = mockCredentials.user(); - const result = await clusterSupplier.getClusters({ credentials }); - expect(result).toHaveLength(2); expect(result[0]).toMatchSnapshot(); }); it('returns the aws cluster details provided by annotations', async () => { - const auth = mockServices.auth(); + const credentials = mockCredentials.user(); const clusterSupplier = CatalogClusterLocator.fromConfig( - mockCatalogApi, - auth, + catalogServiceMock({ entities }), + mockServices.auth(), ); - const credentials = mockCredentials.user(); - const result = await clusterSupplier.getClusters({ credentials }); - expect(result).toHaveLength(2); expect(result[1]).toMatchSnapshot(); }); diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index 64b4bfbd0a..ed42131de4 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -15,16 +15,14 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { CatalogApi } from '@backstage/catalog-client'; import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; import { getCombinedClusterSupplier } from './index'; import { ClusterDetails } from '../types/types'; import { AuthenticationStrategy, DispatchStrategy } from '../auth'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('getCombinedClusterSupplier', () => { - let catalogApi: CatalogApi; - it('should retrieve cluster details from config', async () => { const config: Config = new ConfigReader( { @@ -62,7 +60,7 @@ describe('getCombinedClusterSupplier', () => { const clusterSupplier = getCombinedClusterSupplier( config, - catalogApi, + catalogServiceMock.mock(), mockStrategy, mockServices.logger.mock(), undefined, @@ -106,7 +104,7 @@ describe('getCombinedClusterSupplier', () => { expect(() => getCombinedClusterSupplier( config, - catalogApi, + catalogServiceMock.mock(), new DispatchStrategy({ authStrategyMap: {} }), mockServices.logger.mock(), undefined, @@ -141,31 +139,15 @@ describe('getCombinedClusterSupplier', () => { validateCluster: jest.fn().mockReturnValue([]), presentAuthMetadata: jest.fn(), }; - catalogApi = { - getEntities: jest.fn().mockResolvedValue({ - items: [{ metadata: { annotations: {}, name: 'cluster' } }], - }), - getEntitiesByRefs: jest.fn(), - queryEntities: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityByRef: jest.fn(), - removeEntityByUid: jest.fn(), - refreshEntity: jest.fn(), - getEntityFacets: jest.fn(), - getLocationById: jest.fn(), - getLocationByRef: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - getLocationByEntity: jest.fn(), - validateEntity: jest.fn(), - }; const auth = mockServices.auth(); const credentials = mockCredentials.user(); const clusterSupplier = getCombinedClusterSupplier( config, - catalogApi, + catalogServiceMock({ + entities: [{ metadata: { annotations: {}, name: 'cluster' } } as any], + }), mockStrategy, logger, undefined, diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts index 79d1110968..84728dafbf 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.test.ts @@ -18,8 +18,9 @@ import { mockServices } from '@backstage/backend-test-utils'; import { NotificationsEmailProcessor } from './NotificationsEmailProcessor'; import { ConfigReader } from '@backstage/config'; import { JsonArray } from '@backstage/types'; -import { CatalogClient } from '@backstage/catalog-client'; import { createTransport } from 'nodemailer'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; +import { Entity } from '@backstage/catalog-model'; const sendmailMock = jest.fn(); const mockTransport = { @@ -39,7 +40,7 @@ const DEFAULT_ENTITIES_RESPONSE = { email: 'mock@backstage.io', }, }, - }, + } as unknown as Entity, ], }; @@ -63,13 +64,7 @@ const DEFAULT_SENDMAIL_CONFIG = { describe('NotificationsEmailProcessor', () => { const logger = mockServices.logger.mock(); const auth = mockServices.auth(); - - const getEntityRefMock = jest.fn(); - const getEntitiesMock = jest.fn(); - const mockCatalogClient: Partial = { - getEntityByRef: getEntityRefMock, - getEntities: getEntitiesMock, - }; + const catalog = catalogServiceMock.mock(); beforeEach(() => { jest.resetAllMocks(); @@ -98,7 +93,7 @@ describe('NotificationsEmailProcessor', () => { }, }, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -145,7 +140,7 @@ describe('NotificationsEmailProcessor', () => { }, }, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -189,7 +184,7 @@ describe('NotificationsEmailProcessor', () => { }, }, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -217,11 +212,13 @@ describe('NotificationsEmailProcessor', () => { it('should send user email', async () => { (createTransport as jest.Mock).mockReturnValue(mockTransport); - getEntityRefMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE.items[0]); + catalog.getEntityByRef.mockResolvedValue( + DEFAULT_ENTITIES_RESPONSE.items[0], + ); const processor = new NotificationsEmailProcessor( logger, mockServices.rootConfig({ data: DEFAULT_SENDMAIL_CONFIG }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -251,7 +248,7 @@ describe('NotificationsEmailProcessor', () => { it('should send email to all', async () => { (createTransport as jest.Mock).mockReturnValue(mockTransport); - getEntitiesMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE); + catalog.getEntities.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE); const processor = new NotificationsEmailProcessor( logger, mockServices.rootConfig({ @@ -269,7 +266,7 @@ describe('NotificationsEmailProcessor', () => { }, }, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -299,7 +296,7 @@ describe('NotificationsEmailProcessor', () => { it('should send email to configured addresses', async () => { (createTransport as jest.Mock).mockReturnValue(mockTransport); - getEntitiesMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE); + catalog.getEntities.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE); const processor = new NotificationsEmailProcessor( logger, mockServices.rootConfig({ @@ -318,7 +315,7 @@ describe('NotificationsEmailProcessor', () => { }, }, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -348,13 +345,15 @@ describe('NotificationsEmailProcessor', () => { it('should send email with relative link to given address', async () => { (createTransport as jest.Mock).mockReturnValue(mockTransport); - getEntityRefMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE.items[0]); + catalog.getEntityByRef.mockResolvedValue( + DEFAULT_ENTITIES_RESPONSE.items[0], + ); const processor = new NotificationsEmailProcessor( logger, mockServices.rootConfig({ data: DEFAULT_SENDMAIL_CONFIG, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); @@ -413,13 +412,15 @@ describe('NotificationsEmailProcessor', () => { it('should send email with absolute link to given address', async () => { (createTransport as jest.Mock).mockReturnValue(mockTransport); - getEntityRefMock.mockResolvedValue(DEFAULT_ENTITIES_RESPONSE.items[0]); + catalog.getEntityByRef.mockResolvedValue( + DEFAULT_ENTITIES_RESPONSE.items[0], + ); const processor = new NotificationsEmailProcessor( logger, mockServices.rootConfig({ data: DEFAULT_SENDMAIL_CONFIG, }), - mockCatalogClient as unknown as CatalogClient, + catalog, auth, ); diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts index b06d6f203b..a5ce5d3ff8 100644 --- a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts @@ -13,44 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockServices } from '@backstage/backend-test-utils'; import { getUsersForEntityRef } from './getUsersForEntityRef'; -import { CatalogApi } from '@backstage/catalog-client'; import { RELATION_HAS_MEMBER, RELATION_OWNED_BY, RELATION_PARENT_OF, } from '@backstage/catalog-model'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('getUsersForEntityRef', () => { - const catalogApiMock = { - getEntitiesByRefs: jest.fn(), - getEntityByRef: jest.fn(), - }; - const authMock = mockServices.auth(); - it('should return empty array if entityRef is null', async () => { await expect( getUsersForEntityRef(null, [], { - auth: authMock, - catalogClient: catalogApiMock as unknown as CatalogApi, + auth: mockServices.auth(), + catalogClient: catalogServiceMock.mock(), }), ).resolves.toEqual([]); }); it('should resolve users without calling catalog', async () => { + const catalogClient = catalogServiceMock.mock(); await expect( getUsersForEntityRef(['user:foo', 'user:ignored'], ['user:ignored'], { - auth: authMock, - catalogClient: catalogApiMock as unknown as CatalogApi, + auth: mockServices.auth(), + catalogClient, }), ).resolves.toEqual(['user:foo']); - expect(catalogApiMock.getEntitiesByRefs).not.toHaveBeenCalled(); + expect(catalogClient.getEntitiesByRefs).not.toHaveBeenCalled(); }); it('should resolve group entities to users', async () => { - catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ - items: [ + const catalogClient = catalogServiceMock({ + entities: [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', @@ -68,11 +64,6 @@ describe('getUsersForEntityRef', () => { }, ], }, - ], - }); - - catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ - items: [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', @@ -98,16 +89,16 @@ describe('getUsersForEntityRef', () => { 'group:default/parent_group', ['user:default/ignored'], { - auth: authMock, - catalogClient: catalogApiMock as unknown as CatalogApi, + auth: mockServices.auth(), + catalogClient, }, ), ).resolves.toEqual(['user:default/foo', 'user:default/bar']); }); it('should resolve user owner of entity from entity ref', async () => { - catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ - items: [ + const catalogClient = catalogServiceMock({ + entities: [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -126,15 +117,15 @@ describe('getUsersForEntityRef', () => { await expect( getUsersForEntityRef('component:default/test_component', [], { - auth: authMock, - catalogClient: catalogApiMock as unknown as CatalogApi, + auth: mockServices.auth(), + catalogClient, }), ).resolves.toEqual(['user:default/foo']); }); it('should resolve group owner of entity from entity ref', async () => { - catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ - items: [ + const catalogClient = catalogServiceMock({ + entities: [ { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -148,27 +139,26 @@ describe('getUsersForEntityRef', () => { }, ], }, - ], - }); - - catalogApiMock.getEntityByRef.mockResolvedValueOnce({ - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: 'owner_group', - }, - relations: [ { - type: RELATION_HAS_MEMBER, - targetRef: 'user:default/foo', + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'owner_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + ], }, ], }); await expect( getUsersForEntityRef('component:default/test_component', [], { - auth: authMock, - catalogClient: catalogApiMock as unknown as CatalogApi, + auth: mockServices.auth(), + catalogClient, }), ).resolves.toEqual(['user:default/foo']); }); diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts index b90ebcaf50..5b7a1bb7a0 100644 --- a/plugins/notifications-backend/src/service/router.test.ts +++ b/plugins/notifications-backend/src/service/router.test.ts @@ -20,13 +20,12 @@ import { } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; - import { createRouter } from './router'; import { ConfigReader } from '@backstage/config'; import { SignalsService } from '@backstage/plugin-signals-node'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; import { NotificationSendOptions } from '@backstage/plugin-notifications-node'; -import { CatalogClient } from '@backstage/catalog-client'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; function createDatabase(): PluginDatabaseManager { return DatabaseManager.fromConfig( @@ -56,9 +55,7 @@ describe('createRouter', () => { const config = mockServices.rootConfig({ data: { app: { baseUrl: 'http://localhost' } }, }); - const catalog = new CatalogClient({ - discoveryApi: mockServices.discovery.mock(), - }); + const catalog = catalogServiceMock.mock(); beforeAll(async () => { const router = await createRouter({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts index 549ed923c2..155a58cbac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts @@ -15,24 +15,18 @@ */ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; import { examples } from './fetch.examples'; import yaml from 'yaml'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('catalog:fetch examples', () => { - const getEntityByRef = jest.fn(); - const getEntitiesByRefs = jest.fn(); - - const catalogClient = { - getEntityByRef: getEntityByRef, - getEntitiesByRefs: getEntitiesByRefs, - }; + const catalogClient = catalogServiceMock.mock(); const action = createFetchCatalogEntityAction({ - catalogClient: catalogClient as unknown as CatalogApi, + catalogClient, auth: mockServices.auth(), }); @@ -46,13 +40,14 @@ describe('catalog:fetch examples', () => { const mockContext = createMockActionContext({ secrets: { backstageToken: token }, }); + beforeEach(() => { jest.resetAllMocks(); }); describe('fetch single entity', () => { it('should return entity from catalog', async () => { - getEntityByRef.mockReturnValueOnce({ + catalogClient.getEntityByRef.mockResolvedValueOnce({ metadata: { namespace: 'default', name: 'name', @@ -65,9 +60,10 @@ describe('catalog:fetch examples', () => { input: yaml.parse(examples[0].example).steps[0].input, }); - expect(getEntityByRef).toHaveBeenCalledWith('component:default/name', { - token, - }); + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith( + 'component:default/name', + { token }, + ); expect(mockContext.output).toHaveBeenCalledWith('entity', { metadata: { namespace: 'default', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts index 747414f206..f4ed8595c6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.test.ts @@ -15,22 +15,16 @@ */ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { createFetchCatalogEntityAction } from './fetch'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('catalog:fetch', () => { - const getEntityByRef = jest.fn(); - const getEntitiesByRefs = jest.fn(); - - const catalogClient = { - getEntityByRef: getEntityByRef, - getEntitiesByRefs: getEntitiesByRefs, - }; + const catalogClient = catalogServiceMock.mock(); const action = createFetchCatalogEntityAction({ - catalogClient: catalogClient as unknown as CatalogApi, + catalogClient, auth: mockServices.auth(), }); @@ -51,7 +45,7 @@ describe('catalog:fetch', () => { describe('fetch single entity', () => { it('should return entity from catalog', async () => { - getEntityByRef.mockReturnValueOnce({ + catalogClient.getEntityByRef.mockResolvedValueOnce({ metadata: { namespace: 'default', name: 'test', @@ -66,9 +60,12 @@ describe('catalog:fetch', () => { }, }); - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token, - }); + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith( + 'component:default/test', + { + token, + }, + ); expect(mockContext.output).toHaveBeenCalledWith('entity', { metadata: { namespace: 'default', @@ -79,7 +76,7 @@ describe('catalog:fetch', () => { }); it('should throw error if entity fetch fails from catalog and optional is false', async () => { - getEntityByRef.mockImplementationOnce(() => { + catalogClient.getEntityByRef.mockImplementationOnce(() => { throw new Error('Not found'); }); @@ -92,14 +89,17 @@ describe('catalog:fetch', () => { }), ).rejects.toThrow('Not found'); - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token, - }); + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith( + 'component:default/test', + { + token, + }, + ); expect(mockContext.output).not.toHaveBeenCalled(); }); it('should throw error if entity not in catalog and optional is false', async () => { - getEntityByRef.mockReturnValueOnce(null); + catalogClient.getEntityByRef.mockResolvedValueOnce(null as any); await expect( action.handler({ @@ -110,9 +110,12 @@ describe('catalog:fetch', () => { }), ).rejects.toThrow('Entity component:default/test not found'); - expect(getEntityByRef).toHaveBeenCalledWith('component:default/test', { - token, - }); + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith( + 'component:default/test', + { + token, + }, + ); expect(mockContext.output).not.toHaveBeenCalled(); }); @@ -124,7 +127,7 @@ describe('catalog:fetch', () => { }, kind: 'Group', } as Entity; - getEntityByRef.mockReturnValueOnce(entity); + catalogClient.getEntityByRef.mockResolvedValueOnce(entity); await action.handler({ ...mockContext, @@ -135,16 +138,19 @@ describe('catalog:fetch', () => { }, }); - expect(getEntityByRef).toHaveBeenCalledWith('group:ns/test', { - token, - }); + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith( + 'group:ns/test', + { + token, + }, + ); expect(mockContext.output).toHaveBeenCalledWith('entity', entity); }); }); describe('fetch multiple entities', () => { it('should return entities from catalog', async () => { - getEntitiesByRefs.mockReturnValueOnce({ + catalogClient.getEntitiesByRefs.mockResolvedValueOnce({ items: [ { metadata: { @@ -163,7 +169,7 @@ describe('catalog:fetch', () => { }, }); - expect(getEntitiesByRefs).toHaveBeenCalledWith( + expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith( { entityRefs: ['component:default/test'] }, { token, @@ -181,7 +187,7 @@ describe('catalog:fetch', () => { }); it('should throw error if undefined is returned for some entity', async () => { - getEntitiesByRefs.mockReturnValueOnce({ + catalogClient.getEntitiesByRefs.mockResolvedValueOnce({ items: [ { metadata: { @@ -204,7 +210,7 @@ describe('catalog:fetch', () => { }), ).rejects.toThrow('Entity component:default/test2 not found'); - expect(getEntitiesByRefs).toHaveBeenCalledWith( + expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith( { entityRefs: ['component:default/test', 'component:default/test2'] }, { token, @@ -214,7 +220,7 @@ describe('catalog:fetch', () => { }); it('should return null in case some of the entities not found and optional is true', async () => { - getEntitiesByRefs.mockReturnValueOnce({ + catalogClient.getEntitiesByRefs.mockResolvedValueOnce({ items: [ { metadata: { @@ -235,7 +241,7 @@ describe('catalog:fetch', () => { }, }); - expect(getEntitiesByRefs).toHaveBeenCalledWith( + expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith( { entityRefs: ['component:default/test', 'component:default/test2'] }, { token, @@ -268,7 +274,7 @@ describe('catalog:fetch', () => { }, kind: 'User', } as Entity; - getEntitiesByRefs.mockReturnValueOnce({ + catalogClient.getEntitiesByRefs.mockResolvedValueOnce({ items: [entity1, entity2], }); @@ -281,7 +287,7 @@ describe('catalog:fetch', () => { }, }); - expect(getEntitiesByRefs).toHaveBeenCalledWith( + expect(catalogClient.getEntitiesByRefs).toHaveBeenCalledWith( { entityRefs: ['group:ns/test', 'user:default/test'] }, { token, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts index d279af78dc..27690b0df3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts @@ -15,7 +15,6 @@ */ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createCatalogRegisterAction } from './register'; @@ -23,6 +22,7 @@ import { Entity } from '@backstage/catalog-model'; import { examples } from './register.examples'; import yaml from 'yaml'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('catalog:register', () => { const integrations = ScmIntegrations.fromConfig( @@ -33,14 +33,11 @@ describe('catalog:register', () => { }), ); - const addLocation = jest.fn(); - const catalogClient = { - addLocation: addLocation, - }; + const catalogClient = catalogServiceMock.mock(); const action = createCatalogRegisterAction({ integrations, - catalogClient: catalogClient as unknown as CatalogApi, + catalogClient, auth: mockServices.auth(), }); @@ -57,11 +54,13 @@ describe('catalog:register', () => { }); it('should register location in catalog', async () => { - addLocation + catalogClient.addLocation .mockResolvedValueOnce({ + location: null as any, entities: [], }) .mockResolvedValueOnce({ + location: null as any, entities: [ { metadata: { @@ -77,7 +76,7 @@ describe('catalog:register', () => { input: yaml.parse(examples[0].example).steps[0].input, }); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 1, { type: 'url', @@ -86,7 +85,7 @@ describe('catalog:register', () => { }, { token }, ); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 2, { dryRun: true, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts index 2db232afa0..6d4f7de0d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.test.ts @@ -15,12 +15,12 @@ */ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; -import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { createCatalogRegisterAction } from './register'; import { Entity } from '@backstage/catalog-model'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('catalog:register', () => { const integrations = ScmIntegrations.fromConfig( @@ -31,14 +31,11 @@ describe('catalog:register', () => { }), ); - const addLocation = jest.fn(); - const catalogClient = { - addLocation: addLocation, - }; + const catalogClient = catalogServiceMock.mock(); const action = createCatalogRegisterAction({ integrations, - catalogClient: catalogClient as unknown as CatalogApi, + catalogClient, auth: mockServices.auth(), }); @@ -69,11 +66,13 @@ describe('catalog:register', () => { }); it('should register location in catalog', async () => { - addLocation + catalogClient.addLocation .mockResolvedValueOnce({ + location: null as any, entities: [], }) .mockResolvedValueOnce({ + location: null as any, entities: [ { metadata: { @@ -91,7 +90,7 @@ describe('catalog:register', () => { }, }); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 1, { type: 'url', @@ -99,7 +98,7 @@ describe('catalog:register', () => { }, { token }, ); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 2, { dryRun: true, @@ -120,11 +119,13 @@ describe('catalog:register', () => { }); it('should return entityRef with the Component entity and not the generated location', async () => { - addLocation + catalogClient.addLocation .mockResolvedValueOnce({ + location: null as any, entities: [], }) .mockResolvedValueOnce({ + location: null as any, entities: [ { metadata: { @@ -169,11 +170,13 @@ describe('catalog:register', () => { }); it('should return entityRef with the next non-generated entity if no Component kind can be found', async () => { - addLocation + catalogClient.addLocation .mockResolvedValueOnce({ + location: null as any, entities: [], }) .mockResolvedValueOnce({ + location: null as any, entities: [ { metadata: { @@ -211,11 +214,13 @@ describe('catalog:register', () => { }); it('should return entityRef with the first entity if no non-generated entities can be found', async () => { - addLocation + catalogClient.addLocation .mockResolvedValueOnce({ + location: null as any, entities: [], }) .mockResolvedValueOnce({ + location: null as any, entities: [ { metadata: { @@ -246,11 +251,13 @@ describe('catalog:register', () => { }); it('should not return entityRef if there are no entites', async () => { - addLocation + catalogClient.addLocation .mockResolvedValueOnce({ + location: null as any, entities: [], }) .mockResolvedValueOnce({ + location: null as any, entities: [], }); await action.handler({ @@ -266,7 +273,7 @@ describe('catalog:register', () => { }); it('should ignore failures when dry running the location in the catalog if `optional` is set', async () => { - addLocation + catalogClient.addLocation .mockRejectedValueOnce(new Error('Not found')) .mockRejectedValueOnce(new Error('Not found')); await action.handler({ @@ -277,7 +284,7 @@ describe('catalog:register', () => { }, }); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 1, { type: 'url', @@ -285,7 +292,7 @@ describe('catalog:register', () => { }, { token }, ); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 2, { dryRun: true, @@ -302,9 +309,10 @@ describe('catalog:register', () => { }); it('should fetch entities when adding location in the catalog fails and `optional` is set', async () => { - addLocation + catalogClient.addLocation .mockRejectedValueOnce(new Error('Already registered')) .mockResolvedValueOnce({ + location: null as any, entities: [ { metadata: { @@ -323,7 +331,7 @@ describe('catalog:register', () => { }, }); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 1, { type: 'url', @@ -331,7 +339,7 @@ describe('catalog:register', () => { }, { token }, ); - expect(addLocation).toHaveBeenNthCalledWith( + expect(catalogClient.addLocation).toHaveBeenNthCalledWith( 2, { dryRun: true, diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 9673d19255..3410486104 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -19,7 +19,6 @@ import { loggerToWinstonLogger, PluginDatabaseManager, } from '@backstage/backend-common'; -import { CatalogApi } from '@backstage/catalog-client'; import { ConfigReader } from '@backstage/config'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import express from 'express'; @@ -51,6 +50,7 @@ import { } from '@backstage/backend-test-utils'; import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; import { UrlReaders } from '@backstage/backend-defaults/urlReader'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; const mockAccess = jest.fn(); @@ -91,7 +91,7 @@ describe('createRouter', () => { let app: express.Express; let loggerSpy: jest.SpyInstance; let taskBroker: TaskBroker; - const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi; + const catalogClient = catalogServiceMock.mock(); const permissionApi = { authorize: jest.fn(), authorizeConditional: jest.fn(), @@ -214,21 +214,19 @@ describe('createRouter', () => { }); app = express().use(router); - jest - .spyOn(catalogClient, 'getEntityByRef') - .mockImplementation(async ref => { - const { kind } = parseEntityRef(ref); + catalogClient.getEntityByRef.mockImplementation(async ref => { + const { kind } = parseEntityRef(ref); - if (kind.toLocaleLowerCase() === 'template') { - return getMockTemplate(); - } + if (kind.toLocaleLowerCase() === 'template') { + return getMockTemplate(); + } - if (kind.toLocaleLowerCase() === 'user') { - return mockUser; - } + if (kind.toLocaleLowerCase() === 'user') { + return mockUser; + } - throw new Error(`no mock found for kind: ${kind}`); - }); + throw new Error(`no mock found for kind: ${kind}`); + }); jest .spyOn(permissionApi, 'authorizeConditional') @@ -703,8 +701,6 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ const mockToken = mockCredentials.user.token(); const mockTemplate = getMockTemplate(); - const catalogSpy = jest.spyOn(catalogClient, 'getEntityByRef'); - await request(app) .post('/v2/dry-run') .set('Authorization', `Bearer ${mockToken}`) @@ -717,9 +713,9 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ directoryContents: [], }); - expect(catalogSpy).toHaveBeenCalledTimes(1); + expect(catalogClient.getEntityByRef).toHaveBeenCalledTimes(1); - expect(catalogSpy).toHaveBeenCalledWith( + expect(catalogClient.getEntityByRef).toHaveBeenCalledWith( 'user:default/mock', expect.anything(), ); @@ -755,20 +751,18 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ }); app = express().use(router); - jest - .spyOn(catalogClient, 'getEntityByRef') - .mockImplementation(async ref => { - const { kind } = parseEntityRef(ref); + catalogClient.getEntityByRef.mockImplementation(async ref => { + const { kind } = parseEntityRef(ref); - if (kind.toLocaleLowerCase() === 'template') { - return getMockTemplate(); - } + if (kind.toLocaleLowerCase() === 'template') { + return getMockTemplate(); + } - if (kind.toLocaleLowerCase() === 'user') { - return mockUser; - } - throw new Error(`no mock found for kind: ${kind}`); - }); + if (kind.toLocaleLowerCase() === 'user') { + return mockUser; + } + throw new Error(`no mock found for kind: ${kind}`); + }); jest .spyOn(permissionApi, 'authorizeConditional') diff --git a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts index c4673c6fa4..492f315d58 100644 --- a/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts +++ b/plugins/techdocs-backend/src/service/CachedEntityLoader.test.ts @@ -13,20 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CacheService } from '@backstage/backend-plugin-api'; + import { CachedEntityLoader } from './CachedEntityLoader'; -import { CatalogApi } from '@backstage/catalog-client'; import { CompoundEntityRef } from '@backstage/catalog-model'; +import { mockServices } from '@backstage/backend-test-utils'; +import { catalogServiceMock } from '@backstage/plugin-catalog-node/testUtils'; describe('CachedEntityLoader', () => { - const catalog: jest.Mocked = { - getEntityByRef: jest.fn(), - } as any; - - const cache: jest.Mocked = { - get: jest.fn(), - set: jest.fn(), - } as any; + const cache = mockServices.cache.mock(); const entityName: CompoundEntityRef = { kind: 'component', @@ -45,16 +39,15 @@ describe('CachedEntityLoader', () => { const token = 'test-token'; - const loader = new CachedEntityLoader({ catalog, cache }); - afterEach(() => { jest.resetAllMocks(); }); it('writes entities to cache', async () => { cache.get.mockResolvedValue(undefined); - catalog.getEntityByRef.mockResolvedValue(entity); + const catalog = catalogServiceMock({ entities: [entity] }); + const loader = new CachedEntityLoader({ catalog, cache }); const result = await loader.load(entityName, token); expect(result).toEqual(entity); @@ -66,8 +59,10 @@ describe('CachedEntityLoader', () => { }); it('returns entities from cache', async () => { + const catalog = catalogServiceMock.mock(); cache.get.mockResolvedValue(entity); + const loader = new CachedEntityLoader({ catalog, cache }); const result = await loader.load(entityName, token); expect(result).toEqual(entity); @@ -75,9 +70,10 @@ describe('CachedEntityLoader', () => { }); it('does not cache missing entites', async () => { + const catalog = catalogServiceMock({ entities: [] }); cache.get.mockResolvedValue(undefined); - catalog.getEntityByRef.mockResolvedValue(undefined); + const loader = new CachedEntityLoader({ catalog, cache }); const result = await loader.load(entityName, token); expect(result).toBeUndefined(); @@ -85,9 +81,10 @@ describe('CachedEntityLoader', () => { }); it('uses entity ref as cache key for anonymous users', async () => { + const catalog = catalogServiceMock({ entities: [entity] }); cache.get.mockResolvedValue(undefined); - catalog.getEntityByRef.mockResolvedValue(entity); + const loader = new CachedEntityLoader({ catalog, cache }); const result = await loader.load(entityName, undefined); expect(result).toEqual(entity); @@ -107,8 +104,9 @@ describe('CachedEntityLoader', () => { setTimeout(() => resolve(undefined), 10000); }), ); - catalog.getEntityByRef.mockResolvedValue(entity); + const catalog = catalogServiceMock({ entities: [entity] }); + const loader = new CachedEntityLoader({ catalog, cache }); const result = await loader.load(entityName, token); expect(result).toEqual(entity); From 29e57c795eff874f89bbff7c3b6fd4eed690ea2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Sep 2024 13:21:51 +0200 Subject: [PATCH 04/11] changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/weak-monkeys-occur.md | 6 +++++ .../catalog-client/api-report-testUtils.md | 2 +- .../src/testUtils/InMemoryCatalogClient.ts | 8 ++++++- plugins/catalog-node/api-report-testUtils.md | 18 +++++++-------- .../src/testUtils/catalogServiceMock.ts | 23 +++++++++++++++++-- 5 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 .changeset/weak-monkeys-occur.md diff --git a/.changeset/weak-monkeys-occur.md b/.changeset/weak-monkeys-occur.md new file mode 100644 index 0000000000..8025e10af5 --- /dev/null +++ b/.changeset/weak-monkeys-occur.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-client': minor +'@backstage/plugin-catalog-node': minor +--- + +Add catalog service mocks under the `/testUtils` subpath export. diff --git a/packages/catalog-client/api-report-testUtils.md b/packages/catalog-client/api-report-testUtils.md index a33470e35d..47e917450c 100644 --- a/packages/catalog-client/api-report-testUtils.md +++ b/packages/catalog-client/api-report-testUtils.md @@ -21,7 +21,7 @@ import { QueryEntitiesRequest } from '@backstage/catalog-client'; import { QueryEntitiesResponse } from '@backstage/catalog-client'; import { ValidateEntityResponse } from '@backstage/catalog-client'; -// @public (undocumented) +// @public export class InMemoryCatalogClient implements CatalogApi { constructor(options?: { entities?: Entity[] }); // (undocumented) diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index ff01c99532..cde018a55c 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -38,7 +38,13 @@ import { } from '@backstage/catalog-model'; import { NotFoundError, NotImplementedError } from '@backstage/errors'; -/** @public */ +/** + * Implements a VERY basic fake catalog client that stores entities in memory. + * It has severely limited functionality, and is only useful under certain + * circumstances in tests. + * + * @public + */ export class InMemoryCatalogClient implements CatalogApi { #entities: Entity[]; diff --git a/plugins/catalog-node/api-report-testUtils.md b/plugins/catalog-node/api-report-testUtils.md index f3e465c7ac..0a3b392bad 100644 --- a/plugins/catalog-node/api-report-testUtils.md +++ b/plugins/catalog-node/api-report-testUtils.md @@ -9,21 +9,19 @@ import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceMock } from '@backstage/backend-test-utils'; -// @public (undocumented) +// @public export function catalogServiceMock(options?: { entities?: Entity[]; }): InMemoryCatalogClient; -// @public (undocumented) +// @public export namespace catalogServiceMock { - const // (undocumented) - factory: (options?: { - entities?: Entity[]; - }) => ServiceFactory; - const // (undocumented) - mock: ( - partialImpl?: Partial | undefined, - ) => ServiceMock; + const factory: (options?: { + entities?: Entity[]; + }) => ServiceFactory; + const mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (No @packageDocumentation comment for this package) diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts index dc73176a84..5eeb704ff8 100644 --- a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts @@ -50,19 +50,38 @@ function simpleMock( }; } -/** @public */ +/** + * Creates a fake catalog client that handles entities in memory storage. Note + * that this client may be severely limited in functionality, and advanced + * functions may not be available at all. + * + * @public + */ export function catalogServiceMock(options?: { entities?: Entity[] }) { return new InMemoryCatalogClient(options); } -/** @public */ +/** + * A collection of mock functionality for the catalog service. + * + * @public + */ export namespace catalogServiceMock { + /** + * Creates a fake catalog client that handles entities in memory storage. Note + * that this client may be severely limited in functionality, and advanced + * functions may not be available at all. + */ export const factory = (options?: { entities?: Entity[] }) => createServiceFactory({ service: catalogServiceRef, deps: {}, factory: () => new InMemoryCatalogClient(options), }); + /** + * Creates a catalog client whose methods are mock functions, possibly with + * some of them overloaded by the caller. + */ export const mock = simpleMock(catalogServiceRef, () => ({ getEntities: jest.fn(), getEntitiesByRefs: jest.fn(), From 6f222ac773d3e1affabb0a4c4aab836c8215651c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 11 Sep 2024 13:50:25 +0200 Subject: [PATCH 05/11] add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../testUtils/InMemoryCatalogClient.test.ts | 109 ++++++++++++++++++ .../src/testUtils/InMemoryCatalogClient.ts | 6 +- .../src/testUtils/catalogServiceMock.test.ts | 63 ++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts create mode 100644 plugins/catalog-node/src/testUtils/catalogServiceMock.test.ts diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts new file mode 100644 index 0000000000..0b5f0569bb --- /dev/null +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { InMemoryCatalogClient } from './InMemoryCatalogClient'; +import { Entity } from '@backstage/catalog-model'; + +const entity1: Entity = { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'e1', + uid: 'u1', + }, +}; + +const entity2: Entity = { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'e2', + uid: 'u2', + }, +}; + +const entities = [entity1, entity2]; + +describe('InMemoryCatalogClient', () => { + it('getEntities', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect(client.getEntities()).resolves.toEqual({ items: entities }); + }); + + it('getEntitiesByRefs', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect( + client.getEntitiesByRefs({ + entityRefs: [ + 'customkind:default/e2', + 'customkind:missing/missing', + 'customkind:default/e1', + ], + }), + ).resolves.toEqual({ items: [entity2, undefined, entity1] }); + }); + + it('queryEntities', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect(client.queryEntities()).resolves.toEqual({ + items: entities, + totalItems: 2, + pageInfo: {}, + }); + }); + + it('getEntityAncestors', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect( + client.getEntityAncestors({ entityRef: 'customkind:default/e2' }), + ).resolves.toEqual({ + rootEntityRef: 'customkind:default/e2', + items: [{ entity: entity2, parentEntityRefs: [] }], + }); + }); + + it('getEntityByRef', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect( + client.getEntityByRef('customkind:default/e2'), + ).resolves.toEqual(entity2); + await expect( + client.getEntityByRef('customkind:missing/missing'), + ).resolves.toBeUndefined(); + }); + + it('removeEntityByUid', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect(client.getEntities()).resolves.toEqual({ + items: expect.arrayContaining([entity2]), + }); + await expect( + client.removeEntityByUid(entity2.metadata.uid!), + ).resolves.toBeUndefined(); + await expect(client.getEntities()).resolves.not.toEqual({ + items: expect.arrayContaining([entity2]), + }); + }); + + it('refreshEntity', async () => { + const client = new InMemoryCatalogClient({ entities }); + await expect( + client.refreshEntity('customkind:default/e2'), + ).resolves.toBeUndefined(); + }); +}); diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index cde018a55c..745273c6bc 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -49,14 +49,14 @@ export class InMemoryCatalogClient implements CatalogApi { #entities: Entity[]; constructor(options?: { entities?: Entity[] }) { - this.#entities = options?.entities ?? []; + this.#entities = options?.entities?.slice() ?? []; } async getEntities( _request?: GetEntitiesRequest, ): Promise { // TODO(freben): Fields, filters etc - return { items: this.#entities }; + return { items: this.#entities.slice() }; } async getEntitiesByRefs( @@ -80,7 +80,7 @@ export class InMemoryCatalogClient implements CatalogApi { ): Promise { // TODO(freben): Fields, filters etc return { - items: this.#entities, + items: this.#entities.slice(), totalItems: this.#entities.length, pageInfo: {}, }; diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.test.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.test.ts new file mode 100644 index 0000000000..47850dd600 --- /dev/null +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 { Entity } from '@backstage/catalog-model'; +import { catalogServiceMock } from './catalogServiceMock'; + +const entity1: Entity = { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'e1', + uid: 'u1', + }, +}; + +const entity2: Entity = { + apiVersion: 'v1', + kind: 'CustomKind', + metadata: { + namespace: 'default', + name: 'e2', + uid: 'u2', + }, +}; + +const entities = [entity1, entity2]; + +describe('catalogServiceMock', () => { + it('exports the expected functionality', async () => { + const emptyFake = catalogServiceMock(); + const notEmptyFake = catalogServiceMock({ entities }); + + await expect(emptyFake.getEntities()).resolves.toEqual({ items: [] }); + await expect(notEmptyFake.getEntities()).resolves.toEqual({ + items: entities, + }); + + const mock = catalogServiceMock.mock(); + expect(mock.getEntities).toHaveBeenCalledTimes(0); + expect(mock.getEntities()).toBeUndefined(); + mock.getEntities.mockResolvedValue({ items: entities }); + await expect(mock.getEntities()).resolves.toEqual({ items: entities }); + + const mock2 = catalogServiceMock.mock({ + getEntities: async () => ({ items: [entity1] }), + }); + await expect(mock2.getEntities()).resolves.toEqual({ items: [entity1] }); + }); +}); From c0300626b25bf612b2b063e5b76d7c520b12710e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Sep 2024 00:21:40 +0200 Subject: [PATCH 06/11] catalog-client: filtering and facets for in-memory client Signed-off-by: Patrik Oldsberg --- packages/catalog-client/package.json | 1 + .../src/testUtils/InMemoryCatalogClient.ts | 149 ++++++++++++++---- yarn.lock | 1 + 3 files changed, 123 insertions(+), 28 deletions(-) diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 4119614503..03addce840 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -50,6 +50,7 @@ "dependencies": { "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", "uri-template": "^2.0.0" }, diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index 745273c6bc..a975a79b80 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -17,7 +17,9 @@ import { AddLocationRequest, AddLocationResponse, + CATALOG_FILTER_EXISTS, CatalogApi, + EntityFilterQuery, GetEntitiesByRefsRequest, GetEntitiesByRefsResponse, GetEntitiesRequest, @@ -34,9 +36,72 @@ import { import { CompoundEntityRef, Entity, + parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { NotFoundError, NotImplementedError } from '@backstage/errors'; +import { JsonObject, JsonValue } from '@backstage/types'; + +function pick(obj: JsonObject, key: string): JsonValue | undefined { + const parts = key.split('.'); + + return parts.reduce((acc, part, index) => { + if (!acc) { + return acc; + } + if (typeof acc === 'object' && acc !== null && !Array.isArray(acc)) { + const value = acc[part]; + if (value) { + return value; + } + const rest = parts.slice(index).join('.'); + const restValue = acc[rest]; + if (restValue) { + return restValue; + } + } + return undefined; + }, obj as JsonValue | undefined); +} + +function filterCompare( + expected: (string | symbol) | (string | symbol)[], + value: JsonValue, +) { + const expect = (Array.isArray(expected) ? expected : [expected]).map(e => + e.toString().toLocaleLowerCase('en-US'), + ); + return expect.includes(String(value).toLocaleLowerCase('en-US')); +} + +function createFilter( + filterOrFilters?: EntityFilterQuery, +): (entity: Entity) => boolean { + if (!filterOrFilters) { + return () => true; + } + const filters = Array.isArray(filterOrFilters) + ? filterOrFilters + : [filterOrFilters]; + + return entity => { + return filters.some(filter => { + for (const [key, expectedValue] of Object.entries(filter)) { + const entityValue = pick(entity, key); + if (entityValue === undefined) { + return false; + } + if (expectedValue === CATALOG_FILTER_EXISTS) { + continue; + } + if (!filterCompare(expectedValue, entityValue)) { + return false; + } + } + return true; + }); + }; +} /** * Implements a VERY basic fake catalog client that stores entities in memory. @@ -47,78 +112,106 @@ import { NotFoundError, NotImplementedError } from '@backstage/errors'; */ export class InMemoryCatalogClient implements CatalogApi { #entities: Entity[]; + #entitiesByRef: Map; constructor(options?: { entities?: Entity[] }) { this.#entities = options?.entities?.slice() ?? []; + this.#entitiesByRef = new Map( + this.#entities.map(e => [stringifyEntityRef(e), e]), + ); } async getEntities( - _request?: GetEntitiesRequest, + request?: GetEntitiesRequest | undefined, ): Promise { - // TODO(freben): Fields, filters etc - return { items: this.#entities.slice() }; + const filter = createFilter(request?.filter); + return { items: this.#entities.filter(filter) }; } async getEntitiesByRefs( request: GetEntitiesByRefsRequest, ): Promise { - const entitiesByRef = new Map(); - for (const entity of this.#entities) { - entitiesByRef.set(stringifyEntityRef(entity), entity); - } - - const items = request.entityRefs.map(candidateRef => - entitiesByRef.get(candidateRef), - ); - - // TODO(freben): Fields, filters etc - return { items }; + const filter = createFilter(request.filter); + return { + items: request.entityRefs + .map(ref => this.#entitiesByRef.get(ref)) + .filter(e => (e ? filter(e) : true)), + }; } async queryEntities( - _request?: QueryEntitiesRequest, + request?: QueryEntitiesRequest | undefined, ): Promise { - // TODO(freben): Fields, filters etc + if (request && 'cursor' in request) { + return { items: [], pageInfo: {}, totalItems: 0 }; + } + const filter = createFilter(request?.filter); + // TODO(Rugvip): Pagination return { - items: this.#entities.slice(), - totalItems: this.#entities.length, + items: this.#entities.filter(filter), pageInfo: {}, + totalItems: this.#entities.length, }; } async getEntityAncestors( request: GetEntityAncestorsRequest, ): Promise { - const entity = this.#entities.find( - e => stringifyEntityRef(e) === request.entityRef, - ); + const entity = this.#entitiesByRef.get(request.entityRef); if (!entity) { throw new NotFoundError(`Entity with ref ${request.entityRef} not found`); } return { - rootEntityRef: request.entityRef, items: [{ entity, parentEntityRefs: [] }], + rootEntityRef: request.entityRef, }; } async getEntityByRef( entityRef: string | CompoundEntityRef, ): Promise { - const ref = - typeof entityRef === 'string' ? entityRef : stringifyEntityRef(entityRef); - return this.#entities.find(e => stringifyEntityRef(e) === ref); + return this.#entitiesByRef.get( + stringifyEntityRef(parseEntityRef(entityRef)), + ); } async removeEntityByUid(uid: string): Promise { - this.#entities = this.#entities.filter(e => e.metadata.uid !== uid); + const index = this.#entities.findIndex(e => e.metadata.uid === uid); + if (index !== -1) { + const entity = this.#entities[index]; + this.#entitiesByRef.delete(stringifyEntityRef(entity)); + this.#entities.splice(index, 1); + } } async refreshEntity(_entityRef: string): Promise {} async getEntityFacets( - _request: GetEntityFacetsRequest, + request: GetEntityFacetsRequest, ): Promise { - throw new NotImplementedError('Method not implemented.'); + const filter = createFilter(request.filter); + const filteredEntities = this.#entities.filter(filter); + const facets = Object.fromEntries( + request.facets.map(facet => { + const facetValues = new Map(); + for (const entity of filteredEntities) { + const value = pick(entity, facet); + if (value) { + facetValues.set( + String(value), + (facetValues.get(String(value)) ?? 0) + 1, + ); + } + } + const counts = Array.from(facetValues.entries()).map( + ([value, count]) => ({ value, count }), + ); + return [facet, counts]; + }), + ); + return { + facets, + }; } async getLocationById(_id: string): Promise { diff --git a/yarn.lock b/yarn.lock index 03cb6f421c..9aec0defac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3840,6 +3840,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/types": "workspace:^" cross-fetch: ^4.0.0 msw: ^1.0.0 uri-template: ^2.0.0 From a22687112e4bac17f5c1160b84cd66c755c3db6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 12 Sep 2024 09:50:39 +0200 Subject: [PATCH 07/11] fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-client/api-report-testUtils.md | 8 +++--- .../testUtils/InMemoryCatalogClient.test.ts | 20 ++++++++++++++ .../src/testUtils/InMemoryCatalogClient.ts | 26 +++++++++++-------- .../src/testUtils/catalogServiceMock.ts | 5 +++- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/catalog-client/api-report-testUtils.md b/packages/catalog-client/api-report-testUtils.md index 47e917450c..3962cacce0 100644 --- a/packages/catalog-client/api-report-testUtils.md +++ b/packages/catalog-client/api-report-testUtils.md @@ -27,7 +27,9 @@ export class InMemoryCatalogClient implements CatalogApi { // (undocumented) addLocation(_location: AddLocationRequest): Promise; // (undocumented) - getEntities(_request?: GetEntitiesRequest): Promise; + getEntities( + request?: GetEntitiesRequest | undefined, + ): Promise; // (undocumented) getEntitiesByRefs( request: GetEntitiesByRefsRequest, @@ -42,7 +44,7 @@ export class InMemoryCatalogClient implements CatalogApi { ): Promise; // (undocumented) getEntityFacets( - _request: GetEntityFacetsRequest, + request: GetEntityFacetsRequest, ): Promise; // (undocumented) getLocationByEntity( @@ -54,7 +56,7 @@ export class InMemoryCatalogClient implements CatalogApi { getLocationByRef(_locationRef: string): Promise; // (undocumented) queryEntities( - _request?: QueryEntitiesRequest, + request?: QueryEntitiesRequest | undefined, ): Promise; // (undocumented) refreshEntity(_entityRef: string): Promise; diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts index 0b5f0569bb..3f3eaed477 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.test.ts @@ -43,6 +43,9 @@ describe('InMemoryCatalogClient', () => { it('getEntities', async () => { const client = new InMemoryCatalogClient({ entities }); await expect(client.getEntities()).resolves.toEqual({ items: entities }); + await expect( + client.getEntities({ filter: { 'metadata.uid': 'u2' } }), + ).resolves.toEqual({ items: [entity2] }); }); it('getEntitiesByRefs', async () => { @@ -56,6 +59,16 @@ describe('InMemoryCatalogClient', () => { ], }), ).resolves.toEqual({ items: [entity2, undefined, entity1] }); + await expect( + client.getEntitiesByRefs({ + entityRefs: [ + 'customkind:default/e2', + 'customkind:missing/missing', + 'customkind:default/e1', + ], + filter: { 'metadata.uid': 'u1' }, + }), + ).resolves.toEqual({ items: [undefined, undefined, entity1] }); }); it('queryEntities', async () => { @@ -65,6 +78,13 @@ describe('InMemoryCatalogClient', () => { totalItems: 2, pageInfo: {}, }); + await expect( + client.queryEntities({ filter: { 'metadata.uid': 'u2' } }), + ).resolves.toEqual({ + items: [entity2], + totalItems: 1, + pageInfo: {}, + }); }); it('getEntityAncestors', async () => { diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index a975a79b80..fc0c42bf52 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -46,17 +46,17 @@ function pick(obj: JsonObject, key: string): JsonValue | undefined { const parts = key.split('.'); return parts.reduce((acc, part, index) => { - if (!acc) { + if (acc === undefined) { return acc; } if (typeof acc === 'object' && acc !== null && !Array.isArray(acc)) { const value = acc[part]; - if (value) { + if (value !== undefined) { return value; } const rest = parts.slice(index).join('.'); const restValue = acc[rest]; - if (restValue) { + if (restValue !== undefined) { return restValue; } } @@ -80,9 +80,8 @@ function createFilter( if (!filterOrFilters) { return () => true; } - const filters = Array.isArray(filterOrFilters) - ? filterOrFilters - : [filterOrFilters]; + + const filters = [filterOrFilters].flat(); return entity => { return filters.some(filter => { @@ -133,9 +132,13 @@ export class InMemoryCatalogClient implements CatalogApi { ): Promise { const filter = createFilter(request.filter); return { - items: request.entityRefs - .map(ref => this.#entitiesByRef.get(ref)) - .filter(e => (e ? filter(e) : true)), + items: request.entityRefs.map(ref => { + const entity = this.#entitiesByRef.get(ref); + if (entity && filter(entity)) { + return entity; + } + return undefined; + }), }; } @@ -146,11 +149,12 @@ export class InMemoryCatalogClient implements CatalogApi { return { items: [], pageInfo: {}, totalItems: 0 }; } const filter = createFilter(request?.filter); + const items = this.#entities.filter(filter); // TODO(Rugvip): Pagination return { - items: this.#entities.filter(filter), + items, pageInfo: {}, - totalItems: this.#entities.length, + totalItems: items.length, }; } diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts index 5eeb704ff8..b9988572ac 100644 --- a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts @@ -23,6 +23,7 @@ import { Entity } from '@backstage/catalog-model'; import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; // eslint-disable-next-line @backstage/no-undeclared-imports import { ServiceMock } from '@backstage/backend-test-utils'; +import { CatalogApi } from '@backstage/catalog-client'; /** @internal */ function simpleMock( @@ -57,7 +58,9 @@ function simpleMock( * * @public */ -export function catalogServiceMock(options?: { entities?: Entity[] }) { +export function catalogServiceMock(options?: { + entities?: Entity[]; +}): CatalogApi { return new InMemoryCatalogClient(options); } From 50a530b2a8c6079d0168d0ad045fcb7752e2a6e1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Sep 2024 10:03:47 +0200 Subject: [PATCH 08/11] catalog-client: switch to using search table implementation for filtering Signed-off-by: Patrik Oldsberg --- .../catalog-client/api-report-testUtils.md | 8 +- packages/catalog-client/package.json | 1 - .../src/testUtils/InMemoryCatalogClient.ts | 105 +++++++++--------- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/packages/catalog-client/api-report-testUtils.md b/packages/catalog-client/api-report-testUtils.md index 3962cacce0..88d14b4f7a 100644 --- a/packages/catalog-client/api-report-testUtils.md +++ b/packages/catalog-client/api-report-testUtils.md @@ -27,9 +27,7 @@ export class InMemoryCatalogClient implements CatalogApi { // (undocumented) addLocation(_location: AddLocationRequest): Promise; // (undocumented) - getEntities( - request?: GetEntitiesRequest | undefined, - ): Promise; + getEntities(request?: GetEntitiesRequest): Promise; // (undocumented) getEntitiesByRefs( request: GetEntitiesByRefsRequest, @@ -55,9 +53,7 @@ export class InMemoryCatalogClient implements CatalogApi { // (undocumented) getLocationByRef(_locationRef: string): Promise; // (undocumented) - queryEntities( - request?: QueryEntitiesRequest | undefined, - ): Promise; + queryEntities(request?: QueryEntitiesRequest): Promise; // (undocumented) refreshEntity(_entityRef: string): Promise; // (undocumented) diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 03addce840..4119614503 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -50,7 +50,6 @@ "dependencies": { "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", - "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", "uri-template": "^2.0.0" }, diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index fc0c42bf52..355a10bf0e 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -35,43 +35,41 @@ import { } from '@backstage/catalog-client'; import { CompoundEntityRef, + DEFAULT_NAMESPACE, Entity, parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; import { NotFoundError, NotImplementedError } from '@backstage/errors'; -import { JsonObject, JsonValue } from '@backstage/types'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch'; -function pick(obj: JsonObject, key: string): JsonValue | undefined { - const parts = key.split('.'); +function buildEntitySearch(entity: Entity) { + const rows = traverse(entity); - return parts.reduce((acc, part, index) => { - if (acc === undefined) { - return acc; - } - if (typeof acc === 'object' && acc !== null && !Array.isArray(acc)) { - const value = acc[part]; - if (value !== undefined) { - return value; - } - const rest = parts.slice(index).join('.'); - const restValue = acc[rest]; - if (restValue !== undefined) { - return restValue; - } - } - return undefined; - }, obj as JsonValue | undefined); -} + if (entity.metadata?.name) { + rows.push({ key: 'metadata.name', value: entity.metadata.name }); + } + if (entity.metadata?.namespace) { + rows.push({ key: 'metadata.namespace', value: entity.metadata.namespace }); + } + if (entity.metadata?.uid) { + rows.push({ key: 'metadata.uid', value: entity.metadata.uid }); + } -function filterCompare( - expected: (string | symbol) | (string | symbol)[], - value: JsonValue, -) { - const expect = (Array.isArray(expected) ? expected : [expected]).map(e => - e.toString().toLocaleLowerCase('en-US'), - ); - return expect.includes(String(value).toLocaleLowerCase('en-US')); + if (!entity.metadata.namespace) { + rows.push({ key: 'metadata.namespace', value: DEFAULT_NAMESPACE }); + } + + // Visit relations + for (const relation of entity.relations ?? []) { + rows.push({ + key: `relations.${relation.type}`, + value: relation.targetRef, + }); + } + + return rows; } function createFilter( @@ -84,16 +82,25 @@ function createFilter( const filters = [filterOrFilters].flat(); return entity => { + const rows = buildEntitySearch(entity); + return filters.some(filter => { for (const [key, expectedValue] of Object.entries(filter)) { - const entityValue = pick(entity, key); - if (entityValue === undefined) { + const searchValues = rows + .filter(row => row.key === key.toLocaleLowerCase('en-US')) + .map(row => row.value?.toString().toLocaleLowerCase('en-US')); + + if (searchValues.length === 0) { return false; } if (expectedValue === CATALOG_FILTER_EXISTS) { continue; } - if (!filterCompare(expectedValue, entityValue)) { + if ( + !searchValues?.includes( + String(expectedValue).toLocaleLowerCase('en-US'), + ) + ) { return false; } } @@ -111,17 +118,13 @@ function createFilter( */ export class InMemoryCatalogClient implements CatalogApi { #entities: Entity[]; - #entitiesByRef: Map; constructor(options?: { entities?: Entity[] }) { this.#entities = options?.entities?.slice() ?? []; - this.#entitiesByRef = new Map( - this.#entities.map(e => [stringifyEntityRef(e), e]), - ); } async getEntities( - request?: GetEntitiesRequest | undefined, + request?: GetEntitiesRequest, ): Promise { const filter = createFilter(request?.filter); return { items: this.#entities.filter(filter) }; @@ -131,19 +134,16 @@ export class InMemoryCatalogClient implements CatalogApi { request: GetEntitiesByRefsRequest, ): Promise { const filter = createFilter(request.filter); + const refMap = this.#createEntityRefMap(); return { - items: request.entityRefs.map(ref => { - const entity = this.#entitiesByRef.get(ref); - if (entity && filter(entity)) { - return entity; - } - return undefined; - }), + items: request.entityRefs + .map(ref => refMap.get(ref)) + .map(e => (e && filter(e) ? e : undefined)), }; } async queryEntities( - request?: QueryEntitiesRequest | undefined, + request?: QueryEntitiesRequest, ): Promise { if (request && 'cursor' in request) { return { items: [], pageInfo: {}, totalItems: 0 }; @@ -161,7 +161,7 @@ export class InMemoryCatalogClient implements CatalogApi { async getEntityAncestors( request: GetEntityAncestorsRequest, ): Promise { - const entity = this.#entitiesByRef.get(request.entityRef); + const entity = this.#createEntityRefMap().get(request.entityRef); if (!entity) { throw new NotFoundError(`Entity with ref ${request.entityRef} not found`); } @@ -174,7 +174,7 @@ export class InMemoryCatalogClient implements CatalogApi { async getEntityByRef( entityRef: string | CompoundEntityRef, ): Promise { - return this.#entitiesByRef.get( + return this.#createEntityRefMap().get( stringifyEntityRef(parseEntityRef(entityRef)), ); } @@ -182,8 +182,6 @@ export class InMemoryCatalogClient implements CatalogApi { async removeEntityByUid(uid: string): Promise { const index = this.#entities.findIndex(e => e.metadata.uid === uid); if (index !== -1) { - const entity = this.#entities[index]; - this.#entitiesByRef.delete(stringifyEntityRef(entity)); this.#entities.splice(index, 1); } } @@ -199,7 +197,10 @@ export class InMemoryCatalogClient implements CatalogApi { request.facets.map(facet => { const facetValues = new Map(); for (const entity of filteredEntities) { - const value = pick(entity, facet); + const rows = buildEntitySearch(entity); + const value = rows.find( + row => row.key === facet.toLocaleLowerCase('en-US'), + )?.value; if (value) { facetValues.set( String(value), @@ -248,4 +249,8 @@ export class InMemoryCatalogClient implements CatalogApi { ): Promise { throw new NotImplementedError('Method not implemented.'); } + + #createEntityRefMap() { + return new Map(this.#entities.map(e => [stringifyEntityRef(e), e])); + } } From 5055279d1d1b35668daecae97fdbd3689f6d0256 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Sep 2024 10:04:02 +0200 Subject: [PATCH 09/11] kubernetes-backend: fix test data Signed-off-by: Patrik Oldsberg --- .../CatalogClusterLocator.test.ts | 6 +++++ .../src/cluster-locator/index.test.ts | 23 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts index baa26df91f..0d17314cef 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/CatalogClusterLocator.test.ts @@ -45,6 +45,9 @@ const entities: Entity[] = [ title: 'title', namespace: 'default', }, + spec: { + type: 'kubernetes-cluster', + }, }, { apiVersion: 'backstage.io/v1alpha1', @@ -63,6 +66,9 @@ const entities: Entity[] = [ name: 'owned', namespace: 'default', }, + spec: { + type: 'kubernetes-cluster', + }, }, ]; diff --git a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts index ed42131de4..7e52848bb8 100644 --- a/plugins/kubernetes-backend/src/cluster-locator/index.test.ts +++ b/plugins/kubernetes-backend/src/cluster-locator/index.test.ts @@ -15,7 +15,11 @@ */ import { Config, ConfigReader } from '@backstage/config'; -import { ANNOTATION_KUBERNETES_AUTH_PROVIDER } from '@backstage/plugin-kubernetes-common'; +import { + ANNOTATION_KUBERNETES_API_SERVER, + ANNOTATION_KUBERNETES_API_SERVER_CA, + ANNOTATION_KUBERNETES_AUTH_PROVIDER, +} from '@backstage/plugin-kubernetes-common'; import { getCombinedClusterSupplier } from './index'; import { ClusterDetails } from '../types/types'; import { AuthenticationStrategy, DispatchStrategy } from '../auth'; @@ -146,7 +150,22 @@ describe('getCombinedClusterSupplier', () => { const clusterSupplier = getCombinedClusterSupplier( config, catalogServiceMock({ - entities: [{ metadata: { annotations: {}, name: 'cluster' } } as any], + entities: [ + { + kind: 'Resource', + metadata: { + name: 'cluster', + annotations: { + [ANNOTATION_KUBERNETES_API_SERVER]: 'mock', + [ANNOTATION_KUBERNETES_API_SERVER_CA]: 'mock', + [ANNOTATION_KUBERNETES_AUTH_PROVIDER]: 'mock', + }, + }, + spec: { + type: 'kubernetes-cluster', + }, + } as any, + ], }), mockStrategy, logger, From 9a12d34ce191ba61b2a4004967972f8d7ef5af3c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Sep 2024 10:10:35 +0200 Subject: [PATCH 10/11] catalog-node: API report fixup Signed-off-by: Patrik Oldsberg --- plugins/catalog-node/api-report-testUtils.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/catalog-node/api-report-testUtils.md b/plugins/catalog-node/api-report-testUtils.md index 0a3b392bad..c1dfb0bd7a 100644 --- a/plugins/catalog-node/api-report-testUtils.md +++ b/plugins/catalog-node/api-report-testUtils.md @@ -5,14 +5,13 @@ ```ts import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceMock } from '@backstage/backend-test-utils'; // @public export function catalogServiceMock(options?: { entities?: Entity[]; -}): InMemoryCatalogClient; +}): CatalogApi; // @public export namespace catalogServiceMock { From b8a69588ab05924d00aace5f16029ac36a54ba69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 12 Sep 2024 13:35:47 +0200 Subject: [PATCH 11/11] lockfile update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 9aec0defac..03cb6f421c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3840,7 +3840,6 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" - "@backstage/types": "workspace:^" cross-fetch: ^4.0.0 msw: ^1.0.0 uri-template: ^2.0.0