From 30483f692dab8dc0de33dd108b1b82643e5b9ef3 Mon Sep 17 00:00:00 2001 From: Tomas Dabasinskas Date: Sun, 5 Feb 2023 19:49:23 +0200 Subject: [PATCH] Add tests Signed-off-by: Tomas Dabasinskas --- .../providers/PuppetDBEntityProvider.test.ts | 218 ++++++++++++++++ .../PuppetDBEntityProviderConfig.test.ts | 129 +++++++++ .../src/puppet/read.test.ts | 244 ++++++++++++++++++ .../src/puppet/transformers.test.ts | 85 ++++++ 4 files changed, 676 insertions(+) create mode 100644 plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProvider.test.ts create mode 100644 plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProviderConfig.test.ts create mode 100644 plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts create mode 100644 plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProvider.test.ts new file mode 100644 index 0000000000..3b665b94b4 --- /dev/null +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProvider.test.ts @@ -0,0 +1,218 @@ +/* + * Copyright 2021 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 { TaskInvocationDefinition, TaskRunner } from '@backstage/backend-tasks'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PuppetDBEntityProvider } from './PuppetDBEntityProvider'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import * as p from '../puppet/read'; +import { DEFAULT_OWNER } from './constants'; +import { ANNOTATION_PUPPET_CERTNAME } from '../puppet'; +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, +} from '@backstage/catalog-model/'; +import { ENDPOINT_NODES } from '../puppet/constants'; + +const logger = getVoidLogger(); + +jest.mock('../puppet/read', () => ({ + __esModule: true, + default: jest.fn(), + readPuppetNodes: null, +})); + +class PersistingTaskRunner implements TaskRunner { + private tasks: TaskInvocationDefinition[] = []; + + getTasks() { + return this.tasks; + } + + run(task: TaskInvocationDefinition): Promise { + this.tasks.push(task); + return Promise.resolve(undefined); + } +} + +describe('PuppetEntityProvider', () => { + const config = new ConfigReader({ + catalog: { + providers: { + puppetdb: { + host: 'http://puppetdb:8080', + schedule: { + frequency: { + minutes: 10, + }, + timeout: { + minutes: 10, + }, + }, + }, + }, + }, + }); + + describe('where there are no nodes', () => { + beforeEach(() => { + // @ts-ignore + p.readPuppetNodes = jest.fn().mockResolvedValue([]); + }); + + it('creates no entities', async () => { + const connection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const providers = PuppetDBEntityProvider.fromConfig(config, { + logger, + schedule: new PersistingTaskRunner(), + }); + + await providers[0].connect(connection); + await providers[0].refresh(logger); + + expect(connection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [], + }); + }); + }); + + describe('where there are nodes', () => { + beforeEach(() => { + // @ts-ignore + p.readPuppetNodes = jest.fn().mockResolvedValue([ + { + api_version: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'node1', + namespace: 'default', + annotations: { + [ANNOTATION_PUPPET_CERTNAME]: 'node1', + }, + tags: ['windows'], + description: 'Description 1', + spec: { + type: 'virtual-machine', + owner: DEFAULT_OWNER, + dependsOn: [], + dependencyOf: [], + }, + }, + }, + { + api_version: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'node2', + namespace: 'default', + annotations: { + [ANNOTATION_PUPPET_CERTNAME]: 'node2', + }, + tags: ['linux'], + description: 'Description 2', + spec: { + type: 'physical-server', + owner: DEFAULT_OWNER, + dependsOn: [], + dependencyOf: [], + }, + }, + }, + ]); + }); + + it('creates entities', async () => { + const connection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const providers = PuppetDBEntityProvider.fromConfig(config, { + logger, + schedule: new PersistingTaskRunner(), + }); + + await providers[0].connect(connection); + await providers[0].refresh(logger); + + expect(connection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + locationKey: providers[0].getProviderName(), + entity: { + api_version: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'node1', + namespace: 'default', + annotations: { + [ANNOTATION_PUPPET_CERTNAME]: 'node1', + [ANNOTATION_LOCATION]: `url:${config.getString( + 'catalog.providers.puppetdb.host', + )}${ENDPOINT_NODES}/node1`, + [ANNOTATION_ORIGIN_LOCATION]: `url:${config.getString( + 'catalog.providers.puppetdb.host', + )}${ENDPOINT_NODES}/node1`, + }, + tags: ['windows'], + description: 'Description 1', + spec: { + type: 'virtual-machine', + owner: DEFAULT_OWNER, + dependsOn: [], + dependencyOf: [], + }, + }, + }, + }, + { + locationKey: providers[0].getProviderName(), + entity: { + api_version: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'node2', + namespace: 'default', + annotations: { + [ANNOTATION_PUPPET_CERTNAME]: 'node2', + [ANNOTATION_LOCATION]: `url:${config.getString( + 'catalog.providers.puppetdb.host', + )}${ENDPOINT_NODES}/node2`, + [ANNOTATION_ORIGIN_LOCATION]: `url:${config.getString( + 'catalog.providers.puppetdb.host', + )}${ENDPOINT_NODES}/node2`, + }, + tags: ['linux'], + description: 'Description 2', + spec: { + type: 'physical-server', + owner: DEFAULT_OWNER, + dependsOn: [], + dependencyOf: [], + }, + }, + }, + }, + ], + }); + }); + }); +}); diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProviderConfig.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProviderConfig.test.ts new file mode 100644 index 0000000000..07fe54bbca --- /dev/null +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDBEntityProviderConfig.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2021 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 { ConfigReader } from '@backstage/config'; +import { readProviderConfigs } from './PuppetDBEntityProviderConfig'; +import { Duration } from 'luxon'; + +describe('readProviderConfigs', () => { + afterEach(() => jest.resetAllMocks()); + + it('no provider config', () => { + const config = new ConfigReader({}); + const providerConfigs = readProviderConfigs(config); + + expect(providerConfigs).toHaveLength(0); + }); + + it('single simple provider config', () => { + const config = new ConfigReader({ + catalog: { + providers: { + puppetdb: { + host: 'https://puppetdb', + }, + }, + }, + }); + + const providerConfigs = readProviderConfigs(config); + + expect(providerConfigs).toHaveLength(1); + expect(providerConfigs[0].id).toEqual('default'); + expect(providerConfigs[0].host).toEqual('https://puppetdb'); + }); + + it('single specific provider config', () => { + const config = new ConfigReader({ + catalog: { + providers: { + puppetdb: { + 'my-provider': { + host: 'https://puppetdb', + }, + }, + }, + }, + }); + + const providerConfigs = readProviderConfigs(config); + + expect(providerConfigs).toHaveLength(1); + expect(providerConfigs[0].id).toEqual('my-provider'); + expect(providerConfigs[0].host).toEqual('https://puppetdb'); + }); + + it('multiple provider configs', () => { + const config = new ConfigReader({ + catalog: { + providers: { + puppetdb: { + 'my-provider': { + host: 'https://my-puppet/', + query: 'my-query', + }, + 'your-provider': { + host: 'https://your-puppet', + query: 'your-query', + }, + }, + }, + }, + }); + + const providerConfigs = readProviderConfigs(config); + + expect(providerConfigs).toHaveLength(2); + expect(providerConfigs[0]).toEqual({ + id: 'my-provider', + host: 'https://my-puppet', + query: 'my-query', + }); + expect(providerConfigs[1]).toEqual({ + id: 'your-provider', + host: 'https://your-puppet', + query: 'your-query', + }); + }); + + it('provider config with schedule', () => { + const config = new ConfigReader({ + catalog: { + providers: { + puppetdb: { + host: 'https://puppetdb', + schedule: { + frequency: 'PT30M', + timeout: { + minutes: 10, + }, + }, + }, + }, + }, + }); + + const providerConfigs = readProviderConfigs(config); + + expect(providerConfigs).toHaveLength(1); + expect(providerConfigs[0].schedule).toEqual({ + frequency: Duration.fromISO('PT30M'), + timeout: { + minutes: 10, + }, + }); + }); +}); diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts new file mode 100644 index 0000000000..573be365dd --- /dev/null +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts @@ -0,0 +1,244 @@ +/* + * Copyright 2020 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 { readPuppetNodes } from './read'; +import { + DEFAULT_PROVIDER_ID, + PuppetDBEntityProviderConfig, +} from '../providers'; +import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import fetch from 'node-fetch'; +import { ANNOTATION_PUPPET_CERTNAME, ENDPOINT_FACTSETS } from './constants'; + +jest.mock('node-fetch', () => { + const original = jest.requireActual('node-fetch'); + return { + __esModule: true, + default: jest.fn(), + Headers: original.Headers, + }; +}); +(global as any).fetch = fetch; + +describe('readPuppetNodes', () => { + const mockFetch = fetch as unknown as jest.Mocked; + + describe('where no query is specified', () => { + const config: PuppetDBEntityProviderConfig = { + host: 'https://puppetdb', + id: DEFAULT_PROVIDER_ID, + }; + + beforeEach(async () => { + mockFetch.mockReturnValueOnce( + Promise.resolve( + new Response( + JSON.stringify([ + { + certname: 'node1', + timestamp: 'time1', + hash: 'hash1', + producer_timestamp: 'producer_time1', + producer: 'producer1', + environment: 'environment1', + facts: { + data: [ + { + name: 'is_virtual', + value: true, + }, + { + name: 'kernel', + value: 'Linux', + }, + { + name: 'ipaddress', + value: 'ipaddress1', + }, + { + name: 'clientnoop', + value: true, + }, + { + name: 'clientversion', + value: 'clientversion1', + }, + ], + }, + }, + { + certname: 'node2', + timestamp: 'time2', + hash: 'hash2', + producer_timestamp: 'producer_time2', + producer: 'producer2', + environment: 'environment2', + facts: { + data: [ + { + name: 'is_virtual', + value: false, + }, + { + name: 'kernel', + value: 'Windows', + }, + { + name: 'ipaddress', + value: 'ipaddress2', + }, + { + name: 'clientnoop', + value: false, + }, + { + name: 'clientversion', + value: 'clientversion2', + }, + ], + }, + }, + ]), + ), + ), + ); + }); + + describe('where custom transformer is used', () => { + it('should use it for transforming puppet nodes', async () => { + const entities = await readPuppetNodes(config, { + transformer: async (node, _config) => { + return { + apiVersion: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: `custom-${node.certname}`, + namespace: DEFAULT_NAMESPACE, + }, + spec: { + type: 'Custom', + owner: 'Custom', + dependsOn: [], + dependencyOf: [], + }, + }; + }, + }); + + expect(entities).toHaveLength(2); + expect(entities[0]).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'custom-node1', + namespace: DEFAULT_NAMESPACE, + }, + spec: { + type: 'Custom', + owner: 'Custom', + dependsOn: [], + dependencyOf: [], + }, + }); + expect(entities[1]).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'custom-node2', + namespace: DEFAULT_NAMESPACE, + }, + spec: { + type: 'Custom', + owner: 'Custom', + dependsOn: [], + dependencyOf: [], + }, + }); + }); + }); + + describe('where default transformer is used', () => { + it('should use it for transforming puppet nodes', async () => { + const entities = await readPuppetNodes(config); + + expect(entities).toHaveLength(2); + expect(entities[0].metadata.annotations).toEqual({ + [ANNOTATION_PUPPET_CERTNAME]: 'node1', + }); + expect(entities[1].metadata.annotations).toEqual({ + [ANNOTATION_PUPPET_CERTNAME]: 'node2', + }); + }); + }); + }); + + describe('where query is specified', () => { + const config: PuppetDBEntityProviderConfig = { + host: 'https://puppetdb', + id: DEFAULT_PROVIDER_ID, + query: '["=", "certname", "node1"]', + }; + + describe('where no results are matched', () => { + beforeEach(async () => { + mockFetch.mockReturnValueOnce( + Promise.resolve(new Response(JSON.stringify([]))), + ); + }); + + it('should return empty array', async () => { + const entities = await readPuppetNodes(config); + expect(entities).toHaveLength(0); + }); + }); + + describe('where results are matched', () => { + beforeEach(async () => { + mockFetch.mockReturnValueOnce( + Promise.resolve( + new Response( + JSON.stringify([ + { + certname: 'node1', + timestamp: 'time1', + hash: 'hash1', + producer_timestamp: 'producer_time1', + producer: 'producer1', + environment: 'environment1', + }, + ]), + ), + ), + ); + }); + + it('should return matched results', async () => { + const entities = await readPuppetNodes(config); + expect(mockFetch).toHaveBeenCalledWith( + `${config.host}${ENDPOINT_FACTSETS}?query=%5B%22%3D%22%2C+%22certname%22%2C+%22node1%22%5D`, + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'GET', + }, + ); + expect(entities).toHaveLength(1); + }); + }); + }); +}); diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts new file mode 100644 index 0000000000..30ae79e87b --- /dev/null +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/transformers.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2020 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 { PuppetDBEntityProviderConfig } from '../providers'; +import { PuppetNode } from './types'; +import { defaultResourceTransformer } from './transformers'; +import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { DEFAULT_OWNER } from '../providers'; +import { ANNOTATION_PUPPET_CERTNAME } from './constants'; + +describe('defaultResourceTransformer', () => { + it('should transform a puppet node to a resource entity', async () => { + const config: PuppetDBEntityProviderConfig = { + host: '', + id: '', + }; + const node: PuppetNode = { + certname: 'node1', + timestamp: 'time1', + hash: 'hash1', + producer_timestamp: 'producer_time1', + producer: 'producer1', + environment: 'environment1', + facts: { + href: 'facts1', + data: [ + { + name: 'kernel', + value: 'Linux', + }, + { + name: 'ipaddress', + value: 'ipaddress1', + }, + { + name: 'is_virtual', + value: true, + }, + { + name: 'clientnoop', + value: true, + }, + { + name: 'clientversion', + value: 'clientversion1', + }, + ], + }, + }; + + const entity = await defaultResourceTransformer(node, config); + expect(entity).toEqual({ + apiVersion: 'backstage.io/v1beta1', + kind: 'Resource', + metadata: { + name: 'node1', + namespace: DEFAULT_NAMESPACE, + annotations: { + [ANNOTATION_PUPPET_CERTNAME]: 'node1', + }, + description: 'ipaddress1', + tags: ['linux'], + }, + spec: { + type: 'virtual-machine', + owner: DEFAULT_OWNER, + dependsOn: [], + dependencyOf: [], + }, + }); + }); +});