From 56f6696abb6c8d24ab5c229cc8f74f16f5e90b15 Mon Sep 17 00:00:00 2001
From: Jon Bretman <jon.bretman@gmail.com>
Date: Sat, 11 Feb 2017 19:18:54 +0000
Subject: [PATCH] Add more tests (#98)

---
 lib/api/github.js                          |  13 +-
 test/_fixtures/config/file.js              |   2 +-
 test/api/__snapshots__/github.spec.js.snap | 634 +++++++++++++++++++++
 test/api/github.spec.js                    | 421 ++++++++++++++
 test/api/npm.spec.js                       |  34 ++
 test/helpers/package-json.spec.js          |   5 +
 test/helpers/versions.spec.js              |  26 +
 7 files changed, 1123 insertions(+), 12 deletions(-)
 create mode 100644 test/api/__snapshots__/github.spec.js.snap
 create mode 100644 test/api/github.spec.js
 create mode 100644 test/api/npm.spec.js

diff --git a/lib/api/github.js b/lib/api/github.js
index 13ea7d96a3..7dffbb9c69 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -51,6 +51,7 @@ async function initRepo(repoName, token, endpoint) {
     logger.error(`GitHub init error: ${JSON.stringify(err)}`);
     throw err;
   }
+  return config;
 }
 
 // Search
@@ -223,17 +224,7 @@ async function getFileContent(filePath, branchName = config.baseBranch) {
 }
 
 async function getFileJson(filePath, branchName = config.baseBranch) {
-  try {
-    const file = await getFile(filePath, branchName);
-    return JSON.parse(new Buffer(file, 'base64').toString());
-  } 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;
-  }
+  return JSON.parse(await getFileContent(filePath, branchName));
 }
 
 // Add a new commit, create branch if not existing
