diff --git a/.babelrc b/.babelrc index 58ef8769d905c6105a329d349bccfb9cbf6c36ec..1caf31a8b372456d2af2a06a048e2e5ccb5e71f1 100644 --- a/.babelrc +++ b/.babelrc @@ -15,5 +15,12 @@ "@babel/proposal-object-rest-spread" ], "sourceMaps": true, - "retainLines": true + "retainLines": true, + + // https://github.com/facebook/jest/issues/5920 + "env": { + "test": { + "plugins": ["dynamic-import-node"] + } + } } diff --git a/.vscode/launch.json b/.vscode/launch.json index ac62892b78639fbce05e340d081a2b7631d70505..c72c3d30887e38f7276bf24c7790bfd4e42dadac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -24,7 +24,7 @@ "--collectCoverage=false", "${fileBasenameNoExtension}" ], - "env": { "LOG_LEVEL": "debug" }, + "env": { "NODE_ENV": "test", "LOG_LEVEL": "debug" }, "console": "integratedTerminal", "disableOptimisticBPs": true, "windows": { @@ -40,7 +40,7 @@ "name": "Jest All", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["--runInBand", "--collectCoverage=false"], - "env": { "LOG_LEVEL": "debug" }, + "env": { "NODE_ENV": "test", "LOG_LEVEL": "debug" }, "console": "integratedTerminal", "disableOptimisticBPs": true, "windows": { diff --git a/lib/datasource/github/index.js b/lib/datasource/github/index.js index a00b22ad41a5a0f4109d6d1f75e96fe250c75de0..aef7ae9b67fbecfb48d3635d5c86be1f2334f1b4 100644 --- a/lib/datasource/github/index.js +++ b/lib/datasource/github/index.js @@ -1,4 +1,5 @@ -const ghGot = require('../../platform/github/gh-got-wrapper'); +import ghGot from '../../platform/github/gh-got-wrapper'; + const got = require('../../util/got'); module.exports = { diff --git a/lib/platform/github/gh-got-wrapper.js b/lib/platform/github/gh-got-wrapper.ts similarity index 89% rename from lib/platform/github/gh-got-wrapper.js rename to lib/platform/github/gh-got-wrapper.ts index 11dac1623a8fbfd77a9a1930a55f1120adca83c0..8b8662a925e098472c035881c7fe3677624ea87b 100644 --- a/lib/platform/github/gh-got-wrapper.js +++ b/lib/platform/github/gh-got-wrapper.ts @@ -1,14 +1,19 @@ -const URL = require('url'); -const parseLinkHeader = require('parse-link-header'); -const pAll = require('p-all'); +import URL from 'url'; +import parseLinkHeader from 'parse-link-header'; +import pAll from 'p-all'; -const got = require('../../util/got'); -const { maskToken } = require('../../util/mask'); +import got from '../../util/got'; +import { maskToken } from '../../util/mask'; +import { GotApi } from '../common'; const hostType = 'github'; let baseUrl = 'https://api.github.com/'; -async function get(path, options, okToRetry = true) { +async function get( + path: string, + options?: any, + okToRetry = true +): Promise<any> { const opts = { hostType, baseUrl, @@ -55,14 +60,14 @@ async function get(path, options, okToRetry = true) { const queue = pageNumbers.map(page => () => { const nextUrl = URL.parse(linkHeader.next.url, true); delete nextUrl.search; - nextUrl.query.page = page; + nextUrl.query.page = page.toString(); return get( URL.format(nextUrl), { ...opts, paginate: false }, okToRetry ); }); - const pages = await pAll(queue, { concurrency: 5 }); + const pages = await pAll<{ body: any[] }>(queue, { concurrency: 5 }); res.body = res.body.concat( ...pages.filter(Boolean).map(page => page.body) ); @@ -148,7 +153,7 @@ async function get(path, options, okToRetry = true) { const helpers = ['get', 'post', 'put', 'patch', 'head', 'delete']; for (const x of helpers) { - get[x] = (url, opts) => + (get as any)[x] = (url: string, opts: any) => get(url, Object.assign({}, opts, { method: x.toUpperCase() })); } @@ -156,8 +161,9 @@ get.setAppMode = function setAppMode() { // no-op }; -get.setBaseUrl = u => { +get.setBaseUrl = (u: string) => { baseUrl = u; }; -module.exports = get; +export const api: GotApi = get as any; +export default api; diff --git a/lib/platform/github/index.js b/lib/platform/github/index.ts similarity index 80% rename from lib/platform/github/index.js rename to lib/platform/github/index.ts index 840dfe2cd940be983be53ffc249c3ce56ad3a316..eb54fd4d7e813bd2eb706be0f485bad201970896 100644 --- a/lib/platform/github/index.js +++ b/lib/platform/github/index.ts @@ -1,94 +1,93 @@ -const is = require('@sindresorhus/is'); -const delay = require('delay'); -const semver = require('semver'); -const URL = require('url'); +import is from '@sindresorhus/is'; +import delay from 'delay'; +import semver from 'semver'; +import URL from 'url'; -const get = require('./gh-got-wrapper'); -const hostRules = require('../../util/host-rules'); -const GitStorage = require('../git/storage').Storage; +import { api } from './gh-got-wrapper'; +import * as hostRules from '../../util/host-rules'; +import GitStorage from '../git/storage'; -const { +import { appName, appSlug, configFileNames, urls, -} = require('../../config/app-strings'); +} from '../../config/app-strings'; const defaultConfigFile = configFileNames[0]; -let config = {}; +interface Comment { + id: number; + body: string; +} + +interface Pr { + displayNumber: string; + state: string; + title: string; + branchName: string; + number: number; + comments: Comment[]; +} + +interface RepoConfig { + repositoryName: string; + pushProtection: boolean; + prReviewsRequired: boolean; + repoForceRebase?: boolean; + storage: GitStorage; + parentRepo: string; + baseCommitSHA: string | null; + forkToken?: string; + closedPrList: { [num: number]: Pr } | null; + openPrList: { [num: number]: Pr } | null; + prList: Pr[] | null; + issueList: any[] | null; + mergeMethod: string; + baseBranch: string; + defaultBranch: string; + enterpriseVersion: string; + gitPrivateKey?: string; + repositoryOwner: string; + repository: string | null; + localDir: string; + isGhe: boolean; + renovateUsername: string; +} + +let config: RepoConfig = {} as any; const defaults = { hostType: 'github', endpoint: 'https://api.github.com/', }; -module.exports = { - // Initialization - initPlatform, - getRepos, - cleanRepo, - initRepo, - getRepoStatus, - getRepoForceRebase, - setBaseBranch, - setBranchPrefix, - // Search - getFileList, - // Branch - branchExists, - getAllRenovateBranches, - isBranchStale, - getBranchPr, - getBranchStatus, - getBranchStatusCheck, - setBranchStatus, - deleteBranch, - mergeBranch, - getBranchLastCommitTime, - // issue - findIssue, - ensureIssue, - ensureIssueClosing, - addAssignees, - addReviewers, - deleteLabel, - getIssueList, - // Comments - ensureComment, - ensureCommentRemoval, - // PR - getPrList, - findPr, - createPr, - getPr, - getPrFiles, - updatePr, - mergePr, - getPrBody, - // file - commitFilesToBranch, - getFile, - // Commits - getCommitMessages, - // vulnerability alerts - getVulnerabilityAlerts, -}; - -async function initPlatform({ endpoint, token }) { +export async function initPlatform({ + endpoint, + token, +}: { + endpoint: string; + token: string; +}) { if (!token) { throw new Error('Init: You must configure a GitHub personal access token'); } - const res = {}; + interface PlatformConfig { + gitAuthor: string; + renovateUsername: string; + endpoint: string; + } + + const res: PlatformConfig = {} as any; if (endpoint) { defaults.endpoint = endpoint.replace(/\/?$/, '/'); // always add a trailing slash - get.setBaseUrl(defaults.endpoint); + api.setBaseUrl(defaults.endpoint); } else { logger.info('Using default github endpoint: ' + defaults.endpoint); } res.endpoint = defaults.endpoint; try { - const userData = (await get(res.endpoint + 'user', { + const userData = (await api.get(res.endpoint + 'user', { token, })).body; res.renovateUsername = userData.login; @@ -98,7 +97,7 @@ async function initPlatform({ endpoint, token }) { throw new Error('Init: Authentication failure'); } try { - const userEmail = (await get(res.endpoint + 'user/emails', { + const userEmail = (await api.get(res.endpoint + 'user/emails', { token, })).body; if (userEmail.length && userEmail[0].email) { @@ -118,28 +117,28 @@ async function initPlatform({ endpoint, token }) { } // Get all repositories that the user has access to -async function getRepos() { +export async function getRepos() { logger.info('Autodiscovering GitHub repositories'); try { - const res = await get('user/repos?per_page=100', { paginate: true }); - return res.body.map(repo => repo.full_name); + const res = await api.get('user/repos?per_page=100', { paginate: true }); + return res.body.map((repo: { full_name: string }) => repo.full_name); } catch (err) /* istanbul ignore next */ { logger.error({ err }, `GitHub getRepos error`); throw err; } } -function cleanRepo() { +export function cleanRepo() { // istanbul ignore if if (config.storage) { config.storage.cleanRepo(); } // In theory most of this isn't necessary. In practice.. - config = {}; + config = {} as any; } // Initialize GitHub by getting base branch and SHA -async function initRepo({ +export async function initRepo({ endpoint, repository, forkMode, @@ -148,6 +147,15 @@ async function initRepo({ localDir, includeForks, renovateUsername, +}: { + endpoint: string; + repository: string; + forkMode?: boolean; + forkToken?: string; + gitPrivateKey?: string; + localDir: string; + includeForks: boolean; + renovateUsername: string; }) { logger.debug(`initRepo("${repository}")`); logger.info('Authenticated as user: ' + renovateUsername); @@ -159,7 +167,7 @@ async function initRepo({ // Necessary for Renovate Pro - do not remove logger.debug('Overriding default GitHub endpoint'); defaults.endpoint = endpoint; - get.setBaseUrl(endpoint); + api.setBaseUrl(endpoint); } const opts = hostRules.find({ hostType: 'github', @@ -172,19 +180,19 @@ async function initRepo({ [config.repositoryOwner, config.repositoryName] = repository.split('/'); config.gitPrivateKey = gitPrivateKey; // platformConfig is passed back to the app layer and contains info about the platform they require - const platformConfig = {}; + const platformConfig: { privateRepo: boolean; isFork: boolean } = {} as any; let res; try { - res = await get(`repos/${repository}`); + res = await api.get(`repos/${repository}`); logger.trace({ repositoryDetails: res.body }, 'Repository details'); config.enterpriseVersion = - res.headers && res.headers['x-github-enterprise-version']; + res.headers && (res.headers['x-github-enterprise-version'] as string); // istanbul ignore if if (res.body.fork && !includeForks) { try { const renovateConfig = JSON.parse( Buffer.from( - (await get( + (await api.get( `repos/${config.repository}/contents/${defaultConfigFile}` )).body.content, 'base64' @@ -268,19 +276,22 @@ async function initRepo({ config.parentRepo = config.repository; config.repository = null; // Get list of existing repos - const existingRepos = (await get('user/repos?per_page=100', { - token: forkToken || opts.token, - paginate: true, - })).body.map(r => r.full_name); + const existingRepos = (await api.get<{ full_name: string }[]>( + 'user/repos?per_page=100', + { + token: forkToken || opts.token, + paginate: true, + } + )).body.map(r => r.full_name); try { - config.repository = (await get.post(`repos/${repository}/forks`, { + config.repository = (await api.post(`repos/${repository}/forks`, { token: forkToken || opts.token, })).body.full_name; } catch (err) /* istanbul ignore next */ { logger.info({ err }, 'Error forking repository'); throw new Error('cannot-fork'); } - if (existingRepos.includes(config.repository)) { + if (existingRepos.includes(config.repository!)) { logger.info( { repository_fork: config.repository }, 'Found existing fork' @@ -293,7 +304,7 @@ async function initRepo({ // This is a lovely "hack" by GitHub that lets us force update our fork's master // with the base commit from the parent repository try { - await get.patch( + await api.patch( `repos/${config.repository}/git/refs/heads/${config.baseBranch}`, { body: { @@ -333,7 +344,7 @@ async function initRepo({ logger.debug('Using personal access token for git init'); parsedEndpoint.auth = opts.token; } - parsedEndpoint.host = parsedEndpoint.host.replace( + parsedEndpoint.host = parsedEndpoint.host!.replace( 'api.github.com', 'github.com' ); @@ -348,7 +359,7 @@ async function initRepo({ return platformConfig; } -async function getRepoForceRebase() { +export async function getRepoForceRebase() { if (config.repoForceRebase === undefined) { try { config.repoForceRebase = false; @@ -394,9 +405,9 @@ async function getRepoForceRebase() { } // Return the commit SHA for a branch -async function getBranchCommit(branchName) { +async function getBranchCommit(branchName: string) { try { - const res = await get( + const res = await api.get( `repos/${config.repository}/git/refs/heads/${branchName}` ); return res.body.object.sha; @@ -419,75 +430,75 @@ async function getBaseCommitSHA() { return config.baseCommitSHA; } -async function getBranchProtection(branchName) { +async function getBranchProtection(branchName: string) { // istanbul ignore if if (config.parentRepo) { return {}; } - const res = await get( + const res = await api.get( `repos/${config.repository}/branches/${branchName}/protection` ); return res.body; } // istanbul ignore next -async function setBaseBranch(branchName = config.baseBranch) { +export async function setBaseBranch(branchName = config.baseBranch) { config.baseBranch = branchName; config.baseCommitSHA = null; await config.storage.setBaseBranch(branchName); } // istanbul ignore next -function setBranchPrefix(branchPrefix) { +export function setBranchPrefix(branchPrefix: string) { return config.storage.setBranchPrefix(branchPrefix); } // Search // istanbul ignore next -function getFileList(branchName = config.baseBranch) { +export function getFileList(branchName = config.baseBranch) { return config.storage.getFileList(branchName); } // Branch // istanbul ignore next -function branchExists(branchName) { +export function branchExists(branchName: string) { return config.storage.branchExists(branchName); } // istanbul ignore next -function getAllRenovateBranches(branchPrefix) { +export function getAllRenovateBranches(branchPrefix: string) { return config.storage.getAllRenovateBranches(branchPrefix); } // istanbul ignore next -function isBranchStale(branchName) { +export function isBranchStale(branchName: string) { return config.storage.isBranchStale(branchName); } // istanbul ignore next -function getFile(filePath, branchName) { +export function getFile(filePath: string, branchName?: string) { return config.storage.getFile(filePath, branchName); } // istanbul ignore next -function deleteBranch(branchName) { +export function deleteBranch(branchName: string) { return config.storage.deleteBranch(branchName); } // istanbul ignore next -function getBranchLastCommitTime(branchName) { +export function getBranchLastCommitTime(branchName: string) { return config.storage.getBranchLastCommitTime(branchName); } // istanbul ignore next -function getRepoStatus() { +export function getRepoStatus() { return config.storage.getRepoStatus(); } // istanbul ignore next -function mergeBranch(branchName) { +export function mergeBranch(branchName: string) { if (config.pushProtection) { logger.info( { branch: branchName }, @@ -498,10 +509,10 @@ function mergeBranch(branchName) { } // istanbul ignore next -function commitFilesToBranch( - branchName, - files, - message, +export function commitFilesToBranch( + branchName: string, + files: any[], + message: string, parentBranch = config.baseBranch ) { return config.storage.commitFilesToBranch( @@ -513,19 +524,22 @@ function commitFilesToBranch( } // istanbul ignore next -function getCommitMessages() { +export function getCommitMessages() { return config.storage.getCommitMessages(); } // Returns the Pull Request for a branch. Null if not exists. -async function getBranchPr(branchName) { +export async function getBranchPr(branchName: string) { logger.debug(`getBranchPr(${branchName})`); const existingPr = await findPr(branchName, null, 'open'); return existingPr ? getPr(existingPr.number) : null; } // Returns the combined status for a branch. -async function getBranchStatus(branchName, requiredStatusChecks) { +export async function getBranchStatus( + branchName: string, + requiredStatusChecks: any +) { logger.debug(`getBranchStatus(${branchName})`); if (!requiredStatusChecks) { // null means disable status checks, so it always succeeds @@ -540,7 +554,7 @@ async function getBranchStatus(branchName, requiredStatusChecks) { const commitStatusUrl = `repos/${config.repository}/commits/${branchName}/status`; let commitStatus; try { - commitStatus = (await get(commitStatusUrl)).body; + commitStatus = (await api.get(commitStatusUrl)).body; } catch (err) /* istanbul ignore next */ { if (err.statusCode === 404) { logger.info( @@ -555,7 +569,7 @@ async function getBranchStatus(branchName, requiredStatusChecks) { { state: commitStatus.state, statuses: commitStatus.statuses }, 'branch status check result' ); - let checkRuns = []; + let checkRuns: { name: string; status: string; conclusion: string }[] = []; if (!config.isGhe) { try { const checkRunsUrl = `repos/${config.repository}/commits/${branchName}/check-runs`; @@ -564,13 +578,15 @@ async function getBranchStatus(branchName, requiredStatusChecks) { Accept: 'application/vnd.github.antiope-preview+json', }, }; - const checkRunsRaw = (await get(checkRunsUrl, opts)).body; + const checkRunsRaw = (await api.get(checkRunsUrl, opts)).body; if (checkRunsRaw.check_runs && checkRunsRaw.check_runs.length) { - checkRuns = checkRunsRaw.check_runs.map(run => ({ - name: run.name, - status: run.status, - conclusion: run.conclusion, - })); + checkRuns = checkRunsRaw.check_runs.map( + (run: { name: string; status: string; conclusion: string }) => ({ + name: run.name, + status: run.status, + conclusion: run.conclusion, + }) + ); logger.debug({ checkRuns }, 'check runs result'); } else { // istanbul ignore next @@ -605,10 +621,13 @@ async function getBranchStatus(branchName, requiredStatusChecks) { return 'pending'; } -async function getBranchStatusCheck(branchName, context) { +export async function getBranchStatusCheck( + branchName: string, + context: string +) { const branchCommit = await config.storage.getBranchCommit(branchName); const url = `repos/${config.repository}/commits/${branchCommit}/statuses`; - const res = await get(url); + const res = await api.get(url); for (const check of res.body) { if (check.context === context) { return check.state; @@ -617,12 +636,12 @@ async function getBranchStatusCheck(branchName, context) { return null; } -async function setBranchStatus( - branchName, - context, - description, - state, - targetUrl +export async function setBranchStatus( + branchName: string, + context: string, + description: string, + state: string, + targetUrl: string ) { // istanbul ignore if if (config.parentRepo) { @@ -636,7 +655,7 @@ async function setBranchStatus( logger.info({ branch: branchName, context, state }, 'Setting branch status'); const branchCommit = await config.storage.getBranchCommit(branchName); const url = `repos/${config.repository}/statuses/${branchCommit}`; - const options = { + const options: any = { state, description, context, @@ -644,13 +663,13 @@ async function setBranchStatus( if (targetUrl) { options.target_url = targetUrl; } - await get.post(url, { body: options }); + await api.post(url, { body: options }); } // Issue /* istanbul ignore next */ -async function getGraphqlIssues(afterCursor = null) { +async function getGraphqlIssues(afterCursor: string | null = null) { const url = 'graphql'; const headers = { accept: 'application/vnd.github.merge-info-preview+json', @@ -682,7 +701,7 @@ async function getGraphqlIssues(afterCursor = null) { }; try { - const res = JSON.parse((await get.post(url, options)).body); + const res = JSON.parse((await api.post(url, options)).body); if (!res.data) { logger.info({ query, res }, 'No graphql res.data'); @@ -703,7 +722,14 @@ async function getGraphqlIssues(afterCursor = null) { // istanbul ignore next async function getRestIssues() { logger.debug('Retrieving issueList'); - const res = await get( + const res = await api.get< + { + pull_request: boolean; + number: number; + state: string; + title: string; + }[] + >( `repos/${config.repository}/issues?creator=${config.renovateUsername}&state=all&per_page=100&sort=created&direction=asc`, { paginate: 'all', useCache: false } ); @@ -721,7 +747,7 @@ async function getRestIssues() { })); } -async function getIssueList() { +export async function getIssueList() { if (!config.issueList) { logger.debug('Retrieving issueList'); const filterBySupportMinimumGheVersion = '2.17.0'; @@ -752,7 +778,7 @@ async function getIssueList() { return config.issueList; } -async function findIssue(title) { +export async function findIssue(title: string) { logger.debug(`findIssue(${title})`); const [issue] = (await getIssueList()).filter( i => i.state === 'open' && i.title === title @@ -761,7 +787,7 @@ async function findIssue(title) { return null; } logger.debug('Found issue ' + issue.number); - const issueBody = (await get( + const issueBody = (await api.get( `repos/${config.parentRepo || config.repository}/issues/${issue.number}` )).body.body; return { @@ -770,7 +796,7 @@ async function findIssue(title) { }; } -async function ensureIssue(title, body, once = false) { +export async function ensureIssue(title: string, body: string, once = false) { logger.debug(`ensureIssue()`); try { const issueList = await getIssueList(); @@ -791,7 +817,7 @@ async function ensureIssue(title, body, once = false) { await closeIssue(i.number); } } - const issueBody = (await get( + const issueBody = (await api.get( `repos/${config.parentRepo || config.repository}/issues/${issue.number}` )).body.body; if (issueBody === body && issue.state === 'open') { @@ -799,7 +825,7 @@ async function ensureIssue(title, body, once = false) { return null; } logger.info('Patching issue'); - await get.patch( + await api.patch( `repos/${config.parentRepo || config.repository}/issues/${ issue.number }`, @@ -810,7 +836,7 @@ async function ensureIssue(title, body, once = false) { logger.info('Issue updated'); return 'updated'; } - await get.post(`repos/${config.parentRepo || config.repository}/issues`, { + await api.post(`repos/${config.parentRepo || config.repository}/issues`, { body: { title, body, @@ -836,9 +862,9 @@ async function ensureIssue(title, body, once = false) { return null; } -async function closeIssue(issueNumber) { +async function closeIssue(issueNumber: number) { logger.debug(`closeIssue(${issueNumber})`); - await get.patch( + await api.patch( `repos/${config.parentRepo || config.repository}/issues/${issueNumber}`, { body: { state: 'closed' }, @@ -846,7 +872,7 @@ async function closeIssue(issueNumber) { ); } -async function ensureIssueClosing(title) { +export async function ensureIssueClosing(title: string) { logger.debug(`ensureIssueClosing(${title})`); const issueList = await getIssueList(); for (const issue of issueList) { @@ -857,17 +883,17 @@ async function ensureIssueClosing(title) { } } -async function addAssignees(issueNo, assignees) { +export async function addAssignees(issueNo: number, assignees: string[]) { logger.debug(`Adding assignees ${assignees} to #${issueNo}`); const repository = config.parentRepo || config.repository; - await get.post(`repos/${repository}/issues/${issueNo}/assignees`, { + await api.post(`repos/${repository}/issues/${issueNo}/assignees`, { body: { assignees, }, }); } -async function addReviewers(prNo, reviewers) { +export async function addReviewers(prNo: number, reviewers: string[]) { logger.debug(`Adding reviewers ${reviewers} to #${prNo}`); const userReviewers = reviewers.filter(e => !e.startsWith('team:')); @@ -875,7 +901,7 @@ async function addReviewers(prNo, reviewers) { .filter(e => e.startsWith('team:')) .map(e => e.replace(/^team:/, '')); - await get.post( + await api.post( `repos/${config.parentRepo || config.repository}/pulls/${prNo}/requested_reviewers`, { @@ -887,27 +913,27 @@ async function addReviewers(prNo, reviewers) { ); } -async function addLabels(issueNo, labels) { +async function addLabels(issueNo: number, labels: string[] | null) { logger.debug(`Adding labels ${labels} to #${issueNo}`); const repository = config.parentRepo || config.repository; if (is.array(labels) && labels.length) { - await get.post(`repos/${repository}/issues/${issueNo}/labels`, { + await api.post(`repos/${repository}/issues/${issueNo}/labels`, { body: labels, }); } } -async function deleteLabel(issueNo, label) { +export async function deleteLabel(issueNo: number, label: string) { logger.debug(`Deleting label ${label} from #${issueNo}`); const repository = config.parentRepo || config.repository; try { - await get.delete(`repos/${repository}/issues/${issueNo}/labels/${label}`); + await api.delete(`repos/${repository}/issues/${issueNo}/labels/${label}`); } catch (err) /* istanbul ignore next */ { logger.warn({ err, issueNo, label }, 'Failed to delete label'); } } -async function getComments(issueNo) { +async function getComments(issueNo: number) { const pr = (await getClosedPrs())[issueNo]; if (pr) { logger.debug('Returning closed PR list comments'); @@ -918,7 +944,9 @@ async function getComments(issueNo) { const url = `repos/${config.parentRepo || config.repository}/issues/${issueNo}/comments?per_page=100`; try { - const comments = (await get(url, { paginate: true })).body; + const comments = (await api.get<Comment[]>(url, { + paginate: true, + })).body; logger.debug(`Found ${comments.length} comments`); return comments; } catch (err) /* istanbul ignore next */ { @@ -930,9 +958,9 @@ async function getComments(issueNo) { } } -async function addComment(issueNo, body) { +async function addComment(issueNo: number, body: string) { // POST /repos/:owner/:repo/issues/:number/comments - await get.post( + await api.post( `repos/${config.parentRepo || config.repository}/issues/${issueNo}/comments`, { @@ -941,9 +969,9 @@ async function addComment(issueNo, body) { ); } -async function editComment(commentId, body) { +async function editComment(commentId: number, body: string) { // PATCH /repos/:owner/:repo/issues/comments/:id - await get.patch( + await api.patch( `repos/${config.parentRepo || config.repository}/issues/comments/${commentId}`, { @@ -952,20 +980,24 @@ async function editComment(commentId, body) { ); } -async function deleteComment(commentId) { +async function deleteComment(commentId: number) { // DELETE /repos/:owner/:repo/issues/comments/:id - await get.delete( + await api.delete( `repos/${config.parentRepo || config.repository}/issues/comments/${commentId}` ); } -async function ensureComment(issueNo, topic, content) { +export async function ensureComment( + issueNo: number, + topic: string | null, + content: string +) { try { const comments = await getComments(issueNo); - let body; - let commentId; - let commentNeedsUpdating; + let body: string; + let commentId: number | null = null; + let commentNeedsUpdating = false; if (topic) { logger.debug(`Ensuring comment "${topic}" in #${issueNo}`); body = `### ${topic}\n\n${content}`; @@ -1010,7 +1042,7 @@ async function ensureComment(issueNo, topic, content) { } } -async function ensureCommentRemoval(issueNo, topic) { +export async function ensureCommentRemoval(issueNo: number, topic: string) { logger.debug(`Ensuring comment "${topic}" in #${issueNo} is removed`); const comments = await getComments(issueNo); let commentId; @@ -1030,34 +1062,45 @@ async function ensureCommentRemoval(issueNo, topic) { // Pull Request -async function getPrList() { +export async function getPrList() { logger.trace('getPrList()'); if (!config.prList) { logger.debug('Retrieving PR list'); - const res = await get( + const res = await api.get( `repos/${config.parentRepo || config.repository}/pulls?per_page=100&state=all`, { paginate: true } ); - config.prList = res.body.map(pr => ({ - number: pr.number, - branchName: pr.head.ref, - sha: pr.head.sha, - title: pr.title, - state: - pr.state === 'closed' && pr.merged_at && pr.merged_at.length - ? /* istanbul ignore next */ 'merged' - : pr.state, - createdAt: pr.created_at, - closed_at: pr.closed_at, - sourceRepo: pr.head && pr.head.repo ? pr.head.repo.full_name : undefined, - })); - logger.debug(`Retrieved ${config.prList.length} Pull Requests`); + config.prList = res.body.map( + (pr: { + number: number; + head: { ref: string; sha: string; repo: { full_name: string } }; + title: string; + state: string; + merged_at: string; + created_at: string; + closed_at: string; + }) => ({ + number: pr.number, + branchName: pr.head.ref, + sha: pr.head.sha, + title: pr.title, + state: + pr.state === 'closed' && pr.merged_at && pr.merged_at.length + ? /* istanbul ignore next */ 'merged' + : pr.state, + createdAt: pr.created_at, + closed_at: pr.closed_at, + sourceRepo: + pr.head && pr.head.repo ? pr.head.repo.full_name : undefined, + }) + ); + logger.debug(`Retrieved ${config.prList!.length} Pull Requests`); } - return config.prList; + return config.prList!; } -function matchesState(state, desiredState) { +function matchesState(state: string, desiredState: string) { if (desiredState === 'all') { return true; } @@ -1067,7 +1110,11 @@ function matchesState(state, desiredState) { return state === desiredState; } -async function findPr(branchName, prTitle, state = 'all') { +export async function findPr( + branchName: string, + prTitle?: string | null, + state = 'all' +) { logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`); const prList = await getPrList(); const pr = prList.find( @@ -1083,18 +1130,18 @@ async function findPr(branchName, prTitle, state = 'all') { } // Creates PR and returns PR number -async function createPr( - branchName, - title, - body, - labels, - useDefaultBranch, - platformOptions = {} +export async function createPr( + branchName: string, + title: string, + body: string, + labels: string[] | null, + useDefaultBranch: boolean, + platformOptions: { statusCheckVerify?: boolean } = {} ) { const base = useDefaultBranch ? config.defaultBranch : config.baseBranch; // Include the repository owner to handle forkMode and regular mode - const head = `${config.repository.split('/')[0]}:${branchName}`; - const options = { + const head = `${config.repository!.split('/')[0]}:${branchName}`; + const options: any = { body: { title, head, @@ -1108,7 +1155,7 @@ async function createPr( options.body.maintainer_can_modify = true; } logger.debug({ title, head, base }, 'Creating PR'); - const pr = (await get.post( + const pr = (await api.post<Pr>( `repos/${config.parentRepo || config.repository}/pulls`, options )).body; @@ -1202,7 +1249,7 @@ async function getOpenPrs() { body: JSON.stringify({ query }), json: false, }; - const res = JSON.parse((await get.post(url, options)).body); + const res = JSON.parse((await api.post(url, options)).body); const prNumbers = []; // istanbul ignore if if (!res.data) { @@ -1277,7 +1324,9 @@ async function getOpenPrs() { } } if (pr.labels) { - pr.labels = pr.labels.nodes.map(label => label.name); + pr.labels = pr.labels.nodes.map( + (label: { name: string }) => label.name + ); } delete pr.mergeable; delete pr.mergeStateStatus; @@ -1325,7 +1374,7 @@ async function getClosedPrs() { body: JSON.stringify({ query }), json: false, }; - const res = JSON.parse((await get.post(url, options)).body); + const res = JSON.parse((await api.post(url, options)).body); const prNumbers = []; // istanbul ignore if if (!res.data) { @@ -1341,10 +1390,12 @@ async function getClosedPrs() { pr.state = pr.state.toLowerCase(); pr.branchName = pr.headRefName; delete pr.headRefName; - pr.comments = pr.comments.nodes.map(comment => ({ - id: comment.databaseId, - body: comment.body, - })); + pr.comments = pr.comments.nodes.map( + (comment: { databaseId: number; body: string }) => ({ + id: comment.databaseId, + body: comment.body, + }) + ); pr.body = 'dummy body'; // just in case config.closedPrList[pr.number] = pr; prNumbers.push(pr.number); @@ -1359,7 +1410,7 @@ async function getClosedPrs() { } // Gets details for a PR -async function getPr(prNo) { +export async function getPr(prNo: number) { if (!prNo) { return null; } @@ -1377,7 +1428,7 @@ async function getPr(prNo) { { prNo }, 'PR not found in open or closed PRs list - trying to fetch it directly' ); - const pr = (await get( + const pr = (await api.get( `repos/${config.parentRepo || config.repository}/pulls/${prNo}` )).body; if (!pr) { @@ -1398,7 +1449,7 @@ async function getPr(prNo) { if (pr.commits === 1) { if (global.gitAuthor) { // Check against gitAuthor - const commitAuthorEmail = (await get( + const commitAuthorEmail = (await api.get( `repos/${config.parentRepo || config.repository}/pulls/${prNo}/commits` )).body[0].commit.author.email; @@ -1429,28 +1480,33 @@ async function getPr(prNo) { } else { // Check if only one author of all commits logger.debug({ prNo }, 'Checking all commits'); - const prCommits = (await get( + const prCommits = (await api.get( `repos/${config.parentRepo || config.repository}/pulls/${prNo}/commits` )).body; // Filter out "Update branch" presses - const remainingCommits = prCommits.filter(commit => { - const isWebflow = - commit.committer && commit.committer.login === 'web-flow'; - if (!isWebflow) { - // Not a web UI commit, so keep it + const remainingCommits = prCommits.filter( + (commit: { + committer: { login: string }; + commit: { message: string }; + }) => { + const isWebflow = + commit.committer && commit.committer.login === 'web-flow'; + if (!isWebflow) { + // Not a web UI commit, so keep it + return true; + } + const isUpdateBranch = + commit.commit && + commit.commit.message && + commit.commit.message.startsWith("Merge branch 'master' into"); + if (isUpdateBranch) { + // They just clicked the button + return false; + } + // They must have done some other edit through the web UI return true; } - const isUpdateBranch = - commit.commit && - commit.commit.message && - commit.commit.message.startsWith("Merge branch 'master' into"); - if (isUpdateBranch) { - // They just clicked the button - return false; - } - // They must have done some other edit through the web UI - return true; - }); + ); if (remainingCommits.length <= 1) { pr.canRebase = true; } @@ -1464,24 +1520,24 @@ async function getPr(prNo) { } // Return a list of all modified files in a PR -async function getPrFiles(prNo) { +export async function getPrFiles(prNo: number) { logger.debug({ prNo }, 'getPrFiles'); if (!prNo) { return []; } - const files = (await get( + const files = (await api.get( `repos/${config.parentRepo || config.repository}/pulls/${prNo}/files` )).body; - return files.map(f => f.filename); + return files.map((f: { filename: string }) => f.filename); } -async function updatePr(prNo, title, body) { +export async function updatePr(prNo: number, title: string, body: string) { logger.debug(`updatePr(${prNo}, ${title}, body)`); - const patchBody = { title }; + const patchBody: any = { title }; if (body) { patchBody.body = body; } - const options = { + const options: any = { body: patchBody, }; // istanbul ignore if @@ -1489,7 +1545,7 @@ async function updatePr(prNo, title, body) { options.token = config.forkToken; } try { - await get.patch( + await api.patch( `repos/${config.parentRepo || config.repository}/pulls/${prNo}`, options ); @@ -1502,7 +1558,7 @@ async function updatePr(prNo, title, body) { } } -async function mergePr(prNo, branchName) { +export async function mergePr(prNo: number, branchName: string) { logger.debug(`mergePr(${prNo}, ${branchName})`); // istanbul ignore if if (config.pushProtection) { @@ -1519,8 +1575,10 @@ async function mergePr(prNo, branchName) { 'Branch protection: Attempting to merge PR when PR reviews are enabled' ); const repository = config.parentRepo || config.repository; - const reviews = await get(`repos/${repository}/pulls/${prNo}/reviews`); - const isApproved = reviews.body.some(review => review.state === 'APPROVED'); + const reviews = await api.get(`repos/${repository}/pulls/${prNo}/reviews`); + const isApproved = reviews.body.some( + (review: { state: string }) => review.state === 'APPROVED' + ); if (!isApproved) { logger.info( { branch: branchName, prNo }, @@ -1533,7 +1591,7 @@ async function mergePr(prNo, branchName) { const url = `repos/${config.parentRepo || config.repository}/pulls/${prNo}/merge`; const options = { - body: {}, + body: {} as any, }; let automerged = false; if (config.mergeMethod) { @@ -1541,7 +1599,7 @@ async function mergePr(prNo, branchName) { options.body.merge_method = config.mergeMethod; try { logger.debug({ options, url }, `mergePr`); - await get.put(url, options); + await api.put(url, options); automerged = true; } catch (err) { if (err.statusCode === 405) { @@ -1561,13 +1619,13 @@ async function mergePr(prNo, branchName) { options.body.merge_method = 'rebase'; try { logger.debug({ options, url }, `mergePr`); - await get.put(url, options); + await api.put(url, options); } catch (err1) { logger.debug({ err: err1 }, `Failed to ${options.body.merge_method} PR`); try { options.body.merge_method = 'squash'; logger.debug({ options, url }, `mergePr`); - await get.put(url, options); + await api.put(url, options); } catch (err2) { logger.debug( { err: err2 }, @@ -1576,7 +1634,7 @@ async function mergePr(prNo, branchName) { try { options.body.merge_method = 'merge'; logger.debug({ options, url }, `mergePr`); - await get.put(url, options); + await api.put(url, options); } catch (err3) { logger.debug( { err: err3 }, @@ -1597,7 +1655,7 @@ async function mergePr(prNo, branchName) { } // istanbul ignore next -function smartTruncate(input) { +function smartTruncate(input: string) { if (input.length < 60000) { return input; } @@ -1619,7 +1677,7 @@ function smartTruncate(input) { return input.substring(0, 60000); } -function getPrBody(input) { +export function getPrBody(input: string) { if (config.isGhe) { return smartTruncate(input); } @@ -1631,7 +1689,7 @@ function getPrBody(input) { return smartTruncate(massagedInput); } -async function getVulnerabilityAlerts() { +export async function getVulnerabilityAlerts() { // istanbul ignore if if (config.isGhe) { logger.debug( @@ -1677,10 +1735,10 @@ async function getVulnerabilityAlerts() { }; let alerts = []; try { - const res = JSON.parse((await get.post(url, options)).body); + const res = JSON.parse((await api.post(url, options)).body); if (res.data.repository.vulnerabilityAlerts) { alerts = res.data.repository.vulnerabilityAlerts.edges.map( - edge => edge.node + (edge: { node: any }) => edge.node ); if (alerts.length) { logger.info({ alerts }, 'Found GitHub vulnerability alerts'); diff --git a/lib/types.d.ts b/lib/types.d.ts index 2b8c8fbfa91aa3e1b21c2a78eab21764fab6004e..572e06a9ad460bfe9440fe61c86efaef6ab34a5a 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -23,7 +23,10 @@ declare interface Error { declare namespace NodeJS { interface Global { + appMode?: boolean; gitAuthor?: { name: string; email: string }; logger: Renovate.Logger; + + renovateVersion: string; } } diff --git a/lib/workers/pr/changelog/release-notes.js b/lib/workers/pr/changelog/release-notes.js index 704cfbf0bbc0a3cfa615bde61d9548dd52c6c2b1..3e5b935853b851197e324b29d357d08790464cb4 100644 --- a/lib/workers/pr/changelog/release-notes.js +++ b/lib/workers/pr/changelog/release-notes.js @@ -1,7 +1,8 @@ +import ghGot from '../../../platform/github/gh-got-wrapper'; + const changelogFilenameRegex = require('changelog-filename-regex'); const { linkify } = require('linkify-markdown'); const MarkdownIt = require('markdown-it'); -const ghGot = require('../../../platform/github/gh-got-wrapper'); const markdown = new MarkdownIt('zero'); markdown.enable(['heading', 'lheading']); diff --git a/lib/workers/pr/changelog/source-github.js b/lib/workers/pr/changelog/source-github.js index 305d0c04b6b36766935b3b2fbb7c8ca62e6e08c7..0162daacc44e97d72a9e2d97da2148da663aacc3 100644 --- a/lib/workers/pr/changelog/source-github.js +++ b/lib/workers/pr/changelog/source-github.js @@ -1,7 +1,8 @@ +import ghGot from '../../../platform/github/gh-got-wrapper'; + const URL = require('url'); const hostRules = require('../../../util/host-rules'); const versioning = require('../../../versioning'); -const ghGot = require('../../../platform/github/gh-got-wrapper'); const { addReleaseNotes } = require('./release-notes'); module.exports = { diff --git a/package.json b/package.json index 7a7b72d19bbe0d79fced2cca6e7fa0ebe702fa4e..d510c706216590b6827fd0864fe6ba02c94e1d9e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "eslint-fix": "eslint --ext .js,.ts --fix lib/ test/", "jest": "yarn clean-cache && cross-env NODE_ENV=test LOG_LEVEL=fatal jest", "jest-debug": "cross-env NODE_ENV=test LOG_LEVEL=fatal node --inspect-brk node_modules/jest/bin/jest.js", - "jest-silent": "yarn jest --reporters jest-silent-reporter", + "jest-silent": "cross-env NODE_ENV=test yarn jest --reporters jest-silent-reporter", "lint": "yarn eslint && yarn prettier", "lint-fix": "yarn eslint-fix && yarn prettier-fix", "prettier": "prettier --list-different \"**/*.{ts,js,json,md}\"", @@ -157,6 +157,7 @@ "@babel/node": "7.4.5", "@babel/plugin-proposal-class-properties": "7.4.4", "@babel/plugin-proposal-object-rest-spread": "7.4.4", + "@babel/plugin-syntax-dynamic-import": "7.2.0", "@babel/preset-env": "7.4.5", "@babel/preset-typescript": "7.3.3", "@types/bunyan": "1.8.6", @@ -166,10 +167,12 @@ "@types/jest": "24.0.15", "@types/node": "11.13.15", "@types/parse-link-header": "1.0.0", + "@types/semver": "6.0.1", "@types/tmp": "0.1.0", "@typescript-eslint/eslint-plugin": "1.11.0", "@typescript-eslint/parser": "1.11.0", "babel-jest": "24.8.0", + "babel-plugin-dynamic-import-node": "2.3.0", "chai": "4.2.0", "copyfiles": "2.1.0", "cross-env": "5.2.0", diff --git a/test/datasource/github.spec.js b/test/datasource/github.spec.js index 562553254f1dbffaa225cf420f385fa2fe511c40..6e4871fd9143a77c6a7eef2095ed90f61854cec7 100644 --- a/test/datasource/github.spec.js +++ b/test/datasource/github.spec.js @@ -1,6 +1,7 @@ +import ghGot from '../../lib/platform/github/gh-got-wrapper'; + const datasource = require('../../lib/datasource'); const github = require('../../lib/datasource/github'); -const ghGot = require('../../lib/platform/github/gh-got-wrapper'); const got = require('../../lib/util/got'); const hostRules = require('../../lib/util/host-rules'); diff --git a/test/platform/__snapshots__/index.spec.js.snap b/test/platform/__snapshots__/index.spec.js.snap index 079eeee37eda8ac8d6306f80a9f9c3c39972c7ac..23316d10118bf9cbf6817da0e1d037d21949770b 100644 --- a/test/platform/__snapshots__/index.spec.js.snap +++ b/test/platform/__snapshots__/index.spec.js.snap @@ -47,46 +47,46 @@ Array [ exports[`platform has a list of supported methods for github 1`] = ` Array [ - "initPlatform", - "getRepos", - "cleanRepo", - "initRepo", - "getRepoStatus", - "getRepoForceRebase", - "setBaseBranch", - "setBranchPrefix", - "getFileList", - "branchExists", - "getAllRenovateBranches", - "isBranchStale", - "getBranchPr", - "getBranchStatus", - "getBranchStatusCheck", - "setBranchStatus", - "deleteBranch", - "mergeBranch", - "getBranchLastCommitTime", - "findIssue", - "ensureIssue", - "ensureIssueClosing", "addAssignees", "addReviewers", + "branchExists", + "cleanRepo", + "commitFilesToBranch", + "createPr", + "deleteBranch", "deleteLabel", - "getIssueList", "ensureComment", "ensureCommentRemoval", - "getPrList", + "ensureIssue", + "ensureIssueClosing", + "findIssue", "findPr", - "createPr", + "getAllRenovateBranches", + "getBranchLastCommitTime", + "getBranchPr", + "getBranchStatus", + "getBranchStatusCheck", + "getCommitMessages", + "getFile", + "getFileList", + "getIssueList", "getPr", - "getPrFiles", - "updatePr", - "mergePr", "getPrBody", - "commitFilesToBranch", - "getFile", - "getCommitMessages", + "getPrFiles", + "getPrList", + "getRepoForceRebase", + "getRepoStatus", + "getRepos", "getVulnerabilityAlerts", + "initPlatform", + "initRepo", + "isBranchStale", + "mergeBranch", + "mergePr", + "setBaseBranch", + "setBranchPrefix", + "setBranchStatus", + "updatePr", ] `; diff --git a/test/platform/github/__snapshots__/index.spec.js.snap b/test/platform/github/__snapshots__/index.spec.ts.snap similarity index 100% rename from test/platform/github/__snapshots__/index.spec.js.snap rename to test/platform/github/__snapshots__/index.spec.ts.snap diff --git a/test/platform/github/gh-got-wrapper.spec.js b/test/platform/github/gh-got-wrapper.spec.ts similarity index 81% rename from test/platform/github/gh-got-wrapper.spec.js rename to test/platform/github/gh-got-wrapper.spec.ts index 179642b79a99f42f3757f387748f26b90c903f61..73c3d725cbbb0b5390f2ac07bc7669570b6e7ec3 100644 --- a/test/platform/github/gh-got-wrapper.spec.js +++ b/test/platform/github/gh-got-wrapper.spec.ts @@ -1,19 +1,26 @@ -const delay = require('delay'); -const got = require('../../../lib/util/got'); -const get = require('../../../lib/platform/github/gh-got-wrapper'); +import delay from 'delay'; +import { Response } from 'got'; +import got from '../../../lib/util/got'; +import { api } from '../../../lib/platform/github/gh-got-wrapper'; jest.mock('../../../lib/util/got'); jest.mock('delay'); +const get: <T extends object = any>( + path: string, + options?: any, + okToRetry?: boolean +) => Promise<Response<T>> = api as any; + describe('platform/gh-got-wrapper', () => { beforeEach(() => { jest.resetAllMocks(); delete global.appMode; - delay.mockImplementation(() => Promise.resolve()); + (delay as any).mockImplementation(() => Promise.resolve()); }); it('supports app mode', async () => { global.appMode = true; - await get('some-url', { headers: { accept: 'some-accept' } }); + await api.get('some-url', { headers: { accept: 'some-accept' } }); expect(got.mock.calls[0][1].headers.accept).toBe( 'application/vnd.github.machine-man-preview+json, some-accept' ); @@ -22,8 +29,8 @@ describe('platform/gh-got-wrapper', () => { got.mockImplementationOnce(() => ({ body: '{"data":{', })); - get.setBaseUrl('https://ghe.mycompany.com/api/v3/'); - await get.post('graphql', { + api.setBaseUrl('https://ghe.mycompany.com/api/v3/'); + await api.post('graphql', { body: 'abc', }); expect(got.mock.calls[0][0].includes('/v3')).toBe(false); @@ -47,7 +54,7 @@ describe('platform/gh-got-wrapper', () => { headers: {}, body: ['d'], }); - const res = await get('some-url', { paginate: true }); + const res = await api.get('some-url', { paginate: true }); expect(res.body).toEqual(['a', 'b', 'c', 'd']); expect(got).toHaveBeenCalledTimes(3); }); @@ -63,7 +70,7 @@ describe('platform/gh-got-wrapper', () => { headers: {}, body: ['b'], }); - const res = await get('some-url', { paginate: true }); + const res = await api.get('some-url', { paginate: true }); expect(res.body).toHaveLength(1); expect(got).toHaveBeenCalledTimes(1); }); @@ -75,7 +82,7 @@ describe('platform/gh-got-wrapper', () => { 'Error updating branch: API rate limit exceeded for installation ID 48411. (403)', }) ); - await expect(get('some-url')).rejects.toThrow(); + await expect(api.get('some-url')).rejects.toThrow(); }); it('should throw Bad credentials', async () => { got.mockImplementationOnce(() => @@ -86,7 +93,7 @@ describe('platform/gh-got-wrapper', () => { ); let e; try { - await get('some-url'); + await api.get('some-url'); } catch (err) { e = err; } @@ -105,7 +112,7 @@ describe('platform/gh-got-wrapper', () => { ); let e; try { - await get('some-url'); + await api.get('some-url'); } catch (err) { e = err; } @@ -121,7 +128,7 @@ describe('platform/gh-got-wrapper', () => { ); let e; try { - await get('some-url', {}, 0); + await get('some-url', {}, false); } catch (err) { e = err; } @@ -137,7 +144,7 @@ describe('platform/gh-got-wrapper', () => { ); let e; try { - await get('some-url', {}, 0); + await get('some-url', {}, false); } catch (err) { e = err; } @@ -152,7 +159,7 @@ describe('platform/gh-got-wrapper', () => { ); let e; try { - await get('some-url', {}, 0); + await get('some-url', {}, false); } catch (err) { e = err; } @@ -168,7 +175,7 @@ describe('platform/gh-got-wrapper', () => { ); let e; try { - await get('some-url', {}, 0); + await get('some-url', {}, false); } catch (err) { e = err; } diff --git a/test/platform/github/index.spec.js b/test/platform/github/index.spec.js deleted file mode 100644 index 50b0b0fdd954dd5681668513e6407cce4d3e063f..0000000000000000000000000000000000000000 --- a/test/platform/github/index.spec.js +++ /dev/null @@ -1,1718 +0,0 @@ -const fs = require('fs-extra'); - -describe('platform/github', () => { - let github; - let get; - let hostRules; - let GitStorage; - beforeEach(() => { - // reset module - jest.resetModules(); - jest.mock('delay'); - jest.mock('../../../lib/platform/github/gh-got-wrapper'); - jest.mock('../../../lib/util/host-rules'); - jest.mock('../../../lib/util/got'); - get = require('../../../lib/platform/github/gh-got-wrapper'); - github = require('../../../lib/platform/github'); - hostRules = require('../../../lib/util/host-rules'); - jest.mock('../../../lib/platform/git/storage'); - GitStorage = require('../../../lib/platform/git/storage').Storage; - GitStorage.mockImplementation(() => ({ - initRepo: jest.fn(), - cleanRepo: jest.fn(), - getFileList: jest.fn(), - branchExists: jest.fn(() => true), - isBranchStale: jest.fn(() => false), - setBaseBranch: jest.fn(), - getBranchLastCommitTime: jest.fn(), - getAllRenovateBranches: jest.fn(), - getCommitMessages: jest.fn(), - getFile: jest.fn(), - commitFilesToBranch: jest.fn(), - mergeBranch: jest.fn(), - deleteBranch: jest.fn(), - getRepoStatus: jest.fn(), - getBranchCommit: jest.fn( - () => '0d9c7726c3d628b7e28af234595cfd20febdbf8e' - ), - })); - delete global.gitAuthor; - hostRules.find.mockReturnValue({ - token: 'abc123', - }); - }); - - const graphqlOpenPullRequests = fs.readFileSync( - 'test/platform/github/_fixtures/graphql/pullrequest-1.json', - 'utf8' - ); - const graphqlClosedPullrequests = fs.readFileSync( - 'test/platform/github/_fixtures/graphql/pullrequests-closed.json', - 'utf8' - ); - - function getRepos(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: [ - { - full_name: 'a/b', - }, - { - full_name: 'c/d', - }, - ], - })); - return github.getRepos(...args); - } - - describe('initPlatform()', () => { - it('should throw if no token', async () => { - await expect(github.initPlatform({})).rejects.toThrow(); - }); - it('should throw if user failure', async () => { - get.mockImplementationOnce(() => ({})); - await expect(github.initPlatform({ token: 'abc123' })).rejects.toThrow(); - }); - it('should support default endpoint no email access', async () => { - get.mockImplementationOnce(() => ({ - body: { - login: 'renovate-bot', - }, - })); - expect(await github.initPlatform({ token: 'abc123' })).toMatchSnapshot(); - }); - it('should support default endpoint no email result', async () => { - get.mockImplementationOnce(() => ({ - body: { - login: 'renovate-bot', - }, - })); - get.mockImplementationOnce(() => ({ - body: [{}], - })); - expect(await github.initPlatform({ token: 'abc123' })).toMatchSnapshot(); - }); - it('should support default endpoint with email', async () => { - get.mockImplementationOnce(() => ({ - body: { - login: 'renovate-bot', - }, - })); - get.mockImplementationOnce(() => ({ - body: [ - { - email: 'user@domain.com', - }, - ], - })); - expect(await github.initPlatform({ token: 'abc123' })).toMatchSnapshot(); - }); - it('should support custom endpoint', async () => { - get.mockImplementationOnce(() => ({ - body: { - login: 'renovate-bot', - }, - })); - expect( - await github.initPlatform({ - endpoint: 'https://ghe.renovatebot.com', - token: 'abc123', - }) - ).toMatchSnapshot(); - }); - }); - - describe('getRepos', () => { - it('should return an array of repos', async () => { - const repos = await getRepos(); - expect(get.mock.calls).toMatchSnapshot(); - expect(repos).toMatchSnapshot(); - }); - }); - - function initRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - allow_rebase_merge: true, - allow_squash_merge: true, - allow_merge_commit: true, - }, - })); - if (args.length) { - return github.initRepo(...args); - } - return github.initRepo({ - endpoint: 'https://github.com', - repository: 'some/repo', - token: 'token', - }); - } - - describe('initRepo', () => { - it('should rebase', async () => { - function squashInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - allow_rebase_merge: true, - allow_squash_merge: true, - allow_merge_commit: true, - }, - })); - return github.initRepo(...args); - } - const config = await squashInitRepo({ - repository: 'some/repo', - }); - expect(config).toMatchSnapshot(); - }); - it('should forks when forkMode', async () => { - function forkInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - allow_rebase_merge: true, - allow_squash_merge: true, - allow_merge_commit: true, - }, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - // getRepos - get.mockImplementationOnce(() => ({ - body: [], - })); - // getBranchCommit - get.post.mockImplementationOnce(() => ({ - body: {}, - })); - return github.initRepo(...args); - } - const config = await forkInitRepo({ - repository: 'some/repo', - forkMode: true, - }); - expect(config).toMatchSnapshot(); - }); - it('should update fork when forkMode', async () => { - function forkInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - allow_rebase_merge: true, - allow_squash_merge: true, - allow_merge_commit: true, - }, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - // getRepos - get.mockImplementationOnce(() => ({ - body: [ - { - full_name: 'forked_repo', - }, - ], - })); - // fork - get.post.mockImplementationOnce(() => ({ - body: { full_name: 'forked_repo' }, - })); - return github.initRepo(...args); - } - const config = await forkInitRepo({ - repository: 'some/repo', - forkMode: true, - }); - expect(config).toMatchSnapshot(); - }); - it('should squash', async () => { - function mergeInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - allow_rebase_merge: false, - allow_squash_merge: true, - allow_merge_commit: true, - }, - })); - return github.initRepo(...args); - } - const config = await mergeInitRepo({ - repository: 'some/repo', - }); - expect(config).toMatchSnapshot(); - }); - it('should merge', async () => { - function mergeInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - allow_rebase_merge: false, - allow_squash_merge: false, - allow_merge_commit: true, - }, - })); - return github.initRepo(...args); - } - const config = await mergeInitRepo({ - repository: 'some/repo', - }); - expect(config).toMatchSnapshot(); - }); - it('should not guess at merge', async () => { - function mergeInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - }, - })); - return github.initRepo(...args); - } - const config = await mergeInitRepo({ - repository: 'some/repo', - }); - expect(config).toMatchSnapshot(); - }); - it('should throw error if archived', async () => { - get.mockReturnValueOnce({ - body: { - archived: true, - owner: {}, - }, - }); - await expect( - github.initRepo({ - repository: 'some/repo', - }) - ).rejects.toThrow(); - }); - it('throws not-found', async () => { - get.mockImplementationOnce(() => - Promise.reject({ - statusCode: 404, - }) - ); - await expect( - github.initRepo({ - repository: 'some/repo', - }) - ).rejects.toThrow('not-found'); - }); - it('should throw error if renamed', async () => { - get.mockReturnValueOnce({ - body: { - fork: true, - full_name: 'some/other', - owner: {}, - }, - }); - await expect( - github.initRepo({ - includeForks: true, - repository: 'some/repo', - }) - ).rejects.toThrow('renamed'); - }); - }); - describe('getRepoForceRebase', () => { - it('should detect repoForceRebase', async () => { - get.mockImplementationOnce(() => ({ - body: { - required_pull_request_reviews: { - dismiss_stale_reviews: false, - require_code_owner_reviews: false, - }, - required_status_checks: { - strict: true, - contexts: [], - }, - restrictions: { - users: [ - { - login: 'rarkins', - id: 6311784, - type: 'User', - site_admin: false, - }, - ], - teams: [], - }, - }, - })); - const res = await github.getRepoForceRebase(); - expect(res).toBe(true); - }); - it('should handle 404', async () => { - get.mockImplementationOnce(() => - Promise.reject({ - statusCode: 404, - }) - ); - const res = await github.getRepoForceRebase(); - expect(res).toBe(false); - }); - it('should handle 403', async () => { - get.mockImplementationOnce(() => - Promise.reject({ - statusCode: 403, - }) - ); - const res = await github.getRepoForceRebase(); - expect(res).toBe(false); - }); - it('should throw 401', async () => { - get.mockImplementationOnce(() => - Promise.reject({ - statusCode: 401, - }) - ); - await expect(github.getRepoForceRebase()).rejects.toEqual({ - statusCode: 401, - }); - }); - }); - describe('getBranchPr(branchName)', () => { - it('should return null if no PR exists', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: [], - })); - const pr = await github.getBranchPr('somebranch'); - expect(pr).toBeNull(); - }); - it('should return the PR object', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: [{ number: 91, head: { ref: 'somebranch' }, state: 'open' }], - })); - get.mockImplementationOnce(() => ({ - body: { - number: 91, - additions: 1, - deletions: 1, - commits: 1, - base: { - sha: '1234', - }, - head: { ref: 'somebranch' }, - state: 'open', - }, - })); - get.mockResolvedValue({ body: { object: { sha: '12345' } } }); - const pr = await github.getBranchPr('somebranch'); - expect(get.mock.calls).toMatchSnapshot(); - expect(pr).toMatchSnapshot(); - }); - }); - describe('getBranchStatus()', () => { - it('returns success if requiredStatusChecks null', async () => { - await initRepo({ - repository: 'some/repo', - }); - const res = await github.getBranchStatus('somebranch', null); - expect(res).toEqual('success'); - }); - it('return failed if unsupported requiredStatusChecks', async () => { - await initRepo({ - repository: 'some/repo', - }); - const res = await github.getBranchStatus('somebranch', ['foo']); - expect(res).toEqual('failed'); - }); - it('should pass through success', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - state: 'success', - }, - })); - const res = await github.getBranchStatus('somebranch', []); - expect(res).toEqual('success'); - }); - it('should pass through failed', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - state: 'failed', - }, - })); - const res = await github.getBranchStatus('somebranch', []); - expect(res).toEqual('failed'); - }); - it('should fail if a check run has failed', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - state: 'pending', - statuses: [], - }, - })); - get.mockImplementationOnce(() => ({ - body: { - total_count: 2, - check_runs: [ - { - id: 23950198, - status: 'completed', - conclusion: 'success', - name: 'Travis CI - Pull Request', - }, - { - id: 23950195, - status: 'completed', - conclusion: 'failed', - name: 'Travis CI - Branch', - }, - ], - }, - })); - const res = await github.getBranchStatus('somebranch', []); - expect(res).toEqual('failed'); - }); - it('should suceed if no status and all passed check runs', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - state: 'pending', - statuses: [], - }, - })); - get.mockImplementationOnce(() => ({ - body: { - total_count: 2, - check_runs: [ - { - id: 23950198, - status: 'completed', - conclusion: 'success', - name: 'Travis CI - Pull Request', - }, - { - id: 23950195, - status: 'completed', - conclusion: 'success', - name: 'Travis CI - Branch', - }, - ], - }, - })); - const res = await github.getBranchStatus('somebranch', []); - expect(res).toEqual('success'); - }); - it('should fail if a check run has failed', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - state: 'pending', - statuses: [], - }, - })); - get.mockImplementationOnce(() => ({ - body: { - total_count: 2, - check_runs: [ - { - id: 23950198, - status: 'completed', - conclusion: 'success', - name: 'Travis CI - Pull Request', - }, - { - id: 23950195, - status: 'pending', - name: 'Travis CI - Branch', - }, - ], - }, - })); - const res = await github.getBranchStatus('somebranch', []); - expect(res).toEqual('pending'); - }); - }); - describe('getBranchStatusCheck', () => { - it('returns state if found', async () => { - await initRepo({ - repository: 'some/repo', - token: 'token', - }); - get.mockImplementationOnce(() => ({ - body: [ - { - context: 'context-1', - state: 'state-1', - }, - { - context: 'context-2', - state: 'state-2', - }, - { - context: 'context-3', - state: 'state-3', - }, - ], - })); - const res = await github.getBranchStatusCheck( - 'renovate/future_branch', - 'context-2' - ); - expect(res).toEqual('state-2'); - }); - it('returns null', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: [ - { - context: 'context-1', - state: 'state-1', - }, - { - context: 'context-2', - state: 'state-2', - }, - { - context: 'context-3', - state: 'state-3', - }, - ], - })); - const res = await github.getBranchStatusCheck('somebranch', 'context-4'); - expect(res).toBeNull(); - }); - }); - describe('setBranchStatus', () => { - it('returns if already set', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: [ - { - context: 'some-context', - state: 'some-state', - }, - ], - })); - await github.setBranchStatus( - 'some-branch', - 'some-context', - 'some-description', - 'some-state', - 'some-url' - ); - expect(get.post).toHaveBeenCalledTimes(0); - }); - it('sets branch status', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: [ - { - context: 'context-1', - state: 'state-1', - }, - { - context: 'context-2', - state: 'state-2', - }, - { - context: 'context-3', - state: 'state-3', - }, - ], - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1235', - }, - }, - })); - await github.setBranchStatus( - 'some-branch', - 'some-context', - 'some-description', - 'some-state', - 'some-url' - ); - expect(get.post).toHaveBeenCalledTimes(1); - }); - }); - describe('findIssue()', () => { - beforeEach(() => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - }); - it('returns null if no issue', async () => { - const res = await github.findIssue('title-3'); - expect(res).toBeNull(); - }); - it('finds issue', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - get.mockReturnValueOnce({ body: { body: 'new-content' } }); - const res = await github.findIssue('title-2'); - expect(res).not.toBeNull(); - }); - }); - describe('ensureIssue()', () => { - it('creates issue', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - const res = await github.ensureIssue('new-title', 'new-content'); - expect(res).toEqual('created'); - }); - it('creates issue if not ensuring only once', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'closed', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - const res = await github.ensureIssue('title-1', 'new-content'); - expect(res).toBeNull(); - }); - it('does not create issue if ensuring only once', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'closed', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - const once = true; - const res = await github.ensureIssue('title-1', 'new-content', once); - expect(res).toBeNull(); - }); - it('closes others if ensuring only once', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 3, - state: 'open', - title: 'title-1', - }, - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'closed', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - const once = true; - const res = await github.ensureIssue('title-1', 'new-content', once); - expect(res).toBeNull(); - }); - it('updates issue', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - get.mockReturnValueOnce({ body: { body: 'new-content' } }); - const res = await github.ensureIssue('title-2', 'newer-content'); - expect(res).toEqual('updated'); - }); - it('skips update if unchanged', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - get.mockReturnValueOnce({ body: { body: 'newer-content' } }); - const res = await github.ensureIssue('title-2', 'newer-content'); - expect(res).toBeNull(); - }); - it('deletes if duplicate', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-1', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - get.mockReturnValueOnce({ body: { body: 'newer-content' } }); - const res = await github.ensureIssue('title-1', 'newer-content'); - expect(res).toBeNull(); - }); - }); - describe('ensureIssueClosing()', () => { - it('closes issue', async () => { - get.post.mockImplementationOnce(() => ({ - body: JSON.stringify({ - data: { - repository: { - issues: { - pageInfo: { - startCursor: null, - hasNextPage: false, - endCursor: null, - }, - nodes: [ - { - number: 2, - state: 'open', - title: 'title-2', - }, - { - number: 1, - state: 'open', - title: 'title-1', - }, - ], - }, - }, - }, - }), - })); - await github.ensureIssueClosing('title-2'); - }); - }); - describe('deleteLabel(issueNo, label)', () => { - it('should delete the label', async () => { - await initRepo({ - repository: 'some/repo', - }); - await github.deleteLabel(42, 'rebase'); - expect(get.delete.mock.calls).toMatchSnapshot(); - }); - }); - describe('addAssignees(issueNo, assignees)', () => { - it('should add the given assignees to the issue', async () => { - await initRepo({ - repository: 'some/repo', - }); - await github.addAssignees(42, ['someuser', 'someotheruser']); - expect(get.post.mock.calls).toMatchSnapshot(); - }); - }); - describe('addReviewers(issueNo, reviewers)', () => { - it('should add the given reviewers to the PR', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.post.mockReturnValueOnce({}); - await github.addReviewers(42, [ - 'someuser', - 'someotheruser', - 'team:someteam', - ]); - expect(get.post.mock.calls).toMatchSnapshot(); - }); - }); - describe('ensureComment', () => { - it('add comment if not found', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockReturnValueOnce({ body: [] }); - await github.ensureComment(42, 'some-subject', 'some\ncontent'); - expect(get.post).toHaveBeenCalledTimes(2); - expect(get.post.mock.calls[1]).toMatchSnapshot(); - }); - it('adds comment if found in closed PR list', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.post.mockImplementationOnce(() => ({ - body: graphqlClosedPullrequests, - })); - await github.ensureComment(2499, 'some-subject', 'some\ncontent'); - expect(get.post).toHaveBeenCalledTimes(2); - expect(get.patch).toHaveBeenCalledTimes(0); - }); - it('add updates comment if necessary', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockReturnValueOnce({ - body: [{ id: 1234, body: '### some-subject\n\nblablabla' }], - }); - await github.ensureComment(42, 'some-subject', 'some\ncontent'); - expect(get.post).toHaveBeenCalledTimes(1); - expect(get.patch).toHaveBeenCalledTimes(1); - expect(get.patch.mock.calls).toMatchSnapshot(); - }); - it('skips comment', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockReturnValueOnce({ - body: [{ id: 1234, body: '### some-subject\n\nsome\ncontent' }], - }); - await github.ensureComment(42, 'some-subject', 'some\ncontent'); - expect(get.post).toHaveBeenCalledTimes(1); - expect(get.patch).toHaveBeenCalledTimes(0); - }); - it('handles comment with no description', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.mockReturnValueOnce({ body: [{ id: 1234, body: '!merge' }] }); - await github.ensureComment(42, null, '!merge'); - expect(get.post).toHaveBeenCalledTimes(1); - expect(get.patch).toHaveBeenCalledTimes(0); - }); - }); - describe('ensureCommentRemoval', () => { - it('deletes comment if found', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.mockReturnValueOnce({ - body: [{ id: 1234, body: '### some-subject\n\nblablabla' }], - }); - await github.ensureCommentRemoval(42, 'some-subject'); - expect(get.delete).toHaveBeenCalledTimes(1); - }); - }); - describe('findPr(branchName, prTitle, state)', () => { - it('returns true if no title and all state', async () => { - get.mockReturnValueOnce({ - body: [ - { - number: 1, - head: { ref: 'branch-a' }, - title: 'branch a pr', - state: 'open', - }, - ], - }); - const res = await github.findPr('branch-a', null); - expect(res).toBeDefined(); - }); - it('returns true if not open', async () => { - get.mockReturnValueOnce({ - body: [ - { - number: 1, - head: { ref: 'branch-a' }, - title: 'branch a pr', - state: 'closed', - }, - ], - }); - const res = await github.findPr('branch-a', null, '!open'); - expect(res).toBeDefined(); - }); - it('caches pr list', async () => { - get.mockReturnValueOnce({ - body: [ - { - number: 1, - head: { ref: 'branch-a' }, - title: 'branch a pr', - state: 'open', - }, - ], - }); - let res = await github.findPr('branch-a', null); - expect(res).toBeDefined(); - res = await github.findPr('branch-a', 'branch a pr'); - expect(res).toBeDefined(); - res = await github.findPr('branch-a', 'branch a pr', 'open'); - expect(res).toBeDefined(); - res = await github.findPr('branch-b'); - expect(res).not.toBeDefined(); - }); - }); - describe('createPr()', () => { - it('should create and return a PR object', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.post.mockImplementationOnce(() => ({ - body: { - number: 123, - }, - })); - get.mockImplementationOnce(() => ({ - body: [], - })); - // res.body.object.sha - get.mockImplementationOnce(() => ({ - body: { - object: { sha: 'some-sha' }, - }, - })); - const pr = await github.createPr( - 'some-branch', - 'The Title', - 'Hello world', - ['deps', 'renovate'], - false, - { statusCheckVerify: true } - ); - expect(pr).toMatchSnapshot(); - expect(get.post.mock.calls).toMatchSnapshot(); - }); - it('should use defaultBranch', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.post.mockImplementationOnce(() => ({ - body: { - number: 123, - }, - })); - const pr = await github.createPr( - 'some-branch', - 'The Title', - 'Hello world', - null, - true - ); - expect(pr).toMatchSnapshot(); - expect(get.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).toBeNull(); - }); - it('should return PR from graphql result', async () => { - global.gitAuthor = { - name: 'Renovate Bot', - email: 'bot@renovateapp.com', - }; - await initRepo({ - repository: 'some/repo', - }); - get.post.mockImplementationOnce(() => ({ - body: graphqlOpenPullRequests, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234123412341234123412341234123412341234', - }, - }, - })); - const pr = await github.getPr(2500); - expect(pr).toBeDefined(); - expect(pr).toMatchSnapshot(); - }); - it('should return PR from closed graphql result', async () => { - await initRepo({ - repository: 'some/repo', - }); - get.post.mockImplementationOnce(() => ({ - body: graphqlOpenPullRequests, - })); - get.post.mockImplementationOnce(() => ({ - body: graphqlClosedPullrequests, - })); - const pr = await github.getPr(2499); - expect(pr).toBeDefined(); - expect(pr).toMatchSnapshot(); - }); - it('should return null if no PR is returned from GitHub', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.mockImplementationOnce(() => ({ - body: null, - })); - const pr = await github.getPr(1234); - expect(pr).toBeNull(); - }); - [ - { - number: 1, - state: 'closed', - base: { sha: '1234' }, - mergeable: true, - merged_at: 'sometime', - }, - { - number: 1, - state: 'open', - mergeable_state: 'dirty', - base: { sha: '1234' }, - commits: 1, - }, - { - number: 1, - state: 'open', - base: { sha: '5678' }, - commits: 1, - mergeable: true, - }, - ].forEach((body, i) => { - it(`should return a PR object - ${i}`, async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.mockImplementationOnce(() => ({ - body, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - const pr = await github.getPr(1234); - expect(pr).toMatchSnapshot(); - }); - }); - it('should return a rebaseable PR despite multiple commits', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.mockImplementationOnce(() => ({ - body: { - number: 1, - state: 'open', - mergeable_state: 'dirty', - base: { sha: '1234' }, - commits: 2, - }, - })); - get.mockImplementationOnce(() => ({ - body: [ - { - author: { - login: 'foo', - }, - }, - ], - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - const pr = await github.getPr(1234); - expect(pr).toMatchSnapshot(); - }); - it('should return an unrebaseable PR if multiple authors', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.mockImplementationOnce(() => ({ - body: { - number: 1, - state: 'open', - mergeable_state: 'dirty', - base: { sha: '1234' }, - commits: 2, - }, - })); - get.mockImplementationOnce(() => ({ - body: [ - { - commit: { - author: { - email: 'bar', - }, - }, - }, - { - committer: { - login: 'web-flow', - }, - }, - {}, - ], - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - const pr = await github.getPr(1234); - expect(pr).toMatchSnapshot(); - }); - it('should return a rebaseable PR if web-flow is second author', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - get.mockImplementationOnce(() => ({ - body: { - number: 1, - state: 'open', - mergeable_state: 'dirty', - base: { sha: '1234' }, - commits: 2, - }, - })); - get.mockImplementationOnce(() => ({ - body: [ - { - author: { - login: 'foo', - }, - }, - { - committer: { - login: 'web-flow', - }, - commit: { - message: "Merge branch 'master' into renovate/foo", - }, - parents: [1, 2], - }, - ], - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - const pr = await github.getPr(1234); - expect(pr.canRebase).toBe(true); - expect(pr).toMatchSnapshot(); - }); - it('should return a rebaseable PR if gitAuthor matches 1 commit', async () => { - global.gitAuthor = { - name: 'Renovate Bot', - email: 'bot@renovateapp.com', - }; - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - number: 1, - state: 'open', - mergeable_state: 'dirty', - base: { sha: '1234' }, - commits: 1, - }, - })); - get.mockImplementationOnce(() => ({ - body: [ - { - commit: { - author: { - email: 'bot@renovateapp.com', - }, - }, - }, - ], - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - const pr = await github.getPr(1234); - expect(pr.canRebase).toBe(true); - expect(pr).toMatchSnapshot(); - }); - it('should return a not rebaseable PR if gitAuthor does not match 1 commit', async () => { - global.gitAuthor = { - name: 'Renovate Bot', - email: 'bot@renovateapp.com', - }; - await initRepo({ - repository: 'some/repo', - }); - get.mockImplementationOnce(() => ({ - body: { - number: 1, - state: 'open', - mergeable_state: 'dirty', - base: { sha: '1234' }, - commits: 1, - }, - })); - get.mockImplementationOnce(() => ({ - body: [ - { - commit: { - author: { - email: 'foo@bar.com', - }, - }, - }, - ], - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - const pr = await github.getPr(1234); - expect(pr.canRebase).toBe(false); - expect(pr).toMatchSnapshot(); - }); - }); - describe('getPrFiles()', () => { - it('should return empty if no prNo is passed', async () => { - const prFiles = await github.getPrFiles(null); - expect(prFiles).toEqual([]); - }); - it('returns files', async () => { - get.mockReturnValueOnce({ - body: [ - { filename: 'renovate.json' }, - { filename: 'not renovate.json' }, - ], - }); - const prFiles = await github.getPrFiles(123); - expect(prFiles).toMatchSnapshot(); - expect(prFiles).toHaveLength(2); - }); - }); - describe('updatePr(prNo, title, body)', () => { - it('should update the PR', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - await github.updatePr(1234, 'The New Title', 'Hello world again'); - expect(get.patch.mock.calls).toMatchSnapshot(); - }); - }); - describe('mergePr(prNo)', () => { - it('should merge the PR', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1235', - }, - }, - })); - const pr = { - number: 1234, - head: { - ref: 'someref', - }, - }; - expect(await github.mergePr(pr)).toBe(true); - expect(get.put).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledTimes(1); - }); - it('should handle merge error', async () => { - await initRepo({ repository: 'some/repo', token: 'token' }); - const pr = { - number: 1234, - head: { - ref: 'someref', - }, - }; - get.put.mockImplementationOnce(() => { - throw new Error('merge error'); - }); - expect(await github.mergePr(pr)).toBe(false); - expect(get.put).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledTimes(1); - }); - }); - describe('getPrBody(input)', () => { - it('returns updated pr body', () => { - const input = - 'https://github.com/foo/bar/issues/5 plus also [a link](https://github.com/foo/bar/issues/5)'; - expect(github.getPrBody(input)).toMatchSnapshot(); - }); - it('returns not-updated pr body for GHE', async () => { - get.mockImplementationOnce(() => ({ - body: { - login: 'renovate-bot', - }, - })); - await github.initPlatform({ - endpoint: 'https://github.company.com', - token: 'abc123', - }); - hostRules.find.mockReturnValue({ - token: 'abc123', - }); - await initRepo({ - repository: 'some/repo', - }); - const input = - 'https://github.com/foo/bar/issues/5 plus also [a link](https://github.com/foo/bar/issues/5)'; - expect(github.getPrBody(input)).toEqual(input); - }); - }); - describe('mergePr(prNo) - autodetection', () => { - beforeEach(async () => { - function guessInitRepo(...args) { - // repo info - get.mockImplementationOnce(() => ({ - body: { - owner: { - login: 'theowner', - }, - default_branch: 'master', - }, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1234', - }, - }, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1235', - }, - }, - })); - // getBranchCommit - get.mockImplementationOnce(() => ({ - body: { - object: { - sha: '1235', - }, - }, - })); - return github.initRepo(...args); - } - await guessInitRepo({ repository: 'some/repo', token: 'token' }); - get.put = jest.fn(); - }); - it('should try rebase first', async () => { - const pr = { - number: 1235, - head: { - ref: 'someref', - }, - }; - expect(await github.mergePr(pr)).toBe(true); - expect(get.put).toHaveBeenCalledTimes(1); - }); - it('should try squash after rebase', async () => { - const pr = { - number: 1236, - head: { - ref: 'someref', - }, - }; - get.put.mockImplementationOnce(() => { - throw new Error('no rebasing allowed'); - }); - await github.mergePr(pr); - expect(get.put).toHaveBeenCalledTimes(2); - }); - it('should try merge after squash', async () => { - const pr = { - number: 1237, - head: { - ref: 'someref', - }, - }; - get.put.mockImplementationOnce(() => { - throw new Error('no rebasing allowed'); - }); - get.put.mockImplementationOnce(() => { - throw new Error('no squashing allowed'); - }); - expect(await github.mergePr(pr)).toBe(true); - expect(get.put).toHaveBeenCalledTimes(3); - }); - it('should give up', async () => { - const pr = { - number: 1237, - head: { - ref: 'someref', - }, - }; - get.put.mockImplementationOnce(() => { - throw new Error('no rebasing allowed'); - }); - get.put.mockImplementationOnce(() => { - throw new Error('no squashing allowed'); - }); - get.put.mockImplementationOnce(() => { - throw new Error('no merging allowed'); - }); - expect(await github.mergePr(pr)).toBe(false); - expect(get.put).toHaveBeenCalledTimes(3); - }); - }); - describe('getVulnerabilityAlerts()', () => { - it('returns empty if error', async () => { - get.mockReturnValueOnce({ - body: {}, - }); - const res = await github.getVulnerabilityAlerts(); - expect(res).toHaveLength(0); - }); - it('returns array if found', async () => { - // prettier-ignore - const body = "{\"data\":{\"repository\":{\"vulnerabilityAlerts\":{\"edges\":[{\"node\":{\"externalIdentifier\":\"CVE-2018-1000136\",\"externalReference\":\"https://nvd.nist.gov/vuln/detail/CVE-2018-1000136\",\"affectedRange\":\">= 1.8, < 1.8.3\",\"fixedIn\":\"1.8.3\",\"id\":\"MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1MzE3NDk4MQ==\",\"packageName\":\"electron\"}}]}}}}"; - get.post.mockReturnValueOnce({ - body, - }); - const res = await github.getVulnerabilityAlerts(); - expect(res).toHaveLength(1); - }); - it('returns empty if disabled', async () => { - // prettier-ignore - const body = "{\"data\":{\"repository\":{}}}"; - get.post.mockReturnValueOnce({ - body, - }); - const res = await github.getVulnerabilityAlerts(); - expect(res).toHaveLength(0); - }); - }); -}); diff --git a/test/platform/github/index.spec.ts b/test/platform/github/index.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bb25bce8583e15a26b3f6136a98923cfac143c3 --- /dev/null +++ b/test/platform/github/index.spec.ts @@ -0,0 +1,1977 @@ +import fs from 'fs-extra'; +import { GotApi } from '../../../lib/platform/common'; + +describe('platform/github', () => { + let github: typeof import('../../../lib/platform/github'); + let api: jest.Mocked<GotApi>; + let hostRules: jest.Mocked<typeof import('../../../lib/util/host-rules')>; + let GitStorage: jest.Mock<typeof import('../../../lib/platform/git/storage')>; + beforeEach(async () => { + // reset module + jest.resetModules(); + jest.mock('delay'); + jest.mock('../../../lib/platform/github/gh-got-wrapper'); + jest.mock('../../../lib/util/host-rules'); + jest.mock('../../../lib/util/got'); + api = (await import('../../../lib/platform/github/gh-got-wrapper')) + .api as any; + github = await import('../../../lib/platform/github'); + hostRules = (await import('../../../lib/util/host-rules')) as any; + jest.mock('../../../lib/platform/git/storage'); + GitStorage = (await import('../../../lib/platform/git/storage')) + .Storage as any; + GitStorage.mockImplementation( + () => + ({ + initRepo: jest.fn(), + cleanRepo: jest.fn(), + getFileList: jest.fn(), + branchExists: jest.fn(() => true), + isBranchStale: jest.fn(() => false), + setBaseBranch: jest.fn(), + getBranchLastCommitTime: jest.fn(), + getAllRenovateBranches: jest.fn(), + getCommitMessages: jest.fn(), + getFile: jest.fn(), + commitFilesToBranch: jest.fn(), + mergeBranch: jest.fn(), + deleteBranch: jest.fn(), + getRepoStatus: jest.fn(), + getBranchCommit: jest.fn( + () => '0d9c7726c3d628b7e28af234595cfd20febdbf8e' + ), + } as any) + ); + delete global.gitAuthor; + hostRules.find.mockReturnValue({ + token: 'abc123', + }); + }); + + const graphqlOpenPullRequests = fs.readFileSync( + 'test/platform/github/_fixtures/graphql/pullrequest-1.json', + 'utf8' + ); + const graphqlClosedPullrequests = fs.readFileSync( + 'test/platform/github/_fixtures/graphql/pullrequests-closed.json', + 'utf8' + ); + + function getRepos() { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + full_name: 'a/b', + }, + { + full_name: 'c/d', + }, + ], + } as any) + ); + return github.getRepos(); + } + + describe('initPlatform()', () => { + it('should throw if no token', async () => { + await expect(github.initPlatform({} as any)).rejects.toThrow(); + }); + it('should throw if user failure', async () => { + api.get.mockImplementationOnce(() => ({} as any)); + await expect( + github.initPlatform({ token: 'abc123' } as any) + ).rejects.toThrow(); + }); + it('should support default endpoint no email access', async () => { + api.get.mockImplementationOnce( + () => + ({ + body: { + login: 'renovate-bot', + }, + } as any) + ); + expect( + await github.initPlatform({ token: 'abc123' } as any) + ).toMatchSnapshot(); + }); + it('should support default endpoint no email result', async () => { + api.get.mockImplementationOnce( + () => + ({ + body: { + login: 'renovate-bot', + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [{}], + } as any) + ); + expect( + await github.initPlatform({ token: 'abc123' } as any) + ).toMatchSnapshot(); + }); + it('should support default endpoint with email', async () => { + api.get.mockImplementationOnce( + () => + ({ + body: { + login: 'renovate-bot', + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + email: 'user@domain.com', + }, + ], + } as any) + ); + expect( + await github.initPlatform({ token: 'abc123' } as any) + ).toMatchSnapshot(); + }); + it('should support custom endpoint', async () => { + api.get.mockImplementationOnce( + () => + ({ + body: { + login: 'renovate-bot', + }, + } as any) + ); + expect( + await github.initPlatform({ + endpoint: 'https://ghe.renovatebot.com', + token: 'abc123', + }) + ).toMatchSnapshot(); + }); + }); + + describe('getRepos', () => { + it('should return an array of repos', async () => { + const repos = await getRepos(); + expect(api.get.mock.calls).toMatchSnapshot(); + expect(repos).toMatchSnapshot(); + }); + }); + + function initRepo(args: { repository: string; token?: string }) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + allow_rebase_merge: true, + allow_squash_merge: true, + allow_merge_commit: true, + }, + } as any) + ); + if (args) { + return github.initRepo(args as any); + } + return github.initRepo({ + endpoint: 'https://github.com', + repository: 'some/repo', + } as any); + } + + describe('initRepo', () => { + it('should rebase', async () => { + function squashInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + allow_rebase_merge: true, + allow_squash_merge: true, + allow_merge_commit: true, + }, + } as any) + ); + return github.initRepo(args); + } + const config = await squashInitRepo({ + repository: 'some/repo', + }); + expect(config).toMatchSnapshot(); + }); + it('should forks when forkMode', async () => { + function forkInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + allow_rebase_merge: true, + allow_squash_merge: true, + allow_merge_commit: true, + }, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + // api.getRepos + api.get.mockImplementationOnce( + () => + ({ + body: [], + } as any) + ); + // api.getBranchCommit + api.post.mockImplementationOnce( + () => + ({ + body: {}, + } as any) + ); + return github.initRepo(args); + } + const config = await forkInitRepo({ + repository: 'some/repo', + forkMode: true, + }); + expect(config).toMatchSnapshot(); + }); + it('should update fork when forkMode', async () => { + function forkInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + allow_rebase_merge: true, + allow_squash_merge: true, + allow_merge_commit: true, + }, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + // api.getRepos + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + full_name: 'forked_repo', + }, + ], + } as any) + ); + // fork + api.post.mockImplementationOnce( + () => + ({ + body: { full_name: 'forked_repo' }, + } as any) + ); + return github.initRepo(args); + } + const config = await forkInitRepo({ + repository: 'some/repo', + forkMode: true, + }); + expect(config).toMatchSnapshot(); + }); + it('should squash', async () => { + function mergeInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + allow_rebase_merge: false, + allow_squash_merge: true, + allow_merge_commit: true, + }, + } as any) + ); + return github.initRepo(args); + } + const config = await mergeInitRepo({ + repository: 'some/repo', + }); + expect(config).toMatchSnapshot(); + }); + it('should merge', async () => { + function mergeInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + allow_rebase_merge: false, + allow_squash_merge: false, + allow_merge_commit: true, + }, + } as any) + ); + return github.initRepo(args); + } + const config = await mergeInitRepo({ + repository: 'some/repo', + }); + expect(config).toMatchSnapshot(); + }); + it('should not guess at merge', async () => { + function mergeInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + }, + } as any) + ); + return github.initRepo(args); + } + const config = await mergeInitRepo({ + repository: 'some/repo', + }); + expect(config).toMatchSnapshot(); + }); + it('should throw error if archived', async () => { + api.get.mockReturnValueOnce({ + body: { + archived: true, + owner: {}, + }, + } as any); + await expect( + github.initRepo({ + repository: 'some/repo', + } as any) + ).rejects.toThrow(); + }); + it('throws not-found', async () => { + api.get.mockImplementationOnce( + () => + Promise.reject({ + statusCode: 404, + }) as any + ); + await expect( + github.initRepo({ + repository: 'some/repo', + } as any) + ).rejects.toThrow('not-found'); + }); + it('should throw error if renamed', async () => { + api.get.mockReturnValueOnce({ + body: { + fork: true, + full_name: 'some/other', + owner: {}, + }, + } as any); + await expect( + github.initRepo({ + includeForks: true, + repository: 'some/repo', + } as any) + ).rejects.toThrow('renamed'); + }); + }); + describe('getRepoForceRebase', () => { + it('should detect repoForceRebase', async () => { + api.get.mockImplementationOnce( + () => + ({ + body: { + required_pull_request_reviews: { + dismiss_stale_reviews: false, + require_code_owner_reviews: false, + }, + required_status_checks: { + strict: true, + contexts: [], + }, + restrictions: { + users: [ + { + login: 'rarkins', + id: 6311784, + type: 'User', + site_admin: false, + }, + ], + teams: [], + }, + }, + } as any) + ); + const res = await github.getRepoForceRebase(); + expect(res).toBe(true); + }); + it('should handle 404', async () => { + api.get.mockImplementationOnce( + () => + Promise.reject({ + statusCode: 404, + }) as any + ); + const res = await github.getRepoForceRebase(); + expect(res).toBe(false); + }); + it('should handle 403', async () => { + api.get.mockImplementationOnce( + () => + Promise.reject({ + statusCode: 403, + }) as any + ); + const res = await github.getRepoForceRebase(); + expect(res).toBe(false); + }); + it('should throw 401', async () => { + api.get.mockImplementationOnce( + () => + Promise.reject({ + statusCode: 401, + }) as any + ); + await expect(github.getRepoForceRebase()).rejects.toEqual({ + statusCode: 401, + }); + }); + }); + describe('getBranchPr(branchName)', () => { + it('should return null if no PR exists', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: [], + } as any) + ); + const pr = await github.getBranchPr('somebranch'); + expect(pr).toBeNull(); + }); + it('should return the PR object', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: [{ number: 91, head: { ref: 'somebranch' }, state: 'open' }], + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: { + number: 91, + additions: 1, + deletions: 1, + commits: 1, + base: { + sha: '1234', + }, + head: { ref: 'somebranch' }, + state: 'open', + }, + } as any) + ); + api.get.mockResolvedValue({ body: { object: { sha: '12345' } } } as any); + const pr = await github.getBranchPr('somebranch'); + expect(api.get.mock.calls).toMatchSnapshot(); + expect(pr).toMatchSnapshot(); + }); + }); + describe('getBranchStatus()', () => { + it('returns success if requiredStatusChecks null', async () => { + await initRepo({ + repository: 'some/repo', + }); + const res = await github.getBranchStatus('somebranch', null); + expect(res).toEqual('success'); + }); + it('return failed if unsupported requiredStatusChecks', async () => { + await initRepo({ + repository: 'some/repo', + }); + const res = await github.getBranchStatus('somebranch', ['foo']); + expect(res).toEqual('failed'); + }); + it('should pass through success', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + state: 'success', + }, + } as any) + ); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('success'); + }); + it('should pass through failed', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + state: 'failed', + }, + } as any) + ); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('failed'); + }); + it('should fail if a check run has failed', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + state: 'pending', + statuses: [], + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: { + total_count: 2, + check_runs: [ + { + id: 23950198, + status: 'completed', + conclusion: 'success', + name: 'Travis CI - Pull Request', + }, + { + id: 23950195, + status: 'completed', + conclusion: 'failed', + name: 'Travis CI - Branch', + }, + ], + }, + } as any) + ); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('failed'); + }); + it('should suceed if no status and all passed check runs', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + state: 'pending', + statuses: [], + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: { + total_count: 2, + check_runs: [ + { + id: 23950198, + status: 'completed', + conclusion: 'success', + name: 'Travis CI - Pull Request', + }, + { + id: 23950195, + status: 'completed', + conclusion: 'success', + name: 'Travis CI - Branch', + }, + ], + }, + } as any) + ); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('success'); + }); + it('should fail if a check run has failed', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + state: 'pending', + statuses: [], + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: { + total_count: 2, + check_runs: [ + { + id: 23950198, + status: 'completed', + conclusion: 'success', + name: 'Travis CI - Pull Request', + }, + { + id: 23950195, + status: 'pending', + name: 'Travis CI - Branch', + }, + ], + }, + } as any) + ); + const res = await github.getBranchStatus('somebranch', []); + expect(res).toEqual('pending'); + }); + }); + describe('getBranchStatusCheck', () => { + it('returns state if found', async () => { + await initRepo({ + repository: 'some/repo', + token: 'token', + }); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + context: 'context-1', + state: 'state-1', + }, + { + context: 'context-2', + state: 'state-2', + }, + { + context: 'context-3', + state: 'state-3', + }, + ], + } as any) + ); + const res = await github.getBranchStatusCheck( + 'renovate/future_branch', + 'context-2' + ); + expect(res).toEqual('state-2'); + }); + it('returns null', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + context: 'context-1', + state: 'state-1', + }, + { + context: 'context-2', + state: 'state-2', + }, + { + context: 'context-3', + state: 'state-3', + }, + ], + } as any) + ); + const res = await github.getBranchStatusCheck('somebranch', 'context-4'); + expect(res).toBeNull(); + }); + }); + describe('setBranchStatus', () => { + it('returns if already set', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + context: 'some-context', + state: 'some-state', + }, + ], + } as any) + ); + await github.setBranchStatus( + 'some-branch', + 'some-context', + 'some-description', + 'some-state', + 'some-url' + ); + expect(api.post).toHaveBeenCalledTimes(0); + }); + it('sets branch status', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + context: 'context-1', + state: 'state-1', + }, + { + context: 'context-2', + state: 'state-2', + }, + { + context: 'context-3', + state: 'state-3', + }, + ], + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1235', + }, + }, + } as any) + ); + await github.setBranchStatus( + 'some-branch', + 'some-context', + 'some-description', + 'some-state', + 'some-url' + ); + expect(api.post).toHaveBeenCalledTimes(1); + }); + }); + describe('findIssue()', () => { + beforeEach(() => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + }); + it('returns null if no issue', async () => { + const res = await github.findIssue('title-3'); + expect(res).toBeNull(); + }); + it('finds issue', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + api.get.mockReturnValueOnce({ body: { body: 'new-content' } } as any); + const res = await github.findIssue('title-2'); + expect(res).not.toBeNull(); + }); + }); + describe('ensureIssue()', () => { + it('creates issue', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + const res = await github.ensureIssue('new-title', 'new-content'); + expect(res).toEqual('created'); + }); + it('creates issue if not ensuring only once', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'closed', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + const res = await github.ensureIssue('title-1', 'new-content'); + expect(res).toBeNull(); + }); + it('does not create issue if ensuring only once', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'closed', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + const once = true; + const res = await github.ensureIssue('title-1', 'new-content', once); + expect(res).toBeNull(); + }); + it('closes others if ensuring only once', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 3, + state: 'open', + title: 'title-1', + }, + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'closed', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + const once = true; + const res = await github.ensureIssue('title-1', 'new-content', once); + expect(res).toBeNull(); + }); + it('updates issue', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + api.get.mockReturnValueOnce({ body: { body: 'new-content' } } as any); + const res = await github.ensureIssue('title-2', 'newer-content'); + expect(res).toEqual('updated'); + }); + it('skips update if unchanged', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + api.get.mockReturnValueOnce({ body: { body: 'newer-content' } } as any); + const res = await github.ensureIssue('title-2', 'newer-content'); + expect(res).toBeNull(); + }); + it('deletes if duplicate', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-1', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + api.get.mockReturnValueOnce({ body: { body: 'newer-content' } } as any); + const res = await github.ensureIssue('title-1', 'newer-content'); + expect(res).toBeNull(); + }); + }); + describe('ensureIssueClosing()', () => { + it('closes issue', async () => { + api.post.mockImplementationOnce( + () => + ({ + body: JSON.stringify({ + data: { + repository: { + issues: { + pageInfo: { + startCursor: null, + hasNextPage: false, + endCursor: null, + }, + nodes: [ + { + number: 2, + state: 'open', + title: 'title-2', + }, + { + number: 1, + state: 'open', + title: 'title-1', + }, + ], + }, + }, + }, + }), + } as any) + ); + await github.ensureIssueClosing('title-2'); + }); + }); + describe('deleteLabel(issueNo, label)', () => { + it('should delete the label', async () => { + await initRepo({ + repository: 'some/repo', + }); + await github.deleteLabel(42, 'rebase'); + expect(api.delete.mock.calls).toMatchSnapshot(); + }); + }); + describe('addAssignees(issueNo, assignees)', () => { + it('should add the given assignees to the issue', async () => { + await initRepo({ + repository: 'some/repo', + }); + await github.addAssignees(42, ['someuser', 'someotheruser']); + expect(api.post.mock.calls).toMatchSnapshot(); + }); + }); + describe('addReviewers(issueNo, reviewers)', () => { + it('should add the given reviewers to the PR', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.post.mockReturnValueOnce({} as any); + await github.addReviewers(42, [ + 'someuser', + 'someotheruser', + 'team:someteam', + ]); + expect(api.post.mock.calls).toMatchSnapshot(); + }); + }); + describe('ensureComment', () => { + it('add comment if not found', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockReturnValueOnce({ body: [] } as any); + await github.ensureComment(42, 'some-subject', 'some\ncontent'); + expect(api.post).toHaveBeenCalledTimes(2); + expect(api.post.mock.calls[1]).toMatchSnapshot(); + }); + it('adds comment if found in closed PR list', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.post.mockImplementationOnce( + () => + ({ + body: graphqlClosedPullrequests, + } as any) + ); + await github.ensureComment(2499, 'some-subject', 'some\ncontent'); + expect(api.post).toHaveBeenCalledTimes(2); + expect(api.patch).toHaveBeenCalledTimes(0); + }); + it('add updates comment if necessary', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockReturnValueOnce({ + body: [{ id: 1234, body: '### some-subject\n\nblablabla' }], + } as any); + await github.ensureComment(42, 'some-subject', 'some\ncontent'); + expect(api.post).toHaveBeenCalledTimes(1); + expect(api.patch).toHaveBeenCalledTimes(1); + expect(api.patch.mock.calls).toMatchSnapshot(); + }); + it('skips comment', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockReturnValueOnce({ + body: [{ id: 1234, body: '### some-subject\n\nsome\ncontent' }], + } as any); + await github.ensureComment(42, 'some-subject', 'some\ncontent'); + expect(api.post).toHaveBeenCalledTimes(1); + expect(api.patch).toHaveBeenCalledTimes(0); + }); + it('handles comment with no description', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.get.mockReturnValueOnce({ + body: [{ id: 1234, body: '!merge' }], + } as any); + await github.ensureComment(42, null, '!merge'); + expect(api.post).toHaveBeenCalledTimes(1); + expect(api.patch).toHaveBeenCalledTimes(0); + }); + }); + describe('ensureCommentRemoval', () => { + it('deletes comment if found', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.get.mockReturnValueOnce({ + body: [{ id: 1234, body: '### some-subject\n\nblablabla' }], + } as any); + await github.ensureCommentRemoval(42, 'some-subject'); + expect(api.delete).toHaveBeenCalledTimes(1); + }); + }); + describe('findPr(branchName, prTitle, state)', () => { + it('returns true if no title and all state', async () => { + api.get.mockReturnValueOnce({ + body: [ + { + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + state: 'open', + }, + ], + } as any); + const res = await github.findPr('branch-a', null); + expect(res).toBeDefined(); + }); + it('returns true if not open', async () => { + api.get.mockReturnValueOnce({ + body: [ + { + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + state: 'closed', + }, + ], + } as any); + const res = await github.findPr('branch-a', null, '!open'); + expect(res).toBeDefined(); + }); + it('caches pr list', async () => { + api.get.mockReturnValueOnce({ + body: [ + { + number: 1, + head: { ref: 'branch-a' }, + title: 'branch a pr', + state: 'open', + }, + ], + } as any); + let res = await github.findPr('branch-a', null); + expect(res).toBeDefined(); + res = await github.findPr('branch-a', 'branch a pr'); + expect(res).toBeDefined(); + res = await github.findPr('branch-a', 'branch a pr', 'open'); + expect(res).toBeDefined(); + res = await github.findPr('branch-b'); + expect(res).not.toBeDefined(); + }); + }); + describe('createPr()', () => { + it('should create and return a PR object', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.post.mockImplementationOnce( + () => + ({ + body: { + number: 123, + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [], + } as any) + ); + // res.body.object.sha + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { sha: 'some-sha' }, + }, + } as any) + ); + const pr = await github.createPr( + 'some-branch', + 'The Title', + 'Hello world', + ['deps', 'renovate'], + false, + { statusCheckVerify: true } + ); + expect(pr).toMatchSnapshot(); + expect(api.post.mock.calls).toMatchSnapshot(); + }); + it('should use defaultBranch', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.post.mockImplementationOnce( + () => + ({ + body: { + number: 123, + }, + } as any) + ); + const pr = await github.createPr( + 'some-branch', + 'The Title', + 'Hello world', + null, + true + ); + expect(pr).toMatchSnapshot(); + expect(api.post.mock.calls).toMatchSnapshot(); + }); + }); + describe('getPr(prNo)', () => { + it('should return null if no prNo is passed', async () => { + const pr = await github.getPr(0); + expect(pr).toBeNull(); + }); + it('should return PR from graphql result', async () => { + global.gitAuthor = { + name: 'Renovate Bot', + email: 'bot@renovateapp.com', + }; + await initRepo({ + repository: 'some/repo', + }); + api.post.mockImplementationOnce( + () => + ({ + body: graphqlOpenPullRequests, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234123412341234123412341234123412341234', + }, + }, + } as any) + ); + const pr = await github.getPr(2500); + expect(pr).toBeDefined(); + expect(pr).toMatchSnapshot(); + }); + it('should return PR from closed graphql result', async () => { + await initRepo({ + repository: 'some/repo', + }); + api.post.mockImplementationOnce( + () => + ({ + body: graphqlOpenPullRequests, + } as any) + ); + api.post.mockImplementationOnce( + () => + ({ + body: graphqlClosedPullrequests, + } as any) + ); + const pr = await github.getPr(2499); + expect(pr).toBeDefined(); + expect(pr).toMatchSnapshot(); + }); + it('should return null if no PR is returned from GitHub', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.get.mockImplementationOnce( + () => + ({ + body: null, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr).toBeNull(); + }); + [ + { + number: 1, + state: 'closed', + base: { sha: '1234' }, + mergeable: true, + merged_at: 'sometime', + }, + { + number: 1, + state: 'open', + mergeable_state: 'dirty', + base: { sha: '1234' }, + commits: 1, + }, + { + number: 1, + state: 'open', + base: { sha: '5678' }, + commits: 1, + mergeable: true, + }, + ].forEach((body, i) => { + it(`should return a PR object - ${i}`, async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.get.mockImplementationOnce( + () => + ({ + body, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr).toMatchSnapshot(); + }); + }); + it('should return a rebaseable PR despite multiple commits', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.get.mockImplementationOnce( + () => + ({ + body: { + number: 1, + state: 'open', + mergeable_state: 'dirty', + base: { sha: '1234' }, + commits: 2, + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + author: { + login: 'foo', + }, + }, + ], + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr).toMatchSnapshot(); + }); + it('should return an unrebaseable PR if multiple authors', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.get.mockImplementationOnce( + () => + ({ + body: { + number: 1, + state: 'open', + mergeable_state: 'dirty', + base: { sha: '1234' }, + commits: 2, + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + commit: { + author: { + email: 'bar', + }, + }, + }, + { + committer: { + login: 'web-flow', + }, + }, + {}, + ], + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr).toMatchSnapshot(); + }); + it('should return a rebaseable PR if web-flow is second author', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + api.get.mockImplementationOnce( + () => + ({ + body: { + number: 1, + state: 'open', + mergeable_state: 'dirty', + base: { sha: '1234' }, + commits: 2, + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + author: { + login: 'foo', + }, + }, + { + committer: { + login: 'web-flow', + }, + commit: { + message: "Merge branch 'master' into renovate/foo", + }, + parents: [1, 2], + }, + ], + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr.canRebase).toBe(true); + expect(pr).toMatchSnapshot(); + }); + it('should return a rebaseable PR if gitAuthor matches 1 commit', async () => { + global.gitAuthor = { + name: 'Renovate Bot', + email: 'bot@renovateapp.com', + }; + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + number: 1, + state: 'open', + mergeable_state: 'dirty', + base: { sha: '1234' }, + commits: 1, + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + commit: { + author: { + email: 'bot@renovateapp.com', + }, + }, + }, + ], + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr.canRebase).toBe(true); + expect(pr).toMatchSnapshot(); + }); + it('should return a not rebaseable PR if gitAuthor does not match 1 commit', async () => { + global.gitAuthor = { + name: 'Renovate Bot', + email: 'bot@renovateapp.com', + }; + await initRepo({ + repository: 'some/repo', + }); + api.get.mockImplementationOnce( + () => + ({ + body: { + number: 1, + state: 'open', + mergeable_state: 'dirty', + base: { sha: '1234' }, + commits: 1, + }, + } as any) + ); + api.get.mockImplementationOnce( + () => + ({ + body: [ + { + commit: { + author: { + email: 'foo@bar.com', + }, + }, + }, + ], + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + const pr = await github.getPr(1234); + expect(pr.canRebase).toBe(false); + expect(pr).toMatchSnapshot(); + }); + }); + describe('getPrFiles()', () => { + it('should return empty if no prNo is passed', async () => { + const prFiles = await github.getPrFiles(0); + expect(prFiles).toEqual([]); + }); + it('returns files', async () => { + api.get.mockReturnValueOnce({ + body: [ + { filename: 'renovate.json' }, + { filename: 'not renovate.json' }, + ], + } as any); + const prFiles = await github.getPrFiles(123); + expect(prFiles).toMatchSnapshot(); + expect(prFiles).toHaveLength(2); + }); + }); + describe('updatePr(prNo, title, body)', () => { + it('should update the PR', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + await github.updatePr(1234, 'The New Title', 'Hello world again'); + expect(api.patch.mock.calls).toMatchSnapshot(); + }); + }); + describe('mergePr(prNo)', () => { + it('should merge the PR', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1235', + }, + }, + } as any) + ); + const pr = { + number: 1234, + head: { + ref: 'someref', + }, + }; + expect(await github.mergePr(pr.number, '')).toBe(true); + expect(api.put).toHaveBeenCalledTimes(1); + expect(api.get).toHaveBeenCalledTimes(1); + }); + it('should handle merge error', async () => { + await initRepo({ repository: 'some/repo', token: 'token' }); + const pr = { + number: 1234, + head: { + ref: 'someref', + }, + }; + api.put.mockImplementationOnce(() => { + throw new Error('merge error'); + }); + expect(await github.mergePr(pr.number, '')).toBe(false); + expect(api.put).toHaveBeenCalledTimes(1); + expect(api.get).toHaveBeenCalledTimes(1); + }); + }); + describe('getPrBody(input)', () => { + it('returns updated pr body', () => { + const input = + 'https://github.com/foo/bar/issues/5 plus also [a link](https://github.com/foo/bar/issues/5)'; + expect(github.getPrBody(input)).toMatchSnapshot(); + }); + it('returns not-updated pr body for GHE', async () => { + api.get.mockImplementationOnce( + () => + ({ + body: { + login: 'renovate-bot', + }, + } as any) + ); + await github.initPlatform({ + endpoint: 'https://github.company.com', + token: 'abc123', + }); + hostRules.find.mockReturnValue({ + token: 'abc123', + }); + await initRepo({ + repository: 'some/repo', + }); + const input = + 'https://github.com/foo/bar/issues/5 plus also [a link](https://github.com/foo/bar/issues/5)'; + expect(github.getPrBody(input)).toEqual(input); + }); + }); + describe('mergePr(prNo) - autodetection', () => { + beforeEach(async () => { + function guessInitRepo(args: any) { + // repo info + api.get.mockImplementationOnce( + () => + ({ + body: { + owner: { + login: 'theowner', + }, + default_branch: 'master', + }, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1234', + }, + }, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1235', + }, + }, + } as any) + ); + // api.getBranchCommit + api.get.mockImplementationOnce( + () => + ({ + body: { + object: { + sha: '1235', + }, + }, + } as any) + ); + return github.initRepo(args); + } + await guessInitRepo({ repository: 'some/repo', token: 'token' }); + api.put = jest.fn(); + }); + it('should try rebase first', async () => { + const pr = { + number: 1235, + head: { + ref: 'someref', + }, + }; + expect(await github.mergePr(pr.number, '')).toBe(true); + expect(api.put).toHaveBeenCalledTimes(1); + }); + it('should try squash after rebase', async () => { + const pr = { + number: 1236, + head: { + ref: 'someref', + }, + }; + api.put.mockImplementationOnce(() => { + throw new Error('no rebasing allowed'); + }); + await github.mergePr(pr.number, ''); + expect(api.put).toHaveBeenCalledTimes(2); + }); + it('should try merge after squash', async () => { + const pr = { + number: 1237, + head: { + ref: 'someref', + }, + }; + api.put.mockImplementationOnce(() => { + throw new Error('no rebasing allowed'); + }); + api.put.mockImplementationOnce(() => { + throw new Error('no squashing allowed'); + }); + expect(await github.mergePr(pr.number, '')).toBe(true); + expect(api.put).toHaveBeenCalledTimes(3); + }); + it('should give up', async () => { + const pr = { + number: 1237, + head: { + ref: 'someref', + }, + }; + api.put.mockImplementationOnce(() => { + throw new Error('no rebasing allowed'); + }); + api.put.mockImplementationOnce(() => { + throw new Error('no squashing allowed'); + }); + api.put.mockImplementationOnce(() => { + throw new Error('no merging allowed'); + }); + expect(await github.mergePr(pr.number, '')).toBe(false); + expect(api.put).toHaveBeenCalledTimes(3); + }); + }); + describe('getVulnerabilityAlerts()', () => { + it('returns empty if error', async () => { + api.get.mockReturnValueOnce({ + body: {}, + } as any); + const res = await github.getVulnerabilityAlerts(); + expect(res).toHaveLength(0); + }); + it('returns array if found', async () => { + // prettier-ignore + const body = "{\"data\":{\"repository\":{\"vulnerabilityAlerts\":{\"edges\":[{\"node\":{\"externalIdentifier\":\"CVE-2018-1000136\",\"externalReference\":\"https://nvd.nist.gov/vuln/detail/CVE-2018-1000136\",\"affectedRange\":\">= 1.8, < 1.8.3\",\"fixedIn\":\"1.8.3\",\"id\":\"MDI4OlJlcG9zaXRvcnlWdWxuZXJhYmlsaXR5QWxlcnQ1MzE3NDk4MQ==\",\"packageName\":\"electron\"}}]}}}}"; + api.post.mockReturnValueOnce({ + body, + } as any); + const res = await github.getVulnerabilityAlerts(); + expect(res).toHaveLength(1); + }); + it('returns empty if disabled', async () => { + // prettier-ignore + const body = "{\"data\":{\"repository\":{}}}"; + api.post.mockReturnValueOnce({ + body, + } as any); + const res = await github.getVulnerabilityAlerts(); + expect(res).toHaveLength(0); + }); + }); +}); diff --git a/test/platform/index.spec.js b/test/platform/index.spec.js index 2d3b3e52cc59ead3a7888bcdf4a2aff953910bc1..8aad6447555547dad431a1045fc0ae343123c8c6 100644 --- a/test/platform/index.spec.js +++ b/test/platform/index.spec.js @@ -21,7 +21,7 @@ describe('platform', () => { expect(await platform.initPlatform(config)).toMatchSnapshot(); }); it('has a list of supported methods for github', () => { - const githubMethods = Object.keys(github); + const githubMethods = Object.keys(github).sort(); expect(githubMethods).toMatchSnapshot(); }); diff --git a/test/workers/pr/changelog/index.spec.js b/test/workers/pr/changelog/index.spec.js index 5dbca1c35bb17060277bd60b8279180e03d4081c..16989a7b331b17491a29a6baf5c0d548b023fc12 100644 --- a/test/workers/pr/changelog/index.spec.js +++ b/test/workers/pr/changelog/index.spec.js @@ -1,9 +1,9 @@ +import ghGot from '../../../../lib/platform/github/gh-got-wrapper'; + jest.mock('../../../../lib/platform/github/gh-got-wrapper'); jest.mock('../../../../lib/datasource/npm'); -jest.mock('../../../../lib/platform/github/gh-got-wrapper'); const hostRules = require('../../../../lib/util/host-rules'); -const ghGot = require('../../../../lib/platform/github/gh-got-wrapper'); const { getChangeLogJSON } = require('../../../../lib/workers/pr/changelog'); const releaseNotes = require('../../../../lib/workers/pr/changelog/release-notes'); diff --git a/yarn.lock b/yarn.lock index 5f1cdfd61d7e0e7ecc13f6f3de9e53ac71ff24b3..16d120f4b6f018960e8d57b846676593506ebf60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -344,6 +344,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-dynamic-import@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" + integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-json-strings@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" @@ -1235,6 +1242,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/semver@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.1.tgz#a984b405c702fa5a7ec6abc56b37f2ba35ef5af6" + integrity sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1677,7 +1689,7 @@ babel-jest@24.8.0, babel-jest@^24.8.0: chalk "^2.4.2" slash "^2.0.0" -babel-plugin-dynamic-import-node@^2.3.0: +babel-plugin-dynamic-import-node@2.3.0, babel-plugin-dynamic-import-node@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==