Skip to content

Commit

Permalink
Merge pull request #3030 from goober/feature/support-bitbucket-server
Browse files Browse the repository at this point in the history
Support both Bitbucket Cloud and Server
  • Loading branch information
benjdlambert committed Oct 26, 2020
2 parents 8de84c7 + 834f602 commit 04d6f63
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 225 deletions.
5 changes: 5 additions & 0 deletions .changeset/3030.md
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': minor
---

Add the ability to import components from Bitbucket Server to the service catalog
2 changes: 1 addition & 1 deletion packages/backend-common/package.json
Expand Up @@ -41,7 +41,7 @@
"express": "^4.17.1",
"express-prom-bundle": "^6.1.0",
"express-promise-router": "^3.0.3",
"git-url-parse": "^11.3.0",
"git-url-parse": "^11.4.0",
"helmet": "^4.0.0",
"knex": "^0.21.1",
"lodash": "^4.17.15",
Expand Down
270 changes: 149 additions & 121 deletions packages/backend-common/src/reading/BitbucketUrlReader.test.ts
Expand Up @@ -14,139 +14,167 @@
* limitations under the License.
*/

import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '../logging';
import { BitbucketUrlReader } from './BitbucketUrlReader';
import { msw } from '@backstage/test-utils';

const logger = getVoidLogger();
import {
BitbucketUrlReader,
getApiRequestOptions,
getApiUrl,
ProviderConfig,
readConfig,
} from './BitbucketUrlReader';

describe('BitbucketUrlReader', () => {
const worker = setupServer();
describe('getApiRequestOptions', () => {
it('inserts a token when needed', () => {
const withToken: ProviderConfig = {
host: '',
apiBaseUrl: '',
token: 'A',
};
const withoutToken: ProviderConfig = {
host: '',
apiBaseUrl: '',
};
expect(
(getApiRequestOptions(withToken).headers as any).Authorization,
).toEqual('Bearer A');
expect(
(getApiRequestOptions(withoutToken).headers as any).Authorization,
).toBeUndefined();
});

msw.setupDefaultHandlers(worker);
it('insert basic auth when needed', () => {
const withUsernameAndPassword: ProviderConfig = {
host: '',
apiBaseUrl: '',
username: 'some-user',
appPassword: 'my-secret',
};
const withoutUsernameAndPassword: ProviderConfig = {
host: '',
apiBaseUrl: '',
};
expect(
(getApiRequestOptions(withUsernameAndPassword).headers as any)
.Authorization,
).toEqual('Basic c29tZS11c2VyOm15LXNlY3JldA==');
expect(
(getApiRequestOptions(withoutUsernameAndPassword).headers as any)
.Authorization,
).toBeUndefined();
});
});

beforeEach(() => {
worker.use(
rest.get('*', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
url: req.url.toString(),
headers: req.headers.getAllHeaders(),
}),
describe('getApiUrl', () => {
it('rejects targets that do not look like URLs', () => {
const config: ProviderConfig = { host: '', apiBaseUrl: '' };
expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/);
});
it('happy path for Bitbucket Cloud', () => {
const config: ProviderConfig = {
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
};
expect(
getApiUrl(
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
config,
),
).toEqual(
new URL(
'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
),
);
});
it('happy path for Bitbucket Server', () => {
const config: ProviderConfig = {
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://bitbucket.mycompany.net/rest/api/1.0',
};
expect(
getApiUrl(
'https://bitbucket.mycompany.net/projects/a/repos/b/browse/path/to/c.yaml',
config,
),
).toEqual(
new URL(
'https://bitbucket.mycompany.net/rest/api/1.0/projects/a/repos/b/raw/path/to/c.yaml',
),
),
);
);
});
});

const createConfig = (username?: string, appPassword?: string) =>
new ConfigReader(
{
integrations: {
bitbucket: [
{
host: 'bitbucket.org',
username: username,
appPassword: appPassword,
},
],
describe('readConfig', () => {
function config(
providers: {
host: string;
apiBaseUrl?: string;
token?: string;
username?: string;
password?: string;
}[],
) {
return ConfigReader.fromConfigs([
{
context: '',
data: {
integrations: { bitbucket: providers },
},
},
},
'test-config',
);
]);
}

it.each([
{
url:
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
config: createConfig(),
response: expect.objectContaining({
url:
'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml',
}),
},
{
url:
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
config: createConfig('some-user', 'my-secret'),
response: expect.objectContaining({
headers: expect.objectContaining({
authorization: 'Basic c29tZS11c2VyOm15LXNlY3JldA==',
}),
}),
},
{
url:
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
config: createConfig(),
response: expect.objectContaining({
headers: expect.not.objectContaining({
authorization: expect.anything(),
}),
}),
},
{
url:
'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml',
config: createConfig(undefined, 'only-password-provided'),
response: expect.objectContaining({
headers: expect.not.objectContaining({
authorization: expect.anything(),
}),
}),
},
])('should handle happy path %#', async ({ url, config, response }) => {
const [{ reader }] = BitbucketUrlReader.factory({ config, logger });
it('adds a default Bitbucket Cloud entry when missing', () => {
const output = readConfig(config([]));
expect(output).toEqual([
{
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
},
]);
});

it('injects the correct Bitbucket Cloud API base URL when missing', () => {
const output = readConfig(config([{ host: 'bitbucket.org' }]));
expect(output).toEqual([
{
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
},
]);
});

it('rejects custom targets with no base URLs', () => {
expect(() =>
readConfig(config([{ host: 'bitbucket.mycompany.net' }])),
).toThrow(
"Bitbucket integration for 'bitbucket.mycompany.net' must configure an explicit apiBaseUrl",
);
});

const data = await reader.read(url);
const res = await JSON.parse(data.toString('utf-8'));
expect(res).toEqual(response);
it('rejects funky configs', () => {
expect(() => readConfig(config([{ host: 7 } as any]))).toThrow(/host/);
expect(() => readConfig(config([{ token: 7 } as any]))).toThrow(/token/);
expect(() =>
readConfig(config([{ host: 'bitbucket.org', apiBaseUrl: 7 } as any])),
).toThrow(/apiBaseUrl/);
expect(() =>
readConfig(config([{ host: 'bitbucket.org', token: 7 } as any])),
).toThrow(/token/);
});
});

it.each([
{
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Bitbucket URL or Invalid file path',
},
{
url: 'com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
},
{
url: '',
config: createConfig('', ''),
error:
"Invalid type in config for key 'integrations.bitbucket[0].username' in 'test-config', got empty-string, wanted string",
},
{
url: '',
config: createConfig('only-user-provided', ''),
error:
"Invalid type in config for key 'integrations.bitbucket[0].appPassword' in 'test-config', got empty-string, wanted string",
},
{
url: '',
config: createConfig('', 'only-password-provided'),
error:
"Invalid type in config for key 'integrations.bitbucket[0].username' in 'test-config', got empty-string, wanted string",
},
{
url: '',
config: createConfig('only-user-provided', undefined),
error:
"Missing required config value at 'integrations.bitbucket[0].appPassword'",
},
])('should handle error path %#', async ({ url, config, error }) => {
await expect(async () => {
const [{ reader }] = BitbucketUrlReader.factory({ config, logger });
await reader.read(url);
}).rejects.toThrow(error);
describe('implementation', () => {
it('rejects unknown targets', async () => {
const processor = new BitbucketUrlReader({
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
});
await expect(
processor.read('https://not.bitbucket.com/apa'),
).rejects.toThrow(
'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket URL or file path',
);
});
});
});

0 comments on commit 04d6f63

Please sign in to comment.