diff --git a/test/_fixtures/config/file.js b/test/_fixtures/config/file.js
index 54eda96d35..7ccccf04de 100644
--- a/test/_fixtures/config/file.js
+++ b/test/_fixtures/config/file.js
@@ -1,6 +1,6 @@
 module.exports = {
   token: 'abcdefg',
-  logLevel: 'verbose',
+  logLevel: 'error',
   repositories: [
     'singapore/lint-condo',
     {
diff --git a/test/api/__snapshots__/github.spec.js.snap b/test/api/__snapshots__/github.spec.js.snap
new file mode 100644
index 0000000000..c454a51244
--- /dev/null
+++ b/test/api/__snapshots__/github.spec.js.snap
@@ -0,0 +1,634 @@
+exports[`api/github addAssignees(issueNo, assignees) should add the given assignees to the issue 1`] = `
+Array [
+  Array [
+    "repos/some/repo/issues/42/assignees",
+    Object {
+      "body": Object {
+        "assignees": Array [
+          "someuser",
+          "someotheruser",
+        ],
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/github addLabels(issueNo, labels) should add the given labels to the issue 1`] = `
+Array [
+  Array [
+    "repos/some/repo/issues/42/labels",
+    Object {
+      "body": "[\"foo\",\"bar\"]",
+    },
+  ],
+]
+`;
+
+exports[`api/github addReviewers(issueNo, reviewers) should add the given reviewers to the PR 1`] = `
+Array [
+  Array [
+    "repos/some/repo/pulls/42/requested_reviewers",
+    Object {
+      "body": Object {
+        "reviewers": Array [
+          "someuser",
+          "someotheruser",
+        ],
+      },
+      "headers": Object {
+        "accept": "application/vnd.github.black-cat-preview+json",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/github branchExists(branchName) should propagate unknown errors 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/thebranchname",
+  ],
+]
+`;
+
+exports[`api/github branchExists(branchName) should return false if a 404 is returned 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/thebranchname",
+  ],
+]
+`;
+
+exports[`api/github branchExists(branchName) should return false if a non-200 response is returned 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/thebranchname",
+  ],
+]
+`;
+
+exports[`api/github branchExists(branchName) should return true if the branch exists 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/thebranchname",
+  ],
+]
+`;
+
+exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch) should add a commit to a new branch if the branch does not already exist 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1111",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/package.json",
+  ],
+]
+`;
+
+exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch) should add a commit to a new branch if the branch does not already exist 2`] = `
+Array [
+  Array [
+    "repos/some/repo/git/blobs",
+    Object {
+      "body": Object {
+        "content": "aGVsbG8gd29ybGQ=",
+        "encoding": "base64",
+      },
+    },
+  ],
+  Array [
+    "repos/some/repo/git/trees",
+    Object {
+      "body": Object {
+        "base_tree": "2222",
+        "tree": Array [
+          Object {
+            "mode": "100644",
+            "path": "package.json",
+            "sha": "3333",
+            "type": "blob",
+          },
+        ],
+      },
+    },
+  ],
+  Array [
+    "repos/some/repo/git/commits",
+    Object {
+      "body": Object {
+        "message": "my other commit message",
+        "parents": Array [
+          "1111",
+        ],
+        "tree": "4444",
+      },
+    },
+  ],
+  Array [
+    "repos/some/repo/git/refs",
+    Object {
+      "body": Object {
+        "ref": "refs/heads/package.json",
+        "sha": "5555",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch) should add a commit to a new branch if the branch does not already exist 3`] = `Array []`;
+
+exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch) should add a new commit to the branch 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1111",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/package.json",
+  ],
+]
+`;
+
+exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch) should add a new commit to the branch 2`] = `
+Array [
+  Array [
+    "repos/some/repo/git/blobs",
+    Object {
+      "body": Object {
+        "content": "aGVsbG8gd29ybGQ=",
+        "encoding": "base64",
+      },
+    },
+  ],
+  Array [
+    "repos/some/repo/git/trees",
+    Object {
+      "body": Object {
+        "base_tree": "2222",
+        "tree": Array [
+          Object {
+            "mode": "100644",
+            "path": "package.json",
+            "sha": "3333",
+            "type": "blob",
+          },
+        ],
+      },
+    },
+  ],
+  Array [
+    "repos/some/repo/git/commits",
+    Object {
+      "body": Object {
+        "message": "my commit message",
+        "parents": Array [
+          "1111",
+        ],
+        "tree": "4444",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/github commitFilesToBranch(branchName, files, message, parentBranch) should add a new commit to the branch 3`] = `
+Array [
+  Array [
+    "repos/some/repo/git/refs/heads/package.json",
+    Object {
+      "body": Object {
+        "force": true,
+        "sha": "5555",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/github createPr(branchName, title, body) should create and return a PR object 1`] = `
+Object {
+  "displayNumber": "Pull Request #123",
+  "number": 123,
+}
+`;
+
+exports[`api/github createPr(branchName, title, body) should create and return a PR object 2`] = `
+Array [
+  Array [
+    "repos/some/repo/pulls",
+    Object {
+      "body": Object {
+        "base": "master",
+        "body": "Hello world",
+        "head": "some-branch",
+        "title": "The Title",
+      },
+    },
+  ],
+]
+`;
+
+exports[`api/github findFilePaths(fileName) should return the files matching the fileName 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "search/code?q=repo:some/repo+filename:package.json",
+  ],
+]
+`;
+
+exports[`api/github findFilePaths(fileName) should return the files matching the fileName 2`] = `
+Array [
+  "package.json",
+  "src/app/package.json",
+  "src/otherapp/package.json",
+]
+`;
+
+exports[`api/github findPr(branchName, prTitle, state) should return a PR object 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/pulls?head=theowner:master&state=all",
+  ],
+]
+`;
+
+exports[`api/github findPr(branchName, prTitle, state) should return a PR object 2`] = `
+Object {
+  "displayNumber": "Pull Request #42",
+  "number": 42,
+  "state": "open",
+  "title": "PR Title",
+}
+`;
+
+exports[`api/github findPr(branchName, prTitle, state) should return null if no PR's are found 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/pulls?head=theowner:master&state=all",
+  ],
+]
+`;
+
+exports[`api/github findPr(branchName, prTitle, state) should set the isClosed attribute of the PR to true if the PR is closed 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/pulls?head=theowner:master&state=all",
+  ],
+]
+`;
+
+exports[`api/github findPr(branchName, prTitle, state) should set the isClosed attribute of the PR to true if the PR is closed 2`] = `
+Object {
+  "displayNumber": "Pull Request #42",
+  "isClosed": true,
+  "number": 42,
+  "state": "closed",
+  "title": "PR Title",
+}
+`;
+
+exports[`api/github getBranchPr(branchName) should return null if no PR exists 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/pulls?state=open&base=master&head=theowner:somebranch",
+  ],
+]
+`;
+
+exports[`api/github getBranchPr(branchName) should return the PR object 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/pulls?state=open&base=master&head=theowner:somebranch",
+  ],
+  Array [
+    "repos/some/repo/pulls/91",
+  ],
+]
+`;
+
+exports[`api/github getBranchPr(branchName) should return the PR object 2`] = `
+Object {
+  "additions": 1,
+  "base": Object {
+    "sha": "1234",
+  },
+  "canRebase": true,
+  "commits": 1,
+  "deletions": 1,
+  "displayNumber": "Pull Request #91",
+  "number": 91,
+}
+`;
+
+exports[`api/github getFile(filePatch, branchName) should return the encoded file content 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/contents/package.json?ref=master",
+  ],
+]
+`;
+
+exports[`api/github getFileContent(filePatch, branchName) should return null if GitHub returns a 404 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/contents/package.json?ref=master",
+  ],
+]
+`;
+
+exports[`api/github getFileContent(filePatch, branchName) should return the encoded file content 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/contents/package.json?ref=master",
+  ],
+]
+`;
+
+exports[`api/github getFileJson(filePatch, branchName) should return the file contents parsed as JSON 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+  Array [
+    "repos/some/repo/contents/package.json?ref=master",
+  ],
+]
+`;
+
+exports[`api/github getFileJson(filePatch, branchName) should return the file contents parsed as JSON 2`] = `
+Object {
+  "hello": "world",
+}
+`;
+
+exports[`api/github getPr(prNo) should return a PR object - 0 1`] = `
+Object {
+  "base": Object {
+    "sha": "1234",
+  },
+  "displayNumber": "Pull Request #1",
+  "isClosed": true,
+  "number": 1,
+  "state": "closed",
+}
+`;
+
+exports[`api/github getPr(prNo) should return a PR object - 1 1`] = `
+Object {
+  "base": Object {
+    "sha": "1234",
+  },
+  "displayNumber": "Pull Request #1",
+  "isUnmergeable": true,
+  "mergeable_state": "dirty",
+  "number": 1,
+  "state": "open",
+}
+`;
+
+exports[`api/github getPr(prNo) should return a PR object - 2 1`] = `
+Object {
+  "base": Object {
+    "sha": "5678",
+  },
+  "displayNumber": "Pull Request #1",
+  "isStale": true,
+  "number": 1,
+  "state": "open",
+}
+`;
+
+exports[`api/github initRepo should initialise the config for the repo - 0 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+]
+`;
+
+exports[`api/github initRepo should initialise the config for the repo - 0 2`] = `
+Object {
+  "baseCommitSHA": "1234",
+  "baseTreeSHA": "5678",
+  "defaultBranch": "master",
+  "owner": "theowner",
+  "repoName": "some/repo",
+}
+`;
+
+exports[`api/github initRepo should initialise the config for the repo - 1 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+]
+`;
+
+exports[`api/github initRepo should initialise the config for the repo - 1 2`] = `
+Object {
+  "baseCommitSHA": "1234",
+  "baseTreeSHA": "5678",
+  "defaultBranch": "master",
+  "owner": "theowner",
+  "repoName": "some/repo",
+}
+`;
+
+exports[`api/github initRepo should initialise the config for the repo - 2 1`] = `
+Array [
+  Array [
+    "repos/some/repo",
+  ],
+  Array [
+    "repos/some/repo/git/refs/heads/master",
+  ],
+  Array [
+    "repos/some/repo/git/commits/1234",
+  ],
+]
+`;
+
+exports[`api/github initRepo should initialise the config for the repo - 2 2`] = `
+Object {
+  "baseCommitSHA": "1234",
+  "baseTreeSHA": "5678",
+  "defaultBranch": "master",
+  "owner": "theowner",
+  "repoName": "some/repo",
+}
+`;
+
+exports[`api/github updatePr(prNo, title, body) should update the PR 1`] = `
+Array [
+  Array [
+    "repos/some/repo/pulls/1234",
+    Object {
+      "body": Object {
+        "body": "Hello world again",
+        "title": "The New Title",
+      },
+    },
+  ],
+]
+`;
diff --git a/test/api/github.spec.js b/test/api/github.spec.js
new file mode 100644
index 0000000000..8008f8b9e1
--- /dev/null
+++ b/test/api/github.spec.js
@@ -0,0 +1,421 @@
+describe('api/github', () => {
+  let github;
+  let ghGot;
+  beforeEach(() => {
+    // clean up env
+    delete process.env.GITHUB_TOKEN;
+    delete process.env.GITHUB_ENDPOINT;
+
+    // reset module
+    jest.resetModules();
+    jest.mock('gh-got');
+    github = require('../../lib/api/github');
+    ghGot = require('gh-got');
+  });
+
+  async function initRepo(...args) {
+    // repo info
+    ghGot.mockImplementationOnce(() => ({
+      body: {
+        owner: {
+          login: 'theowner',
+        },
+        default_branch: 'master',
+      },
+    }));
+    // getBranchCommit
+    ghGot.mockImplementationOnce(() => ({
+      body: {
+        object: {
+          sha: '1234',
+        },
+      },
+    }));
+    // getCommitTree
+    ghGot.mockImplementationOnce(() => ({
+      body: {
+        tree: {
+          sha: '5678',
+        },
+      },
+    }));
+    return github.initRepo(...args);
+  }
+
+  describe('initRepo', () => {
+    [
+      [undefined, ['mytoken'], 'mytoken', undefined],
+      [undefined, ['mytoken', 'https://my.custom.endpoint/'], 'mytoken', 'https://my.custom.endpoint/'],
+      ['myenvtoken', [], 'myenvtoken', undefined],
+    ].forEach(([envToken, args, token, endpoint], i) => {
+      it(`should initialise the config for the repo - ${i}`, async () => {
+        if (envToken !== undefined) {
+          process.env.GITHUB_TOKEN = envToken;
+        }
+        const config = await initRepo('some/repo', ...args);
+        expect(ghGot.mock.calls).toMatchSnapshot();
+        expect(config).toMatchSnapshot();
+        expect(process.env.GITHUB_TOKEN).toBe(token);
+        expect(process.env.GITHUB_ENDPOINT).toBe(endpoint);
+      });
+    });
+    it('should throw an error if no token is provided', async () => {
+      let err;
+      try {
+        await github.initRepo('some/repo');
+      } catch (e) {
+        err = e;
+      }
+      expect(err.message).toBe('No token found for GitHub repository some/repo');
+    });
+  });
+  describe('findFilePaths(fileName)', () => {
+    it('should return the files matching the fileName', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          items: [
+              { name: 'package.json', path: '/package.json' },
+              { name: 'package.json.something-else', path: 'some-dir/package.json.some-thing-else' },
+              { name: 'package.json', path: 'src/app/package.json' },
+              { name: 'package.json', path: 'src/otherapp/package.json' },
+          ],
+        },
+      }));
+      const files = await github.findFilePaths('package.json');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(files).toMatchSnapshot();
+    });
+  });
+  describe('branchExists(branchName)', () => {
+    it('should return true if the branch exists', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        statusCode: 200,
+      }));
+      const exists = await github.branchExists('thebranchname');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(exists).toBe(true);
+    });
+    it('should return false if a non-200 response is returned', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        statusCode: 123,
+      }));
+      const exists = await github.branchExists('thebranchname');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(exists).toBe(false);
+    });
+    it('should return false if a 404 is returned', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => Promise.reject({
+        statusCode: 404,
+      }));
+      const exists = await github.branchExists('thebranchname');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(exists).toBe(false);
+    });
+    it('should propagate unknown errors', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => Promise.reject(new Error('Something went wrong')));
+      let err;
+      try {
+        await github.branchExists('thebranchname');
+      } catch (e) {
+        err = e;
+      }
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(err.message).toBe('Something went wrong');
+    });
+  });
+  describe('getBranchPr(branchName)', () => {
+    it('should return null if no PR exists', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: [],
+      }));
+      const pr = await github.getBranchPr('somebranch');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(pr).toBe(null);
+    });
+    it('should return the PR object', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: [
+            { number: 91 },
+        ],
+      }));
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          number: 91,
+          additions: 1,
+          deletions: 1,
+          commits: 1,
+          base: {
+            sha: '1234',
+          },
+        },
+      }));
+      const pr = await github.getBranchPr('somebranch');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(pr).toMatchSnapshot();
+    });
+  });
+  describe('addAssignees(issueNo, assignees)', () => {
+    it('should add the given assignees to the issue', async () => {
+      await initRepo('some/repo', 'token');
+      await github.addAssignees(42, ['someuser', 'someotheruser']);
+      expect(ghGot.post.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('addReviewers(issueNo, reviewers)', () => {
+    it('should add the given reviewers to the PR', async () => {
+      await initRepo('some/repo', 'token');
+      await github.addReviewers(42, ['someuser', 'someotheruser']);
+      expect(ghGot.post.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('addLabels(issueNo, labels)', () => {
+    it('should add the given labels to the issue', async () => {
+      await initRepo('some/repo', 'token');
+      await github.addLabels(42, ['foo', 'bar']);
+      expect(ghGot.post.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('findPr(branchName, prTitle, state)', () => {
+    it('should return a PR object', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: [
+            { title: 'PR Title', state: 'open', number: 42 },
+        ],
+      }));
+      const pr = await github.findPr('master', 'PR Title');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(pr).toMatchSnapshot();
+    });
+    it('should return null if no PR\'s are found', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: [],
+      }));
+      const pr = await github.findPr('master', 'PR Title');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(pr).toBe(null);
+    });
+    it('should set the isClosed attribute of the PR to true if the PR is closed', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: [
+            { title: 'PR Title', state: 'closed', number: 42 },
+        ],
+      }));
+      const pr = await github.findPr('master');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(pr).toMatchSnapshot();
+    });
+  });
+  describe('checkForClosedPr(branchName, prTitle)', () => {
+    [
+      ['some-branch', 'foo', true],
+      ['some-branch', 'bar', false],
+      ['some-branch', 'bop', false],
+    ].forEach(([branch, title, expected], i) => {
+      it(`should return true if a closed PR is found - ${i}`, async () => {
+        await initRepo('some/repo', 'token');
+        ghGot.mockImplementationOnce(() => ({
+          body: [
+              { title: 'foo', head: { label: 'theowner:some-branch' } },
+              { title: 'bar', head: { label: 'theowner:some-other-branch' } },
+              { title: 'baz', head: { label: 'theowner:some-branch' } },
+          ],
+        }));
+        const res = await github.checkForClosedPr(branch, title);
+        expect(res).toBe(expected);
+      });
+    });
+  });
+  describe('createPr(branchName, title, body)', () => {
+    it('should create and return a PR object', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.post.mockImplementationOnce(() => ({
+        body: {
+          number: 123,
+        },
+      }));
+      const pr = await github.createPr('some-branch', 'The Title', 'Hello world');
+      expect(pr).toMatchSnapshot();
+      expect(ghGot.post.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('getPr(prNo)', () => {
+    it('should return null if no prNo is passed', async () => {
+      const pr = await github.getPr(null);
+      expect(pr).toBe(null);
+    });
+    it('should return null if no PR is returned from GitHub', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: null,
+      }));
+      const pr = await github.getPr(1234);
+      expect(pr).toBe(null);
+    });
+    [
+      { number: 1, state: 'closed', base: { sha: '1234' } },
+      { number: 1, state: 'open', mergeable_state: 'dirty', base: { sha: '1234' } },
+      { number: 1, state: 'open', base: { sha: '5678' } },
+    ].forEach((body, i) => {
+      it(`should return a PR object - ${i}`, async () => {
+        await initRepo('some/repo', 'token');
+        ghGot.mockImplementationOnce(() => ({
+          body,
+        }));
+        const pr = await github.getPr(1234);
+        expect(pr).toMatchSnapshot();
+      });
+    });
+  });
+  describe('updatePr(prNo, title, body)', () => {
+    it('should update the PR', async () => {
+      await initRepo('some/repo', 'token');
+      await github.updatePr(1234, 'The New Title', 'Hello world again');
+      expect(ghGot.patch.mock.calls).toMatchSnapshot();
+    });
+  });
+  describe('getFile(filePatch, branchName)', () => {
+    it('should return the encoded file content', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          content: 'hello',
+        },
+      }));
+      const content = await github.getFile('package.json');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(content).toBe('hello');
+    });
+  });
+  describe('getFileContent(filePatch, branchName)', () => {
+    it('should return the encoded file content', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          content: Buffer.from('hello world').toString('base64'),
+        },
+      }));
+      const content = await github.getFileContent('package.json');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(content).toBe('hello world');
+    });
+    it('should return null if GitHub returns a 404', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => {
+        const error = new Error();
+        error.statusCode = 404;
+        throw error;
+      });
+      const content = await github.getFileContent('package.json');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(content).toBe(null);
+    });
+    it('should return propagate unknown errors', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => {
+        throw new Error('Something went wrong');
+      });
+      let err;
+      try {
+        await github.getFileContent('package.json');
+      } catch (e) {
+        err = e;
+      }
+      expect(err.message).toBe('Something went wrong');
+    });
+  });
+  describe('getFileJson(filePatch, branchName)', () => {
+    it('should return the file contents parsed as JSON', async () => {
+      await initRepo('some/repo', 'token');
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          content: Buffer.from('{"hello": "world"}').toString('base64'),
+        },
+      }));
+      const content = await github.getFileJson('package.json');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(content).toMatchSnapshot();
+    });
+  });
+  describe('commitFilesToBranch(branchName, files, message, parentBranch)', () => {
+    beforeEach(async () => {
+      await initRepo('some/repo', 'token');
+
+      // getBranchCommit
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          object: {
+            sha: '1111',
+          },
+        },
+      }));
+
+      // getCommitTree
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          tree: {
+            sha: '2222',
+          },
+        },
+      }));
+
+      // createBlob
+      ghGot.post.mockImplementationOnce(() => ({
+        body: {
+          sha: '3333',
+        },
+      }));
+
+      // createTree
+      ghGot.post.mockImplementationOnce(() => ({
+        body: {
+          sha: '4444',
+        },
+      }));
+
+      // createCommit
+      ghGot.post.mockImplementationOnce(() => ({
+        body: {
+          sha: '5555',
+        },
+      }));
+    });
+    it('should add a new commit to the branch', async () => {
+      // branchExists
+      ghGot.mockImplementationOnce(() => ({
+        statusCode: 200,
+      }));
+      const files = [{
+        name: 'package.json',
+        contents: 'hello world',
+      }];
+      await github.commitFilesToBranch('package.json', files, 'my commit message');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(ghGot.post.mock.calls).toMatchSnapshot();
+      expect(ghGot.patch.mock.calls).toMatchSnapshot();
+    });
+    it('should add a commit to a new branch if the branch does not already exist', async () => {
+      // branchExists
+      ghGot.mockImplementationOnce(() => ({
+        statusCode: 404,
+      }));
+      const files = [{
+        name: 'package.json',
+        contents: 'hello world',
+      }];
+      await github.commitFilesToBranch('package.json', files, 'my other commit message');
+      expect(ghGot.mock.calls).toMatchSnapshot();
+      expect(ghGot.post.mock.calls).toMatchSnapshot();
+      expect(ghGot.patch.mock.calls).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/api/npm.spec.js b/test/api/npm.spec.js
new file mode 100644
index 0000000000..fe9b5ac4e7
--- /dev/null
+++ b/test/api/npm.spec.js
@@ -0,0 +1,34 @@
+const npm = require('../../lib/api/npm');
+const got = require('got');
+const registryUrl = require('registry-url');
+const registryAuthToken = require('registry-auth-token');
+
+jest.mock('registry-url');
+jest.mock('registry-auth-token');
+jest.mock('got');
+
+describe('api/npm', () => {
+  beforeEach(() => {
+    jest.resetAllMocks();
+  });
+  it('should fetch package info from npm', async () => {
+    registryUrl.mockImplementation(() => 'https://npm.mycustomregistry.com/');
+    got.mockImplementation(() => Promise.resolve({ body: { some: 'data' } }));
+    const res = await npm.getDependency('foobar');
+    expect(res).toMatchObject({ some: 'data' });
+    const call = got.mock.calls[0];
+    expect(call).toMatchObject(['https://npm.mycustomregistry.com/foobar', { json: true, headers: {} }]);
+  });
+  it('should send an authorization header if provided', async () => {
+    registryUrl.mockImplementation(() => 'https://npm.mycustomregistry.com/');
+    registryAuthToken.mockImplementation(() => ({ type: 'Basic', token: '1234' }));
+    got.mockImplementation(() => Promise.resolve({ body: { some: 'data' } }));
+    const res = await npm.getDependency('foobar');
+    expect(res).toMatchObject({ some: 'data' });
+    const call = got.mock.calls[0];
+    expect(call).toMatchObject(['https://npm.mycustomregistry.com/foobar', { json: true,
+      headers: {
+        authorization: 'Basic 1234',
+      } }]);
+  });
+});
diff --git a/test/helpers/package-json.spec.js b/test/helpers/package-json.spec.js
index da891e3888..3d00eac92d 100644
--- a/test/helpers/package-json.spec.js
+++ b/test/helpers/package-json.spec.js
@@ -51,5 +51,10 @@ describe('helpers/package-json', () => {
         packageJson.setNewValue(input01Content, 'devDependencies', 'angular-sanitize', '1.6.1');
       testContent.should.equal(outputContent);
     });
+    it('handles the case where the desired version is already supported', () => {
+      const testContent =
+        packageJson.setNewValue(input01Content, 'devDependencies', 'angular-touch', '1.5.8');
+      testContent.should.equal(input01Content);
+    });
   });
 });
