Merge pull request #26628 from backstage/freben/catalog-service-mock
catalog: add service mocks
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
'@backstage/plugin-catalog-node': minor
|
||||
---
|
||||
|
||||
Add catalog service mocks under the `/testUtils` subpath export.
|
||||
@@ -0,0 +1,71 @@
|
||||
## 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 { 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
|
||||
export class InMemoryCatalogClient implements CatalogApi {
|
||||
constructor(options?: { entities?: Entity[] });
|
||||
// (undocumented)
|
||||
addLocation(_location: AddLocationRequest): Promise<AddLocationResponse>;
|
||||
// (undocumented)
|
||||
getEntities(request?: GetEntitiesRequest): Promise<GetEntitiesResponse>;
|
||||
// (undocumented)
|
||||
getEntitiesByRefs(
|
||||
request: GetEntitiesByRefsRequest,
|
||||
): Promise<GetEntitiesByRefsResponse>;
|
||||
// (undocumented)
|
||||
getEntityAncestors(
|
||||
request: GetEntityAncestorsRequest,
|
||||
): Promise<GetEntityAncestorsResponse>;
|
||||
// (undocumented)
|
||||
getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
): Promise<Entity | undefined>;
|
||||
// (undocumented)
|
||||
getEntityFacets(
|
||||
request: GetEntityFacetsRequest,
|
||||
): Promise<GetEntityFacetsResponse>;
|
||||
// (undocumented)
|
||||
getLocationByEntity(
|
||||
_entityRef: string | CompoundEntityRef,
|
||||
): Promise<Location_2 | undefined>;
|
||||
// (undocumented)
|
||||
getLocationById(_id: string): Promise<Location_2 | undefined>;
|
||||
// (undocumented)
|
||||
getLocationByRef(_locationRef: string): Promise<Location_2 | undefined>;
|
||||
// (undocumented)
|
||||
queryEntities(request?: QueryEntitiesRequest): Promise<QueryEntitiesResponse>;
|
||||
// (undocumented)
|
||||
refreshEntity(_entityRef: string): Promise<void>;
|
||||
// (undocumented)
|
||||
removeEntityByUid(uid: string): Promise<void>;
|
||||
// (undocumented)
|
||||
removeLocationById(_id: string): Promise<void>;
|
||||
// (undocumented)
|
||||
validateEntity(
|
||||
_entity: Entity,
|
||||
_locationRef: string,
|
||||
): Promise<ValidateEntityResponse>;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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';
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 });
|
||||
await expect(
|
||||
client.getEntities({ filter: { 'metadata.uid': 'u2' } }),
|
||||
).resolves.toEqual({ items: [entity2] });
|
||||
});
|
||||
|
||||
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] });
|
||||
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 () => {
|
||||
const client = new InMemoryCatalogClient({ entities });
|
||||
await expect(client.queryEntities()).resolves.toEqual({
|
||||
items: entities,
|
||||
totalItems: 2,
|
||||
pageInfo: {},
|
||||
});
|
||||
await expect(
|
||||
client.queryEntities({ filter: { 'metadata.uid': 'u2' } }),
|
||||
).resolves.toEqual({
|
||||
items: [entity2],
|
||||
totalItems: 1,
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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,
|
||||
CATALOG_FILTER_EXISTS,
|
||||
CatalogApi,
|
||||
EntityFilterQuery,
|
||||
GetEntitiesByRefsRequest,
|
||||
GetEntitiesByRefsResponse,
|
||||
GetEntitiesRequest,
|
||||
GetEntitiesResponse,
|
||||
GetEntityAncestorsRequest,
|
||||
GetEntityAncestorsResponse,
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
Location,
|
||||
QueryEntitiesRequest,
|
||||
QueryEntitiesResponse,
|
||||
ValidateEntityResponse,
|
||||
} from '@backstage/catalog-client';
|
||||
import {
|
||||
CompoundEntityRef,
|
||||
DEFAULT_NAMESPACE,
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { NotFoundError, NotImplementedError } from '@backstage/errors';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { traverse } from '../../../../plugins/catalog-backend/src/database/operations/stitcher/buildEntitySearch';
|
||||
|
||||
function buildEntitySearch(entity: Entity) {
|
||||
const rows = traverse(entity);
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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(
|
||||
filterOrFilters?: EntityFilterQuery,
|
||||
): (entity: Entity) => boolean {
|
||||
if (!filterOrFilters) {
|
||||
return () => true;
|
||||
}
|
||||
|
||||
const filters = [filterOrFilters].flat();
|
||||
|
||||
return entity => {
|
||||
const rows = buildEntitySearch(entity);
|
||||
|
||||
return filters.some(filter => {
|
||||
for (const [key, expectedValue] of Object.entries(filter)) {
|
||||
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 (
|
||||
!searchValues?.includes(
|
||||
String(expectedValue).toLocaleLowerCase('en-US'),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
|
||||
constructor(options?: { entities?: Entity[] }) {
|
||||
this.#entities = options?.entities?.slice() ?? [];
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
request?: GetEntitiesRequest,
|
||||
): Promise<GetEntitiesResponse> {
|
||||
const filter = createFilter(request?.filter);
|
||||
return { items: this.#entities.filter(filter) };
|
||||
}
|
||||
|
||||
async getEntitiesByRefs(
|
||||
request: GetEntitiesByRefsRequest,
|
||||
): Promise<GetEntitiesByRefsResponse> {
|
||||
const filter = createFilter(request.filter);
|
||||
const refMap = this.#createEntityRefMap();
|
||||
return {
|
||||
items: request.entityRefs
|
||||
.map(ref => refMap.get(ref))
|
||||
.map(e => (e && filter(e) ? e : undefined)),
|
||||
};
|
||||
}
|
||||
|
||||
async queryEntities(
|
||||
request?: QueryEntitiesRequest,
|
||||
): Promise<QueryEntitiesResponse> {
|
||||
if (request && 'cursor' in request) {
|
||||
return { items: [], pageInfo: {}, totalItems: 0 };
|
||||
}
|
||||
const filter = createFilter(request?.filter);
|
||||
const items = this.#entities.filter(filter);
|
||||
// TODO(Rugvip): Pagination
|
||||
return {
|
||||
items,
|
||||
pageInfo: {},
|
||||
totalItems: items.length,
|
||||
};
|
||||
}
|
||||
|
||||
async getEntityAncestors(
|
||||
request: GetEntityAncestorsRequest,
|
||||
): Promise<GetEntityAncestorsResponse> {
|
||||
const entity = this.#createEntityRefMap().get(request.entityRef);
|
||||
if (!entity) {
|
||||
throw new NotFoundError(`Entity with ref ${request.entityRef} not found`);
|
||||
}
|
||||
return {
|
||||
items: [{ entity, parentEntityRefs: [] }],
|
||||
rootEntityRef: request.entityRef,
|
||||
};
|
||||
}
|
||||
|
||||
async getEntityByRef(
|
||||
entityRef: string | CompoundEntityRef,
|
||||
): Promise<Entity | undefined> {
|
||||
return this.#createEntityRefMap().get(
|
||||
stringifyEntityRef(parseEntityRef(entityRef)),
|
||||
);
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const index = this.#entities.findIndex(e => e.metadata.uid === uid);
|
||||
if (index !== -1) {
|
||||
this.#entities.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshEntity(_entityRef: string): Promise<void> {}
|
||||
|
||||
async getEntityFacets(
|
||||
request: GetEntityFacetsRequest,
|
||||
): Promise<GetEntityFacetsResponse> {
|
||||
const filter = createFilter(request.filter);
|
||||
const filteredEntities = this.#entities.filter(filter);
|
||||
const facets = Object.fromEntries(
|
||||
request.facets.map(facet => {
|
||||
const facetValues = new Map<string, number>();
|
||||
for (const entity of filteredEntities) {
|
||||
const rows = buildEntitySearch(entity);
|
||||
const value = rows.find(
|
||||
row => row.key === facet.toLocaleLowerCase('en-US'),
|
||||
)?.value;
|
||||
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<Location | undefined> {
|
||||
throw new NotImplementedError('Method not implemented.');
|
||||
}
|
||||
|
||||
async getLocationByRef(_locationRef: string): Promise<Location | undefined> {
|
||||
throw new NotImplementedError('Method not implemented.');
|
||||
}
|
||||
|
||||
async addLocation(
|
||||
_location: AddLocationRequest,
|
||||
): Promise<AddLocationResponse> {
|
||||
throw new NotImplementedError('Method not implemented.');
|
||||
}
|
||||
|
||||
async removeLocationById(_id: string): Promise<void> {
|
||||
throw new NotImplementedError('Method not implemented.');
|
||||
}
|
||||
|
||||
async getLocationByEntity(
|
||||
_entityRef: string | CompoundEntityRef,
|
||||
): Promise<Location | undefined> {
|
||||
throw new NotImplementedError('Method not implemented.');
|
||||
}
|
||||
|
||||
async validateEntity(
|
||||
_entity: Entity,
|
||||
_locationRef: string,
|
||||
): Promise<ValidateEntityResponse> {
|
||||
throw new NotImplementedError('Method not implemented.');
|
||||
}
|
||||
|
||||
#createEntityRefMap() {
|
||||
return new Map(this.#entities.map(e => [stringifyEntityRef(e), e]));
|
||||
}
|
||||
}
|
||||
@@ -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<TokenManager> = {
|
||||
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<CatalogApi> 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<CatalogApi> as CatalogApi,
|
||||
catalogApi,
|
||||
tokenManager,
|
||||
});
|
||||
|
||||
|
||||
+11
-21
@@ -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<string, string> },
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
## 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
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { ServiceMock } from '@backstage/backend-test-utils';
|
||||
|
||||
// @public
|
||||
export function catalogServiceMock(options?: {
|
||||
entities?: Entity[];
|
||||
}): CatalogApi;
|
||||
|
||||
// @public
|
||||
export namespace catalogServiceMock {
|
||||
const factory: (options?: {
|
||||
entities?: Entity[];
|
||||
}) => ServiceFactory<CatalogApi, 'plugin', 'singleton'>;
|
||||
const mock: (
|
||||
partialImpl?: Partial<CatalogApi> | undefined,
|
||||
) => ServiceMock<CatalogApi>;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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 { catalogServiceMock } from './testUtils/catalogServiceMock';
|
||||
@@ -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] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 {
|
||||
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';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
/** @internal */
|
||||
function simpleMock<TService>(
|
||||
ref: ServiceRef<TService, any>,
|
||||
mockFactory: () => jest.Mocked<TService>,
|
||||
): (partialImpl?: Partial<TService>) => ServiceMock<TService> {
|
||||
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<TService>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
}): CatalogApi {
|
||||
return new InMemoryCatalogClient(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
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(),
|
||||
}));
|
||||
}
|
||||
@@ -22,103 +22,89 @@ 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',
|
||||
},
|
||||
spec: {
|
||||
type: 'kubernetes-cluster',
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
spec: {
|
||||
type: 'kubernetes-cluster',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('CatalogClusterLocator', () => {
|
||||
it('returns empty cluster details when the cluster is empty', async () => {
|
||||
const emptyMockCatalogApi = {
|
||||
getEntityByRef: jest.fn(),
|
||||
getEntities: async () => ({
|
||||
items: [],
|
||||
}),
|
||||
} as Partial<CatalogApi> 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();
|
||||
});
|
||||
|
||||
@@ -15,16 +15,18 @@
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
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';
|
||||
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 +64,7 @@ describe('getCombinedClusterSupplier', () => {
|
||||
|
||||
const clusterSupplier = getCombinedClusterSupplier(
|
||||
config,
|
||||
catalogApi,
|
||||
catalogServiceMock.mock(),
|
||||
mockStrategy,
|
||||
mockServices.logger.mock(),
|
||||
undefined,
|
||||
@@ -106,7 +108,7 @@ describe('getCombinedClusterSupplier', () => {
|
||||
expect(() =>
|
||||
getCombinedClusterSupplier(
|
||||
config,
|
||||
catalogApi,
|
||||
catalogServiceMock.mock(),
|
||||
new DispatchStrategy({ authStrategyMap: {} }),
|
||||
mockServices.logger.mock(),
|
||||
undefined,
|
||||
@@ -141,31 +143,30 @@ 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: [
|
||||
{
|
||||
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,
|
||||
undefined,
|
||||
|
||||
+23
-22
@@ -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<CatalogClient> = {
|
||||
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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
+9
-13
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
+8
-9
@@ -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,
|
||||
|
||||
+27
-19
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const cache: jest.Mocked<CacheService> = {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user