From 273009ba390933c83487ca6597ae7fc96ebaf003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 29 Sep 2022 19:33:40 +0200 Subject: [PATCH] One more integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../catalog-backend/src/integration.test.ts | 144 +++++++++++++++++- .../DefaultCatalogProcessingEngine.ts | 4 +- 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-backend/src/integration.test.ts b/plugins/catalog-backend/src/integration.test.ts index 67b63428db..dcfe9a1489 100644 --- a/plugins/catalog-backend/src/integration.test.ts +++ b/plugins/catalog-backend/src/integration.test.ts @@ -34,7 +34,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { DefaultCatalogRulesEnforcer } from './ingestion/CatalogRules'; import { Stitcher } from './stitching/Stitcher'; import { DefaultEntitiesCatalog } from './service/DefaultEntitiesCatalog'; -import { DefaultCatalogProcessingEngine } from './processing/DefaultCatalogProcessingEngine'; +import { + DefaultCatalogProcessingEngine, + ProgressTracker, +} from './processing/DefaultCatalogProcessingEngine'; import { createHash } from 'crypto'; import { DefaultRefreshService } from './service/DefaultRefreshService'; import { connectEntityProviders } from './processing/connectEntityProviders'; @@ -69,10 +72,6 @@ class TestProvider implements EntityProvider { } } -type ProgressTracker = NonNullable< - ConstructorParameters[7] ->; - class ProxyProgressTracker implements ProgressTracker { #inner: ProgressTracker; @@ -540,4 +539,139 @@ describe('Catalog Backend Integration', () => { await expect(harness.getOutputEntities()).resolves.toEqual(outputEntities); }); + + // NOTE(freben): This test documents existing behavior, but it would be more correct to mark the cycle as orphans + it('leaves behind orphaned cycles without orphan markers', async () => { + function mkEntity(name: string) { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name, + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + }; + } + + const harness = await TestHarness.create({ + async processEntity( + entity: Entity, + location: LocationSpec, + emit: CatalogProcessorEmit, + ) { + if (entity.spec?.noEmit) { + return entity; + } + switch (entity.metadata.name) { + case 'a': + emit(processingResult.entity(location, mkEntity('b'))); + break; + case 'b': + emit(processingResult.entity(location, mkEntity('c'))); + break; + case 'c': + emit(processingResult.entity(location, mkEntity('d'))); + break; + case 'd': + emit(processingResult.entity(location, mkEntity('b'))); + break; + default: + } + return entity; + }, + }); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'a', + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + }, + ]); + + await expect(harness.getOutputEntities()).resolves.toEqual({}); + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/a': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'a' }), + }), + 'component:default/b': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'b' }), + }), + 'component:default/c': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'c' }), + }), + 'component:default/d': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'd' }), + }), + }); + // NOTE(freben): Avoid .toHaveProperty here, since it treats dots as path separators + expect( + (await harness.getOutputEntities())['component:default/b'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/c'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/d'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'a', + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + }, + }, + spec: { noEmit: true }, + }, + ]); + + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/a': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'a' }), + }), + 'component:default/b': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'b' }), + }), + 'component:default/c': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'c' }), + }), + 'component:default/d': expect.objectContaining({ + metadata: expect.objectContaining({ name: 'd' }), + }), + }); + // TODO(freben): Ideally these should be orphaned now + expect( + (await harness.getOutputEntities())['component:default/b'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/c'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + expect( + (await harness.getOutputEntities())['component:default/d'].metadata + .annotations!['backstage.io/orphan'], + ).toBeUndefined(); + }); }); diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 4693ffc559..2cc0e2dc40 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -31,6 +31,8 @@ import { startTaskPipeline } from './TaskPipeline'; const CACHE_TTL = 5; +export type ProgressTracker = ReturnType; + export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { private stopFunc?: () => void; @@ -45,7 +47,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { unprocessedEntity: Entity; errors: Error[]; }) => Promise | void, - private readonly tracker = progressTracker(), + private readonly tracker: ProgressTracker = progressTracker(), ) {} async start() {