From 4eb0bce2994e0c205790b3505833bfb3f84782d4 Mon Sep 17 00:00:00 2001 From: Patrick Jungermann Date: Wed, 23 Nov 2022 01:56:39 +0100 Subject: [PATCH] fix(catalog/bitbucketCloud,events): fix repo:push topic not matching `BitbucketCloudEventRouter` The sub-topic separator was changed from `/` to `.` as part of the change and review process at the original PR introducing this capability. Unfortunately, the adjustment at the entity provider was forgotten. Additionally, this change adds a few missing test for the `onEvent`/`onRepoPush` feature. Relates-to: PR #13931 Signed-off-by: Patrick Jungermann --- .changeset/stupid-gifts-serve.md | 5 + .../src/BitbucketCloudEntityProvider.test.ts | 387 +++++++++++++++--- .../src/BitbucketCloudEntityProvider.ts | 6 +- 3 files changed, 343 insertions(+), 55 deletions(-) create mode 100644 .changeset/stupid-gifts-serve.md diff --git a/.changeset/stupid-gifts-serve.md b/.changeset/stupid-gifts-serve.md new file mode 100644 index 0000000000..3110a1976c --- /dev/null +++ b/.changeset/stupid-gifts-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +--- + +Fix repo:push topic not matching `BitbucketCloudEventRouter`. diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts index 72974a1c94..2856a26a13 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts @@ -14,18 +14,31 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, TokenManager } from '@backstage/backend-common'; import { PluginTaskScheduler, TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { ConfigReader } from '@backstage/config'; -import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { BitbucketCloudEntityProvider } from './BitbucketCloudEntityProvider'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + Entity, + LocationEntity, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { + EntityProviderConnection, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-backend'; +import { Events } from '@backstage/plugin-bitbucket-cloud-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { + ANNOTATION_BITBUCKET_CLOUD_REPO_URL, + BitbucketCloudEntityProvider, +} from './BitbucketCloudEntityProvider'; class PersistingTaskRunner implements TaskRunner { private tasks: TaskInvocationDefinition[] = []; @@ -38,6 +51,10 @@ class PersistingTaskRunner implements TaskRunner { this.tasks.push(task); return Promise.resolve(undefined); } + + reset() { + this.tasks = []; + } } const logger = getVoidLogger(); @@ -46,10 +63,103 @@ const server = setupServer(); describe('BitbucketCloudEntityProvider', () => { setupRequestMockHandlers(server); - afterEach(() => jest.resetAllMocks()); + + const simpleConfig = new ConfigReader({ + catalog: { + providers: { + bitbucketCloud: { + workspace: 'test-ws', + }, + }, + }, + }); + const defaultConfig = new ConfigReader({ + catalog: { + providers: { + bitbucketCloud: { + myProvider: { + workspace: 'test-ws', + catalogPath: 'catalog-custom.yaml', + filters: { + projectKey: 'test-.*', + repoSlug: 'test-.*', + }, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const tokenManager = { + getToken: async () => { + return { token: 'fake-token' }; + }, + } as any as TokenManager; + const repoPushEvent: Events.RepoPushEvent = { + actor: { + type: 'user', + }, + repository: { + type: 'repository', + slug: 'test-repo', + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo', + }, + }, + workspace: { + type: 'workspace', + slug: 'test-ws', + }, + project: { + type: 'project', + key: 'test-project', + }, + }, + push: { + changes: [ + // ... + ], + }, + }; + const repoPushEventParams = { + topic: 'bitbucketCloud.repo:push', + eventPayload: repoPushEvent, + metadata: { 'x-event-key': 'repo:push' }, + }; + + const createLocationEntity = ( + repoUrl: string, + branch: string, + targetPath: string, + ): LocationEntity => { + const target = `${repoUrl}/src/${branch}/${targetPath}`; + + const entity = locationSpecToLocationEntity({ + location: { + type: 'url', + target: target, + presence: 'required', + }, + }); + entity.metadata.annotations = { + ...entity.metadata.annotations, + [ANNOTATION_BITBUCKET_CLOUD_REPO_URL]: repoUrl, + }; + + return entity; + }; + + afterEach(() => { + jest.resetAllMocks(); + schedule.reset(); + }); it('no provider config', () => { - const schedule = new PersistingTaskRunner(); const config = new ConfigReader({}); const providers = BitbucketCloudEntityProvider.fromConfig(config, { logger, @@ -60,17 +170,7 @@ describe('BitbucketCloudEntityProvider', () => { }); it('single simple provider config', () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - workspace: 'test-ws', - }, - }, - }, - }); - const providers = BitbucketCloudEntityProvider.fromConfig(config, { + const providers = BitbucketCloudEntityProvider.fromConfig(simpleConfig, { logger, schedule, }); @@ -82,18 +182,8 @@ describe('BitbucketCloudEntityProvider', () => { }); it('fail without schedule and scheduler', () => { - const config = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - workspace: 'test-ws', - }, - }, - }, - }); - expect(() => - BitbucketCloudEntityProvider.fromConfig(config, { + BitbucketCloudEntityProvider.fromConfig(simpleConfig, { logger, }), ).toThrow('Either schedule or scheduler must be provided.'); @@ -151,7 +241,6 @@ describe('BitbucketCloudEntityProvider', () => { }); it('multiple provider configs', () => { - const schedule = new PersistingTaskRunner(); const config = new ConfigReader({ catalog: { providers: { @@ -181,28 +270,7 @@ describe('BitbucketCloudEntityProvider', () => { }); it('apply full update on scheduled execution', async () => { - const config = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - myProvider: { - workspace: 'test-ws', - catalogPath: 'custom/path/catalog-custom.yaml', - filters: { - projectKey: 'test-.*', - repoSlug: 'test-.*', - }, - }, - }, - }, - }, - }); - const schedule = new PersistingTaskRunner(); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - const provider = BitbucketCloudEntityProvider.fromConfig(config, { + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { logger, schedule, })[0]; @@ -354,4 +422,221 @@ describe('BitbucketCloudEntityProvider', () => { entities: expectedEntities, }); }); + + it('update onRepoPush', async () => { + const keptModule = createLocationEntity( + 'https://bitbucket.org/test-ws/test-repo', + 'main', + 'kept-module/catalog-custom.yaml', + ); + const removedModule = createLocationEntity( + 'https://bitbucket.org/test-ws/test-repo', + 'main', + 'removed-module/catalog-custom.yaml', + ); + const addedModule = createLocationEntity( + 'https://bitbucket.org/test-ws/test-repo', + 'main', + 'added-module/catalog-custom.yaml', + ); + + const catalogApi = { + getEntities: async ( + request: { filter: Record }, + options: { token: string }, + ): Promise<{ items: Entity[] }> => { + if ( + options.token !== 'fake-token' || + request.filter.kind !== 'Location' || + request.filter['metadata.annotations.bitbucket.org/repo-url'] !== + 'https://bitbucket.org/test-ws/test-repo' + ) { + return { items: [] }; + } + + return { + items: [keptModule, removedModule], + }; + }, + refreshEntity: jest.fn(), + }; + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + tokenManager, + })[0]; + + server.use( + rest.get( + `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, + (req, res, ctx) => { + const query = req.url.searchParams.get('search_query'); + if (!query || !query.includes('repo:test-repo')) { + return res(ctx.json({ values: [] })); + } + + const response = { + values: [ + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'kept-module/catalog-custom.yaml', + commit: { + repository: { + slug: 'test-repo', + project: { + key: 'test-project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo', + }, + }, + }, + }, + }, + }, + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'added-module/catalog-custom.yaml', + commit: { + repository: { + slug: 'test-repo', + project: { + key: 'test-project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo', + }, + }, + }, + }, + }, + }, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + await provider.connect(entityProviderConnection); + await provider.onEvent(repoPushEventParams); + + const addedEntities = [ + { + entity: addedModule, + locationKey: 'bitbucketCloud-provider:myProvider', + }, + ]; + const removedEntities = [ + { + entity: removedModule, + locationKey: 'bitbucketCloud-provider:myProvider', + }, + ]; + + expect(catalogApi.refreshEntity).toHaveBeenCalledTimes(1); + expect(catalogApi.refreshEntity).toHaveBeenCalledWith( + stringifyEntityRef(keptModule), + { token: 'fake-token' }, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: addedEntities, + removed: removedEntities, + }); + }); + + it('onRepoPush fail on incomplete setup', async () => { + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + logger, + schedule, + })[0]; + + await expect(provider.onEvent(repoPushEventParams)).rejects.toThrow( + 'bitbucketCloud-provider:myProvider not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.', + ); + }); + + it('no onRepoPush update on non-matching workspace slug', async () => { + const catalogApi = { + getEntities: jest.fn(), + refreshEntity: jest.fn(), + }; + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await provider.onEvent({ + ...repoPushEventParams, + eventPayload: { + ...repoPushEventParams.eventPayload, + repository: { + ...repoPushEventParams.eventPayload.repository, + workspace: { + ...repoPushEventParams.eventPayload.repository.workspace, + slug: 'not-matching', + }, + }, + }, + }); + + expect(catalogApi.refreshEntity).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('no onRepoPush update on non-matching repo slug', async () => { + const catalogApi = { + getEntities: jest.fn(), + refreshEntity: jest.fn(), + }; + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await provider.onEvent({ + ...repoPushEventParams, + eventPayload: { + ...repoPushEventParams.eventPayload, + repository: { + ...repoPushEventParams.eventPayload.repository, + slug: 'not-matching', + }, + }, + }); + + expect(catalogApi.refreshEntity).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts index 37ad60426d..33b92589eb 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts @@ -49,7 +49,7 @@ import * as uuid from 'uuid'; import { Logger } from 'winston'; const DEFAULT_BRANCH = 'master'; -const TOPIC_REPO_PUSH = 'bitbucketCloud/repo:push'; +const TOPIC_REPO_PUSH = 'bitbucketCloud.repo:push'; /** @public */ export const ANNOTATION_BITBUCKET_CLOUD_REPO_URL = 'bitbucket.org/repo-url'; @@ -211,9 +211,7 @@ export class BitbucketCloudEntityProvider return; } - if (params.metadata?.['x-event-key'] === 'repo:push') { - await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); - } + await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); } private canHandleEvents(): boolean {