From 309f39539c5d918d8a47075587aa8720a9c127f7 Mon Sep 17 00:00:00 2001 From: Ian Lovell <102812633+ianlnf@users.noreply.github.com> Date: Thu, 26 May 2022 11:13:06 +0100 Subject: [PATCH] Merge pull request from GHSA-v5vr-h3xq-8v6w * fix: add pr commit author check * chore: update prettier settings * chore: update prettier settings * feat: add verify commit signatures * chore: revert prettier * test: add action test * refactor: remove commits fallback * test: add verify test * feat: add spawn gpg * Fixing PR comments: Moved import from func to global; Iterate with hwp; Added GPG information to ReadMe * Removing Key from files * Fixing node_modules * different approach to validating the commit. Checking that the commit is verified, the author is dependabot[bot], the committer is GitHub * Adding fixes to tests * Fix for the tests * eslint, naming and removal of magic text * chore: formatting * Refactored tests * Removed unecessary async function. Iterating through very small amounts of commits.Renamed file for consistency * removed unecessary dependency Co-authored-by: marceloFerreira90 Co-authored-by: marceloFerreira90 Co-authored-by: Simone Busoli --- dist/105.index.js | 315 ++++++++++++++++++++++++++++ dist/index.js | 95 ++++++++- src/action.js | 18 +- src/getDependabotDetails.js | 9 + src/github-client.js | 10 + src/verifyCommitSignatures.js | 42 ++++ test/action.test.js | 80 +++++++ test/github-client.test.js | 12 ++ test/verifyCommitSignatures.test.js | 218 +++++++++++++++++++ 9 files changed, 797 insertions(+), 2 deletions(-) create mode 100644 dist/105.index.js create mode 100644 src/getDependabotDetails.js create mode 100644 src/verifyCommitSignatures.js create mode 100644 test/verifyCommitSignatures.test.js diff --git a/dist/105.index.js b/dist/105.index.js new file mode 100644 index 00000000..040f5d07 --- /dev/null +++ b/dist/105.index.js @@ -0,0 +1,315 @@ +"use strict"; +exports.id = 105; +exports.ids = [105]; +exports.modules = { + +/***/ 8770: +/***/ ((module, __unused_webpack_exports, __webpack_require__) => { + + +const fs = __webpack_require__(7147); +const os = __webpack_require__(2037); + +const tempDirectorySymbol = Symbol.for('__RESOLVED_TEMP_DIRECTORY__'); + +if (!global[tempDirectorySymbol]) { + Object.defineProperty(global, tempDirectorySymbol, { + value: fs.realpathSync(os.tmpdir()) + }); +} + +module.exports = global[tempDirectorySymbol]; + + +/***/ }), + +/***/ 4105: +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + "rootTemporaryDirectory": () => (/* reexport */ temp_dir), + "temporaryDirectory": () => (/* binding */ temporaryDirectory), + "temporaryDirectoryTask": () => (/* binding */ temporaryDirectoryTask), + "temporaryFile": () => (/* binding */ temporaryFile), + "temporaryFileTask": () => (/* binding */ temporaryFileTask), + "temporaryWrite": () => (/* binding */ temporaryWrite), + "temporaryWriteSync": () => (/* binding */ temporaryWriteSync), + "temporaryWriteTask": () => (/* binding */ temporaryWriteTask) +}); + +// EXTERNAL MODULE: external "node:fs" +var external_node_fs_ = __webpack_require__(7561); +// EXTERNAL MODULE: external "node:fs/promises" +var promises_ = __webpack_require__(3977); +// EXTERNAL MODULE: external "node:path" +var external_node_path_ = __webpack_require__(9411); +// EXTERNAL MODULE: external "node:stream" +var external_node_stream_ = __webpack_require__(4492); +// EXTERNAL MODULE: external "node:util" +var external_node_util_ = __webpack_require__(7261); +// EXTERNAL MODULE: external "util" +var external_util_ = __webpack_require__(3837); +// EXTERNAL MODULE: external "crypto" +var external_crypto_ = __webpack_require__(6113); +;// CONCATENATED MODULE: ./node_modules/crypto-random-string/index.js + + + +const randomBytesAsync = (0,external_util_.promisify)(external_crypto_.randomBytes); + +const urlSafeCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split(''); +const numericCharacters = '0123456789'.split(''); +const distinguishableCharacters = 'CDEHKMPRTUWXY012458'.split(''); +const asciiPrintableCharacters = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'.split(''); +const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'.split(''); + +const generateForCustomCharacters = (length, characters) => { + // Generating entropy is faster than complex math operations, so we use the simplest way + const characterCount = characters.length; + const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division + const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low + let string = ''; + let stringLength = 0; + + while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it + const entropy = external_crypto_.randomBytes(entropyLength); + let entropyPosition = 0; + + while (entropyPosition < entropyLength && stringLength < length) { + const entropyValue = entropy.readUInt16LE(entropyPosition); + entropyPosition += 2; + if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division + continue; + } + + string += characters[entropyValue % characterCount]; + stringLength++; + } + } + + return string; +}; + +const generateForCustomCharactersAsync = async (length, characters) => { + // Generating entropy is faster than complex math operations, so we use the simplest way + const characterCount = characters.length; + const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division + const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low + let string = ''; + let stringLength = 0; + + while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it + const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop + let entropyPosition = 0; + + while (entropyPosition < entropyLength && stringLength < length) { + const entropyValue = entropy.readUInt16LE(entropyPosition); + entropyPosition += 2; + if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division + continue; + } + + string += characters[entropyValue % characterCount]; + stringLength++; + } + } + + return string; +}; + +const generateRandomBytes = (byteLength, type, length) => external_crypto_.randomBytes(byteLength).toString(type).slice(0, length); + +const generateRandomBytesAsync = async (byteLength, type, length) => { + const buffer = await randomBytesAsync(byteLength); + return buffer.toString(type).slice(0, length); +}; + +const allowedTypes = new Set([ + undefined, + 'hex', + 'base64', + 'url-safe', + 'numeric', + 'distinguishable', + 'ascii-printable', + 'alphanumeric' +]); + +const createGenerator = (generateForCustomCharacters, generateRandomBytes) => ({length, type, characters}) => { + if (!(length >= 0 && Number.isFinite(length))) { + throw new TypeError('Expected a `length` to be a non-negative finite number'); + } + + if (type !== undefined && characters !== undefined) { + throw new TypeError('Expected either `type` or `characters`'); + } + + if (characters !== undefined && typeof characters !== 'string') { + throw new TypeError('Expected `characters` to be string'); + } + + if (!allowedTypes.has(type)) { + throw new TypeError(`Unknown type: ${type}`); + } + + if (type === undefined && characters === undefined) { + type = 'hex'; + } + + if (type === 'hex' || (type === undefined && characters === undefined)) { + return generateRandomBytes(Math.ceil(length * 0.5), 'hex', length); // Need 0.5 byte entropy per character + } + + if (type === 'base64') { + return generateRandomBytes(Math.ceil(length * 0.75), 'base64', length); // Need 0.75 byte of entropy per character + } + + if (type === 'url-safe') { + return generateForCustomCharacters(length, urlSafeCharacters); + } + + if (type === 'numeric') { + return generateForCustomCharacters(length, numericCharacters); + } + + if (type === 'distinguishable') { + return generateForCustomCharacters(length, distinguishableCharacters); + } + + if (type === 'ascii-printable') { + return generateForCustomCharacters(length, asciiPrintableCharacters); + } + + if (type === 'alphanumeric') { + return generateForCustomCharacters(length, alphanumericCharacters); + } + + if (characters.length === 0) { + throw new TypeError('Expected `characters` string length to be greater than or equal to 1'); + } + + if (characters.length > 0x10000) { + throw new TypeError('Expected `characters` string length to be less or equal to 65536'); + } + + return generateForCustomCharacters(length, characters.split('')); +}; + +const cryptoRandomString = createGenerator(generateForCustomCharacters, generateRandomBytes); + +cryptoRandomString.async = createGenerator(generateForCustomCharactersAsync, generateRandomBytesAsync); + +/* harmony default export */ const crypto_random_string = (cryptoRandomString); + +;// CONCATENATED MODULE: ./node_modules/unique-string/index.js + + +function uniqueString() { + return crypto_random_string({length: 32}); +} + +// EXTERNAL MODULE: ./node_modules/temp-dir/index.js +var temp_dir = __webpack_require__(8770); +;// CONCATENATED MODULE: ./node_modules/tempy/node_modules/is-stream/index.js +function isStream(stream) { + return stream !== null + && typeof stream === 'object' + && typeof stream.pipe === 'function'; +} + +function isWritableStream(stream) { + return isStream(stream) + && stream.writable !== false + && typeof stream._write === 'function' + && typeof stream._writableState === 'object'; +} + +function isReadableStream(stream) { + return isStream(stream) + && stream.readable !== false + && typeof stream._read === 'function' + && typeof stream._readableState === 'object'; +} + +function isDuplexStream(stream) { + return isWritableStream(stream) + && isReadableStream(stream); +} + +function isTransformStream(stream) { + return isDuplexStream(stream) + && typeof stream._transform === 'function'; +} + +;// CONCATENATED MODULE: ./node_modules/tempy/index.js + + + + + + + + + +const pipeline = (0,external_node_util_.promisify)(external_node_stream_.pipeline); // TODO: Use `node:stream/promises` when targeting Node.js 16. + +const getPath = (prefix = '') => external_node_path_.join(temp_dir, prefix + uniqueString()); + +const writeStream = async (filePath, data) => pipeline(data, external_node_fs_.createWriteStream(filePath)); + +async function runTask(temporaryPath, callback) { + try { + return await callback(temporaryPath); + } finally { + await promises_.rm(temporaryPath, {recursive: true, force: true}); + } +} + +function temporaryFile({name, extension} = {}) { + if (name) { + if (extension !== undefined && extension !== null) { + throw new Error('The `name` and `extension` options are mutually exclusive'); + } + + return external_node_path_.join(temporaryDirectory(), name); + } + + return getPath() + (extension === undefined || extension === null ? '' : '.' + extension.replace(/^\./, '')); +} + +const temporaryFileTask = async (callback, options) => runTask(temporaryFile(options), callback); + +function temporaryDirectory({prefix = ''} = {}) { + const directory = getPath(prefix); + external_node_fs_.mkdirSync(directory); + return directory; +} + +const temporaryDirectoryTask = async (callback, options) => runTask(temporaryDirectory(options), callback); + +async function temporaryWrite(fileContent, options) { + const filename = temporaryFile(options); + const write = isStream(fileContent) ? writeStream : promises_.writeFile; + await write(filename, fileContent); + return filename; +} + +const temporaryWriteTask = async (fileContent, callback, options) => runTask(await temporaryWrite(fileContent, options), callback); + +function temporaryWriteSync(fileContent, options) { + const filename = temporaryFile(options); + external_node_fs_.writeFileSync(filename, fileContent); + return filename; +} + + + + +/***/ }) + +}; +; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 000d5256..650b9ee5 100644 --- a/dist/index.js +++ b/dist/index.js @@ -9701,6 +9701,8 @@ const { getModuleVersionChanges, checkModuleVersionChanges, } = __nccwpck_require__(9488) +const { verifyCommits } = __nccwpck_require__(3094) +const { dependabotAuthor } = __nccwpck_require__(500) const { GITHUB_TOKEN, @@ -9728,11 +9730,25 @@ module.exports = async function run() { const pr = pull_request || (await client.getPullRequest(PR_NUMBER)) - const isDependabotPR = pr.user.login === 'dependabot[bot]' + const isDependabotPR = pr.user.login === dependabotAuthor if (!isDependabotPR) { return logWarning('Not a dependabot PR, skipping.') } + const commits = await client.getPullRequestCommits(pr.number) + + if (!commits.every(commit => commit.author.login === dependabotAuthor)) { + return logWarning('PR contains non dependabot commits, skipping.') + } + + try { + await verifyCommits(commits) + } catch { + return logWarning( + 'PR contains invalid dependabot commit signatures, skipping.' + ) + } + const prDiff = await client.getPullRequestDiff(pr.number) // Get changed modules from diff if available or from PR title as fallback @@ -9814,6 +9830,23 @@ function parsePrTitle(pullRequest) { } +/***/ }), + +/***/ 500: +/***/ ((module) => { + +"use strict"; + +const dependabotAuthor = 'dependabot[bot]' + +const dependabotCommitter = 'GitHub' + +module.exports = { + dependabotAuthor, + dependabotCommitter, +} + + /***/ }), /***/ 5013: @@ -9913,6 +9946,16 @@ function githubClient(githubToken) { }) return pullRequest }, + + async getPullRequestCommits(pullRequestNumber) { + const { data } = await octokit.rest.pulls.listCommits({ + owner, + repo: repoName, + pull_number: pullRequestNumber, + }) + + return data + }, } } @@ -10125,6 +10168,56 @@ exports.isValidSemver = function (version) { } +/***/ }), + +/***/ 3094: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + dependabotAuthor, + dependabotCommitter, +} = __nccwpck_require__(500) + +function verifyCommits(commits) { + commits.forEach(function (commit) { + const { + commit: { + verification: { verified }, + committer, + author, + }, + sha, + } = commit + verifyCommitSignatureCommitterAndAuthor(sha, author, committer, verified) + }) +} + +function verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified +) { + if ( + !verified || + committer.name !== dependabotCommitter || + author.name !== dependabotAuthor + ) { + throw new Error( + `Signature for commit ${sha} could not be verified - Not a dependabot commit` + ) + } +} + +module.exports = { + verifyCommits, + verifyCommitSignatureCommitterAndAuthor, +} + + /***/ }), /***/ 2877: diff --git a/src/action.js b/src/action.js index 17a5d566..ef2ef113 100644 --- a/src/action.js +++ b/src/action.js @@ -20,6 +20,8 @@ const { getModuleVersionChanges, checkModuleVersionChanges, } = require('./moduleVersionChanges') +const { verifyCommits } = require('./verifyCommitSignatures') +const { dependabotAuthor } = require('./getDependabotDetails') const { GITHUB_TOKEN, @@ -47,11 +49,25 @@ module.exports = async function run() { const pr = pull_request || (await client.getPullRequest(PR_NUMBER)) - const isDependabotPR = pr.user.login === 'dependabot[bot]' + const isDependabotPR = pr.user.login === dependabotAuthor if (!isDependabotPR) { return logWarning('Not a dependabot PR, skipping.') } + const commits = await client.getPullRequestCommits(pr.number) + + if (!commits.every(commit => commit.author.login === dependabotAuthor)) { + return logWarning('PR contains non dependabot commits, skipping.') + } + + try { + await verifyCommits(commits) + } catch { + return logWarning( + 'PR contains invalid dependabot commit signatures, skipping.' + ) + } + const prDiff = await client.getPullRequestDiff(pr.number) // Get changed modules from diff if available or from PR title as fallback diff --git a/src/getDependabotDetails.js b/src/getDependabotDetails.js new file mode 100644 index 00000000..3d1b2791 --- /dev/null +++ b/src/getDependabotDetails.js @@ -0,0 +1,9 @@ +'use strict' +const dependabotAuthor = 'dependabot[bot]' + +const dependabotCommitter = 'GitHub' + +module.exports = { + dependabotAuthor, + dependabotCommitter, +} diff --git a/src/github-client.js b/src/github-client.js index 9b0d37da..23a53222 100644 --- a/src/github-client.js +++ b/src/github-client.js @@ -54,6 +54,16 @@ function githubClient(githubToken) { }) return pullRequest }, + + async getPullRequestCommits(pullRequestNumber) { + const { data } = await octokit.rest.pulls.listCommits({ + owner, + repo: repoName, + pull_number: pullRequestNumber, + }) + + return data + }, } } diff --git a/src/verifyCommitSignatures.js b/src/verifyCommitSignatures.js new file mode 100644 index 00000000..2d1478bf --- /dev/null +++ b/src/verifyCommitSignatures.js @@ -0,0 +1,42 @@ +'use strict' + +const { + dependabotAuthor, + dependabotCommitter, +} = require('./getDependabotDetails') + +function verifyCommits(commits) { + commits.forEach(function (commit) { + const { + commit: { + verification: { verified }, + committer, + author, + }, + sha, + } = commit + verifyCommitSignatureCommitterAndAuthor(sha, author, committer, verified) + }) +} + +function verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified +) { + if ( + !verified || + committer.name !== dependabotCommitter || + author.name !== dependabotAuthor + ) { + throw new Error( + `Signature for commit ${sha} could not be verified - Not a dependabot commit` + ) + } +} + +module.exports = { + verifyCommits, + verifyCommitSignatureCommitterAndAuthor, +} diff --git a/test/action.test.js b/test/action.test.js index 021758ec..90bed4ae 100644 --- a/test/action.test.js +++ b/test/action.test.js @@ -13,6 +13,7 @@ const { diffs } = require('./moduleChanges') const actionLog = require('../src/log') const actionUtil = require('../src/util') const actionGithubClient = require('../src/github-client') +const verifyCommits = require('../src/verifyCommitSignatures') const GITHUB_TOKEN = 'the-token' const BOT_NAME = 'dependabot[bot]' @@ -34,15 +35,22 @@ function buildStubbedAction({ payload, inputs }) { const prStub = sinon.stub() const prDiffStub = sinon.stub() + const prCommitsStub = sinon.stub() const approveStub = sinon.stub() const mergeStub = sinon.stub() + const clientStub = sinon.stub(actionGithubClient, 'githubClient').returns({ getPullRequest: prStub.resolves(), approvePullRequest: approveStub.resolves(), mergePullRequest: mergeStub.resolves(), getPullRequestDiff: prDiffStub.resolves(), + getPullRequestCommits: prCommitsStub.resolves([]), }) + const verifyCommitsStub = sinon + .stub(verifyCommits, 'verifyCommits') + .returns(Promise.resolve()) + const action = proxyquire('../src/action', { '@actions/core': coreStub, 'actions-toolkit': toolkitStub, @@ -63,6 +71,8 @@ function buildStubbedAction({ payload, inputs }) { approveStub, mergeStub, prDiffStub, + prCommitsStub, + verifyCommitsStub, }, } } @@ -124,6 +134,76 @@ tap.test('should skip non-dependabot PR', async () => { sinon.assert.notCalled(stubs.mergeStub) }) +tap.test('should skip PR with non dependabot commit', async () => { + const PR_NUMBER = Math.random() + const { action, stubs } = buildStubbedAction({ + payload: { + pull_request: { + user: { + login: BOT_NAME, + }, + number: PR_NUMBER, + }, + }, + inputs: { PR_NUMBER }, + }) + + stubs.prCommitsStub.resolves([ + { + author: { + login: 'not dependabot', + }, + }, + ]) + + await action() + + sinon.assert.calledOnce(stubs.prCommitsStub) + sinon.assert.calledWithExactly( + stubs.logStub.logWarning, + 'PR contains non dependabot commits, skipping.' + ) + sinon.assert.notCalled(stubs.approveStub) + sinon.assert.notCalled(stubs.mergeStub) +}) + +tap.test( + 'should skip PR if dependabot commit signatures cannot be verified', + async () => { + const PR_NUMBER = Math.random() + const { action, stubs } = buildStubbedAction({ + payload: { + pull_request: { + user: { + login: BOT_NAME, + }, + number: PR_NUMBER, + }, + }, + inputs: { PR_NUMBER }, + }) + + stubs.prCommitsStub.resolves([ + { + author: { + login: 'dependabot[bot]', + }, + }, + ]) + + stubs.verifyCommitsStub.rejects() + + await action() + + sinon.assert.calledWithExactly( + stubs.logStub.logWarning, + 'PR contains invalid dependabot commit signatures, skipping.' + ) + sinon.assert.notCalled(stubs.approveStub) + sinon.assert.notCalled(stubs.mergeStub) + } +) + tap.test('should process dependabot PR and skip PR not in target', async () => { const PR_NUMBER = Math.random() const { action, stubs } = buildStubbedAction({ diff --git a/test/github-client.test.js b/test/github-client.test.js index 00962c5f..47a097be 100644 --- a/test/github-client.test.js +++ b/test/github-client.test.js @@ -18,6 +18,7 @@ const octokitStubs = { get: sinon.stub().returns(Promise.resolve({ data })), createReview: sinon.stub().returns(Promise.resolve({ data })), merge: sinon.stub().returns(Promise.resolve({ data })), + listCommits: sinon.stub().returns(Promise.resolve({ data })), } const { githubClient } = tap.mock('../src/github-client', { @@ -95,4 +96,15 @@ tap.test('githubClient', async t => { }, }) }) + + t.test('getPullRequestCommits', async () => { + const result = await githubClient(TOKEN).getPullRequestCommits(PR_NUMBER) + tap.equal(result, data) + + sinon.assert.calledWith(octokitStubs.listCommits, { + owner: githubContext.repository.owner.login, + repo: githubContext.repository.name, + pull_number: PR_NUMBER, + }) + }) }) diff --git a/test/verifyCommitSignatures.test.js b/test/verifyCommitSignatures.test.js new file mode 100644 index 00000000..4e57b66a --- /dev/null +++ b/test/verifyCommitSignatures.test.js @@ -0,0 +1,218 @@ +'use strict' + +const tap = require('tap') + +const { + dependabotAuthor, + dependabotCommitter, +} = require('../src/getDependabotDetails') +const { + verifyCommits, + verifyCommitSignatureCommitterAndAuthor, +} = require('../src/verifyCommitSignatures') + +const ValidAuthorValidCommitterVerifiedCommitMock = { + commit: { + author: { name: dependabotAuthor }, + committer: { name: dependabotCommitter }, + verification: { verified: true }, + }, + sha: 'sha', +} +const ValidAuthorValidCommitterUnverifiedCommitMock = { + commit: { + author: { name: dependabotAuthor }, + committer: { name: dependabotCommitter }, + verification: { verified: false }, + }, + sha: 'sha', +} +const InvalidAuthorValidCommitterVerifiedCommitMock = { + commit: { + author: { name: 'testUser' }, + committer: { name: dependabotCommitter }, + verification: { verified: true }, + }, + sha: 'sha', +} +const ValidAuthorInvalidCommitterVerifiedCommitMock = { + commit: { + author: { name: dependabotAuthor }, + committer: { name: 'testUser' }, + verification: { verified: true }, + }, + sha: 'sha', +} +const InvalidAuthorInvalidCommitterUnverifiedCommitMock = { + commit: { + author: { name: 'testUser' }, + committer: { name: 'testUser' }, + verification: { verified: false }, + }, + sha: 'sha', +} + +const prCommitsValid = [ + ValidAuthorValidCommitterVerifiedCommitMock, + ValidAuthorValidCommitterVerifiedCommitMock, +] +const prCommitsInvalid = [ + InvalidAuthorValidCommitterVerifiedCommitMock, + InvalidAuthorValidCommitterVerifiedCommitMock, +] +const prCommitsValidAndInvalid = [ + ValidAuthorValidCommitterVerifiedCommitMock, + InvalidAuthorValidCommitterVerifiedCommitMock, +] + +tap.test('verifyCommitSignatureCommitterAndAuthor', async t => { + t.test('shoud throw error when', async t => { + t.test('the verified flag is set to false', async t => { + const { + commit: { + author, + committer, + verification: { verified }, + }, + sha, + } = ValidAuthorValidCommitterUnverifiedCommitMock + t.throws( + () => + verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified + ), + new Error( + `Signature for commit ${sha} could not be verified - Not a dependabot commit` + ) + ) + }) + + t.test('the committer name is not GitHub', async t => { + const { + commit: { + author, + committer, + verification: { verified }, + }, + sha, + } = ValidAuthorInvalidCommitterVerifiedCommitMock + t.throws( + () => + verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified + ), + new Error( + `Signature for commit ${sha} could not be verified - Not a dependabot commit` + ) + ) + }) + + t.test('the authot name is not dependabot[bot]', async t => { + const { + commit: { + author, + committer, + verification: { verified }, + }, + sha, + } = InvalidAuthorValidCommitterVerifiedCommitMock + t.throws( + () => + verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified + ), + new Error( + `Signature for commit ${sha} could not be verified - Not a dependabot commit` + ) + ) + }) + + t.test( + 'the committer name is not Github, the author is not dependabot[bot] and is not verified', + async t => { + const { + commit: { + author, + committer, + verification: { verified }, + }, + sha, + } = InvalidAuthorInvalidCommitterUnverifiedCommitMock + t.throws( + () => + verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified + ), + new Error( + `Signature for commit ${sha} could not be verified - Not a dependabot commit` + ) + ) + } + ) + }) + + t.test('should not throw an error when', async t => { + t.test( + 'the committer name is Github, the author is dependabot[bot] and is verified', + async t => { + const { + commit: { + author, + committer, + verification: { verified }, + }, + sha, + } = ValidAuthorValidCommitterVerifiedCommitMock + t.doesNotThrow( + () => + verifyCommitSignatureCommitterAndAuthor( + sha, + author, + committer, + verified + ), + {} + ) + } + ) + }) +}) + +tap.test('VerifyCommits', async t => { + t.test('Should throw error when', async t => { + t.test('At least one commit does not match the requirements', async t => { + t.throws( + () => verifyCommits(prCommitsValidAndInvalid), + new Error( + `Signature for commit sha could not be verified - Not a dependabot commit` + ) + ) + }) + + t.test('At least one commit does not match the requirements', async t => { + t.throws( + () => verifyCommits(prCommitsInvalid), + new Error( + `Signature for commit sha could not be verified - Not a dependabot commit` + ) + ) + }) + }) + t.test('Should not throw error when', async t => { + t.test('All commits match the requirements', async t => { + t.doesNotThrow(() => verifyCommits(prCommitsValid), {}) + }) + }) +})