From f8a515a790a2f57f8881414c853cbfbb513b930d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 21 Sep 2021 13:54:59 +0200 Subject: [PATCH] catalog-backend: added some basic tests for the default orchestrator Signed-off-by: Patrik Oldsberg --- .../DefaultCatalogProcessingEngine.test.ts | 8 +- ...faultCatalogProcessingOrchestrator.test.ts | 186 ++++++++++++++++-- 2 files changed, 178 insertions(+), 16 deletions(-) diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts index b4b309beef..c569bf8135 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts @@ -84,7 +84,7 @@ describe('DefaultCatalogProcessingEngine', () => { metadata: { name: 'test' }, }, resultHash: '', - state: {}, + state: [], nextUpdateAt: DateTime.now(), lastDiscoveryAt: DateTime.now(), }, @@ -100,7 +100,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, - state: expect.anything(), + state: [], // State is forwarded as is, even if it's a bad format }); }); await engine.stop(); @@ -147,7 +147,7 @@ describe('DefaultCatalogProcessingEngine', () => { metadata: { name: 'test' }, }, resultHash: '', - state: {}, + state: { cache: { myProcessor: { myKey: 'myValue' } } }, nextUpdateAt: DateTime.now(), lastDiscoveryAt: DateTime.now(), }, @@ -163,7 +163,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, - state: expect.anything(), + state: { cache: { myProcessor: { myKey: 'myValue' } } }, }); }); await engine.stop(); diff --git a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts index c93734b37f..f8cf65d4e8 100644 --- a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -16,21 +16,186 @@ import { getVoidLogger } from '@backstage/backend-common'; import { + Entity, + EntityPolicies, EntityPolicy, LocationEntity, + LocationSpec, LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { ScmIntegrationRegistry } from '@backstage/integration'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { CatalogProcessor, + CatalogProcessorCache, + CatalogProcessorEmit, CatalogProcessorParser, results, } from '../../ingestion'; import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules'; import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; +import { defaultEntityDataParser } from '../../ingestion/processors/util/parse'; +import { ConfigReader } from '@backstage/config'; + +class FooBarProcessor implements CatalogProcessor { + getProcessorName = () => 'foo-bar'; + + async validateEntityKind(entity: Entity) { + return entity.kind.toLocaleLowerCase('en-US') === 'foobar'; + } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + cache: CatalogProcessorCache, + ) { + if (await cache.get('emit')) { + emit( + results.entity( + { type: 'url', target: './new-place' }, + { + apiVersion: 'my-api/v1', + kind: 'FooBar', + metadata: { + name: 'my-new-foo-bar', + }, + }, + ), + ); + emit( + results.relation({ + type: 'my-type', + source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, + target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, + }), + ); + } + return entity; + } +} describe('DefaultCatalogProcessingOrchestrator', () => { + describe('2', () => { + const entity = { + apiVersion: 'my-api/v1', + kind: 'FooBar', + metadata: { + name: 'my-foo-bar', + annotations: { + [LOCATION_ANNOTATION]: 'url:./here', + [ORIGIN_LOCATION_ANNOTATION]: 'url:./there', + }, + }, + }; + + const rulesEnforcer: CatalogRulesEnforcer = { + isAllowed: () => true, + }; + + const orchestrator = new DefaultCatalogProcessingOrchestrator({ + processors: [new FooBarProcessor()], + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + logger: getVoidLogger(), + parser: defaultEntityDataParser, + policy: EntityPolicies.allOf([]), + rulesEnforcer, + }); + + it('runs a minimal processing', async () => { + await expect(orchestrator.process({ entity })).resolves.toEqual({ + ok: true, + completedEntity: entity, + deferredEntities: [], + errors: [], + relations: [], + state: { + cache: {}, + }, + }); + }); + + it('emits some things', async () => { + await expect( + orchestrator.process({ + entity, + state: { cache: { 'foo-bar': { emit: true } } }, + }), + ).resolves.toEqual({ + ok: true, + completedEntity: entity, + deferredEntities: [ + { + locationKey: 'url:./new-place', + entity: { + apiVersion: 'my-api/v1', + kind: 'FooBar', + metadata: { + name: 'my-new-foo-bar', + annotations: { + [LOCATION_ANNOTATION]: 'url:./new-place', + [ORIGIN_LOCATION_ANNOTATION]: 'url:./there', + }, + }, + }, + }, + ], + errors: [], + relations: [ + { + type: 'my-type', + source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, + target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, + }, + ], + state: { + cache: { 'foo-bar': { emit: true } }, + }, + }); + }); + + it('accepts any state input', async () => { + await expect( + orchestrator.process({ entity, state: null as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: [] as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: Symbol() as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: undefined }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: 3 as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: '}{' as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: { cache: null } }), + ).resolves.toMatchObject({ + ok: true, + }); + }); + }); + it('enforces catalog rules', async () => { const entity: LocationEntity = { apiVersion: 'backstage.io/v1beta1', @@ -48,6 +213,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { }, }; + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); const processor: jest.Mocked = { validateEntityKind: jest.fn(async () => true), readLocation: jest.fn(async (_l, _o, emit) => { @@ -55,11 +221,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { return true; }), }; - const integrations: jest.Mocked = {} as any; const parser: CatalogProcessorParser = jest.fn(); - const policy: jest.Mocked = { - enforce: jest.fn(async x => x), - }; const rulesEnforcer: jest.Mocked = { isAllowed: jest.fn(), }; @@ -69,18 +231,18 @@ describe('DefaultCatalogProcessingOrchestrator', () => { integrations, logger: getVoidLogger(), parser, - policy, + policy: EntityPolicies.allOf([]), rulesEnforcer, }); rulesEnforcer.isAllowed.mockReturnValueOnce(true); - await expect( - orchestrator.process({ entity, state: new Map() }), - ).resolves.toEqual(expect.objectContaining({ ok: true })); + await expect(orchestrator.process({ entity, state: {} })).resolves.toEqual( + expect.objectContaining({ ok: true }), + ); rulesEnforcer.isAllowed.mockReturnValueOnce(false); - await expect( - orchestrator.process({ entity, state: new Map() }), - ).resolves.toEqual(expect.objectContaining({ ok: false })); + await expect(orchestrator.process({ entity, state: {} })).resolves.toEqual( + expect.objectContaining({ ok: false }), + ); }); });