Update RepoUrlPicker validation for bitbucket fields

Also,
- Add and update tests
- Add changeset

Signed-off-by: Mustansar Anwar ul Samad <mustansar.samad@gmail.com>
This commit is contained in:
Mustansar Anwar ul Samad
2021-08-10 17:04:49 +12:00
parent dc962cab25
commit e30646aebb
6 changed files with 227 additions and 32 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder': minor
'@backstage/plugin-scaffolder-backend': minor
---
Add bitbucket workspace and project fields to RepoUrlPicker to support bitbucket cloud and server
@@ -68,21 +68,21 @@ describe('publish:azure', () => {
await expect(
action.handler({
...mockContext,
input: { repoUrl: 'azure.com?repo=bob' },
input: { repoUrl: 'dev.azure.com?repo=bob' },
}),
).rejects.toThrow(/missing owner/);
await expect(
action.handler({
...mockContext,
input: { repoUrl: 'azure.com?owner=owner' },
input: { repoUrl: 'dev.azure.com?owner=owner' },
}),
).rejects.toThrow(/missing repo/);
await expect(
action.handler({
...mockContext,
input: { repoUrl: 'azure.com?owner=owner&repo=repo' },
input: { repoUrl: 'dev.azure.com?owner=owner&repo=repo' },
}),
).rejects.toThrow(/missing organization/);
});
@@ -145,7 +145,13 @@ describe('publish:bitbucket', () => {
),
);
await action.handler(mockContext);
await action.handler({
...mockContext,
input: {
...mockContext.input,
repoUrl: 'bitbucket.org?workspace=workspace&project=project&repo=repo',
},
});
});
it('should call the correct APIs when the host is hosted bitbucket', async () => {
@@ -209,7 +215,7 @@ describe('publish:bitbucket', () => {
expect.assertions(1);
server.use(
rest.post(
'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos',
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
(_, res, ctx) => {
return res(
ctx.status(201),
@@ -219,7 +225,7 @@ describe('publish:bitbucket', () => {
},
),
rest.put(
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled',
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Bearer thing');
return res(ctx.status(204));
@@ -231,7 +237,7 @@ describe('publish:bitbucket', () => {
...mockContext,
input: {
...mockContext.input,
repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo',
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
enableLFS: true,
},
});
@@ -240,7 +246,7 @@ describe('publish:bitbucket', () => {
it('should report an error if enabling LFS fails', async () => {
server.use(
rest.post(
'https://hosted.bitbucket.com/rest/api/1.0/projects/owner/repos',
'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos',
(_, res, ctx) => {
return res(
ctx.status(201),
@@ -250,7 +256,7 @@ describe('publish:bitbucket', () => {
},
),
rest.put(
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/owner/repos/repo/enabled',
'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled',
(_, res, ctx) => {
return res(ctx.status(500));
},
@@ -262,7 +268,7 @@ describe('publish:bitbucket', () => {
...mockContext,
input: {
...mockContext.input,
repoUrl: 'hosted.bitbucket.com?owner=owner&repo=repo',
repoUrl: 'hosted.bitbucket.com?project=project&repo=repo',
enableLFS: true,
},
}),
@@ -299,7 +305,7 @@ describe('publish:bitbucket', () => {
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
defaultBranch: 'master',
auth: { username: 'x-token-auth', password: 'tokenlols' },
logger: mockContext.logger,
@@ -310,7 +316,7 @@ describe('publish:bitbucket', () => {
it('should call initAndPush with the correct default branch', async () => {
server.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/owner/repo',
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
(_, res, ctx) =>
res(
ctx.status(200),
@@ -318,12 +324,12 @@ describe('publish:bitbucket', () => {
ctx.json({
links: {
html: {
href: 'https://bitbucket.org/owner/repo',
href: 'https://bitbucket.org/workspace/repo',
},
clone: [
{
name: 'https',
href: 'https://bitbucket.org/owner/cloneurl',
href: 'https://bitbucket.org/workspace/cloneurl',
},
],
},
@@ -342,7 +348,7 @@ describe('publish:bitbucket', () => {
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
defaultBranch: 'main',
auth: { username: 'x-token-auth', password: 'tokenlols' },
logger: mockContext.logger,
@@ -385,7 +391,7 @@ describe('publish:bitbucket', () => {
server.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/owner/repo',
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
(_, res, ctx) =>
res(
ctx.status(200),
@@ -393,12 +399,12 @@ describe('publish:bitbucket', () => {
ctx.json({
links: {
html: {
href: 'https://bitbucket.org/owner/repo',
href: 'https://bitbucket.org/workspace/repo',
},
clone: [
{
name: 'https',
href: 'https://bitbucket.org/owner/cloneurl',
href: 'https://bitbucket.org/workspace/cloneurl',
},
],
},
@@ -411,7 +417,7 @@ describe('publish:bitbucket', () => {
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
auth: { username: 'x-token-auth', password: 'tokenlols' },
logger: mockContext.logger,
defaultBranch: 'master',
@@ -451,7 +457,7 @@ describe('publish:bitbucket', () => {
server.use(
rest.post(
'https://api.bitbucket.org/2.0/repositories/owner/repo',
'https://api.bitbucket.org/2.0/repositories/workspace/repo',
(_, res, ctx) =>
res(
ctx.status(200),
@@ -459,12 +465,12 @@ describe('publish:bitbucket', () => {
ctx.json({
links: {
html: {
href: 'https://bitbucket.org/owner/repo',
href: 'https://bitbucket.org/workspace/repo',
},
clone: [
{
name: 'https',
href: 'https://bitbucket.org/owner/cloneurl',
href: 'https://bitbucket.org/workspace/cloneurl',
},
],
},
@@ -477,7 +483,7 @@ describe('publish:bitbucket', () => {
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://bitbucket.org/owner/cloneurl',
remoteUrl: 'https://bitbucket.org/workspace/cloneurl',
auth: { username: 'x-token-auth', password: 'tokenlols' },
logger: mockContext.logger,
defaultBranch: 'master',
@@ -61,7 +61,9 @@ export const parseRepoUrl = (
const type = integrations.byHost(host)?.type;
if (!type) {
throw new InputError(`Unable to find host ${host} in integrations`);
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
if (type === 'bitbucket') {
@@ -16,6 +16,9 @@
import { repoPickerValidation } from './validation';
import { FieldValidation } from '@rjsf/core';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/core-app-api';
import { ApiHolder } from '@backstage/core-plugin-api';
describe('RepoPicker Validation', () => {
const fieldValidator = () =>
@@ -23,30 +26,60 @@ describe('RepoPicker Validation', () => {
addError: jest.fn(),
} as unknown as FieldValidation);
const config = new ConfigReader({
integrations: {
bitbucket: [
{
host: 'bitbucket.org',
},
{
host: 'server.bitbucket.com',
},
],
github: [
{
host: 'github.com',
},
],
},
});
const scmIntegrations = ScmIntegrations.fromConfig(config);
const apiHolderMock: jest.Mocked<ApiHolder> = {
get: jest.fn().mockImplementation(() => {
return scmIntegrations;
}),
};
it('validates when no repo', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('github.com?owner=a', mockFieldValidation);
repoPickerValidation('github.com?owner=a', mockFieldValidation, {
apiHolder: apiHolderMock,
});
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided',
'Incomplete repository location provided, repo not provided',
);
});
it('validates when no owner', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('github.com?repo=a', mockFieldValidation);
repoPickerValidation('github.com?repo=a', mockFieldValidation, {
apiHolder: apiHolderMock,
});
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided',
'Incomplete repository location provided, owner not provided',
);
});
it('validates when not a real url', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('', mockFieldValidation);
repoPickerValidation('', mockFieldValidation, { apiHolder: apiHolderMock });
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Unable to parse the Repository URL',
@@ -56,8 +89,116 @@ describe('RepoPicker Validation', () => {
it('validates properly with proper input', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('github.com?owner=a&repo=b', mockFieldValidation);
repoPickerValidation('github.com?owner=a&repo=b', mockFieldValidation, {
apiHolder: apiHolderMock,
});
expect(mockFieldValidation.addError).not.toHaveBeenCalled();
});
it('validates when no workspace, project or repo provided for bitbucket cloud', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('bitbucket.org', mockFieldValidation, {
apiHolder: apiHolderMock,
});
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
1,
'Incomplete repository location provided, workspace not provided',
);
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
2,
'Incomplete repository location provided, project not provided',
);
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
3,
'Incomplete repository location provided, repo not provided',
);
});
it('validates when no workspace provided for bitbucket cloud', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation(
'bitbucket.org?project=p&repo=r',
mockFieldValidation,
{ apiHolder: apiHolderMock },
);
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided, workspace not provided',
);
});
it('validates when no project provided for bitbucket cloud', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation(
'bitbucket.org?workspace=w&repo=r',
mockFieldValidation,
{ apiHolder: apiHolderMock },
);
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided, project not provided',
);
});
it('validates when no repo provided for bitbucket cloud', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation(
'bitbucket.org?workspace=w&project=p',
mockFieldValidation,
{ apiHolder: apiHolderMock },
);
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided, repo not provided',
);
});
it('validates when no project or repo provided for bitbucket server', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('server.bitbucket.com', mockFieldValidation, {
apiHolder: apiHolderMock,
});
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
1,
'Incomplete repository location provided, project not provided',
);
expect(mockFieldValidation.addError).toHaveBeenNthCalledWith(
2,
'Incomplete repository location provided, repo not provided',
);
});
it('validates when no project provided for bitbucket server', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation('server.bitbucket.com?repo=r', mockFieldValidation, {
apiHolder: apiHolderMock,
});
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided, project not provided',
);
});
it('validates when no repo provided for bitbucket server', () => {
const mockFieldValidation = fieldValidator();
repoPickerValidation(
'server.bitbucket.com?project=p',
mockFieldValidation,
{ apiHolder: apiHolderMock },
);
expect(mockFieldValidation.addError).toHaveBeenCalledWith(
'Incomplete repository location provided, repo not provided',
);
});
});
@@ -15,16 +15,56 @@
*/
import { FieldValidation } from '@rjsf/core';
import { ApiHolder } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
export const repoPickerValidation = (
value: string,
validation: FieldValidation,
context: { apiHolder: ApiHolder },
) => {
try {
const { host, searchParams } = new URL(`https://${value}`);
if (!host || !searchParams.get('repo')) {
validation.addError('Incomplete repository location provided');
const integrationApi = context.apiHolder.get(scmIntegrationsApiRef);
if (!host) {
validation.addError(
'Incomplete repository location provided, host not provided',
);
} else {
if (integrationApi?.byHost(host)?.type === 'bitbucket') {
// workspace is only applicable for bitbucket cloud
if (host === 'bitbucket.org' && !searchParams.get('workspace')) {
validation.addError(
'Incomplete repository location provided, workspace not provided',
);
}
if (!searchParams.get('project')) {
validation.addError(
'Incomplete repository location provided, project not provided',
);
}
}
// For anything other than bitbucket
else {
if (!searchParams.get('owner')) {
validation.addError(
'Incomplete repository location provided, owner not provided',
);
}
}
// Do this for all hosts
if (!searchParams.get('repo')) {
validation.addError(
'Incomplete repository location provided, repo not provided',
);
}
}
// if (!host || !searchParams.get('owner') || !searchParams.get('repo')) {
} catch {
validation.addError('Unable to parse the Repository URL');
}