feat(bitbucketCloudPipelinesRun): adds authorization headers and refactors tests to use msw

Signed-off-by: Matthew Prinold <matthewprinold@gmail.com>
This commit is contained in:
Matthew Prinold
2023-12-19 12:18:11 +00:00
parent 315c1903c2
commit 2db42a19df
4 changed files with 262 additions and 327 deletions
@@ -15,313 +15,252 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { Writable } from 'stream';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { PassThrough } from 'stream';
import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun';
import fetch from 'node-fetch';
import yaml from 'yaml';
import { examples } from './bitbucketCloudPipelinesRun.examples';
jest.mock('node-fetch');
const { Response } = jest.requireActual('node-fetch');
const createdResponse = {
type: '<string>',
uuid: '<string>',
build_number: 33,
creator: {
type: '<string>',
},
repository: {
type: '<string>',
},
target: {
type: '<string>',
},
trigger: {
type: '<string>',
},
state: {
type: '<string>',
},
created_on: '<string>',
completed_on: '<string>',
build_seconds_used: 50,
};
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
describe('bitbucket:pipelines:run', () => {
const logStream = {
write: jest.fn(),
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
const mockDir = createMockDirectory();
const workspacePath = mockDir.resolve('workspace');
let action: TemplateAction<any>;
const config = new ConfigReader({
integrations: {
bitbucketCloud: [
{
username: 'u',
appPassword: 'p',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createBitbucketPipelinesRunAction({ integrations });
const mockContext = {
input: {},
baseUrl: 'somebase',
workspacePath,
workspacePath: 'wsp',
logger: getVoidLogger(),
logStream,
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
};
const worker = setupServer();
setupRequestMockHandlers(worker);
beforeEach(() => {
jest.resetAllMocks();
action = createBitbucketPipelinesRunAction();
});
it('should trigger a pipeline for a branch', async () => {
expect.assertions(2);
worker.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
expect(req.body).toEqual({
target: {
ref_type: 'branch',
type: 'pipeline_ref_target',
ref_name: 'master',
},
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[0].example).steps[0].input,
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: JSON.stringify({
target: {
ref_type: 'branch',
type: 'pipeline_ref_target',
ref_name: 'master',
},
}),
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
});
it('should trigger a pipeline for a commit on a branch', async () => {
expect.assertions(2);
worker.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
expect(req.body).toEqual({
target: {
commit: {
type: 'commit',
hash: 'ce5b7431602f7cbba007062eeb55225c6e18e956',
},
ref_type: 'branch',
type: 'pipeline_ref_target',
ref_name: 'master',
},
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[1].example).steps[0].input,
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: JSON.stringify({
target: {
commit: {
type: 'commit',
hash: 'ce5b7431602f7cbba007062eeb55225c6e18e956',
},
ref_type: 'branch',
type: 'pipeline_ref_target',
ref_name: 'master',
},
}),
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
});
it('should trigger a specific pipeline definition for a commit', async () => {
expect.assertions(2);
worker.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
expect(req.body).toEqual({
target: {
commit: {
type: 'commit',
hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923',
},
selector: {
type: 'custom',
pattern: 'Deploy to production',
},
type: 'pipeline_commit_target',
},
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[2].example).steps[0].input,
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: JSON.stringify({
target: {
commit: {
type: 'commit',
hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923',
},
selector: {
type: 'custom',
pattern: 'Deploy to production',
},
type: 'pipeline_commit_target',
},
}),
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
});
it('should trigger a specific pipeline definition for a commit on a branch or tag', async () => {
expect.assertions(2);
worker.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
expect(req.body).toEqual({
target: {
commit: {
type: 'commit',
hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923',
},
selector: {
type: 'custom',
pattern: 'Deploy to production',
},
type: 'pipeline_ref_target',
ref_name: 'master',
ref_type: 'branch',
},
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[3].example).steps[0].input,
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: JSON.stringify({
target: {
commit: {
type: 'commit',
hash: 'a3c4e02c9a3755eccdc3764e6ea13facdf30f923',
},
selector: {
type: 'custom',
pattern: 'Deploy to production',
},
type: 'pipeline_ref_target',
ref_name: 'master',
ref_type: 'branch',
},
}),
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
});
it('should trigger a custom pipeline with variables', async () => {
expect.assertions(2);
worker.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
expect(req.body).toEqual({
target: {
type: 'pipeline_ref_target',
ref_name: 'master',
ref_type: 'branch',
selector: {
type: 'custom',
pattern: 'Deploy to production',
},
},
variables: [
{ key: 'var1key', value: 'var1value', secured: true },
{
key: 'var2key',
value: 'var2value',
},
],
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[4].example).steps[0].input,
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: JSON.stringify({
target: {
type: 'pipeline_ref_target',
ref_name: 'master',
ref_type: 'branch',
selector: {
type: 'custom',
pattern: 'Deploy to production',
},
},
variables: [
{ key: 'var1key', value: 'var1value', secured: true },
{
key: 'var2key',
value: 'var2value',
},
],
}),
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
});
it('should trigger a pull request pipeline', async () => {
expect.assertions(2);
worker.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
expect(req.body).toEqual({
target: {
type: 'pipeline_pullrequest_target',
source: 'pull-request-branch',
destination: 'master',
destination_commit: {
hash: '9f848b7',
},
commit: {
hash: '1a372fc',
},
pull_request: {
id: '3',
},
selector: {
type: 'pull-requests',
pattern: '**',
},
},
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const ctx = Object.assign({}, mockContext, {
input: yaml.parse(examples[5].example).steps[0].input,
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: JSON.stringify({
target: {
type: 'pipeline_pullrequest_target',
source: 'pull-request-branch',
destination: 'master',
destination_commit: {
hash: '9f848b7',
},
commit: {
hash: '1a372fc',
},
pull_request: {
id: '3',
},
selector: {
type: 'pull-requests',
pattern: '**',
},
},
}),
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
});
});
@@ -15,90 +15,64 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { Writable } from 'stream';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { PassThrough } from 'stream';
import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun';
import fetch from 'node-fetch';
jest.mock('node-fetch');
const { Response } = jest.requireActual('node-fetch');
const createdResponse = {
type: '<string>',
uuid: '<string>',
build_number: 33,
creator: {
type: '<string>',
},
repository: {
type: '<string>',
},
target: {
type: '<string>',
},
trigger: {
type: '<string>',
},
state: {
type: '<string>',
},
created_on: '<string>',
completed_on: '<string>',
build_seconds_used: 50,
};
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
describe('bitbucket:pipelines:run', () => {
const logStream = {
write: jest.fn(),
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
const mockDir = createMockDirectory();
const workspacePath = mockDir.resolve('workspace');
let action: TemplateAction<any>;
const config = new ConfigReader({
integrations: {
bitbucketCloud: [
{
username: 'u',
appPassword: 'p',
},
],
},
});
const integrations = ScmIntegrations.fromConfig(config);
const action = createBitbucketPipelinesRunAction({ integrations });
const mockContext = {
input: {},
baseUrl: 'somebase',
workspacePath,
workspacePath: 'wsp',
logger: getVoidLogger(),
logStream,
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
};
const worker = setupServer();
setupRequestMockHandlers(worker);
beforeEach(() => {
jest.resetAllMocks();
action = createBitbucketPipelinesRunAction();
jest.clearAllMocks();
});
it('should call the bitbucket api for running a pipeline', async () => {
expect.assertions(1);
const workspace = 'test-workspace';
const repo_slug = 'test-repo-slug';
const ctx = Object.assign({}, mockContext, {
worker.use(
rest.post(
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`,
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic dTpw');
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
);
const testContext = Object.assign({}, mockContext, {
input: { workspace, repo_slug },
});
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(
new Response(JSON.stringify(createdResponse), {
url: 'url',
status: 201,
statusText: 'Created',
}),
);
await action.handler(ctx);
expect(fetch).toHaveBeenCalledWith(
'https://api.bitbucket.org/2.0/repositories/test-workspace/test-repo-slug/pipelines',
{
body: {},
headers: {
Accept: 'application/json',
Authorization: 'Bearer testToken',
'Content-Type': 'application/json',
},
method: 'POST',
},
);
expect(logStream.write).toHaveBeenCalledTimes(2);
expect(logStream.write).toHaveBeenNthCalledWith(1, 'Response: 201 Created');
expect(logStream.write).toHaveBeenNthCalledWith(2, createdResponse);
await action.handler(testContext);
});
});
@@ -18,6 +18,9 @@ import { examples } from './bitbucketCloudPipelinesRun.examples';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import fetch from 'node-fetch';
import * as inputProps from './inputProperties';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { InputError } from '@backstage/errors';
import { getAuthorizationHeader } from './helpers';
const id = 'bitbucket:pipelines:run';
/**
@@ -25,11 +28,15 @@ const id = 'bitbucket:pipelines:run';
*
* @public
*/
export const createBitbucketPipelinesRunAction = () => {
export const createBitbucketPipelinesRunAction = (options: {
integrations: ScmIntegrationRegistry;
}) => {
const { integrations } = options;
return createTemplateAction<{
workspace: string;
repo_slug: string;
body?: object;
token?: string;
}>({
id,
description: '',
@@ -42,32 +49,41 @@ export const createBitbucketPipelinesRunAction = () => {
workspace: inputProps.workspace,
repo_slug: inputProps.repo_slug,
body: inputProps.pipelinesRunBody,
token: inputProps.token,
},
},
},
supportsDryRun: false,
async handler(ctx) {
const { workspace, repo_slug, body } = ctx.input;
const { workspace, repo_slug, body, token } = ctx.input;
const host = 'bitbucket.org';
const integrationConfig = integrations.bitbucketCloud.byHost(host);
if (!integrationConfig) {
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
const authorization = getAuthorizationHeader(
token ? { token } : integrationConfig.config,
);
try {
const response = await fetch(
`https://api.bitbucket.org/2.0/repositories/${workspace}/${repo_slug}/pipelines`,
{
method: 'POST',
headers: {
Authorization: 'Bearer testToken',
Authorization: authorization,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body) ?? {},
},
);
ctx.logStream.write(
`Response: ${response.status} ${response.statusText}`,
);
const data = await response.json();
ctx.logStream.write(data);
} catch (err) {
ctx.logStream.write(err);
} catch (e) {
throw new Error(`Unable to run pipeline, ${e}`);
}
},
});
@@ -76,6 +76,12 @@ const secured = {
type: 'boolean',
};
const token = {
title: 'Authentication Token',
type: 'string',
description: 'The token to use for authorization to BitBucket Cloud',
};
const destination_commit = {
title: 'destination_commit',
type: 'object',
@@ -145,4 +151,4 @@ const pipelinesRunBody = {
},
};
export { workspace, repo_slug, pipelinesRunBody };
export { workspace, repo_slug, pipelinesRunBody, token };