diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js index f08e37861d6a5675505da668b8c1d8842497c43d..36afaccba3e45805eddd895fd7589a747187f4b2 100644 --- a/lib/api/gitlab.js +++ b/lib/api/gitlab.js @@ -10,6 +10,7 @@ module.exports = { findFilePaths, // Branch branchExists, + createBranch, getBranch, getBranchPr, getBranchStatus, @@ -30,6 +31,8 @@ module.exports = { getFile, getFileContent, getFileJson, + createFile, + updateFile, }; // Get all repositories that the user has access to @@ -340,17 +343,8 @@ async function getFileContent(filePath, branchName) { } async function getFileJson(filePath, branchName) { - try { - const fileContent = await getFileContent(filePath, branchName); - return JSON.parse(fileContent); - } catch (error) { - if (error.statusCode === 404) { - // If file not found, then return null JSON - return null; - } - // Propagate if it's any other error - throw error; - } + const fileContent = await getFileContent(filePath, branchName); + return JSON.parse(fileContent); } async function createFile(branchName, filePath, fileContents, message) { diff --git a/test/_fixtures/logger/index.js b/test/_fixtures/logger/index.js new file mode 100644 index 0000000000000000000000000000000000000000..94858060fbc11941687072df541322ffeb05b47b --- /dev/null +++ b/test/_fixtures/logger/index.js @@ -0,0 +1,9 @@ +module.exports = { + fatal: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + child: jest.fn(() => module.exports), +}; diff --git a/test/api/__snapshots__/gitlab.spec.js.snap b/test/api/__snapshots__/gitlab.spec.js.snap index d54a7bf4216db663f641f4d60a0035a1c1fa3e6e..9e82ead5d4e3b6a4ad7170f104a390d320d2f54b 100644 --- a/test/api/__snapshots__/gitlab.spec.js.snap +++ b/test/api/__snapshots__/gitlab.spec.js.snap @@ -1,5 +1,242 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`api/gitlab addAssignees(issueNo, assignees) should add the given assignees to the issue 1`] = ` +Array [ + Array [ + "projects/some%2Frepo/merge_requests/42?assignee_id=someuser", + ], +] +`; + +exports[`api/gitlab addAssignees(issueNo, assignees) should log error if more than one assignee 1`] = ` +Array [ + Array [ + "projects/some%2Frepo/merge_requests/42?assignee_id=someuser", + ], +] +`; + +exports[`api/gitlab addLabels(issueNo, labels) should add the given labels to the issue 1`] = ` +Array [ + Array [ + "projects/some%2Frepo/merge_requests/42?labels=foo,bar", + ], +] +`; + +exports[`api/gitlab createFile(branchName, filePath, fileContents, message) createBranch(branchName) creates file with v3 1`] = ` +Array [ + Array [ + "projects/some-repo/repository/branches", + Object { + "body": Object { + "branch_name": "some-branch", + "ref": undefined, + }, + }, + ], +] +`; + +exports[`api/gitlab createFile(branchName, filePath, fileContents, message) createBranch(branchName) creates file with v4 1`] = ` +Array [ + Array [ + "projects/undefined/repository/branches", + Object { + "body": Object { + "branch": "some-branch", + "ref": undefined, + }, + }, + ], +] +`; + +exports[`api/gitlab createFile(branchName, filePath, fileContents, message) creates file with v3 1`] = ` +Array [ + Array [ + "projects/some-repo/repository/files", + Object { + "body": Object { + "branch_name": "some-branch", + "commit_message": "some-message", + "content": "c29tZS1jb250ZW50cw==", + "encoding": "base64", + "file_path": "some-path", + }, + }, + ], +] +`; + +exports[`api/gitlab createFile(branchName, filePath, fileContents, message) creates file with v4 1`] = ` +Array [ + Array [ + "projects/undefined/repository/files/some-path", + Object { + "body": Object { + "branch": "some-branch", + "commit_message": "some-message", + "content": "c29tZS1jb250ZW50cw==", + "encoding": "base64", + }, + }, + ], +] +`; + +exports[`api/gitlab createFile(branchName, filePath, fileContents, message) updateFile(branchName, filePath, fileContents, message) creates file with v3 1`] = ` +Array [ + Array [ + "projects/some-repo/repository/files", + Object { + "body": Object { + "branch_name": "some-branch", + "commit_message": "some-message", + "content": "c29tZS1jb250ZW50cw==", + "encoding": "base64", + "file_path": "some-path", + }, + }, + ], +] +`; + +exports[`api/gitlab createFile(branchName, filePath, fileContents, message) updateFile(branchName, filePath, fileContents, message) creates file with v4 1`] = ` +Array [ + Array [ + "projects/undefined/repository/files/some-path", + Object { + "body": Object { + "branch": "some-branch", + "commit_message": "some-message", + "content": "c29tZS1jb250ZW50cw==", + "encoding": "base64", + }, + }, + ], +] +`; + +exports[`api/gitlab createPr(branchName, title, body) returns the PR 1`] = ` +Object { + "displayNumber": "Merge Request #12345", + "id": 1, + "iid": 12345, + "number": 1, +} +`; + +exports[`api/gitlab createPr(branchName, title, body) returns the PR 2`] = ` +Array [ + Array [ + "projects/undefined/merge_requests", + Object { + "body": Object { + "description": "the-body", + "remove_source_branch": true, + "source_branch": "some-branch", + "target_branch": undefined, + "title": "some-title", + }, + }, + ], +] +`; + +exports[`api/gitlab getBranch returns a branch 1`] = `"foo"`; + +exports[`api/gitlab getBranchPr(branchName) should return null if no PR exists 1`] = ` +Array [ + Array [ + "projects/owned", + ], + Array [ + "projects/some%2Frepo", + ], + Array [ + "user", + ], + Array [ + "projects/some%2Frepo/merge_requests?state=opened", + ], +] +`; + +exports[`api/gitlab getBranchPr(branchName) should return the PR object 1`] = ` +Array [ + Array [ + "projects/owned", + ], + Array [ + "projects/some%2Frepo", + ], + Array [ + "user", + ], + Array [ + "projects/some%2Frepo/merge_requests?state=opened", + ], + Array [ + "projects/some%2Frepo/merge_requests/undefined", + ], + Array [ + "projects/some%2Frepo/repository/branches/undefined", + ], +] +`; + +exports[`api/gitlab getBranchPr(branchName) should return the PR object 2`] = ` +Object { + "additions": 1, + "base": Object { + "sha": "1234", + }, + "body": undefined, + "commits": 1, + "deletions": 1, + "displayNumber": "Merge Request #undefined", + "number": undefined, +} +`; + +exports[`api/gitlab getFile(filePath, branchName) gets the file with v3 1`] = ` +Object { + "apiVersion": "v3", + "defaultBranch": undefined, + "email": undefined, + "repoName": "some-repo", +} +`; + +exports[`api/gitlab getFile(filePath, branchName) gets the file with v3 2`] = `"foo"`; + +exports[`api/gitlab getFile(filePath, branchName) gets the file with v4 by default 1`] = `"foo"`; + +exports[`api/gitlab getFileContent(filePath, branchName) gets the file 1`] = `"~�"`; + +exports[`api/gitlab getFileContent(filePath, branchName) throws error for non-404 1`] = ` +Object { + "statusCode": 403, +} +`; + +exports[`api/gitlab getPr(prNo) returns the PR 1`] = ` +Object { + "body": "a merge request", + "canRebase": true, + "description": "a merge request", + "displayNumber": "Merge Request #12345", + "id": 1, + "iid": 12345, + "isClosed": true, + "isUnmergeable": true, + "merge_status": "cannot_be_merged", + "number": 12345, + "state": "merged", +} +`; + exports[`api/gitlab getRepos should return an array of repos 1`] = ` Array [ Array [ diff --git a/test/api/github.spec.js b/test/api/github.spec.js index 8405d9704956bb33c3f91cbd3f3cd2e1f25c8a04..b7d6a366a28c4d38dd1ffc175a344ac2367ec7f5 100644 --- a/test/api/github.spec.js +++ b/test/api/github.spec.js @@ -1,10 +1,4 @@ -const bunyan = require('bunyan'); - -const logger = bunyan.createLogger({ - name: 'test', - stream: process.stdout, - level: 'fatal', -}); +const logger = require('../_fixtures/logger'); describe('api/github', () => { let github; diff --git a/test/api/gitlab.spec.js b/test/api/gitlab.spec.js index a02d24eccd8d2345f223b69ba3c3ab5266388250..7a2b2538543921d7696d421fea3f2447fcfd3a67 100644 --- a/test/api/gitlab.spec.js +++ b/test/api/gitlab.spec.js @@ -1,10 +1,4 @@ -const bunyan = require('bunyan'); - -const logger = bunyan.createLogger({ - name: 'test', - stream: process.stdout, - level: 'fatal', -}); +const logger = require('../_fixtures/logger'); describe('api/gitlab', () => { let gitlab; @@ -204,4 +198,429 @@ describe('api/gitlab', () => { expect(e.statusCode).toBe(500); }); }); + describe('getBranch', () => { + it('returns a branch', async () => { + glGot.mockReturnValueOnce({ body: 'foo' }); + const branch = await gitlab.getBranch('branch-name'); + expect(branch).toMatchSnapshot(); + }); + it('nulls on error', async () => { + glGot.mockImplementationOnce(() => { + throw new Error('not found'); + }); + const branch = await gitlab.getBranch('branch-name'); + expect(branch).toBe(null); + }); + }); + describe('getBranchPr(branchName)', () => { + it('should return null if no PR exists', async () => { + await initRepo('some/repo', 'token'); + glGot.mockImplementationOnce(() => ({ + body: [], + })); + const pr = await gitlab.getBranchPr('somebranch'); + expect(glGot.mock.calls).toMatchSnapshot(); + expect(pr).toBe(null); + }); + it('should return the PR object', async () => { + await initRepo('some/repo', 'token'); + glGot.mockImplementationOnce(() => ({ + body: [{ number: 91, source_branch: 'somebranch' }], + })); + glGot.mockImplementationOnce(() => ({ + body: { + number: 91, + additions: 1, + deletions: 1, + commits: 1, + base: { + sha: '1234', + }, + }, + })); + const pr = await gitlab.getBranchPr('somebranch'); + expect(glGot.mock.calls).toMatchSnapshot(); + expect(pr).toMatchSnapshot(); + }); + }); + describe('getBranchStatus(branchName)', () => { + beforeEach(() => { + glGot.mockReturnValueOnce({ + body: { + commit: { + id: 1, + }, + }, + }); + }); + it('returns pending if no results', async () => { + glGot.mockReturnValueOnce({ + body: [], + }); + const res = await gitlab.getBranchStatus('some-branch'); + expect(res).toEqual('pending'); + }); + it('returns success if all are success', async () => { + glGot.mockReturnValueOnce({ + body: [{ status: 'success' }, { status: 'success' }], + }); + const res = await gitlab.getBranchStatus('some-branch'); + expect(res).toEqual('success'); + }); + it('returns failure if any are failed', async () => { + glGot.mockReturnValueOnce({ + body: [{ status: 'success' }, { status: 'failed' }], + }); + const res = await gitlab.getBranchStatus('some-branch'); + expect(res).toEqual('failure'); + }); + it('returns custom statuses', async () => { + glGot.mockReturnValueOnce({ + body: [{ status: 'success' }, { status: 'foo' }], + }); + const res = await gitlab.getBranchStatus('some-branch'); + expect(res).toEqual('foo'); + }); + }); + describe('deleteBranch(branchName)', () => { + it('should send delete', async () => { + glGot.delete = jest.fn(); + await gitlab.deleteBranch('some-branch'); + expect(glGot.delete.mock.calls.length).toBe(1); + }); + }); + describe('addAssignees(issueNo, assignees)', () => { + it('should add the given assignees to the issue', async () => { + await initRepo('some/repo', 'token'); + await gitlab.addAssignees(42, ['someuser']); + expect(glGot.put.mock.calls).toMatchSnapshot(); + }); + it('should log error if more than one assignee', async () => { + await initRepo('some/repo', 'token'); + await gitlab.addAssignees(42, ['someuser', 'someotheruser']); + expect(glGot.put.mock.calls).toMatchSnapshot(); + }); + }); + describe('addReviewers(issueNo, reviewers)', () => { + it('should add the given reviewers to the PR', async () => { + await initRepo('some/repo', 'token'); + await gitlab.addReviewers(42, ['someuser', 'someotheruser']); + }); + }); + describe('addLabels(issueNo, labels)', () => { + it('should add the given labels to the issue', async () => { + await initRepo('some/repo', 'token'); + await gitlab.addLabels(42, ['foo', 'bar']); + expect(glGot.put.mock.calls).toMatchSnapshot(); + }); + }); + describe('findPr(branchName, prTitle, state)', () => { + it('returns null if no results', async () => { + glGot.mockReturnValueOnce({ + body: [], + }); + const pr = await gitlab.findPr('some-branch'); + expect(pr).toBe(null); + }); + it('returns null if no matching titles', async () => { + glGot.mockReturnValueOnce({ + body: [ + { + source_branch: 'some-branch', + id: 1, + }, + { + source_branch: 'some-branch', + id: 2, + title: 'foo', + }, + ], + }); + const pr = await gitlab.findPr('some-branch', 'some-title'); + expect(pr).toBe(null); + }); + it('returns last result if multiple match', async () => { + glGot.mockReturnValueOnce({ + body: [ + { + source_branch: 'some-branch', + id: 1, + }, + { + source_branch: 'some-branch', + id: 2, + }, + ], + }); + const pr = await gitlab.findPr('some-branch'); + expect(pr.number).toBe(2); + }); + }); + describe('checkForClosedPr(branchName, prTitle)', () => { + it('returns true if pr exists', async () => { + glGot.mockReturnValueOnce({ + body: [ + { + source_branch: 'some-branch', + id: 1, + }, + { + source_branch: 'some-branch', + id: 2, + }, + ], + }); + const res = await gitlab.checkForClosedPr('some-branch'); + expect(res).toBe(true); + }); + it('returns false if pr does not exist', async () => { + glGot.mockReturnValueOnce({ + body: [], + }); + const res = await gitlab.checkForClosedPr('some-branch'); + expect(res).toBe(false); + }); + }); + describe('createPr(branchName, title, body)', () => { + it('returns the PR', async () => { + glGot.post.mockReturnValueOnce({ + body: { + id: 1, + iid: 12345, + }, + }); + const pr = await gitlab.createPr('some-branch', 'some-title', 'the-body'); + expect(pr).toMatchSnapshot(); + expect(glGot.post.mock.calls).toMatchSnapshot(); + }); + }); + describe('getPr(prNo)', () => { + it('returns the PR', async () => { + glGot.mockReturnValueOnce({ + body: { + id: 1, + iid: 12345, + description: 'a merge request', + state: 'merged', + merge_status: 'cannot_be_merged', + }, + }); + glGot.mockReturnValueOnce({ + body: { + commit: {}, + }, + }); + const pr = await gitlab.getPr(12345); + expect(pr).toMatchSnapshot(); + }); + }); + describe('updatePr(prNo, title, body)', () => { + jest.resetAllMocks(); + it('updates the PR', async () => { + await gitlab.updatePr(); + expect(glGot.put.mock.calls.length).toEqual(1); + }); + }); + describe('mergePr(pr)', () => { + jest.resetAllMocks(); + it('merges the PR', async () => { + await gitlab.mergePr({ number: 1 }); + expect(glGot.put.mock.calls.length).toEqual(1); + }); + }); + describe('getFile(filePath, branchName)', () => { + it('gets the file with v4 by default', async () => { + glGot.mockReturnValueOnce({ + body: { + content: 'foo', + }, + }); + const res = await gitlab.getFile('some-path', 'some-branch'); + expect(res).toMatchSnapshot(); + expect(glGot.mock.calls[0][0].indexOf('file_path')).toBe(-1); + }); + it('gets the file with v3', async () => { + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: { + content: 'foo', + }, + }); + const config = await gitlab.initRepo('some-repo', 'some-token'); + expect(config).toMatchSnapshot(); + const res = await gitlab.getFile('some-path', 'some-branch'); + expect(res).toMatchSnapshot(); + expect(glGot.mock.calls[3][0].indexOf('file_path')).not.toBe(-1); + }); + }); + describe('getFileContent(filePath, branchName)', () => { + it('gets the file', async () => { + glGot.mockReturnValueOnce({ + body: { + content: 'foo', + }, + }); + const res = await gitlab.getFileContent('some-path', 'some-branch'); + expect(res).toMatchSnapshot(); + }); + it('returns null for 404', async () => { + glGot.mockImplementationOnce(() => Promise.reject({ statusCode: 404 })); + const res = await gitlab.getFileContent('some-path', 'some-branch'); + expect(res).toBe(null); + }); + it('throws error for non-404', async () => { + glGot.mockImplementationOnce(() => Promise.reject({ statusCode: 403 })); + let e; + try { + await gitlab.getFileContent('some-path', 'some-branch'); + } catch (err) { + e = err; + } + expect(e).toMatchSnapshot(); + }); + }); + describe('getFileJson(filePath, branchName)', () => { + it('returns null for 404', async () => { + glGot.mockImplementationOnce(() => Promise.reject({ statusCode: 404 })); + const res = await gitlab.getFileJson('some-path', 'some-branch'); + expect(res).toBe(null); + }); + }); + describe('createFile(branchName, filePath, fileContents, message)', () => { + it('creates file with v4', async () => { + await gitlab.createFile( + 'some-branch', + 'some-path', + 'some-contents', + 'some-message' + ); + expect(glGot.post.mock.calls).toMatchSnapshot(); + expect(glGot.post.mock.calls[0][1].body.file_path).not.toBeDefined(); + }); + it('creates file with v3', async () => { + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + await gitlab.initRepo('some-repo', 'some-token'); + await gitlab.createFile( + 'some-branch', + 'some-path', + 'some-contents', + 'some-message' + ); + expect(glGot.post.mock.calls).toMatchSnapshot(); + expect(glGot.post.mock.calls[0][1].body.file_path).toBeDefined(); + }); + describe('updateFile(branchName, filePath, fileContents, message)', () => { + it('creates file with v4', async () => { + await gitlab.updateFile( + 'some-branch', + 'some-path', + 'some-contents', + 'some-message' + ); + expect(glGot.put.mock.calls).toMatchSnapshot(); + expect(glGot.put.mock.calls[0][1].body.file_path).not.toBeDefined(); + }); + it('creates file with v3', async () => { + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + await gitlab.initRepo('some-repo', 'some-token'); + await gitlab.updateFile( + 'some-branch', + 'some-path', + 'some-contents', + 'some-message' + ); + expect(glGot.put.mock.calls).toMatchSnapshot(); + expect(glGot.put.mock.calls[0][1].body.file_path).toBeDefined(); + }); + }); + describe('createBranch(branchName)', () => { + it('creates file with v4', async () => { + await gitlab.createBranch('some-branch'); + expect(glGot.post.mock.calls).toMatchSnapshot(); + expect(glGot.post.mock.calls[0][1].body.branch_name).not.toBeDefined(); + }); + it('creates file with v3', async () => { + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + glGot.mockReturnValueOnce({ + body: {}, + }); + await gitlab.initRepo('some-repo', 'some-token'); + await gitlab.createBranch('some-branch'); + expect(glGot.post.mock.calls).toMatchSnapshot(); + expect(glGot.post.mock.calls[0][1].body.branch_name).toBeDefined(); + }); + }); + describe('commitFilesToBranch(branchName, files, message, parentBranch)', () => { + it('creates branch', async () => { + glGot.mockReturnValueOnce({ statusCode: 404 }); + await gitlab.commitFilesToBranch( + 'some-branch', + [], + 'some-message', + 'parent-branch' + ); + }); + it('does not create branch and updates file', async () => { + glGot.mockReturnValueOnce({ statusCode: 200 }); + glGot.mockReturnValueOnce({ + body: { + content: 'hello', + }, + }); + const file = { + name: 'foo', + contents: 'bar', + }; + await gitlab.commitFilesToBranch( + 'some-branch', + [file], + 'some-message', + 'parent-branch' + ); + }); + it('does not create branch and creates file', async () => { + glGot.mockReturnValueOnce({ statusCode: 200 }); + glGot.mockReturnValueOnce(Promise.reject({ statusCode: 404 })); + const file = { + name: 'foo', + contents: 'bar', + }; + await gitlab.commitFilesToBranch( + 'some-branch', + [file], + 'some-message', + 'parent-branch' + ); + }); + }); + }); });