Add tests and conform to latest changes to the scaffolder

This commit is contained in:
Mathias Åhsberg
2021-01-04 11:36:10 +00:00
parent 37a5244ef2
commit 0337151237
4 changed files with 311 additions and 28 deletions
@@ -0,0 +1,145 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
jest.doMock('fs-extra', () => ({
promises: {
mkdtemp: jest.fn(dir => `${dir}-static`),
},
}));
import { BitbucketPreparer } from './bitbucket';
import {
TemplateEntityV1alpha1,
LOCATION_ANNOTATION,
} from '@backstage/catalog-model';
import { getVoidLogger, Git } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
describe('BitbucketPreparer', () => {
let mockEntity: TemplateEntityV1alpha1;
const mockGitClient = {
clone: jest.fn(),
};
jest.spyOn(Git, 'fromAuth').mockReturnValue(mockGitClient as any);
beforeEach(() => {
jest.clearAllMocks();
mockEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Template',
metadata: {
annotations: {
[LOCATION_ANNOTATION]:
'bitbucket/api:https://bitbucket.org/backstage-project/backstage-repo',
},
name: 'graphql-starter',
title: 'GraphQL Service',
description:
'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n',
uid: '9cf16bad-16e0-4213-b314-c4eec773c50b',
etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2',
generation: 1,
},
spec: {
type: 'website',
templater: 'cookiecutter',
path: './template',
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
required: ['storePath', 'owner'],
properties: {
owner: {
type: 'string',
title: 'Owner',
description: 'Who is going to own this component',
},
storePath: {
type: 'string',
title: 'Store path',
description: 'GitHub store path in org/repo format',
},
},
},
},
};
});
it('calls the clone command with the correct arguments for a repository', async () => {
const preparer = new BitbucketPreparer(new ConfigReader({}));
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://bitbucket.org/backstage-project/backstage-repo',
dir: expect.any(String),
});
});
it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => {
const preparer = new BitbucketPreparer(
new ConfigReader({
integrations: {
bitbucket: [
{
host: 'bitbucket.org',
username: 'fake-user',
appPassword: 'fake-password',
},
],
},
}),
);
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://bitbucket.org/backstage-project/backstage-repo',
dir: expect.any(String),
});
});
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
const preparer = new BitbucketPreparer(new ConfigReader({}));
delete mockEntity.spec.path;
await preparer.prepare(mockEntity, { logger: getVoidLogger() });
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://bitbucket.org/backstage-project/backstage-repo',
dir: expect.any(String),
});
});
it('return the temp directory with the path to the folder if it is specified', async () => {
const preparer = new BitbucketPreparer(new ConfigReader({}));
mockEntity.spec.path = './template/test/1/2/3';
const response = await preparer.prepare(mockEntity, {
logger: getVoidLogger(),
});
expect(response.split('\\').join('/')).toMatch(
/\/template\/test\/1\/2\/3$/,
);
});
it('return the working directory with the path to the folder if it is specified', async () => {
const preparer = new BitbucketPreparer(new ConfigReader({}));
mockEntity.spec.path = './template/test/1/2/3';
const response = await preparer.prepare(mockEntity, {
logger: getVoidLogger(),
workingDirectory: '/workDir',
});
expect(response.split('\\').join('/')).toMatch(
/\/workDir\/graphql-starter-static\/template\/test\/1\/2\/3$/,
);
});
});
@@ -18,10 +18,9 @@ import fs from 'fs-extra';
import path from 'path';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import { parseLocationAnnotation } from '../helpers';
import { InputError } from '@backstage/backend-common';
import { InputError, Git } from '@backstage/backend-common';
import { PreparerBase, PreparerOptions } from './types';
import GitUriParser from 'git-url-parse';
import { Clone, Cred } from 'nodegit';
import { Config } from '@backstage/config';
export class BitbucketPreparer implements PreparerBase {
@@ -41,6 +40,7 @@ export class BitbucketPreparer implements PreparerBase {
): Promise<string> {
const { protocol, location } = parseLocationAnnotation(template);
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
const { logger } = opts;
if (!['bitbucket/api', 'url'].includes(protocol)) {
throw new InputError(
@@ -61,19 +61,21 @@ export class BitbucketPreparer implements PreparerBase {
template.spec.path ?? '.',
);
const options = this.privateToken
? {
fetchOpts: {
callbacks: {
credentials: () =>
Cred.userpassPlaintextNew(this.username, this.privateToken),
},
},
}
: {};
const checkoutLocation = path.resolve(tempDir, templateDirectory);
await Clone.clone(repositoryCheckoutUrl, tempDir, options);
const git = this.privateToken
? Git.fromAuth({
username: this.username,
password: this.privateToken,
logger,
})
: Git.fromAuth({ logger });
return path.resolve(tempDir, templateDirectory);
await git.clone({
url: repositoryCheckoutUrl,
dir: tempDir,
});
return checkoutLocation;
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
const mockResponse = jest.fn();
jest.mock('./helpers');
jest.mock('cross-fetch', () => mockResponse);
import { BitbucketPublisher } from './bitbucket';
import { initRepoAndPush } from './helpers';
import { getVoidLogger } from '@backstage/backend-common';
describe('Bitbucket Publisher', () => {
const logger = getVoidLogger();
beforeEach(() => {
jest.clearAllMocks();
});
describe('publish: createRemoteInBitbucketCloud', () => {
it('should create repo in bitbucket cloud', async () => {
const publisher = new BitbucketPublisher(
'https://bitbucket.org',
'fake-user',
'fake-token',
);
mockResponse.mockResolvedValue({
status: 200,
json: () =>
Promise.resolve({
links: {
html: {
href: 'https://bitbucket.org/project/repo',
},
clone: [
{
name: 'https',
href: 'https://bitbucket.org/project/repo',
},
],
},
}),
});
const result = await publisher.publish({
values: {
storePath: 'project/repo',
owner: 'bob',
},
directory: '/tmp/test',
logger: logger,
});
expect(result).toEqual({
remoteUrl: 'https://bitbucket.org/project/repo',
catalogInfoUrl:
'https://bitbucket.org/project/repo/src/master/catalog-info.yaml',
});
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: '/tmp/test',
remoteUrl: 'https://bitbucket.org/project/repo',
auth: { username: 'fake-user', password: 'fake-token' },
logger: logger,
});
});
});
describe('publish: createRemoteInBitbucketServer', () => {
it('should create repo in bitbucket server', async () => {
const publisher = new BitbucketPublisher(
'https://bitbucket.mycompany.com',
'fake-user',
'fake-token',
);
mockResponse.mockResolvedValue({
status: 201,
json: () =>
Promise.resolve({
links: {
self: [
{
href:
'https://bitbucket.mycompany.com/projects/project/repos/repo',
},
],
clone: [
{
name: 'http',
href: 'https://bitbucket.mycompany.com/scm/project/repo',
},
],
},
}),
});
const result = await publisher.publish({
values: {
storePath: 'project/repo',
owner: 'bob',
},
directory: '/tmp/test',
logger: logger,
});
expect(result).toEqual({
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
catalogInfoUrl:
'https://bitbucket.mycompany.com/projects/project/repos/repo/catalog-info.yaml',
});
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: '/tmp/test',
remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo',
auth: { username: 'fake-user', password: 'fake-token' },
logger: logger,
});
});
});
});
@@ -15,7 +15,7 @@
*/
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
import { pushToRemoteUserPass } from './helpers';
import { initRepoAndPush } from './helpers';
import { RequiredTemplateValues } from '../templater';
import { JsonValue } from '../../../../../../packages/config/src';
import fetch from 'cross-fetch';
@@ -34,32 +34,36 @@ export class BitbucketPublisher implements PublisherBase {
async publish({
values,
directory,
logger,
}: PublisherOptions): Promise<PublisherResult> {
const result = await this.createRemote(values);
await pushToRemoteUserPass(
directory,
result.remoteUrl,
this.username,
this.token,
);
await initRepoAndPush({
dir: directory,
remoteUrl: result.remoteUrl,
auth: {
username: this.username,
password: this.token,
},
logger,
});
return result;
}
private async createRemote(
values: RequiredTemplateValues & Record<string, JsonValue>,
): Promise<PublisherResult> {
const [project, name] = values.storePath.split('/');
if (this.host === 'https://bitbucket.org') {
return this.createBitbucketCloudRepository(project, name);
return this.createBitbucketCloudRepository(values);
}
return this.createBitbucketServerRepository(project, name);
return this.createBitbucketServerRepository(values);
}
private async createBitbucketCloudRepository(
project: string,
name: string,
values: RequiredTemplateValues & Record<string, JsonValue>,
): Promise<PublisherResult> {
const [project, name] = values.storePath.split('/');
let response: Response;
const buffer = Buffer.from(`${this.username}:${this.token}`, 'utf8');
@@ -96,14 +100,16 @@ export class BitbucketPublisher implements PublisherBase {
}
private async createBitbucketServerRepository(
project: string,
name: string,
values: RequiredTemplateValues & Record<string, JsonValue>,
): Promise<PublisherResult> {
const [project, name] = values.storePath.split('/');
let response: Response;
const options: RequestInit = {
method: 'POST',
body: JSON.stringify({
name: name,
description: values.description,
}),
headers: {
Authorization: `Bearer ${this.token}`,