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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch
|
||||
---
|
||||
|
||||
Fix repo:push topic not matching `BitbucketCloudEventRouter`.
|
||||
+336
-51
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user