diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 6cac8a0e1c..9e55be473a 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -481,4 +481,104 @@ describe('publish:bitbucketCloud', () => { 'https://bitbucket.org/workspace/repo/src/main', ); }); + + it('should call initAndPush with username and token credentials', async () => { + const tokenConfig = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'test-user', + token: 'api-token-123', + }, + ], + }, + }); + + const tokenIntegrations = ScmIntegrations.fromConfig(tokenConfig); + const tokenAction = createPublishBitbucketCloudAction({ + integrations: tokenIntegrations, + config: tokenConfig, + }); + + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await tokenAction.handler(mockContext); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + defaultBranch: 'master', + auth: { + username: 'x-bitbucket-api-token-auth', + password: 'api-token-123', + }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); + + it('should call initAndPush with token passed through ctx.input', async () => { + server.use( + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + html: { + href: 'https://bitbucket.org/workspace/repo', + }, + clone: [ + { + name: 'https', + href: 'https://bitbucket.org/workspace/cloneurl', + }, + ], + }, + }), + ), + ), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + token: 'input-token-override', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.org/workspace/cloneurl', + defaultBranch: 'master', + auth: { username: 'x-token-auth', password: 'input-token-override' }, + logger: mockContext.logger, + gitAuthorInfo: {}, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts index b4db3f78c6..c0947a616f 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPullRequest.test.ts @@ -415,4 +415,50 @@ describe('publish:bitbucketCloud:pull-request', () => { 'https://bitbucket.org/workspace/repo/pull-requests/1', ); }); + + it('should use username and token credentials from integration config', async () => { + const tokenConfig = new ConfigReader({ + integrations: { + bitbucketCloud: [ + { + username: 'test-user', + token: 'api-token-123', + }, + ], + }, + }); + + const tokenIntegrations = ScmIntegrations.fromConfig(tokenConfig); + const tokenAction = createPublishBitbucketCloudPullRequestAction({ + integrations: tokenIntegrations, + config: tokenConfig, + }); + + server.use( + ...handlers, + rest.post( + 'https://api.bitbucket.org/2.0/repositories/workspace/repo/refs/branches', + (_, res, ctx) => { + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + ); + + await tokenAction.handler({ + ...mockContext, + input: { + ...mockContext.input, + sourceBranch: 'new-branch', + }, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pullRequestUrl', + 'https://bitbucket.org/workspace/repo/pull-requests/1', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.test.ts new file mode 100644 index 0000000000..c236bb941a --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/helpers.test.ts @@ -0,0 +1,240 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + getBitbucketClient, + getAuthorizationHeader, + getGitAuth, +} from './helpers'; +import { getBitbucketCloudOAuthToken } from '@backstage/integration'; + +jest.mock('@backstage/integration', () => ({ + getBitbucketCloudOAuthToken: jest.fn(), +})); + +describe('helpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getBitbucketClient', () => { + it('should create client with OAuth token', async () => { + (getBitbucketCloudOAuthToken as jest.Mock).mockResolvedValue( + 'oauth-token-123', + ); + + const client = await getBitbucketClient({ + clientId: 'client-id', + clientSecret: 'client-secret', + }); + + expect(client).toBeDefined(); + expect(getBitbucketCloudOAuthToken).toHaveBeenCalledWith( + 'client-id', + 'client-secret', + ); + }); + + it('should create client with standalone token', async () => { + const client = await getBitbucketClient({ + token: 'standalone-token', + }); + + expect(client).toBeDefined(); + }); + + it('should create client with username and API token', async () => { + const client = await getBitbucketClient({ + username: 'test-user', + token: 'api-token', + }); + + expect(client).toBeDefined(); + }); + + it('should create client with username and appPassword', async () => { + const client = await getBitbucketClient({ + username: 'test-user', + appPassword: 'app-password', + }); + + expect(client).toBeDefined(); + }); + + it('should throw error when no credentials provided', async () => { + await expect(getBitbucketClient({})).rejects.toThrow( + /Authorization has not been provided for Bitbucket Cloud/, + ); + }); + }); + + describe('getAuthorizationHeader', () => { + it('should return Bearer token for OAuth credentials', async () => { + (getBitbucketCloudOAuthToken as jest.Mock).mockResolvedValue( + 'oauth-token-123', + ); + + const result = await getAuthorizationHeader({ + clientId: 'client-id', + clientSecret: 'client-secret', + }); + + expect(result).toBe('Bearer oauth-token-123'); + expect(getBitbucketCloudOAuthToken).toHaveBeenCalledWith( + 'client-id', + 'client-secret', + ); + }); + + it('should return Basic auth for username and token', async () => { + const result = await getAuthorizationHeader({ + username: 'test-user', + token: 'api-token', + }); + + const expectedAuth = Buffer.from('test-user:api-token', 'utf8').toString( + 'base64', + ); + expect(result).toBe(`Basic ${expectedAuth}`); + }); + + it('should return Basic auth for username and appPassword', async () => { + const result = await getAuthorizationHeader({ + username: 'test-user', + appPassword: 'app-password', + }); + + const expectedAuth = Buffer.from( + 'test-user:app-password', + 'utf8', + ).toString('base64'); + expect(result).toBe(`Basic ${expectedAuth}`); + }); + + it('should prefer token over appPassword when both are provided', async () => { + const result = await getAuthorizationHeader({ + username: 'test-user', + token: 'api-token', + appPassword: 'app-password', + }); + + const expectedAuth = Buffer.from('test-user:api-token', 'utf8').toString( + 'base64', + ); + expect(result).toBe(`Basic ${expectedAuth}`); + }); + + it('should return Bearer token for standalone token', async () => { + const result = await getAuthorizationHeader({ + token: 'standalone-token', + }); + + expect(result).toBe('Bearer standalone-token'); + }); + + it('should throw error when no credentials provided', async () => { + await expect(getAuthorizationHeader({})).rejects.toThrow( + /Authorization has not been provided for Bitbucket Cloud/, + ); + }); + }); + + describe('getGitAuth', () => { + it('should return x-token-auth for OAuth credentials', async () => { + (getBitbucketCloudOAuthToken as jest.Mock).mockResolvedValue( + 'oauth-token-123', + ); + + const result = await getGitAuth({ + clientId: 'client-id', + clientSecret: 'client-secret', + }); + + expect(result).toEqual({ + username: 'x-token-auth', + password: 'oauth-token-123', + }); + expect(getBitbucketCloudOAuthToken).toHaveBeenCalledWith( + 'client-id', + 'client-secret', + ); + }); + + it('should return x-token-auth for standalone token', async () => { + const result = await getGitAuth({ + token: 'standalone-token', + }); + + expect(result).toEqual({ + username: 'x-token-auth', + password: 'standalone-token', + }); + }); + + it('should return x-bitbucket-api-token-auth for username + API token', async () => { + const result = await getGitAuth({ + username: 'test-user', + token: 'api-token', + }); + + expect(result).toEqual({ + username: 'x-bitbucket-api-token-auth', + password: 'api-token', + }); + }); + + it('should return username and appPassword for username + appPassword', async () => { + const result = await getGitAuth({ + username: 'test-user', + appPassword: 'app-password', + }); + + expect(result).toEqual({ + username: 'test-user', + password: 'app-password', + }); + }); + + it('should prefer token over appPassword when both are provided', async () => { + const result = await getGitAuth({ + username: 'test-user', + token: 'api-token', + appPassword: 'app-password', + }); + + expect(result).toEqual({ + username: 'x-bitbucket-api-token-auth', + password: 'api-token', + }); + }); + + it('should throw error when no credentials provided', async () => { + await expect(getGitAuth({})).rejects.toThrow( + /Authorization has not been provided for Bitbucket Cloud/, + ); + }); + + it('should throw error when only username is provided', async () => { + await expect( + getGitAuth({ + username: 'test-user', + }), + ).rejects.toThrow( + /Authorization has not been provided for Bitbucket Cloud/, + ); + }); + }); +});