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 <Patrick.Jungermann@gmail.com>
This commit is contained in:
Patrick Jungermann
2022-11-23 01:56:39 +01:00
parent b1828da592
commit 4eb0bce299
3 changed files with 343 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
---
Fix repo:push topic not matching `BitbucketCloudEventRouter`.
@@ -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<string, string> },
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);
});
});
@@ -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 {