implement in some tests

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-09-11 10:41:02 +02:00
parent f19f848077
commit 0e77776b51
18 changed files with 285 additions and 356 deletions
@@ -49,7 +49,8 @@ export class InMemoryCatalogClient implements CatalogApi {
async getEntities(
_request?: GetEntitiesRequest,
): Promise<GetEntitiesResponse> {
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<QueryEntitiesResponse> {
throw new NotImplementedError('Method not implemented.');
// TODO(freben): Fields, filters etc
return {
items: this.#entities,
totalItems: this.#entities.length,
pageInfo: {},
};
}
async getEntityAncestors(
@@ -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';
@@ -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,
});
@@ -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,
@@ -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<CatalogApi, 'plugin', 'singleton'>;
const // (undocumented)
mock: (
partialImpl?: Partial<CatalogApi> | undefined,
) => ServiceMock<CatalogApi>;
}
// (No @packageDocumentation comment for this package)
```
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './testUtils';
export { catalogServiceMock } from './testUtils/catalogServiceMock';
@@ -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';
@@ -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<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,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,
@@ -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({
@@ -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,
@@ -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,
@@ -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);