diff --git a/test/helpers/versions.spec.js b/test/helpers/versions.spec.js
index 318c27fb03..c68aa2c942 100644
--- a/test/helpers/versions.spec.js
+++ b/test/helpers/versions.spec.js
@@ -91,6 +91,32 @@ describe('helpers/versions', () => {
       ];
       versionsHelper.determineUpgrades(qJson, '^2.0.0', defaultConfig).should.eql(upgradeVersions);
     });
+    it('should ignore unstable versions if the current version is stable', () => {
+      versionsHelper.determineUpgrades({
+        name: 'amazing-package',
+        versions: {
+          '1.0.0': {},
+          '1.1.0-beta': {},
+        },
+      }, '1.0.0', defaultConfig).should.eql([]);
+    });
+    it('should allow unstable versions if the current version is unstable', () => {
+      const upgradeVersions = [
+        {
+          newVersion: '1.1.0-beta',
+          newVersionMajor: 1,
+          upgradeType: 'minor',
+          workingVersion: '1.0.0-beta',
+        },
+      ];
+      versionsHelper.determineUpgrades({
+        name: 'amazing-package',
+        versions: {
+          '1.0.0-beta': {},
+          '1.1.0-beta': {},
+        },
+      }, '1.0.0-beta', defaultConfig).should.eql(upgradeVersions);
+    });
   });
   describe('.isRange(input)', () => {
     it('rejects simple semver', () => {
-- 
GitLab