diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index e53099eb2b..2296f5b5e8 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -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: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - 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> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - let action: TemplateAction; + 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).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).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).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).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).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).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); }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts index d5f9da2f5d..97cdc05d2f 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -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: '', - uuid: '', - build_number: 33, - creator: { - type: '', - }, - repository: { - type: '', - }, - target: { - type: '', - }, - trigger: { - type: '', - }, - state: { - type: '', - }, - created_on: '', - completed_on: '', - 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> as jest.Mocked; - const mockDir = createMockDirectory(); - const workspacePath = mockDir.resolve('workspace'); - let action: TemplateAction; + 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).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); }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts index d523cb6c6e..a8ad93ce9b 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucketCloudPipelinesRun.ts @@ -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}`); } }, }); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts index 395c3f1c28..1a4a845e63 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/inputProperties.ts @@ -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 };