diff --git a/.vscode/settings.json b/.vscode/settings.json index 90d173613..dba4a2d94 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "source.fixAll.eslint": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true + "editor.formatOnSave": true, + "cSpell.words": ["Deltafied"] } diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js index 142a58a33..063386bf1 100755 --- a/packages/git-proxy-cli/index.js +++ b/packages/git-proxy-cli/index.js @@ -176,7 +176,8 @@ async function authoriseGitPush(id) { if (error.response) { switch (error.response.status) { case 401: - errorMessage = 'Error: Authorise: Authentication required'; + errorMessage = + 'Error: Authorise: Authentication required (401): ' + error?.response?.data?.message; process.exitCode = 3; break; case 404: @@ -223,7 +224,8 @@ async function rejectGitPush(id) { if (error.response) { switch (error.response.status) { case 401: - errorMessage = 'Error: Reject: Authentication required'; + errorMessage = + 'Error: Reject: Authentication required (401): ' + error?.response?.data?.message; process.exitCode = 3; break; case 404: @@ -270,7 +272,8 @@ async function cancelGitPush(id) { if (error.response) { switch (error.response.status) { case 401: - errorMessage = 'Error: Cancel: Authentication required'; + errorMessage = + 'Error: Cancel: Authentication required (401): ' + error?.response?.data?.message; process.exitCode = 3; break; case 404: diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js index fbfce0fe3..70a445cbe 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -22,6 +22,11 @@ const TEST_REPO_CONFIG = { url: 'https://github.com/finos/git-proxy-test.git', }; const TEST_REPO = 'finos/git-proxy-test.git'; +// user for test cases +const TEST_USER = 'testuser'; +const TEST_PASSWORD = 'testpassword'; +const TEST_EMAIL = 'jane.doe@email.com'; +const TEST_GIT_ACCOUNT = 'testGitAccount'; describe('test git-proxy-cli', function () { // *** help *** @@ -87,16 +92,12 @@ describe('test git-proxy-cli', function () { // *** login *** describe('test git-proxy-cli :: login', function () { - const testUser = 'testuser'; - const testPassword = 'testpassword'; - const testEmail = 'jane.doe@email.com'; - before(async function () { - await helper.addUserToDb(testUser, testPassword, testEmail, 'testGitAccount'); + await helper.addUserToDb(TEST_USER, TEST_PASSWORD, TEST_EMAIL, TEST_GIT_ACCOUNT); }); after(async function () { - await helper.removeUserFromDb(testUser); + await helper.removeUserFromDb(TEST_USER); }); it('login should fail when server is down', async function () { @@ -140,9 +141,9 @@ describe('test git-proxy-cli', function () { }); it('login shoud be successful with valid credentials (non-admin)', async function () { - const cli = `npx -- @finos/git-proxy-cli login --username ${testUser} --password ${testPassword}`; + const cli = `npx -- @finos/git-proxy-cli login --username ${TEST_USER} --password ${TEST_PASSWORD}`; const expectedExitCode = 0; - const expectedMessages = [`Login "${testUser}" <${testEmail}>: OK`]; + const expectedMessages = [`Login "${TEST_USER}" <${TEST_EMAIL}>: OK`]; const expectedErrorMessages = null; try { await helper.startServer(service); @@ -219,11 +220,13 @@ describe('test git-proxy-cli', function () { before(async function () { await helper.addRepoToDb(TEST_REPO_CONFIG); - await helper.addGitPushToDb(pushId, TEST_REPO); + await helper.addUserToDb(TEST_USER, TEST_PASSWORD, TEST_EMAIL, TEST_GIT_ACCOUNT); + await helper.addGitPushToDb(pushId, TEST_USER, TEST_EMAIL, TEST_REPO); }); after(async function () { await helper.removeGitPushFromDb(pushId); + await helper.removeUserFromDb(TEST_USER); await helper.removeRepoFromDb(TEST_REPO_CONFIG.name); }); @@ -294,11 +297,13 @@ describe('test git-proxy-cli', function () { before(async function () { await helper.addRepoToDb(TEST_REPO_CONFIG); - await helper.addGitPushToDb(pushId, TEST_REPO); + await helper.addUserToDb(TEST_USER, TEST_PASSWORD, TEST_EMAIL, TEST_GIT_ACCOUNT); + await helper.addGitPushToDb(pushId, TEST_USER, TEST_EMAIL, TEST_REPO); }); after(async function () { await helper.removeGitPushFromDb(pushId); + await helper.removeUserFromDb(TEST_USER); await helper.removeRepoFromDb(TEST_REPO_CONFIG.name); }); @@ -415,11 +420,13 @@ describe('test git-proxy-cli', function () { before(async function () { await helper.addRepoToDb(TEST_REPO_CONFIG); - await helper.addGitPushToDb(pushId, TEST_REPO); + await helper.addUserToDb(TEST_USER, TEST_PASSWORD, TEST_EMAIL, TEST_GIT_ACCOUNT); + await helper.addGitPushToDb(pushId, TEST_USER, TEST_EMAIL, TEST_REPO); }); after(async function () { await helper.removeGitPushFromDb(pushId); + await helper.removeUserFromDb(TEST_USER); await helper.removeRepoFromDb(TEST_REPO_CONFIG.name); }); @@ -487,12 +494,17 @@ describe('test git-proxy-cli', function () { describe('test git-proxy-cli :: git push administration', function () { const pushId = `0000000000000000000000000000000000000000__${Date.now()}`; - const gitAccount = 'testGitAccount1'; before(async function () { await helper.addRepoToDb(TEST_REPO_CONFIG); - await helper.addUserToDb('testuser1', 'testpassword', 'test@email.com', gitAccount); - await helper.addGitPushToDb(pushId, TEST_REPO, gitAccount); + await helper.addUserToDb(TEST_USER, TEST_PASSWORD, TEST_EMAIL, TEST_GIT_ACCOUNT); + await helper.addGitPushToDb(pushId, TEST_REPO, TEST_USER, TEST_EMAIL); + }); + + after(async function () { + await helper.removeGitPushFromDb(pushId); + await helper.removeUserFromDb(TEST_USER); + await helper.removeRepoFromDb(TEST_REPO_CONFIG.name); }); after(async function () { diff --git a/packages/git-proxy-cli/test/testCliUtils.js b/packages/git-proxy-cli/test/testCliUtils.js index 557857619..b6a9b5be7 100644 --- a/packages/git-proxy-cli/test/testCliUtils.js +++ b/packages/git-proxy-cli/test/testCliUtils.js @@ -177,9 +177,10 @@ async function removeRepoFromDb(repoName) { * @param {string} id The ID of the git push. * @param {string} repo The repository of the git push. * @param {string} user The user who pushed the git push. + * @param {string} userEmail The email of the user who pushed the git push. * @param {boolean} debug Flag to enable logging for debugging. */ -async function addGitPushToDb(id, repo, user = null, debug = false) { +async function addGitPushToDb(id, repo, user = null, userEmail = null, debug = false) { const action = new actions.Action( id, 'push', // type @@ -188,6 +189,7 @@ async function addGitPushToDb(id, repo, user = null, debug = false) { repo, ); action.user = user; + action.userEmail = userEmail; const step = new steps.Step( 'authBlock', // stepName false, // error diff --git a/src/proxy/actions/Action.ts b/src/proxy/actions/Action.ts index e4e7ce2f6..1be3030eb 100644 --- a/src/proxy/actions/Action.ts +++ b/src/proxy/actions/Action.ts @@ -7,6 +7,7 @@ import { Step } from './Step'; export interface Commit { message: string; committer: string; + committerEmail: string; tree: string; parent: string; author: string; @@ -45,6 +46,7 @@ class Action { message?: string; author?: string; user?: string; + userEmail?: string; attestation?: string; lastStep?: Step; proxyGitPath?: string; diff --git a/src/proxy/processors/push-action/checkUserPushPermission.ts b/src/proxy/processors/push-action/checkUserPushPermission.ts index 144eb53d3..e6ebe1ca0 100644 --- a/src/proxy/processors/push-action/checkUserPushPermission.ts +++ b/src/proxy/processors/push-action/checkUserPushPermission.ts @@ -5,28 +5,29 @@ import { trimTrailingDotGit } from '../../../db/helper'; // Execute if the repo is approved const exec = async (req: any, action: Action): Promise => { const step = new Step('checkUserPushPermission'); - const user = action.user; + const userEmail = action.userEmail; - if (!user) { + if (!userEmail) { step.setError('Push blocked: User not found. Please contact an administrator for support.'); action.addStep(step); step.error = true; return action; } - return await validateUser(user, action, step); + return await validateUser(userEmail, action, step); }; /** * Helper that validates the user's push permission. * This can be used by other actions that need it. - * @param {string} user The user to validate + * @param {string} userEmail The user to validate * @param {Action} action The action object * @param {Step} step The step object * @return {Promise} The action object */ -const validateUser = async (user: string, action: Action, step: Step): Promise => { +const validateUser = async (userEmail: string, action: Action, step: Step): Promise => { const repoSplit = trimTrailingDotGit(action.repo.toLowerCase()).split('/'); + // we expect there to be exactly one / separating org/repoName if (repoSplit.length != 2) { step.setError('Server-side issue extracting repoName'); @@ -37,35 +38,44 @@ const validateUser = async (user: string, action: Action, step: Step): Promise 1) { + console.error(`Multiple users found with email address ${userEmail}, ending`); + step.error = true; + step.log( + `Multiple Users have email <${userEmail}> so we cannot uniquely identify the user, ending`, + ); - if (list.length == 1) { - user = list[0].username; - isUserAllowed = await isUserPushAllowed(repoName, user); + step.setError( + `Your push has been blocked (there are multiple users with email ${action.userEmail})`, + ); + action.addStep(step); + return action; + } else if (list.length == 0) { + console.error(`No user with email address ${userEmail} found`); + } else { + isUserAllowed = await isUserPushAllowed(repoName, list[0].username); } - console.log(`User ${user} permission on Repo ${repoName} : ${isUserAllowed}`); + console.log(`User ${userEmail} permission on Repo ${repoName} : ${isUserAllowed}`); if (!isUserAllowed) { console.log('User not allowed to Push'); step.error = true; - step.log(`User ${user} is not allowed to push on repo ${action.repo}, ending`); - - console.log('setting error'); + step.log(`User ${userEmail} is not allowed to push on repo ${action.repo}, ending`); step.setError( - `Rejecting push as user ${action.user} ` + + `Your push has been blocked (${action.userEmail} ` + `is not allowed to push on repo ` + - `${action.repo}`, + `${action.repo})`, ); action.addStep(step); return action; } - step.log(`User ${user} is allowed to push on repo ${action.repo}`); + step.log(`User ${userEmail} is allowed to push on repo ${action.repo}`); action.addStep(step); return action; }; diff --git a/src/proxy/processors/push-action/parsePush.ts b/src/proxy/processors/push-action/parsePush.ts index fe7559a25..86fa3ddcd 100644 --- a/src/proxy/processors/push-action/parsePush.ts +++ b/src/proxy/processors/push-action/parsePush.ts @@ -3,13 +3,7 @@ import zlib from 'zlib'; import fs from 'fs'; import lod from 'lodash'; -import { - CommitContent, - CommitData, - CommitHeader, - PackMeta, - PersonLine, -} from '../types'; +import { CommitContent, CommitData, CommitHeader, PackMeta, PersonLine } from '../types'; import { BRANCH_PREFIX, EMPTY_COMMIT_HASH, @@ -36,10 +30,7 @@ async function exec(req: any, action: Action): Promise { const step = new Step('parsePackFile'); try { if (!req.body || req.body.length === 0) { - step.log('No data received in request body.'); - step.setError('Your push has been blocked. No data received in request body.'); - action.addStep(step); - return action; + throw new Error('No body found in request'); } const [packetLines, packDataOffset] = parsePacketLines(req.body); const refUpdates = packetLines.filter((line) => line.includes(BRANCH_PREFIX)); @@ -47,22 +38,17 @@ async function exec(req: any, action: Action): Promise { if (refUpdates.length !== 1) { step.log('Invalid number of branch updates.'); step.log(`Expected 1, but got ${refUpdates.length}`); - step.setError( + throw new Error( 'Your push has been blocked. Please make sure you are pushing to a single branch.', ); - action.addStep(step); - return action; } - const [commitParts, capParts] = refUpdates[0].split('\0'); + const [commitParts] = refUpdates[0].split('\0'); const parts = commitParts.split(' '); - console.log({ commitParts, capParts, parts }); if (parts.length !== 3) { step.log('Invalid number of parts in ref update.'); step.log(`Expected 3, but got ${parts.length}`); - step.setError('Your push has been blocked. Invalid ref update format.'); - action.addStep(step); - return action; + throw new Error('Your push has been blocked. Invalid ref update format.'); } const [oldCommit, newCommit, ref] = parts; @@ -75,9 +61,7 @@ async function exec(req: any, action: Action): Promise { // Check if the offset is valid and if there's data after it if (packDataOffset >= req.body.length) { step.log('No PACK data found after packet lines.'); - step.setError('Your push has been blocked. PACK data is missing.'); - action.addStep(step); - return action; + throw new Error('Your push has been blocked. PACK data is missing.'); } const buf = req.body.slice(packDataOffset); @@ -85,23 +69,25 @@ async function exec(req: any, action: Action): Promise { // Verify that data actually starts with PACK signature if (buf.length < PACKET_SIZE || buf.toString('utf8', 0, PACKET_SIZE) !== PACK_SIGNATURE) { step.log(`Expected PACK signature at offset ${packDataOffset}, but found something else.`); - step.setError('Your push has been blocked. Invalid PACK data structure.'); - action.addStep(step); - return action; + throw new Error('Your push has been blocked. Invalid PACK data structure.'); } const [meta, contentBuff] = getPackMeta(buf); const contents = getContents(contentBuff as any, meta.entries as number); action.commitData = getCommitData(contents as any); + if (action.commitData.length === 0) { step.log('No commit data found when parsing push.'); } else { if (action.commitFrom === EMPTY_COMMIT_HASH) { action.commitFrom = action.commitData[action.commitData.length - 1].parent; } - const user = action.commitData[action.commitData.length - 1].committer; - action.user = user; + + const { committer, committerEmail } = action.commitData[action.commitData.length - 1]; + console.log(`Push Request received from user ${committer} with email ${committerEmail}`); + action.user = committer; + action.userEmail = committerEmail; } step.content = { @@ -141,7 +127,7 @@ const parsePersonLine = (line: string): PersonLine => { * @return {CommitHeader} An object containing the parsed commit header. */ const getParsedData = (headerLines: string[]): CommitHeader => { - const parsedData: CommitHeader = { + const parsedData: CommitHeader = { parents: [], tree: '', author: { name: '', email: '', timestamp: '' }, @@ -206,7 +192,7 @@ const validateParsedData = (parsedData: CommitHeader): void => { if (missing.length > 0) { throw new Error(`Invalid commit data: Missing ${missing.join(', ')}`); } -} +}; /** * Checks if a person line is blank. @@ -215,18 +201,17 @@ const validateParsedData = (parsedData: CommitHeader): void => { */ const isBlankPersonLine = (personLine: PersonLine): boolean => { return personLine.name === '' && personLine.email === '' && personLine.timestamp === ''; -} +}; /** * Parses the commit data from the contents of a pack file. - * + * * Filters out all objects except for commits. * @param {CommitContent[]} contents - The contents of the pack file. * @return {CommitData[]} An array of commit data objects. * @see https://git-scm.com/docs/pack-format#_object_types */ const getCommitData = (contents: CommitContent[]): CommitData[] => { - console.log({ contents }); return lod .chain(contents) .filter({ type: GIT_OBJECT_TYPE_COMMIT }) @@ -266,9 +251,10 @@ const getCommitData = (contents: CommitContent[]): CommitData[] => { parent, author: author.name, committer: committer.name, - authorEmail: author.email, commitTimestamp: committer.timestamp, message, + authorEmail: author.email, + committerEmail: committer.email, }; }) .value(); @@ -349,7 +335,7 @@ const getContent = (item: number, buffer: Buffer): [CommitContent, Buffer] => { let size = [m.getBit(7), m.getBit(8), m.getBit(9), m.getBit(10)]; const type = getInt([m.getBit(4), m.getBit(5), m.getBit(6)]); - // Object IDs if this is a deltatfied blob + // Object IDs if this is a deltafied blob let objectRef: string | null = null; // If we have a more flag get the next @@ -373,10 +359,10 @@ const getContent = (item: number, buffer: Buffer): [CommitContent, Buffer] => { more = nextM.getBit(3); } - // NOTE Size is the unziped size, not the zipped size + // NOTE Size is the unzipped size, not the zipped size const intSize = getInt(size); - // Deltafied objectives have a 20 byte identifer + // Deltafied objectives have a 20 byte identifier if (type == 7 || type == 6) { objectRef = buffer.slice(0, 20).toString('hex'); buffer = buffer.slice(20); @@ -385,7 +371,7 @@ const getContent = (item: number, buffer: Buffer): [CommitContent, Buffer] => { const contentBuffer = buffer.slice(1); const [content, deflatedSize] = unpack(contentBuffer); - // NOTE Size is the unziped size, not the zipped size + // NOTE Size is the unzipped size, not the zipped size // so it's kind of useless for us in terms of reading the stream const result = { item: item, diff --git a/src/proxy/processors/push-action/pullRemote.ts b/src/proxy/processors/push-action/pullRemote.ts index 2f7c808a2..991a62ca9 100644 --- a/src/proxy/processors/push-action/pullRemote.ts +++ b/src/proxy/processors/push-action/pullRemote.ts @@ -22,7 +22,7 @@ const exec = async (req: any, action: Action): Promise => { } const cmd = `git clone ${action.url}`; - step.log(`Exectuting ${cmd}`); + step.log(`Executing ${cmd}`); const authHeader = req.headers?.authorization; const [username, password] = Buffer.from(authHeader.split(' ')[1], 'base64') diff --git a/src/proxy/processors/types.ts b/src/proxy/processors/types.ts index 38f989f8a..0c89b6d24 100644 --- a/src/proxy/processors/types.ts +++ b/src/proxy/processors/types.ts @@ -17,20 +17,20 @@ export type CommitContent = { deflatedSize: number; objectRef: any; content: string; -} +}; export type PersonLine = { name: string; email: string; timestamp: string; -} +}; export type CommitHeader = { tree: string; parents: string[]; author: PersonLine; committer: PersonLine; -} +}; export type CommitData = { tree: string; @@ -38,12 +38,13 @@ export type CommitData = { author: string; committer: string; authorEmail: string; + committerEmail: string; commitTimestamp: string; message: string; -} +}; export type PackMeta = { sig: string; version: number; entries: number; -} +}; diff --git a/src/service/passport/index.js b/src/service/passport/index.js index 72918282f..e1cc9e0b5 100644 --- a/src/service/passport/index.js +++ b/src/service/passport/index.js @@ -1,4 +1,4 @@ -const passport = require("passport"); +const passport = require('passport'); const local = require('./local'); const activeDirectory = require('./activeDirectory'); const oidc = require('./oidc'); @@ -19,12 +19,12 @@ const configure = async () => { for (const auth of authMethods) { const strategy = authStrategies[auth.type.toLowerCase()]; - if (strategy && typeof strategy.configure === "function") { + if (strategy && typeof strategy.configure === 'function') { await strategy.configure(passport); } } - if (authMethods.some(auth => auth.type.toLowerCase() === "local")) { + if (authMethods.some((auth) => auth.type.toLowerCase() === 'local')) { await local.createDefaultAdmin(); } diff --git a/src/service/passport/local.js b/src/service/passport/local.js index 579d47234..e453f2c41 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -1,8 +1,8 @@ -const bcrypt = require("bcryptjs"); -const LocalStrategy = require("passport-local").Strategy; -const db = require("../../db"); +const bcrypt = require('bcryptjs'); +const LocalStrategy = require('passport-local').Strategy; +const db = require('../../db'); -const type = "local"; +const type = 'local'; const configure = async (passport) => { passport.use( @@ -10,19 +10,19 @@ const configure = async (passport) => { try { const user = await db.findUser(username); if (!user) { - return done(null, false, { message: "Incorrect username." }); + return done(null, false, { message: 'Incorrect username.' }); } const passwordCorrect = await bcrypt.compare(password, user.password); if (!passwordCorrect) { - return done(null, false, { message: "Incorrect password." }); + return done(null, false, { message: 'Incorrect password.' }); } return done(null, user); } catch (err) { return done(err); } - }) + }), ); passport.serializeUser((user, done) => { @@ -45,9 +45,9 @@ const configure = async (passport) => { * Create the default admin user if it doesn't exist */ const createDefaultAdmin = async () => { - const admin = await db.findUser("admin"); + const admin = await db.findUser('admin'); if (!admin) { - await db.createUser("admin", "admin", "admin@place.com", "none", true); + await db.createUser('admin', 'admin', 'admin@place.com', 'none', true); } }; diff --git a/src/service/routes/push.js b/src/service/routes/push.js index 9750375ca..dd746a11f 100644 --- a/src/service/routes/push.js +++ b/src/service/routes/push.js @@ -36,20 +36,17 @@ router.get('/:id', async (req, res) => { router.post('/:id/reject', async (req, res) => { if (req.user) { const id = req.params.id; - console.log({ id }); // Get the push request const push = await db.getPush(id); - console.log({ push }); - // Get the Internal Author of the push via their Git Account name - const gitAccountauthor = push.user; - const list = await db.getUsers({ gitAccount: gitAccountauthor }); - console.log({ list }); + // Get the committer of the push via their email + const committerEmail = push.userEmail; + const list = await db.getUsers({ email: committerEmail }); if (list.length === 0) { res.status(401).send({ - message: `The git account ${gitAccountauthor} could not be found`, + message: `There was no registered user with the committer's email address: ${committerEmail}`, }); return; } @@ -86,6 +83,8 @@ router.post('/:id/authorise', async (req, res) => { const questions = req.body.params?.attestation; console.log({ questions }); + // TODO: compare attestation to configuration and ensure all questions are answered + // - we shouldn't go on the definition in the request! const attestationComplete = questions?.every((question) => !!question.checked); console.log({ attestationComplete }); @@ -97,14 +96,14 @@ router.post('/:id/authorise', async (req, res) => { const push = await db.getPush(id); console.log({ push }); - // Get the Internal Author of the push via their Git Account name - const gitAccountauthor = push.user; - const list = await db.getUsers({ gitAccount: gitAccountauthor }); + // Get the committer of the push via their email address + const committerEmail = push.userEmail; + const list = await db.getUsers({ email: committerEmail }); console.log({ list }); if (list.length === 0) { res.status(401).send({ - message: `The git account ${gitAccountauthor} could not be found`, + message: `There was no registered user with the committer's email address: ${committerEmail}`, }); return; } diff --git a/src/ui/components/RouteGuard/RouteGuard.tsx b/src/ui/components/RouteGuard/RouteGuard.tsx index a729b1660..4efb2c3c1 100644 --- a/src/ui/components/RouteGuard/RouteGuard.tsx +++ b/src/ui/components/RouteGuard/RouteGuard.tsx @@ -46,11 +46,11 @@ const RouteGuard = ({ component: Component, fullRoutePath }: RouteGuardProps) => } if (loginRequired && !user) { - return ; + return ; } if (adminOnly && !user?.admin) { - return ; + return ; } return ; diff --git a/src/ui/services/config.js b/src/ui/services/config.js index 5536e4a35..286aab9a0 100644 --- a/src/ui/services/config.js +++ b/src/ui/services/config.js @@ -32,9 +32,4 @@ const getUIRouteAuth = async (setData) => { }); }; -export { - getAttestationConfig, - getURLShortener, - getEmailContact, - getUIRouteAuth, -}; +export { getAttestationConfig, getURLShortener, getEmailContact, getUIRouteAuth }; diff --git a/src/ui/utils.tsx b/src/ui/utils.tsx index be15a19ef..16d08f083 100644 --- a/src/ui/utils.tsx +++ b/src/ui/utils.tsx @@ -5,13 +5,13 @@ */ export const getCookie = (name: string): string | null => { if (!document.cookie) return null; - + const cookies = document.cookie .split(';') .map((c) => c.trim()) .filter((c) => c.startsWith(name + '=')); - + if (!cookies.length) return null; - + return decodeURIComponent(cookies[0].split('=')[1]); -}; \ No newline at end of file +}; diff --git a/test/ConfigLoader.test.js b/test/ConfigLoader.test.js index ac408a2ed..f8bfde26d 100644 --- a/test/ConfigLoader.test.js +++ b/test/ConfigLoader.test.js @@ -38,6 +38,10 @@ describe('ConfigLoader', () => { }); }); + after(() => { + // restore default config + }); + describe('loadFromFile', () => { it('should load configuration from file', async () => { const testConfig = { @@ -251,7 +255,6 @@ describe('ConfigLoader', () => { configLoader = new ConfigLoader(mockConfig); configLoader.reloadTimer = setInterval(() => {}, 1000); await configLoader.start(); - expect(configLoader.reloadTimer).to.be.null; }); @@ -297,7 +300,6 @@ describe('ConfigLoader', () => { configLoader = new ConfigLoader(mockConfig); configLoader.reloadTimer = setInterval(() => {}, 1000); expect(configLoader.reloadTimer).to.not.be.null; - await configLoader.stop(); expect(configLoader.reloadTimer).to.be.null; }); diff --git a/test/processors/blockForAuth.test.js b/test/processors/blockForAuth.test.js index f566f1b2f..e242cb247 100644 --- a/test/processors/blockForAuth.test.js +++ b/test/processors/blockForAuth.test.js @@ -17,12 +17,12 @@ describe('blockForAuth', () => { beforeEach(() => { req = { protocol: 'https', - headers: { host: 'example.com' } + headers: { host: 'example.com' }, }; action = { id: 'push_123', - addStep: sinon.stub() + addStep: sinon.stub(), }; stepInstance = new Step('temp'); @@ -34,7 +34,7 @@ describe('blockForAuth', () => { const blockForAuth = proxyquire('../../src/proxy/processors/push-action/blockForAuth', { '../../../service/urls': { getServiceUIURL: getServiceUIURLStub }, - '../../actions': { Step: StepSpy } + '../../actions': { Step: StepSpy }, }); exec = blockForAuth.exec; @@ -45,7 +45,6 @@ describe('blockForAuth', () => { }); describe('exec', () => { - it('should generate a correct shareable URL', async () => { await exec(req, action); expect(getServiceUIURLStub.calledOnce).to.be.true; diff --git a/test/processors/checkAuthorEmails.test.js b/test/processors/checkAuthorEmails.test.js index 52d8ffc6e..849842704 100644 --- a/test/processors/checkAuthorEmails.test.js +++ b/test/processors/checkAuthorEmails.test.js @@ -4,7 +4,7 @@ const { expect } = require('chai'); describe('checkAuthorEmails', () => { let action; - let commitConfig + let commitConfig; let exec; let getCommitConfigStub; let stepSpy; @@ -25,9 +25,9 @@ describe('checkAuthorEmails', () => { author: { email: { domain: { allow: null }, - local: { block: null } - } - } + local: { block: null }, + }, + }, }; getCommitConfigStub = sinon.stub().returns(commitConfig); @@ -37,13 +37,16 @@ describe('checkAuthorEmails', () => { action.step = new StepStub(); Object.assign(action.step, step); return action.step; - }) + }), }; - const checkAuthorEmails = proxyquire('../../src/proxy/processors/push-action/checkAuthorEmails', { - '../../../config': { getCommitConfig: getCommitConfigStub }, - '../../actions': { Step: StepStub } - }); + const checkAuthorEmails = proxyquire( + '../../src/proxy/processors/push-action/checkAuthorEmails', + { + '../../../config': { getCommitConfig: getCommitConfigStub }, + '../../actions': { Step: StepStub }, + }, + ); exec = checkAuthorEmails.exec; }); @@ -56,7 +59,7 @@ describe('checkAuthorEmails', () => { it('should allow valid emails when no restrictions', async () => { action.commitData = [ { authorEmail: 'valid@example.com' }, - { authorEmail: 'another.valid@test.org' } + { authorEmail: 'another.valid@test.org' }, ]; await exec({}, action); @@ -68,47 +71,48 @@ describe('checkAuthorEmails', () => { commitConfig.author.email.domain.allow = 'example\\.com$'; action.commitData = [ { authorEmail: 'valid@example.com' }, - { authorEmail: 'invalid@forbidden.org' } + { authorEmail: 'invalid@forbidden.org' }, ]; await exec({}, action); expect(action.step.error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit author e-mails are illegal: invalid@forbidden.org' - )).to.be.true; - expect(StepStub.prototype.setError.calledWith( - 'Your push has been blocked. Please verify your Git configured e-mail address is valid (e.g. john.smith@example.com)' - )).to.be.true; + expect( + stepSpy.calledWith( + 'The following commit author e-mails are illegal: invalid@forbidden.org', + ), + ).to.be.true; + expect( + StepStub.prototype.setError.calledWith( + 'Your push has been blocked. Please verify your Git configured e-mail address is valid (e.g. john.smith@example.com)', + ), + ).to.be.true; }); it('should block emails with forbidden usernames', async () => { commitConfig.author.email.local.block = 'blocked'; action.commitData = [ { authorEmail: 'allowed@example.com' }, - { authorEmail: 'blocked.user@test.org' } + { authorEmail: 'blocked.user@test.org' }, ]; await exec({}, action); expect(action.step.error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit author e-mails are illegal: blocked.user@test.org' - )).to.be.true; + expect( + stepSpy.calledWith( + 'The following commit author e-mails are illegal: blocked.user@test.org', + ), + ).to.be.true; }); it('should handle empty email strings', async () => { - action.commitData = [ - { authorEmail: '' }, - { authorEmail: 'valid@example.com' } - ]; + action.commitData = [{ authorEmail: '' }, { authorEmail: 'valid@example.com' }]; await exec({}, action); expect(action.step.error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit author e-mails are illegal: ' - )).to.be.true; + expect(stepSpy.calledWith('The following commit author e-mails are illegal: ')).to.be.true; }); it('should allow emails when both checks pass', async () => { @@ -116,7 +120,7 @@ describe('checkAuthorEmails', () => { commitConfig.author.email.local.block = 'forbidden'; action.commitData = [ { authorEmail: 'allowed@example.com' }, - { authorEmail: 'also.allowed@example.com' } + { authorEmail: 'also.allowed@example.com' }, ]; await exec({}, action); @@ -127,29 +131,24 @@ describe('checkAuthorEmails', () => { it('should block emails that fail both checks', async () => { commitConfig.author.email.domain.allow = 'example\\.com$'; commitConfig.author.email.local.block = 'forbidden'; - action.commitData = [ - { authorEmail: 'forbidden@wrong.org' } - ]; + action.commitData = [{ authorEmail: 'forbidden@wrong.org' }]; await exec({}, action); expect(action.step.error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit author e-mails are illegal: forbidden@wrong.org' - )).to.be.true; + expect( + stepSpy.calledWith('The following commit author e-mails are illegal: forbidden@wrong.org'), + ).to.be.true; }); it('should handle emails without domain', async () => { - action.commitData = [ - { authorEmail: 'nodomain@' } - ]; + action.commitData = [{ authorEmail: 'nodomain@' }]; await exec({}, action); expect(action.step.error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit author e-mails are illegal: nodomain@' - )).to.be.true; + expect(stepSpy.calledWith('The following commit author e-mails are illegal: nodomain@')).to.be + .true; }); it('should handle multiple illegal emails', async () => { @@ -157,15 +156,17 @@ describe('checkAuthorEmails', () => { action.commitData = [ { authorEmail: 'invalid1@bad.org' }, { authorEmail: 'invalid2@wrong.net' }, - { authorEmail: 'valid@example.com' } + { authorEmail: 'valid@example.com' }, ]; await exec({}, action); expect(action.step.error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit author e-mails are illegal: invalid1@bad.org,invalid2@wrong.net' - )).to.be.true; + expect( + stepSpy.calledWith( + 'The following commit author e-mails are illegal: invalid1@bad.org,invalid2@wrong.net', + ), + ).to.be.true; }); }); }); diff --git a/test/processors/checkCommitMessages.test.js b/test/processors/checkCommitMessages.test.js index 5eeb0fff2..abea984bf 100644 --- a/test/processors/checkCommitMessages.test.js +++ b/test/processors/checkCommitMessages.test.js @@ -19,16 +19,19 @@ describe('checkCommitMessages', () => { message: { block: { literals: ['secret', 'password'], - patterns: ['\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b'] // Credit card pattern - } - } + patterns: ['\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b'], // Credit card pattern + }, + }, }; getCommitConfigStub = sinon.stub().returns(commitConfig); - const checkCommitMessages = proxyquire('../../src/proxy/processors/push-action/checkCommitMessages', { - '../../../config': { getCommitConfig: getCommitConfigStub } - }); + const checkCommitMessages = proxyquire( + '../../src/proxy/processors/push-action/checkCommitMessages', + { + '../../../config': { getCommitConfig: getCommitConfigStub }, + }, + ); exec = checkCommitMessages.exec; }); @@ -44,16 +47,10 @@ describe('checkCommitMessages', () => { beforeEach(() => { req = {}; - action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.commitData = [ { message: 'Fix bug', author: 'test@example.com' }, - { message: 'Update docs', author: 'test@example.com' } + { message: 'Update docs', author: 'test@example.com' }, ]; stepSpy = sinon.spy(Step.prototype, 'log'); }); @@ -63,7 +60,8 @@ describe('checkCommitMessages', () => { expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.false; - expect(logStub.calledWith('The following commit messages are legal: Fix bug,Update docs')).to.be.true; + expect(logStub.calledWith('The following commit messages are legal: Fix bug,Update docs')).to + .be.true; }); it('should block commit with illegal messages', async () => { @@ -73,33 +71,33 @@ describe('checkCommitMessages', () => { expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit messages are illegal: secret password here' - )).to.be.true; + expect(stepSpy.calledWith('The following commit messages are illegal: secret password here')) + .to.be.true; expect(result.steps[0].errorMessage).to.include('Your push has been blocked'); - expect(logStub.calledWith('The following commit messages are illegal: secret password here')).to.be.true; + expect(logStub.calledWith('The following commit messages are illegal: secret password here')) + .to.be.true; }); it('should handle duplicate messages only once', async () => { action.commitData = [ { message: 'secret', author: 'test@example.com' }, { message: 'secret', author: 'test@example.com' }, - { message: 'password', author: 'test@example.com' } + { message: 'password', author: 'test@example.com' }, ]; const result = await exec(req, action); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit messages are illegal: secret,password' - )).to.be.true; - expect(logStub.calledWith('The following commit messages are illegal: secret,password')).to.be.true; + expect(stepSpy.calledWith('The following commit messages are illegal: secret,password')).to.be + .true; + expect(logStub.calledWith('The following commit messages are illegal: secret,password')).to.be + .true; }); it('should not error when commit data is empty', async () => { // Empty commit data happens when making a branch from an unapproved commit // or when pushing an empty branch or deleting a branch - // This is handled in the checkEmptyBranch.exec action + // This is handled in the checkEmptyBranch.exec action action.commitData = []; const result = await exec(req, action); @@ -111,7 +109,7 @@ describe('checkCommitMessages', () => { it('should handle commit data with null values', async () => { action.commitData = [ { message: null, author: 'test@example.com' }, - { message: undefined, author: 'test@example.com' } + { message: undefined, author: 'test@example.com' }, ]; const result = await exec(req, action); @@ -123,33 +121,33 @@ describe('checkCommitMessages', () => { it('should handle commit messages of incorrect type', async () => { action.commitData = [ { message: 123, author: 'test@example.com' }, - { message: {}, author: 'test@example.com' } + { message: {}, author: 'test@example.com' }, ]; const result = await exec(req, action); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit messages are illegal: 123,[object Object]' - )).to.be.true; - expect(logStub.calledWith('The following commit messages are illegal: 123,[object Object]')).to.be.true; + expect(stepSpy.calledWith('The following commit messages are illegal: 123,[object Object]')) + .to.be.true; + expect(logStub.calledWith('The following commit messages are illegal: 123,[object Object]')) + .to.be.true; }); it('should handle a mix of valid and invalid messages', async () => { action.commitData = [ { message: 'Fix bug', author: 'test@example.com' }, - { message: 'secret password here', author: 'test@example.com' } + { message: 'secret password here', author: 'test@example.com' }, ]; const result = await exec(req, action); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith( - 'The following commit messages are illegal: secret password here' - )).to.be.true; - expect(logStub.calledWith('The following commit messages are illegal: secret password here')).to.be.true; + expect(stepSpy.calledWith('The following commit messages are illegal: secret password here')) + .to.be.true; + expect(logStub.calledWith('The following commit messages are illegal: secret password here')) + .to.be.true; }); }); }); diff --git a/test/processors/checkIfWaitingAuth.test.js b/test/processors/checkIfWaitingAuth.test.js index f9a66a3a6..5306f8da4 100644 --- a/test/processors/checkIfWaitingAuth.test.js +++ b/test/processors/checkIfWaitingAuth.test.js @@ -13,9 +13,12 @@ describe('checkIfWaitingAuth', () => { beforeEach(() => { getPushStub = sinon.stub(); - const checkIfWaitingAuth = proxyquire('../../src/proxy/processors/push-action/checkIfWaitingAuth', { - '../../../db': { getPush: getPushStub } - }); + const checkIfWaitingAuth = proxyquire( + '../../src/proxy/processors/push-action/checkIfWaitingAuth', + { + '../../../db': { getPush: getPushStub }, + }, + ); exec = checkIfWaitingAuth.exec; }); @@ -30,23 +33,11 @@ describe('checkIfWaitingAuth', () => { beforeEach(() => { req = {}; - action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); }); it('should set allowPush when action exists and is authorized', async () => { - const authorizedAction = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const authorizedAction = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); authorizedAction.authorised = true; getPushStub.resolves(authorizedAction); @@ -59,13 +50,7 @@ describe('checkIfWaitingAuth', () => { }); it('should not set allowPush when action exists but not authorized', async () => { - const unauthorizedAction = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const unauthorizedAction = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); unauthorizedAction.authorised = false; getPushStub.resolves(unauthorizedAction); @@ -88,13 +73,7 @@ describe('checkIfWaitingAuth', () => { it('should not modify action when it has an error', async () => { action.error = true; - const authorizedAction = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const authorizedAction = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); authorizedAction.authorised = true; getPushStub.resolves(authorizedAction); diff --git a/test/processors/checkUserPushPermission.test.js b/test/processors/checkUserPushPermission.test.js index 2aa241023..fee0c7a64 100644 --- a/test/processors/checkUserPushPermission.test.js +++ b/test/processors/checkUserPushPermission.test.js @@ -14,16 +14,18 @@ describe('checkUserPushPermission', () => { beforeEach(() => { logStub = sinon.stub(console, 'log'); - getUsersStub = sinon.stub(); isUserPushAllowedStub = sinon.stub(); - const checkUserPushPermission = proxyquire('../../src/proxy/processors/push-action/checkUserPushPermission', { - '../../../db': { - getUsers: getUsersStub, - isUserPushAllowed: isUserPushAllowedStub - } - }); + const checkUserPushPermission = proxyquire( + '../../src/proxy/processors/push-action/checkUserPushPermission', + { + '../../../db': { + getUsers: getUsersStub, + isUserPushAllowed: isUserPushAllowedStub, + }, + }, + ); exec = checkUserPushPermission.exec; }); @@ -39,40 +41,43 @@ describe('checkUserPushPermission', () => { beforeEach(() => { req = {}; - action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo.git' - ); + action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo.git'); action.user = 'git-user'; + action.userEmail = 'db-user@test.com'; stepSpy = sinon.spy(Step.prototype, 'log'); }); it('should allow push when user has permission', async () => { - getUsersStub.resolves([{ username: 'db-user', gitAccount: 'git-user' }]); + getUsersStub.resolves([ + { username: 'db-user', email: 'db-user@test.com', gitAccount: 'git-user' }, + ]); isUserPushAllowedStub.resolves(true); const result = await exec(req, action); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.false; - expect(stepSpy.calledWith('User db-user is allowed to push on repo test/repo.git')).to.be.true; - expect(logStub.calledWith('User db-user permission on Repo repo : true')).to.be.true; + expect(stepSpy.lastCall.args[0]).to.equal( + 'User db-user@test.com is allowed to push on repo test/repo.git', + ); + expect(logStub.lastCall.args[0]).to.equal( + 'User db-user@test.com permission on Repo repo : true', + ); }); it('should reject push when user has no permission', async () => { - getUsersStub.resolves([{ username: 'db-user', gitAccount: 'git-user' }]); + getUsersStub.resolves([ + { username: 'db-user', email: 'db-user@test.com', gitAccount: 'git-user' }, + ]); isUserPushAllowedStub.resolves(false); const result = await exec(req, action); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith('User db-user is not allowed to push on repo test/repo.git, ending')).to.be.true; - expect(result.steps[0].errorMessage).to.include('Rejecting push as user git-user'); - expect(logStub.calledWith('User not allowed to Push')).to.be.true; + expect(result.steps[0].errorMessage).to.equal( + 'Your push has been blocked (db-user@test.com is not allowed to push on repo test/repo.git)', + ); }); it('should reject push when no user found for git account', async () => { @@ -82,29 +87,34 @@ describe('checkUserPushPermission', () => { expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith('User git-user is not allowed to push on repo test/repo.git, ending')).to.be.true; - expect(result.steps[0].errorMessage).to.include('Rejecting push as user git-user'); + expect(result.steps[0].errorMessage).to.include('Your push has been blocked'); }); - it('should handle multiple users for git account by rejecting push', async () => { + it('should handle multiple users for git account by rejecting the push', async () => { getUsersStub.resolves([ - { username: 'user1', gitAccount: 'git-user' }, - { username: 'user2', gitAccount: 'git-user' } + { username: 'user1', email: 'db-user@test.com', gitAccount: 'git-user' }, + { username: 'user2', email: 'db-user@test.com', gitAccount: 'git-user' }, ]); const result = await exec(req, action); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(logStub.calledWith('Users for this git account: [{"username":"user1","gitAccount":"git-user"},{"username":"user2","gitAccount":"git-user"}]')).to.be.true; + expect(result.steps[0].errorMessage).to.equal( + 'Your push has been blocked (there are multiple users with email db-user@test.com)', + ); }); it('should return error when no user is set in the action', async () => { action.user = null; + action.userEmail = null; + getUsersStub.resolves([]); const result = await exec(req, action); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(result.steps[0].errorMessage).to.include('Push blocked: User not found. Please contact an administrator for support.'); + expect(result.steps[0].errorMessage).to.include( + 'Push blocked: User not found. Please contact an administrator for support.', + ); }); }); }); diff --git a/test/processors/getDiff.test.js b/test/processors/getDiff.test.js index 327dfe034..7f5bb4cf3 100644 --- a/test/processors/getDiff.test.js +++ b/test/processors/getDiff.test.js @@ -10,13 +10,13 @@ const expect = chai.expect; describe('getDiff', () => { let tempDir; let git; - + before(async () => { // Create a temp repo to avoid mocking simple-git tempDir = path.join(__dirname, 'temp-test-repo'); await fs.mkdir(tempDir, { recursive: true }); git = simpleGit(tempDir); - + await git.init(); await git.addConfig('user.name', 'test'); await git.addConfig('user.email', 'test@test.com'); @@ -25,53 +25,37 @@ describe('getDiff', () => { await git.add('.'); await git.commit('initial commit'); }); - + after(async () => { await fs.rmdir(tempDir, { recursive: true }); }); - + it('should get diff between commits', async () => { await fs.writeFile(path.join(tempDir, 'test.txt'), 'modified content'); await git.add('.'); await git.commit('second commit'); - - const action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + + const action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = __dirname; // Temp dir parent path action.repoName = 'temp-test-repo'; action.commitFrom = 'HEAD~1'; action.commitTo = 'HEAD'; - action.commitData = [ - { parent: '0000000000000000000000000000000000000000' } - ]; - + action.commitData = [{ parent: '0000000000000000000000000000000000000000' }]; + const result = await exec({}, action); - + expect(result.steps[0].error).to.be.false; expect(result.steps[0].content).to.include('modified content'); expect(result.steps[0].content).to.include('initial content'); }); it('should get diff between commits with no changes', async () => { - const action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = __dirname; // Temp dir parent path action.repoName = 'temp-test-repo'; action.commitFrom = 'HEAD~1'; action.commitTo = 'HEAD'; - action.commitData = [ - { parent: '0000000000000000000000000000000000000000' } - ]; + action.commitData = [{ parent: '0000000000000000000000000000000000000000' }]; const result = await exec({}, action); @@ -80,13 +64,7 @@ describe('getDiff', () => { }); it('should throw an error if no commit data is provided', async () => { - const action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = __dirname; // Temp dir parent path action.repoName = 'temp-test-repo'; action.commitFrom = 'HEAD~1'; @@ -95,17 +73,13 @@ describe('getDiff', () => { const result = await exec({}, action); expect(result.steps[0].error).to.be.true; - expect(result.steps[0].errorMessage).to.contain('Your push has been blocked because no commit data was found'); + expect(result.steps[0].errorMessage).to.contain( + 'Your push has been blocked because no commit data was found', + ); }); it('should throw an error if no commit data is provided', async () => { - const action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = __dirname; // Temp dir parent path action.repoName = 'temp-test-repo'; action.commitFrom = 'HEAD~1'; @@ -114,7 +88,9 @@ describe('getDiff', () => { const result = await exec({}, action); expect(result.steps[0].error).to.be.true; - expect(result.steps[0].errorMessage).to.contain('Your push has been blocked because no commit data was found'); + expect(result.steps[0].errorMessage).to.contain( + 'Your push has been blocked because no commit data was found', + ); }); it('should handle empty commit hash in commitFrom', async () => { @@ -126,21 +102,13 @@ describe('getDiff', () => { const parentCommit = log.all[1].hash; const headCommit = log.all[0].hash; - const action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + const action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = path.dirname(tempDir); action.repoName = path.basename(tempDir); action.commitFrom = '0000000000000000000000000000000000000000'; action.commitTo = headCommit; - action.commitData = [ - { parent: parentCommit } - ]; + action.commitData = [{ parent: parentCommit }]; const result = await exec({}, action); diff --git a/test/processors/gitLeaks.test.js b/test/processors/gitLeaks.test.js index eeed7f8e2..9157cf301 100644 --- a/test/processors/gitLeaks.test.js +++ b/test/processors/gitLeaks.test.js @@ -22,9 +22,9 @@ describe('gitleaks', () => { fs: { stat: sinon.stub(), access: sinon.stub(), - constants: { R_OK: 0 } + constants: { R_OK: 0 }, }, - spawn: sinon.stub() + spawn: sinon.stub(), }; logStub = sinon.stub(console, 'log'); @@ -33,19 +33,13 @@ describe('gitleaks', () => { const gitleaksModule = proxyquire('../../src/proxy/processors/push-action/gitleaks', { '../../../config': { getAPIs: stubs.getAPIs }, 'node:fs/promises': stubs.fs, - 'node:child_process': { spawn: stubs.spawn } + 'node:child_process': { spawn: stubs.spawn }, }); exec = gitleaksModule.exec; req = {}; - action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = '/tmp'; action.repoName = 'test-repo'; action.commitFrom = 'abc123'; @@ -66,8 +60,10 @@ describe('gitleaks', () => { expect(result.error).to.be.true; expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith('failed setup gitleaks, please contact an administrator\n')).to.be.true; - expect(errorStub.calledWith('failed to get gitleaks config, please fix the error:')).to.be.true; + expect(stepSpy.calledWith('failed setup gitleaks, please contact an administrator\n')).to.be + .true; + expect(errorStub.calledWith('failed to get gitleaks config, please fix the error:')).to.be + .true; }); it('should skip scanning when plugin is disabled', async () => { @@ -87,31 +83,33 @@ describe('gitleaks', () => { const gitRootCommitMock = { exitCode: 0, stdout: 'rootcommit123\n', - stderr: '' + stderr: '', }; const gitleaksMock = { exitCode: 0, stdout: '', - stderr: 'No leaks found' + stderr: 'No leaks found', }; stubs.spawn - .onFirstCall().returns({ + .onFirstCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitRootCommitMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitRootCommitMock.stdout) }, - stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) } + stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) }, }) - .onSecondCall().returns({ + .onSecondCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitleaksMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitleaksMock.stdout) }, - stderr: { on: (_, cb) => cb(gitleaksMock.stderr) } + stderr: { on: (_, cb) => cb(gitleaksMock.stderr) }, }); const result = await exec(req, action); @@ -129,31 +127,33 @@ describe('gitleaks', () => { const gitRootCommitMock = { exitCode: 0, stdout: 'rootcommit123\n', - stderr: '' + stderr: '', }; const gitleaksMock = { exitCode: 99, stdout: 'Found secret in file.txt\n', - stderr: 'Warning: potential leak' + stderr: 'Warning: potential leak', }; stubs.spawn - .onFirstCall().returns({ + .onFirstCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitRootCommitMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitRootCommitMock.stdout) }, - stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) } + stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) }, }) - .onSecondCall().returns({ + .onSecondCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitleaksMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitleaksMock.stdout) }, - stderr: { on: (_, cb) => cb(gitleaksMock.stderr) } + stderr: { on: (_, cb) => cb(gitleaksMock.stderr) }, }); const result = await exec(req, action); @@ -170,31 +170,33 @@ describe('gitleaks', () => { const gitRootCommitMock = { exitCode: 0, stdout: 'rootcommit123\n', - stderr: '' + stderr: '', }; const gitleaksMock = { exitCode: 1, stdout: '', - stderr: 'Command failed' + stderr: 'Command failed', }; stubs.spawn - .onFirstCall().returns({ + .onFirstCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitRootCommitMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitRootCommitMock.stdout) }, - stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) } + stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) }, }) - .onSecondCall().returns({ + .onSecondCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitleaksMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitleaksMock.stdout) }, - stderr: { on: (_, cb) => cb(gitleaksMock.stderr) } + stderr: { on: (_, cb) => cb(gitleaksMock.stderr) }, }); const result = await exec(req, action); @@ -202,7 +204,8 @@ describe('gitleaks', () => { expect(result.error).to.be.true; expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith('failed to run gitleaks, please contact an administrator\n')).to.be.true; + expect(stepSpy.calledWith('failed to run gitleaks, please contact an administrator\n')).to.be + .true; }); it('should handle gitleaks spawn failure', async () => { @@ -214,7 +217,8 @@ describe('gitleaks', () => { expect(result.error).to.be.true; expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(stepSpy.calledWith('failed to spawn gitleaks, please contact an administrator\n')).to.be.true; + expect(stepSpy.calledWith('failed to spawn gitleaks, please contact an administrator\n')).to + .be.true; }); it('should handle empty gitleaks entry in proxy.config.json', async () => { @@ -233,7 +237,7 @@ describe('gitleaks', () => { return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb('') }, - stderr: { on: (_, cb) => cb('') } + stderr: { on: (_, cb) => cb('') }, }); const result = await exec(req, action); @@ -244,11 +248,11 @@ describe('gitleaks', () => { }); it('should handle custom config path', async () => { - stubs.getAPIs.returns({ - gitleaks: { + stubs.getAPIs.returns({ + gitleaks: { enabled: true, - configPath: `../fixtures/gitleaks-config.toml` - } + configPath: `../fixtures/gitleaks-config.toml`, + }, }); stubs.fs.stat.resolves({ isFile: () => true }); @@ -257,46 +261,50 @@ describe('gitleaks', () => { const gitRootCommitMock = { exitCode: 0, stdout: 'rootcommit123\n', - stderr: '' + stderr: '', }; const gitleaksMock = { exitCode: 0, stdout: '', - stderr: 'No leaks found' + stderr: 'No leaks found', }; stubs.spawn - .onFirstCall().returns({ + .onFirstCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitRootCommitMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitRootCommitMock.stdout) }, - stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) } + stderr: { on: (_, cb) => cb(gitRootCommitMock.stderr) }, }) - .onSecondCall().returns({ + .onSecondCall() + .returns({ on: (event, cb) => { if (event === 'close') cb(gitleaksMock.exitCode); return { stdout: { on: () => {} }, stderr: { on: () => {} } }; }, stdout: { on: (_, cb) => cb(gitleaksMock.stdout) }, - stderr: { on: (_, cb) => cb(gitleaksMock.stderr) } + stderr: { on: (_, cb) => cb(gitleaksMock.stderr) }, }); const result = await exec(req, action); expect(result.error).to.be.false; expect(result.steps[0].error).to.be.false; - expect(stubs.spawn.secondCall.args[1]).to.include('--config=../fixtures/gitleaks-config.toml'); + expect(stubs.spawn.secondCall.args[1]).to.include( + '--config=../fixtures/gitleaks-config.toml', + ); }); it('should handle invalid custom config path', async () => { - stubs.getAPIs.returns({ - gitleaks: { + stubs.getAPIs.returns({ + gitleaks: { enabled: true, - configPath: '/invalid/path.toml' - } + configPath: '/invalid/path.toml', + }, }); stubs.fs.stat.rejects(new Error('File not found')); @@ -306,7 +314,11 @@ describe('gitleaks', () => { expect(result.error).to.be.true; expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.true; - expect(errorStub.calledWith('could not read file at the config path provided, will not be fed to gitleaks')).to.be.true; + expect( + errorStub.calledWith( + 'could not read file at the config path provided, will not be fed to gitleaks', + ), + ).to.be.true; }); }); }); diff --git a/test/processors/writePack.test.js b/test/processors/writePack.test.js index 856e6439a..2cff64204 100644 --- a/test/processors/writePack.test.js +++ b/test/processors/writePack.test.js @@ -26,8 +26,8 @@ describe('writePack', () => { stepSetErrorSpy = sinon.spy(Step.prototype, 'setError'); const writePack = proxyquire('../../src/proxy/processors/push-action/writePack', { - 'child_process': { spawnSync: spawnSyncStub }, - 'fs': { readdirSync: readdirSyncStub }, + child_process: { spawnSync: spawnSyncStub }, + fs: { readdirSync: readdirSyncStub }, }); exec = writePack.exec; @@ -43,15 +43,9 @@ describe('writePack', () => { beforeEach(() => { req = { - body: 'pack data' + body: 'pack data', }; - action = new Action( - '1234567890', - 'push', - 'POST', - 1234567890, - 'test/repo' - ); + action = new Action('1234567890', 'push', 'POST', 1234567890, 'test/repo'); action.proxyGitPath = '/path/to'; action.repoName = 'repo'; }); @@ -71,7 +65,7 @@ describe('writePack', () => { expect(spawnSyncStub.secondCall.args[1]).to.deep.equal(['receive-pack', 'repo']); expect(spawnSyncStub.secondCall.args[2]).to.include({ cwd: '/path/to', - input: 'pack data' + input: 'pack data', }); expect(stepLogSpy.calledWith('new idx files: new1.idx')).to.be.true; diff --git a/test/testAuthMethods.test.js b/test/testAuthMethods.test.js index c6c5b9c0c..fc7054071 100644 --- a/test/testAuthMethods.test.js +++ b/test/testAuthMethods.test.js @@ -21,19 +21,19 @@ describe('auth methods', async () => { { type: 'openidconnect', enabled: false }, ], }); - + const fsStub = { existsSync: sinon.stub().returns(true), readFileSync: sinon.stub().returns(newConfig), }; - + const config = proxyquire('../src/config', { fs: fsStub, }); // Initialize the user config after proxyquiring to load the stubbed config config.initUserConfig(); - + expect(() => config.getAuthMethods()).to.throw(Error, 'No authentication method enabled'); }); @@ -63,5 +63,5 @@ describe('auth methods', async () => { expect(authMethods[0].type).to.equal('local'); expect(authMethods[1].type).to.equal('ActiveDirectory'); expect(authMethods[2].type).to.equal('openidconnect'); - }) + }); }); diff --git a/test/testCheckUserPushPermission.test.js b/test/testCheckUserPushPermission.test.js new file mode 100644 index 000000000..7eb281d5f --- /dev/null +++ b/test/testCheckUserPushPermission.test.js @@ -0,0 +1,60 @@ +const chai = require('chai'); +const processor = require('../src/proxy/processors/push-action/checkUserPushPermission'); +const { Action } = require('../src/proxy/actions/Action'); +const { expect } = chai; +const db = require('../src/db'); +chai.should(); + +const TEST_ORG = 'finos'; +const TEST_REPO = 'test'; +const TEST_URL = 'https://github.com/finos/user-push-perms-test.git'; +const TEST_USERNAME_1 = 'push-perms-test'; +const TEST_EMAIL_1 = 'push-perms-test@test.com'; +const TEST_USERNAME_2 = 'push-perms-test-2'; +const TEST_EMAIL_2 = 'push-perms-test-2@test.com'; +const TEST_EMAIL_3 = 'push-perms-test-3@test.com'; + +describe('CheckUserPushPermissions...', async () => { + before(async function () { + await db.deleteRepo(TEST_REPO); + await db.deleteUser(TEST_USERNAME_1); + await db.deleteUser(TEST_USERNAME_2); + await db.createRepo({ + project: TEST_ORG, + name: TEST_REPO, + url: TEST_URL, + }); + await db.createUser(TEST_USERNAME_1, 'abc', TEST_EMAIL_1, TEST_USERNAME_1, false); + await db.addUserCanPush(TEST_REPO, TEST_USERNAME_1); + await db.createUser(TEST_USERNAME_2, 'abc', TEST_EMAIL_2, TEST_USERNAME_2, false); + }); + + after(async function () { + await db.deleteRepo(TEST_REPO); + await db.deleteUser(TEST_USERNAME_1); + await db.deleteUser(TEST_USERNAME_2); + }); + + it('A committer that is approved should be allowed to push...', async () => { + const action = new Action('1', 'type', 'method', 1, TEST_ORG + '/' + TEST_REPO); + action.userEmail = TEST_EMAIL_1; + const { error } = await processor.exec(null, action); + expect(error).to.be.false; + }); + + it('A committer that is NOT approved should NOT be allowed to push...', async () => { + const action = new Action('1', 'type', 'method', 1, TEST_ORG + '/' + TEST_REPO); + action.userEmail = TEST_EMAIL_2; + const { error, errorMessage } = await processor.exec(null, action); + expect(error).to.be.true; + expect(errorMessage).to.contains('Your push has been blocked'); + }); + + it('An unknown committer should NOT be allowed to push...', async () => { + const action = new Action('1', 'type', 'method', 1, TEST_ORG + '/' + TEST_REPO); + action.userEmail = TEST_EMAIL_3; + const { error, errorMessage } = await processor.exec(null, action); + expect(error).to.be.true; + expect(errorMessage).to.contains('Your push has been blocked'); + }); +}); diff --git a/test/testDb.test.js b/test/testDb.test.js index 620376648..0af10c976 100644 --- a/test/testDb.test.js +++ b/test/testDb.test.js @@ -128,7 +128,6 @@ describe('Database clients', async () => { it('should be able to delete a repo', async function () { await db.deleteRepo(TEST_REPO.name); const repos = await db.getRepos(); - const cleanRepos = cleanResponseData(TEST_REPO, repos); expect(cleanRepos).to.not.deep.include(TEST_REPO); }); @@ -211,6 +210,8 @@ describe('Database clients', async () => { TEST_USER.admin, ); const users = await db.getUsers(); + console.log('TEST USER:', JSON.stringify(TEST_USER, null, 2)); + console.log('USERS:', JSON.stringify(users, null, 2)); // remove password as it will have been hashed // eslint-disable-next-line no-unused-vars const { password: _, ...TEST_USER_CLEAN } = TEST_USER; diff --git a/test/testParsePush.js b/test/testParsePush.js deleted file mode 100644 index a4106d6ce..000000000 --- a/test/testParsePush.js +++ /dev/null @@ -1,595 +0,0 @@ -const chai = require('chai'); -const actions = require('../src/proxy/actions/Action'); -const processor = require('../src/proxy/processors/push-action/parsePush'); -const expect = chai.expect; - -// request with a single commit -const reqBody = Buffer.from([ - 48, 48, 98, 100, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, - 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 32, 53, 56, 51, - 101, 48, 50, 48, 57, 54, 102, 49, 99, 54, 98, 100, 100, 52, 52, 49, 48, 54, 56, 102, 101, 54, 101, - 101, 102, 97, 53, 99, 54, 53, 52, 54, 99, 56, 57, 100, 57, 32, 114, 101, 102, 115, 47, 104, 101, - 97, 100, 115, 47, 57, 55, 49, 45, 116, 101, 115, 116, 0, 32, 114, 101, 112, 111, 114, 116, 45, - 115, 116, 97, 116, 117, 115, 45, 118, 50, 32, 115, 105, 100, 101, 45, 98, 97, 110, 100, 45, 54, - 52, 107, 32, 111, 98, 106, 101, 99, 116, 45, 102, 111, 114, 109, 97, 116, 61, 115, 104, 97, 49, - 32, 97, 103, 101, 110, 116, 61, 103, 105, 116, 47, 50, 46, 51, 57, 46, 53, 46, 40, 65, 112, 112, - 108, 101, 46, 71, 105, 116, 45, 49, 53, 52, 41, 48, 48, 48, 48, 80, 65, 67, 75, 0, 0, 0, 2, 0, 0, - 0, 14, 153, 17, 120, 156, 165, 77, 193, 78, 197, 48, 12, 187, 247, 43, 114, 71, 122, 106, 182, - 116, 235, 16, 66, 220, 249, 0, 206, 33, 205, 216, 128, 173, 83, 151, 233, 253, 62, 65, 124, 2, - 185, 56, 182, 101, 219, 154, 42, 116, 148, 211, 52, 163, 202, 76, 152, 120, 204, 154, 144, 48, 83, - 41, 25, 123, 154, 10, 137, 223, 251, 192, 49, 28, 220, 116, 55, 96, 77, 99, 212, 161, 143, 168, - 89, 138, 120, 14, 71, 157, 132, 138, 215, 96, 55, 105, 44, 220, 11, 117, 18, 248, 178, 165, 54, - 120, 109, 235, 9, 111, 122, 26, 60, 125, 249, 107, 245, 88, 180, 221, 238, 46, 188, 236, 108, 191, - 120, 147, 186, 61, 3, 142, 52, 36, 95, 78, 61, 60, 68, 140, 49, 184, 186, 173, 102, 250, 143, 138, - 96, 238, 61, 2, 151, 207, 203, 195, 133, 141, 65, 248, 176, 171, 41, 220, 87, 91, 224, 111, 2, - 182, 243, 3, 230, 86, 55, 231, 251, 89, 191, 53, 252, 0, 42, 141, 90, 85, 154, 15, 120, 156, 165, - 140, 193, 14, 194, 48, 12, 67, 239, 253, 138, 220, 145, 166, 46, 105, 86, 134, 16, 226, 206, 7, - 112, 78, 215, 84, 32, 4, 155, 186, 76, 252, 62, 221, 55, 112, 177, 173, 103, 217, 86, 85, 97, 192, - 33, 150, 88, 2, 75, 68, 164, 192, 33, 104, 161, 137, 48, 17, 138, 150, 193, 103, 196, 62, 170, - 207, 110, 145, 170, 31, 131, 192, 189, 98, 36, 225, 20, 104, 143, 37, 113, 200, 40, 60, 210, 49, - 101, 150, 182, 161, 72, 156, 147, 147, 205, 30, 115, 133, 91, 125, 174, 112, 215, 213, 224, 252, - 106, 209, 230, 229, 161, 181, 251, 54, 112, 253, 136, 237, 222, 77, 243, 251, 2, 125, 12, 3, 211, - 24, 113, 132, 131, 239, 189, 119, 141, 190, 159, 102, 250, 199, 133, 179, 214, 157, 96, 87, 200, - 98, 2, 147, 44, 182, 85, 117, 63, 50, 250, 77, 243, 240, 2, 224, 16, 152, 60, 5, 50, 165, 6, 153, - 5, 155, 56, 162, 8, 96, 31, 156, 9, 238, 140, 120, 156, 187, 200, 115, 145, 103, 195, 74, 86, 17, - 25, 105, 157, 254, 148, 168, 195, 170, 243, 171, 47, 212, 199, 116, 38, 95, 127, 24, 243, 226, - 241, 228, 189, 172, 83, 0, 225, 116, 15, 62, 254, 1, 137, 133, 157, 21, 251, 180, 160, 188, 206, - 135, 191, 105, 98, 239, 115, 191, 202, 198, 146, 137, 120, 156, 219, 204, 180, 153, 105, 194, 46, - 145, 132, 148, 30, 206, 0, 117, 161, 251, 231, 23, 48, 88, 10, 26, 120, 204, 49, 76, 112, 251, 51, - 241, 92, 42, 0, 175, 39, 11, 238, 254, 1, 179, 193, 177, 252, 130, 173, 32, 172, 145, 25, 128, - 205, 95, 40, 239, 37, 5, 13, 48, 103, 120, 156, 219, 192, 184, 129, 113, 66, 181, 72, 95, 193, 78, - 15, 155, 135, 7, 210, 124, 95, 180, 243, 55, 89, 173, 139, 153, 94, 42, 165, 55, 177, 95, 17, 0, - 203, 243, 12, 234, 166, 9, 120, 156, 51, 52, 48, 48, 51, 49, 81, 200, 204, 75, 73, 173, 208, 43, - 41, 102, 136, 168, 106, 126, 242, 175, 223, 38, 98, 70, 233, 150, 78, 150, 124, 14, 231, 143, 7, - 62, 94, 51, 49, 0, 2, 133, 130, 162, 84, 221, 130, 162, 252, 228, 212, 226, 226, 252, 34, 134, - 208, 157, 186, 31, 47, 168, 153, 190, 187, 95, 115, 108, 227, 228, 83, 235, 117, 121, 131, 174, - 139, 64, 213, 149, 22, 103, 232, 38, 38, 151, 100, 230, 231, 49, 120, 186, 158, 186, 240, 178, - 233, 194, 73, 102, 190, 226, 174, 151, 186, 110, 98, 38, 139, 254, 174, 54, 132, 216, 88, 82, 89, - 144, 90, 12, 178, 113, 183, 90, 205, 75, 54, 177, 89, 121, 201, 63, 152, 99, 2, 84, 150, 73, 249, - 91, 22, 138, 0, 0, 241, 97, 60, 0, 240, 2, 29, 168, 35, 56, 248, 127, 160, 194, 136, 30, 255, 23, - 237, 239, 105, 74, 117, 135, 167, 30, 120, 156, 1, 32, 0, 223, 255, 144, 5, 144, 5, 176, 219, 1, - 20, 127, 49, 60, 26, 210, 115, 209, 143, 23, 37, 16, 222, 210, 126, 210, 106, 226, 169, 206, 174, - 147, 239, 1, 161, 221, 228, 15, 87, 247, 35, 150, 215, 66, 131, 112, 24, 188, 81, 66, 54, 3, 47, - 142, 65, 47, 209, 115, 29, 175, 162, 120, 156, 133, 144, 205, 74, 195, 64, 20, 133, 9, 168, 72, - 213, 98, 65, 112, 39, 71, 16, 82, 181, 166, 110, 20, 108, 113, 233, 70, 240, 7, 170, 187, 108, - 166, 201, 109, 50, 154, 204, 196, 228, 6, 44, 84, 250, 4, 130, 208, 7, 240, 25, 164, 27, 93, 186, - 113, 231, 75, 248, 16, 174, 77, 127, 108, 107, 41, 56, 171, 59, 223, 185, 231, 91, 220, 175, 195, - 239, 163, 167, 183, 13, 25, 70, 58, 102, 236, 64, 36, 72, 89, 6, 104, 196, 58, 132, 217, 27, 205, - 106, 238, 229, 181, 96, 180, 129, 114, 25, 142, 47, 148, 71, 144, 138, 99, 237, 166, 14, 185, 104, - 232, 24, 142, 136, 56, 141, 165, 242, 192, 148, 48, 92, 193, 2, 66, 185, 96, 141, 186, 72, 8, 236, - 211, 32, 209, 42, 7, 56, 90, 37, 58, 32, 43, 208, 94, 49, 251, 2, 102, 141, 123, 101, 217, 144, - 153, 47, 74, 19, 191, 111, 168, 192, 86, 49, 221, 89, 117, 237, 54, 43, 182, 106, 239, 77, 63, 91, - 153, 216, 237, 11, 128, 211, 218, 197, 185, 149, 12, 53, 205, 226, 111, 111, 123, 180, 96, 218, - 106, 134, 64, 56, 44, 181, 170, 204, 204, 166, 228, 7, 99, 251, 160, 245, 159, 219, 44, 101, 113, - 247, 195, 120, 156, 235, 94, 45, 4, 243, 157, 247, 165, 173, 194, 176, 0, 88, 55, 90, 170, 162, - 137, 206, 243, 98, 177, 187, 190, 252, 105, 92, 143, 162, 77, 142, 137, 208, 106, 141, 65, 36, 98, - 82, 252, 7, 137, 148, 253, 236, 240, 147, 200, 209, 97, 40, 153, 105, 146, 118, 86, 243, 107, 251, - 227, 149, 144, 146, 68, 120, 52, 195, 116, 18, 10, 25, 12, 97, 247, 56, 111, 172, 108, 128, 238, - 201, 41, 193, 35, 190, 20, 206, 237, 25, 177, 40, 33, 85, 81, 54, 227, 161, 154, 251, 1, 177, 118, - 158, 164, 240, 2, 224, 16, 152, 60, 5, 50, 165, 6, 153, 5, 155, 56, 162, 8, 96, 31, 156, 9, 238, - 140, 120, 156, 1, 32, 0, 223, 255, 209, 12, 209, 12, 176, 169, 5, 20, 74, 65, 131, 50, 107, 113, - 230, 114, 231, 216, 102, 25, 202, 231, 246, 248, 64, 46, 179, 105, 147, 189, 5, 148, 248, 3, 15, - 241, 254, 1, 137, 133, 157, 21, 251, 180, 160, 188, 206, 135, 191, 105, 98, 239, 115, 191, 202, - 198, 146, 137, 120, 156, 1, 30, 0, 225, 255, 179, 2, 179, 2, 144, 186, 20, 89, 17, 51, 252, 210, - 252, 47, 239, 152, 111, 118, 189, 211, 171, 86, 247, 39, 115, 216, 188, 145, 206, 101, 231, 103, - 16, 69, 254, 1, 179, 193, 177, 252, 130, 173, 32, 172, 145, 25, 128, 205, 95, 40, 239, 37, 5, 13, - 48, 103, 120, 156, 219, 192, 184, 129, 113, 66, 181, 200, 76, 253, 153, 162, 149, 114, 17, 45, 5, - 123, 236, 213, 75, 27, 95, 69, 95, 87, 191, 160, 57, 177, 95, 17, 0, 183, 85, 12, 112, 166, 9, - 120, 156, 51, 52, 48, 48, 51, 49, 81, 200, 204, 75, 73, 173, 208, 43, 41, 102, 136, 168, 106, 126, - 242, 175, 223, 38, 98, 70, 233, 150, 78, 150, 124, 14, 231, 143, 7, 62, 94, 51, 49, 0, 2, 133, - 130, 162, 84, 221, 130, 162, 252, 228, 212, 226, 226, 252, 34, 134, 208, 157, 186, 31, 47, 168, - 153, 190, 187, 95, 115, 108, 227, 228, 83, 235, 117, 121, 131, 174, 139, 64, 213, 149, 22, 103, - 232, 38, 38, 151, 100, 230, 231, 49, 164, 103, 172, 223, 223, 241, 226, 254, 99, 158, 21, 50, 31, - 222, 124, 93, 101, 95, 244, 124, 249, 113, 67, 136, 141, 37, 149, 5, 169, 197, 32, 27, 119, 171, - 213, 188, 100, 19, 155, 149, 151, 252, 131, 57, 38, 64, 101, 153, 148, 191, 101, 161, 8, 0, 99, - 166, 62, 150, 240, 2, 29, 168, 35, 56, 248, 127, 160, 194, 136, 30, 255, 23, 237, 239, 105, 74, - 117, 135, 167, 30, 120, 156, 1, 32, 0, 223, 255, 144, 5, 144, 5, 176, 219, 1, 20, 71, 34, 183, 94, - 158, 203, 170, 37, 254, 94, 138, 249, 65, 34, 11, 142, 121, 227, 207, 19, 147, 239, 1, 161, 227, - 254, 14, 190, 239, 3, 132, 123, 120, 156, 251, 102, 249, 208, 98, 194, 206, 141, 55, 218, 25, 181, - 173, 98, 242, 212, 21, 180, 21, 188, 130, 253, 253, 244, 138, 75, 138, 50, 243, 210, 51, 211, 42, - 53, 138, 82, 11, 53, 129, 162, 234, 49, 121, 137, 201, 37, 153, 249, 121, 86, 10, 64, 69, 147, 31, - 48, 137, 111, 22, 103, 190, 47, 9, 0, 1, 31, 22, 59, 61, 23, 179, 2, 16, 216, 91, 106, 130, 85, - 17, 219, 41, 45, 234, 231, 203, 152, 97, 30, -]); - -const actionData = { - id: '1746542004301', - type: 'push', - method: 'POST', - timestamp: 1746542004301, - repo: 'kriswest/git-proxy.git', -}; - -// truncated request (should through an error and not parse) -const truncatedReqBody = Buffer.from([ - 48, 48, 98, 100, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, - 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 32, 53, 56, 51, - 101, 48, 50, 48, 57, 54, 102, 49, 99, 54, 98, 100, 100, 52, 52, 49, 48, 54, 56, 102, 101, 54, 101, - 101, 102, 97, 53, 99, 54, 53, 52, 54, 99, 56, 57, 100, 57, 32, 114, 101, 102, 115, 47, 104, 101, - 97, 100, 115, 47, 57, 55, 49, 45, 116, 101, 115, 116, 0, 32, 114, 101, 112, 111, 114, 116, 45, - 115, 116, 97, 116, 117, 115, 45, 118, 50, 32, 115, 105, 100, 101, 45, 98, 97, 110, 100, 45, 54, - 52, 107, 32, 111, 98, 106, 101, 99, 116, 45, 102, 111, 114, 109, 97, 116, 61, 115, 104, 97, 49, - 32, 97, 103, 101, 110, 116, 61, 103, 105, 116, 47, 50, 46, 51, 57, 46, 53, 46, 40, 65, 112, 112, - 108, 101, 46, 71, 105, 116, 45, 49, 53, 52, 41, 48, 48, 48, 48, 80, 65, 67, 75, 0, 0, 0, 2, 0, 0, - 0, 14, 153, 17, 120, 156, 165, 77, 193, 78, 197, 48, 12, 187, 247, 43, 114, 71, 122, 106, 182, - 116, 235, 16, 66, 220, 249, 0, 206, 33, 205, 216, 128, 173, 83, 151, 233, 253, 62, 65, 124, 2, - 185, 56, 182, 101, 219, 154, 42, 116, 148, 211, 52, 163, 202, 76, 152, 120, 204, 154, 144, 48, 83, - 41, 25, 123, 154, 10, 137, 223, 251, 192, 49, 28, 220, 116, 55, 96, 77, 99, 212, 161, 143, 168, - 89, 138, 120, 14, 71, 157, 132, 138, 215, 96, 55, 105, 44, 220, 11, 117, 18, 248, 178, 165, 54, - 120, 109, 235, 9, 111, 122, 26, 60, 125, 249, 107, 245, 88, 180, 221, 238, 46, 188, 236, 108, 191, - 120, 147, 186, 61, 3, 142, 52, 36, 95, 78, 61, 60, 68, 140, 49, 184, 186, 173, 102, 250, 143, 138, - 96, 238, 61, 2, 151, 207, 203, 195, 133, 141, 65, 248, 176, -]); - -// push with multiple commits -const reqBody2 = Buffer.from([ - 48, 48, 98, 100, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, - 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 32, 56, 55, 50, - 53, 48, 57, 101, 54, 55, 102, 50, 51, 53, 49, 56, 101, 99, 56, 53, 57, 100, 102, 51, 52, 100, 100, - 99, 53, 99, 102, 48, 101, 49, 50, 57, 49, 48, 54, 98, 101, 32, 114, 101, 102, 115, 47, 104, 101, - 97, 100, 115, 47, 57, 55, 49, 45, 116, 101, 115, 116, 0, 32, 114, 101, 112, 111, 114, 116, 45, - 115, 116, 97, 116, 117, 115, 45, 118, 50, 32, 115, 105, 100, 101, 45, 98, 97, 110, 100, 45, 54, - 52, 107, 32, 111, 98, 106, 101, 99, 116, 45, 102, 111, 114, 109, 97, 116, 61, 115, 104, 97, 49, - 32, 97, 103, 101, 110, 116, 61, 103, 105, 116, 47, 50, 46, 51, 57, 46, 53, 46, 40, 65, 112, 112, - 108, 101, 46, 71, 105, 116, 45, 49, 53, 52, 41, 48, 48, 48, 48, 80, 65, 67, 75, 0, 0, 0, 2, 0, 0, - 0, 23, 146, 15, 120, 156, 165, 142, 205, 10, 194, 48, 16, 132, 239, 121, 138, 189, 11, 101, 243, - 219, 68, 68, 188, 123, 241, 230, 57, 77, 54, 180, 136, 109, 73, 182, 248, 250, 198, 103, 240, 52, - 195, 247, 193, 48, 92, 137, 160, 100, 99, 163, 74, 50, 169, 144, 148, 214, 137, 108, 240, 104, - 179, 30, 71, 163, 163, 138, 113, 194, 98, 149, 46, 36, 246, 88, 105, 101, 176, 94, 19, 42, 12, - 174, 200, 228, 166, 156, 141, 145, 232, 124, 33, 71, 84, 162, 77, 206, 26, 151, 124, 200, 65, 196, - 131, 231, 173, 194, 189, 46, 13, 158, 212, 24, 46, 175, 94, 121, 219, 103, 170, 195, 167, 131, - 219, 26, 249, 151, 67, 218, 222, 87, 144, 163, 113, 78, 42, 171, 61, 156, 80, 34, 138, 78, 223, - 11, 51, 253, 49, 33, 184, 187, 51, 244, 231, 141, 30, 71, 155, 197, 23, 61, 188, 75, 134, 153, 17, - 120, 156, 165, 77, 193, 78, 197, 48, 12, 187, 247, 43, 114, 71, 122, 106, 182, 116, 235, 16, 66, - 220, 249, 0, 206, 33, 205, 216, 128, 173, 83, 151, 233, 253, 62, 65, 124, 2, 185, 56, 182, 101, - 219, 154, 42, 116, 148, 211, 52, 163, 202, 76, 152, 120, 204, 154, 144, 48, 83, 41, 25, 123, 154, - 10, 137, 223, 251, 192, 49, 28, 220, 116, 55, 96, 77, 99, 212, 161, 143, 168, 89, 138, 120, 14, - 71, 157, 132, 138, 215, 96, 55, 105, 44, 220, 11, 117, 18, 248, 178, 165, 54, 120, 109, 235, 9, - 111, 122, 26, 60, 125, 249, 107, 245, 88, 180, 221, 238, 46, 188, 236, 108, 191, 120, 147, 186, - 61, 3, 142, 52, 36, 95, 78, 61, 60, 68, 140, 49, 184, 186, 173, 102, 250, 143, 138, 96, 238, 61, - 2, 151, 207, 203, 195, 133, 141, 65, 248, 176, 171, 41, 220, 87, 91, 224, 111, 2, 182, 243, 3, - 230, 86, 55, 231, 251, 89, 191, 53, 252, 0, 42, 141, 90, 85, 154, 15, 120, 156, 165, 140, 193, 14, - 194, 48, 12, 67, 239, 253, 138, 220, 145, 166, 46, 105, 86, 134, 16, 226, 206, 7, 112, 78, 215, - 84, 32, 4, 155, 186, 76, 252, 62, 221, 55, 112, 177, 173, 103, 217, 86, 85, 97, 192, 33, 150, 88, - 2, 75, 68, 164, 192, 33, 104, 161, 137, 48, 17, 138, 150, 193, 103, 196, 62, 170, 207, 110, 145, - 170, 31, 131, 192, 189, 98, 36, 225, 20, 104, 143, 37, 113, 200, 40, 60, 210, 49, 101, 150, 182, - 161, 72, 156, 147, 147, 205, 30, 115, 133, 91, 125, 174, 112, 215, 213, 224, 252, 106, 209, 230, - 229, 161, 181, 251, 54, 112, 253, 136, 237, 222, 77, 243, 251, 2, 125, 12, 3, 211, 24, 113, 132, - 131, 239, 189, 119, 141, 190, 159, 102, 250, 199, 133, 179, 214, 157, 96, 87, 200, 98, 2, 147, 44, - 182, 85, 117, 63, 50, 250, 77, 243, 255, 3, 224, 16, 152, 60, 5, 50, 165, 6, 153, 5, 155, 56, 162, - 8, 96, 31, 156, 9, 238, 140, 120, 156, 1, 63, 0, 192, 255, 209, 12, 209, 12, 176, 169, 5, 51, 171, - 220, 45, 227, 151, 201, 226, 229, 151, 165, 60, 241, 181, 207, 58, 186, 15, 201, 90, 195, 52, 48, - 48, 48, 48, 32, 116, 101, 115, 116, 0, 87, 113, 162, 182, 62, 45, 13, 76, 251, 198, 227, 112, 206, - 171, 155, 238, 214, 239, 99, 232, 147, 220, 5, 117, 0, 42, 32, 167, 254, 1, 137, 133, 157, 21, - 251, 180, 160, 188, 206, 135, 191, 105, 98, 239, 115, 191, 202, 198, 146, 137, 120, 156, 219, 204, - 180, 153, 105, 194, 46, 145, 235, 130, 65, 10, 143, 217, 159, 232, 39, 91, 252, 220, 155, 45, 230, - 108, 226, 31, 253, 39, 99, 226, 185, 84, 0, 195, 139, 13, 59, 254, 1, 179, 193, 177, 252, 130, - 173, 32, 172, 145, 25, 128, 205, 95, 40, 239, 37, 5, 13, 48, 103, 120, 156, 1, 30, 0, 225, 255, - 176, 1, 176, 1, 144, 123, 20, 210, 206, 238, 143, 53, 246, 183, 146, 232, 236, 128, 238, 253, 113, - 179, 224, 199, 158, 244, 147, 145, 143, 33, 14, 113, 18, 131, 166, 9, 120, 156, 51, 52, 48, 48, - 51, 49, 81, 200, 204, 75, 73, 173, 208, 43, 41, 102, 136, 168, 106, 126, 242, 175, 223, 38, 98, - 70, 233, 150, 78, 150, 124, 14, 231, 143, 7, 62, 94, 51, 49, 0, 2, 133, 130, 162, 84, 221, 130, - 162, 252, 228, 212, 226, 226, 252, 34, 134, 208, 157, 186, 31, 47, 168, 153, 190, 187, 95, 115, - 108, 227, 228, 83, 235, 117, 121, 131, 174, 139, 64, 213, 149, 22, 103, 232, 38, 38, 151, 100, - 230, 231, 49, 52, 40, 206, 93, 105, 157, 116, 86, 124, 173, 114, 144, 189, 237, 242, 87, 182, 11, - 252, 252, 37, 13, 33, 54, 150, 84, 22, 164, 22, 131, 108, 220, 173, 86, 243, 146, 77, 108, 86, 94, - 242, 15, 230, 152, 0, 149, 101, 82, 254, 150, 133, 34, 0, 149, 149, 58, 0, 240, 2, 29, 168, 35, - 56, 248, 127, 160, 194, 136, 30, 255, 23, 237, 239, 105, 74, 117, 135, 167, 30, 120, 156, 1, 32, - 0, 223, 255, 144, 5, 144, 5, 176, 219, 1, 20, 63, 52, 140, 77, 183, 79, 72, 185, 200, 244, 208, - 70, 171, 244, 153, 171, 125, 118, 158, 108, 147, 239, 1, 161, 237, 231, 15, 244, 245, 38, 150, - 215, 66, 131, 112, 24, 188, 81, 66, 54, 3, 47, 142, 65, 47, 209, 115, 29, 175, 162, 120, 156, 133, - 145, 207, 74, 195, 64, 16, 198, 9, 168, 72, 213, 98, 65, 240, 38, 35, 8, 73, 49, 166, 34, 232, 33, - 197, 163, 23, 241, 31, 84, 111, 185, 108, 55, 211, 100, 53, 217, 141, 201, 4, 44, 180, 244, 21, - 132, 122, 247, 25, 36, 23, 61, 122, 241, 230, 75, 248, 36, 198, 38, 182, 181, 20, 220, 211, 238, - 247, 205, 247, 155, 217, 221, 175, 163, 39, 251, 241, 237, 229, 181, 166, 13, 0, 26, 13, 224, 62, - 147, 30, 130, 144, 20, 43, 55, 229, 232, 66, 71, 197, 192, 89, 68, 105, 44, 164, 7, 132, 9, 129, - 203, 136, 1, 147, 46, 144, 130, 54, 75, 16, 200, 199, 194, 81, 178, 2, 192, 149, 76, 84, 128, 86, - 160, 60, 35, 63, 2, 232, 45, 250, 9, 139, 142, 200, 121, 81, 154, 248, 35, 130, 13, 142, 140, 241, - 222, 106, 43, 183, 107, 59, 114, 176, 55, 187, 28, 169, 195, 238, 8, 0, 112, 218, 186, 188, 176, - 146, 18, 211, 53, 126, 115, 245, 113, 129, 238, 200, 57, 0, 198, 73, 40, 105, 207, 245, 102, 224, - 135, 19, 122, 145, 250, 143, 173, 155, 185, 157, 125, 104, 253, 133, 179, 233, 27, 235, 101, 79, - 208, 205, 217, 169, 11, 199, 4, 153, 6, 129, 9, 7, 245, 122, 51, 39, 196, 152, 63, 173, 132, 194, - 107, 86, 250, 217, 245, 82, 176, 56, 124, 95, 217, 169, 149, 221, 1, 172, 91, 37, 164, 161, 195, - 240, 121, 217, 200, 54, 87, 63, 181, 155, 177, 181, 77, 49, 34, 244, 122, 19, 33, 98, 49, 74, 250, - 35, 177, 148, 252, 252, 23, 167, 37, 174, 194, 80, 16, 225, 180, 58, 92, 175, 110, 236, 79, 74, - 66, 76, 18, 230, 225, 28, 210, 73, 200, 68, 80, 138, 217, 113, 85, 91, 219, 2, 124, 64, 110, 130, - 135, 116, 197, 248, 221, 57, 18, 51, 33, 149, 81, 190, 135, 126, 179, 242, 13, 46, 251, 173, 171, - 252, 2, 168, 181, 18, 111, 58, 162, 44, 164, 252, 45, 191, 90, 102, 62, 188, 201, 11, 195, 100, - 87, 120, 156, 123, 207, 58, 155, 109, 131, 3, 147, 66, 98, 81, 113, 106, 64, 105, 113, 134, 94, - 86, 49, 195, 161, 147, 121, 109, 119, 159, 248, 237, 12, 120, 86, 168, 177, 53, 168, 198, 185, 33, - 84, 99, 241, 100, 19, 166, 221, 0, 158, 48, 18, 254, 188, 162, 5, 120, 156, 197, 90, 93, 143, 219, - 198, 21, 125, 247, 175, 224, 155, 214, 128, 44, 113, 134, 51, 28, 142, 3, 23, 109, 218, 230, 161, - 45, 146, 0, 9, 208, 135, 32, 15, 92, 137, 235, 85, 172, 93, 109, 68, 110, 29, 163, 200, 127, 239, - 220, 115, 238, 165, 180, 201, 122, 99, 215, 41, 26, 68, 22, 69, 206, 199, 253, 56, 247, 220, 123, - 103, 185, 57, 220, 142, 83, 181, 185, 238, 119, 213, 171, 234, 56, 252, 120, 191, 59, 14, 23, 11, - 249, 189, 120, 254, 217, 179, 13, 158, 246, 155, 105, 87, 46, 206, 7, 172, 86, 235, 241, 184, 89, - 223, 29, 15, 63, 189, 91, 235, 243, 245, 159, 240, 125, 154, 87, 158, 110, 134, 113, 60, 28, 223, - 59, 115, 30, 49, 174, 239, 238, 199, 235, 23, 92, 105, 125, 215, 31, 199, 225, 235, 114, 227, 180, - 214, 240, 211, 221, 176, 153, 202, 66, 34, 218, 138, 191, 62, 123, 166, 15, 203, 226, 159, 31, - 182, 239, 202, 211, 207, 239, 175, 174, 134, 227, 234, 234, 120, 184, 185, 248, 238, 89, 85, 133, - 110, 137, 79, 46, 31, 87, 215, 203, 249, 198, 239, 244, 57, 219, 226, 247, 250, 52, 126, 89, 197, - 166, 124, 218, 242, 113, 178, 131, 171, 29, 31, 69, 85, 32, 166, 242, 9, 162, 81, 25, 27, 114, 81, - 47, 243, 198, 172, 38, 254, 137, 158, 31, 25, 129, 105, 129, 171, 98, 26, 22, 229, 34, 167, 77, - 240, 36, 39, 10, 96, 139, 66, 24, 175, 27, 100, 21, 44, 217, 30, 137, 18, 59, 167, 75, 233, 234, - 46, 150, 61, 49, 232, 180, 69, 182, 73, 243, 83, 40, 18, 85, 194, 40, 15, 90, 91, 197, 217, 207, - 250, 87, 27, 56, 252, 116, 118, 79, 6, 149, 201, 162, 195, 60, 9, 91, 97, 49, 151, 108, 63, 60, - 83, 43, 54, 38, 163, 171, 227, 108, 50, 199, 65, 98, 68, 206, 55, 67, 202, 221, 162, 125, 217, 33, - 194, 114, 179, 206, 206, 76, 110, 98, 139, 125, 76, 32, 179, 196, 44, 104, 157, 207, 4, 107, 103, - 37, 197, 66, 114, 187, 24, 161, 236, 208, 168, 7, 92, 221, 204, 250, 214, 231, 115, 120, 223, 20, - 133, 21, 69, 68, 162, 5, 22, 197, 117, 195, 239, 80, 158, 181, 113, 54, 154, 167, 171, 59, 83, - 183, 140, 72, 238, 193, 122, 234, 13, 115, 122, 112, 191, 70, 104, 167, 107, 182, 101, 175, 20, - 225, 33, 254, 239, 245, 162, 236, 33, 34, 139, 206, 178, 14, 92, 224, 229, 14, 208, 39, 83, 147, - 220, 202, 229, 89, 18, 81, 160, 189, 92, 136, 140, 93, 249, 225, 69, 173, 208, 168, 225, 32, 161, - 247, 102, 231, 142, 74, 136, 180, 190, 17, 185, 197, 50, 242, 67, 182, 240, 34, 188, 124, 203, - 208, 166, 145, 139, 50, 196, 3, 10, 94, 182, 72, 229, 94, 39, 43, 139, 181, 60, 70, 136, 140, 173, - 87, 59, 249, 32, 122, 200, 6, 93, 212, 112, 233, 44, 92, 188, 19, 247, 74, 28, 4, 111, 248, 18, - 185, 189, 248, 24, 208, 104, 177, 99, 185, 74, 242, 12, 183, 32, 21, 162, 0, 209, 22, 130, 26, - 177, 1, 121, 200, 170, 216, 182, 177, 1, 98, 167, 70, 108, 224, 33, 155, 232, 158, 53, 136, 69, 1, - 104, 137, 173, 37, 112, 114, 75, 91, 10, 238, 60, 204, 7, 140, 192, 116, 45, 232, 169, 19, 145, - 155, 206, 60, 96, 230, 68, 0, 3, 196, 13, 20, 208, 181, 162, 133, 132, 136, 169, 91, 89, 16, 201, - 56, 120, 9, 62, 107, 231, 152, 224, 194, 2, 110, 184, 35, 27, 230, 197, 99, 94, 124, 35, 143, 69, - 73, 248, 6, 209, 227, 5, 102, 157, 44, 35, 88, 242, 30, 174, 232, 136, 71, 39, 247, 125, 131, 136, - 2, 56, 220, 105, 19, 48, 74, 167, 145, 32, 58, 6, 165, 56, 25, 158, 35, 225, 36, 207, 100, 207, - 86, 102, 11, 254, 177, 109, 23, 116, 42, 16, 128, 208, 244, 177, 54, 91, 53, 176, 85, 110, 85, 14, - 89, 194, 27, 70, 32, 48, 162, 78, 118, 112, 13, 246, 117, 132, 139, 26, 4, 203, 58, 70, 11, 172, - 38, 32, 206, 248, 17, 212, 28, 138, 41, 217, 86, 118, 20, 223, 54, 50, 80, 141, 238, 27, 167, 222, - 134, 195, 100, 110, 132, 255, 25, 83, 192, 155, 172, 158, 101, 78, 52, 172, 196, 95, 6, 150, 236, - 33, 42, 35, 182, 16, 128, 57, 156, 98, 75, 2, 214, 55, 89, 33, 79, 207, 211, 174, 8, 163, 86, 29, - 32, 254, 135, 165, 154, 192, 232, 242, 226, 70, 68, 84, 98, 232, 121, 90, 26, 176, 17, 195, 10, - 144, 32, 79, 84, 35, 139, 69, 32, 187, 196, 21, 252, 27, 103, 18, 66, 236, 211, 9, 162, 161, 96, - 75, 166, 136, 86, 32, 190, 12, 83, 193, 204, 226, 32, 192, 129, 68, 3, 143, 182, 65, 35, 2, 35, - 16, 88, 192, 124, 99, 36, 146, 12, 240, 113, 54, 4, 189, 215, 82, 28, 151, 192, 80, 240, 43, 185, - 21, 234, 39, 73, 11, 88, 200, 66, 206, 137, 43, 132, 223, 27, 216, 64, 188, 84, 219, 150, 240, 33, - 22, 64, 196, 215, 202, 180, 130, 102, 87, 27, 151, 147, 190, 33, 70, 103, 177, 153, 188, 185, 11, - 96, 14, 73, 41, 170, 177, 188, 8, 128, 101, 167, 49, 227, 82, 80, 187, 193, 216, 30, 219, 129, - 163, 162, 114, 57, 92, 147, 21, 78, 222, 103, 179, 71, 103, 128, 138, 243, 10, 244, 58, 64, 14, - 130, 241, 36, 15, 0, 18, 67, 189, 237, 234, 137, 80, 135, 216, 35, 134, 155, 153, 48, 156, 162, 8, - 246, 113, 224, 196, 0, 155, 1, 137, 249, 65, 116, 33, 33, 194, 83, 9, 20, 21, 148, 123, 196, 25, - 96, 250, 26, 206, 200, 116, 50, 204, 17, 130, 218, 11, 56, 23, 119, 10, 171, 202, 106, 88, 242, - 36, 113, 96, 202, 97, 140, 25, 223, 138, 11, 34, 135, 131, 163, 112, 31, 92, 27, 21, 160, 140, 21, - 89, 159, 98, 192, 223, 114, 51, 107, 244, 51, 130, 78, 113, 101, 114, 170, 127, 152, 218, 141, 15, - 196, 63, 164, 21, 152, 106, 46, 41, 68, 73, 79, 198, 7, 106, 59, 227, 143, 84, 171, 252, 100, 140, - 100, 196, 61, 151, 14, 34, 2, 70, 122, 104, 106, 218, 150, 24, 132, 47, 192, 42, 157, 89, 95, 224, - 36, 233, 172, 86, 132, 50, 41, 69, 13, 60, 236, 175, 177, 65, 47, 64, 44, 67, 6, 216, 215, 129, - 159, 17, 101, 53, 182, 112, 72, 170, 217, 202, 2, 184, 6, 238, 118, 118, 31, 25, 206, 161, 242, 1, - 97, 52, 15, 18, 60, 178, 36, 51, 30, 214, 143, 115, 193, 2, 46, 226, 30, 48, 34, 224, 132, 154, - 215, 50, 118, 173, 30, 7, 132, 9, 73, 194, 77, 147, 38, 10, 23, 166, 149, 83, 98, 21, 126, 7, 5, - 19, 228, 206, 234, 102, 216, 10, 185, 147, 36, 234, 132, 67, 168, 137, 122, 122, 54, 80, 202, 70, - 21, 128, 23, 18, 55, 34, 82, 120, 10, 98, 34, 110, 69, 106, 198, 71, 71, 10, 137, 116, 21, 144, - 40, 43, 139, 37, 160, 153, 114, 17, 96, 242, 208, 50, 74, 45, 178, 49, 130, 85, 198, 182, 173, 6, - 172, 236, 2, 223, 107, 109, 36, 166, 50, 220, 194, 47, 73, 9, 6, 117, 10, 70, 118, 134, 39, 25, - 67, 76, 32, 84, 179, 154, 62, 91, 185, 130, 140, 29, 85, 69, 66, 81, 139, 53, 100, 55, 98, 82, 44, - 72, 218, 103, 64, 156, 132, 143, 90, 227, 88, 21, 40, 191, 197, 242, 224, 153, 218, 124, 33, 38, - 198, 254, 9, 188, 216, 161, 140, 211, 66, 135, 245, 48, 50, 123, 59, 87, 205, 72, 45, 144, 79, - 166, 162, 232, 105, 180, 97, 67, 164, 204, 121, 32, 205, 148, 142, 120, 15, 10, 13, 48, 49, 98, - 79, 16, 47, 202, 154, 96, 100, 7, 194, 30, 190, 68, 204, 7, 205, 71, 167, 91, 240, 54, 66, 10, - 129, 200, 100, 231, 45, 129, 193, 24, 77, 160, 153, 58, 141, 32, 228, 184, 70, 195, 151, 202, 0, - 229, 96, 192, 206, 204, 203, 234, 30, 73, 54, 233, 14, 93, 163, 85, 18, 81, 238, 231, 124, 147, - 88, 74, 137, 215, 36, 193, 129, 231, 29, 106, 62, 175, 48, 242, 2, 67, 49, 13, 115, 27, 236, 89, - 27, 237, 55, 170, 51, 221, 5, 62, 138, 157, 94, 129, 39, 29, 193, 68, 115, 138, 149, 230, 42, 8, - 66, 67, 95, 230, 26, 18, 3, 194, 169, 214, 244, 215, 41, 158, 24, 46, 50, 10, 23, 13, 178, 82, - 203, 176, 4, 97, 51, 235, 9, 129, 195, 18, 126, 169, 197, 34, 180, 17, 221, 177, 151, 68, 77, 138, - 186, 136, 128, 17, 168, 5, 43, 73, 194, 108, 225, 113, 175, 69, 45, 93, 70, 4, 52, 172, 33, 37, - 95, 56, 181, 140, 200, 166, 205, 7, 162, 61, 49, 3, 212, 103, 233, 33, 171, 139, 26, 229, 126, - 214, 108, 62, 25, 245, 229, 160, 168, 132, 37, 89, 140, 209, 86, 86, 46, 129, 189, 152, 136, 147, - 198, 161, 147, 90, 170, 121, 152, 39, 200, 20, 245, 210, 74, 119, 36, 139, 64, 66, 176, 239, 52, - 211, 0, 17, 165, 146, 136, 239, 90, 37, 54, 198, 55, 178, 125, 157, 21, 224, 222, 202, 141, 86, - 243, 182, 146, 192, 60, 188, 54, 208, 186, 54, 43, 151, 26, 166, 194, 172, 134, 241, 170, 159, - 115, 72, 212, 44, 44, 99, 160, 22, 210, 44, 242, 71, 107, 208, 68, 80, 128, 205, 59, 70, 187, 60, - 18, 223, 53, 192, 63, 182, 208, 238, 137, 213, 140, 229, 10, 88, 62, 69, 13, 179, 51, 67, 177, - 132, 14, 6, 5, 20, 116, 81, 129, 95, 235, 115, 243, 90, 66, 153, 131, 120, 16, 44, 206, 189, 172, - 40, 132, 78, 166, 37, 246, 64, 159, 96, 45, 224, 150, 149, 154, 97, 15, 176, 79, 6, 10, 212, 159, - 22, 169, 50, 72, 84, 64, 7, 201, 243, 139, 172, 229, 67, 12, 150, 199, 208, 24, 136, 193, 90, 150, - 39, 96, 17, 180, 221, 74, 36, 160, 134, 100, 160, 131, 202, 50, 68, 152, 160, 97, 56, 161, 110, - 110, 52, 84, 211, 220, 58, 88, 216, 161, 40, 246, 201, 132, 150, 218, 84, 240, 28, 173, 179, 136, - 89, 97, 133, 182, 20, 139, 100, 107, 54, 104, 227, 28, 9, 90, 24, 174, 81, 132, 34, 232, 176, 109, - 164, 1, 192, 10, 40, 207, 4, 241, 34, 164, 124, 39, 6, 47, 138, 165, 89, 97, 12, 101, 140, 39, - 234, 34, 238, 154, 107, 78, 118, 15, 89, 45, 159, 145, 193, 128, 73, 184, 63, 168, 25, 4, 25, 44, - 116, 59, 245, 23, 202, 59, 80, 60, 234, 173, 100, 94, 11, 170, 155, 8, 0, 69, 12, 118, 193, 178, - 6, 171, 102, 184, 57, 90, 210, 200, 86, 66, 224, 148, 15, 119, 26, 132, 61, 83, 170, 182, 181, - 156, 213, 153, 166, 212, 30, 176, 210, 160, 5, 104, 101, 193, 196, 109, 132, 167, 144, 203, 245, - 64, 46, 88, 54, 203, 90, 111, 204, 229, 114, 109, 157, 25, 91, 120, 30, 19, 36, 234, 137, 236, - 205, 40, 81, 148, 47, 181, 176, 66, 131, 204, 154, 208, 179, 126, 36, 215, 91, 255, 7, 139, 55, - 122, 204, 135, 196, 220, 217, 249, 7, 60, 156, 180, 42, 214, 230, 24, 173, 162, 179, 194, 158, - 221, 183, 157, 75, 120, 103, 71, 44, 52, 47, 211, 132, 182, 151, 56, 28, 1, 109, 184, 89, 44, 162, - 172, 166, 229, 100, 182, 185, 28, 253, 96, 171, 135, 136, 172, 210, 157, 134, 18, 194, 132, 177, - 210, 90, 172, 216, 177, 4, 18, 10, 200, 13, 69, 31, 144, 44, 150, 8, 167, 3, 61, 242, 45, 112, - 146, 31, 244, 180, 60, 81, 8, 122, 46, 101, 69, 74, 52, 202, 2, 16, 88, 239, 128, 197, 19, 75, 72, - 33, 42, 156, 142, 212, 22, 233, 45, 155, 37, 48, 58, 140, 24, 148, 79, 216, 141, 130, 8, 144, 155, - 120, 192, 23, 21, 75, 60, 71, 209, 54, 73, 129, 152, 20, 9, 52, 137, 29, 4, 224, 0, 130, 7, 46, - 86, 82, 36, 29, 205, 35, 34, 152, 187, 153, 211, 113, 176, 158, 194, 219, 169, 96, 120, 88, 43, - 163, 176, 22, 215, 117, 38, 46, 79, 78, 180, 180, 5, 85, 192, 149, 48, 163, 140, 110, 237, 144, 7, - 58, 228, 185, 25, 176, 154, 89, 251, 186, 108, 198, 139, 179, 219, 81, 219, 66, 39, 160, 166, 209, - 226, 86, 15, 52, 16, 81, 236, 9, 148, 88, 105, 66, 88, 194, 234, 73, 145, 94, 72, 86, 32, 214, - 105, 211, 73, 33, 209, 140, 226, 39, 128, 96, 38, 87, 92, 69, 107, 233, 63, 170, 155, 251, 224, - 94, 110, 62, 15, 121, 50, 75, 51, 101, 249, 179, 11, 30, 233, 72, 42, 213, 174, 95, 194, 23, 96, - 147, 29, 32, 78, 157, 78, 200, 101, 243, 110, 37, 41, 79, 27, 217, 3, 47, 217, 44, 241, 126, 176, - 234, 163, 13, 122, 212, 146, 172, 186, 208, 163, 173, 172, 213, 130, 157, 181, 1, 237, 167, 190, - 239, 227, 251, 185, 15, 236, 230, 196, 78, 239, 235, 231, 28, 11, 29, 235, 48, 89, 215, 36, 214, - 102, 243, 119, 176, 227, 53, 156, 126, 33, 241, 179, 18, 103, 117, 237, 216, 103, 68, 205, 6, 148, - 0, 56, 37, 239, 88, 63, 235, 137, 71, 103, 176, 79, 214, 176, 69, 51, 43, 90, 64, 244, 97, 243, - 161, 59, 251, 128, 186, 97, 30, 47, 214, 205, 203, 79, 234, 236, 158, 236, 235, 244, 47, 62, 31, - 221, 217, 33, 216, 121, 44, 214, 88, 160, 163, 44, 10, 150, 47, 197, 98, 248, 75, 3, 207, 59, 103, - 190, 144, 101, 80, 48, 104, 95, 33, 234, 101, 45, 220, 232, 88, 68, 73, 90, 254, 178, 179, 99, - 246, 239, 52, 238, 121, 248, 198, 182, 110, 62, 80, 253, 196, 198, 238, 201, 182, 14, 168, 253, - 228, 198, 238, 137, 182, 14, 140, 254, 201, 141, 221, 83, 109, 157, 229, 214, 79, 107, 236, 158, - 106, 235, 172, 210, 249, 205, 198, 14, 199, 158, 0, 93, 242, 198, 93, 248, 135, 7, 66, 173, 66, - 30, 167, 72, 177, 35, 41, 224, 47, 93, 154, 47, 188, 218, 55, 91, 149, 146, 81, 67, 5, 189, 205, - 179, 121, 32, 54, 153, 55, 101, 229, 198, 242, 76, 84, 62, 68, 37, 129, 146, 9, 252, 151, 25, 174, - 86, 162, 51, 60, 113, 86, 6, 65, 230, 130, 99, 166, 2, 18, 104, 162, 182, 237, 92, 2, 192, 202, - 161, 155, 169, 202, 122, 21, 182, 119, 57, 207, 53, 58, 98, 38, 126, 114, 127, 247, 72, 119, 135, - 10, 253, 119, 236, 239, 152, 55, 156, 34, 173, 227, 81, 140, 229, 60, 254, 65, 33, 105, 153, 73, - 215, 89, 97, 12, 95, 160, 168, 64, 203, 185, 212, 202, 131, 93, 27, 234, 134, 164, 246, 125, 164, - 181, 211, 99, 72, 141, 36, 166, 108, 48, 216, 82, 207, 219, 252, 67, 226, 138, 22, 193, 216, 149, - 53, 110, 167, 86, 243, 115, 137, 3, 171, 225, 47, 64, 145, 124, 38, 122, 36, 173, 161, 16, 229, - 12, 49, 75, 66, 188, 232, 52, 26, 89, 252, 224, 159, 185, 250, 71, 207, 34, 23, 209, 206, 175, - 208, 200, 2, 39, 162, 115, 64, 82, 154, 255, 78, 129, 242, 215, 177, 106, 37, 187, 182, 118, 84, - 21, 236, 244, 64, 107, 51, 67, 109, 108, 12, 214, 94, 243, 137, 108, 32, 144, 107, 205, 118, 12, - 14, 224, 133, 133, 179, 29, 43, 212, 22, 244, 50, 138, 164, 227, 72, 13, 242, 88, 74, 216, 86, - 143, 37, 231, 12, 216, 106, 134, 34, 126, 91, 85, 189, 35, 155, 39, 69, 5, 254, 156, 26, 85, 112, - 22, 5, 245, 124, 220, 65, 196, 61, 251, 254, 249, 252, 98, 3, 223, 135, 248, 75, 63, 245, 213, - 171, 234, 223, 101, 161, 221, 246, 101, 181, 40, 156, 210, 198, 80, 168, 57, 52, 181, 91, 200, - 250, 211, 187, 187, 161, 60, 144, 119, 40, 240, 251, 102, 152, 174, 15, 50, 244, 235, 175, 190, - 249, 150, 35, 118, 55, 195, 56, 245, 55, 119, 47, 171, 7, 211, 229, 217, 113, 184, 59, 148, 177, - 111, 142, 187, 241, 109, 25, 180, 126, 189, 155, 94, 224, 253, 140, 85, 185, 42, 179, 127, 46, - 242, 108, 135, 113, 115, 220, 93, 14, 23, 139, 63, 95, 15, 155, 55, 213, 116, 221, 79, 149, 236, - 55, 140, 213, 166, 191, 173, 46, 135, 10, 111, 109, 108, 23, 203, 170, 31, 223, 221, 110, 170, - 139, 231, 213, 171, 63, 80, 232, 233, 98, 241, 229, 161, 26, 142, 199, 195, 177, 26, 175, 15, 247, - 251, 173, 140, 151, 109, 143, 211, 176, 173, 174, 202, 237, 190, 250, 87, 191, 223, 109, 177, 100, - 245, 118, 55, 93, 151, 59, 227, 238, 246, 245, 126, 168, 54, 135, 155, 155, 93, 177, 197, 237, 86, - 47, 97, 143, 211, 58, 220, 183, 58, 220, 79, 143, 236, 93, 85, 231, 150, 44, 86, 188, 29, 222, - 218, 11, 45, 43, 190, 176, 114, 129, 97, 213, 153, 177, 87, 187, 237, 242, 215, 55, 197, 200, 143, - 220, 166, 173, 31, 27, 111, 38, 127, 228, 153, 232, 206, 219, 197, 217, 39, 41, 143, 195, 143, - 226, 232, 234, 242, 176, 125, 247, 114, 126, 173, 229, 231, 135, 67, 198, 251, 189, 188, 8, 211, - 191, 237, 119, 103, 239, 216, 172, 134, 159, 134, 205, 69, 153, 178, 212, 125, 206, 22, 62, 236, - 135, 213, 254, 240, 250, 98, 193, 201, 197, 215, 203, 234, 111, 223, 124, 245, 229, 106, 156, 142, - 197, 198, 187, 171, 119, 23, 124, 178, 172, 110, 239, 247, 251, 2, 202, 231, 58, 155, 47, 218, - 232, 211, 21, 60, 248, 124, 53, 29, 86, 151, 195, 234, 170, 223, 143, 195, 99, 163, 46, 247, 135, - 205, 155, 97, 251, 155, 227, 198, 105, 184, 27, 191, 171, 191, 199, 197, 151, 253, 205, 128, 25, - 195, 143, 247, 253, 254, 98, 193, 87, 128, 250, 205, 155, 47, 118, 251, 97, 241, 168, 52, 69, 245, - 31, 202, 207, 243, 89, 6, 225, 199, 39, 220, 31, 247, 231, 131, 175, 167, 233, 110, 124, 185, 22, - 184, 95, 223, 95, 174, 10, 182, 214, 239, 9, 129, 71, 87, 187, 60, 246, 183, 155, 235, 243, 5, - 143, 195, 213, 184, 190, 30, 250, 237, 184, 206, 201, 189, 152, 222, 47, 200, 56, 28, 207, 39, - 254, 189, 108, 91, 253, 243, 189, 195, 137, 250, 111, 15, 231, 83, 98, 215, 12, 117, 105, 87, 218, - 43, 183, 105, 47, 183, 219, 16, 10, 231, 116, 87, 67, 59, 12, 87, 125, 220, 148, 0, 111, 55, 93, - 222, 230, 167, 86, 252, 226, 120, 184, 57, 95, 179, 31, 98, 170, 135, 182, 176, 194, 208, 109, - 182, 155, 97, 115, 229, 210, 144, 55, 97, 219, 197, 92, 56, 119, 168, 183, 125, 179, 9, 126, 243, - 212, 154, 130, 110, 241, 105, 241, 223, 112, 59, 253, 175, 86, 159, 142, 195, 3, 180, 148, 148, - 31, 243, 149, 43, 107, 6, 23, 251, 212, 13, 177, 100, 173, 46, 108, 183, 93, 169, 240, 242, 54, - 108, 202, 127, 151, 109, 95, 127, 200, 218, 253, 125, 137, 231, 143, 119, 143, 77, 231, 175, 233, - 191, 112, 240, 67, 1, 254, 122, 211, 239, 246, 191, 196, 246, 116, 184, 187, 30, 142, 43, 129, - 232, 31, 111, 251, 73, 190, 101, 242, 135, 172, 90, 184, 104, 236, 95, 159, 91, 77, 89, 105, 33, - 56, 125, 89, 245, 219, 31, 238, 11, 189, 108, 133, 91, 55, 253, 221, 116, 127, 28, 200, 195, 74, - 191, 55, 227, 235, 74, 94, 171, 51, 66, 89, 60, 160, 175, 223, 182, 200, 183, 198, 134, 231, 58, - 49, 21, 185, 46, 54, 255, 111, 21, 126, 150, 164, 91, 85, 235, 53, 178, 213, 55, 76, 46, 227, 48, - 85, 135, 55, 175, 192, 96, 213, 238, 170, 186, 61, 76, 213, 238, 182, 162, 131, 118, 143, 39, 187, - 178, 194, 135, 228, 156, 69, 169, 195, 202, 236, 133, 36, 21, 249, 126, 61, 72, 246, 114, 168, 11, - 22, 211, 245, 110, 84, 118, 91, 75, 166, 120, 177, 27, 95, 148, 189, 95, 28, 222, 208, 76, 103, - 123, 60, 157, 14, 200, 230, 220, 122, 9, 185, 255, 81, 64, 116, 90, 227, 9, 122, 159, 142, 247, - 131, 142, 19, 219, 200, 231, 63, 180, 195, 212, 177, 240, 2, 224, 16, 152, 60, 5, 50, 165, 6, 153, - 5, 155, 56, 162, 8, 96, 31, 156, 9, 238, 140, 120, 156, 187, 200, 115, 145, 103, 195, 74, 86, 17, - 25, 105, 157, 254, 148, 168, 195, 170, 243, 171, 47, 212, 199, 116, 38, 95, 127, 24, 243, 226, - 241, 228, 189, 172, 83, 0, 225, 116, 15, 62, 254, 1, 137, 133, 157, 21, 251, 180, 160, 188, 206, - 135, 191, 105, 98, 239, 115, 191, 202, 198, 146, 137, 120, 156, 219, 204, 180, 153, 105, 194, 46, - 145, 132, 148, 30, 206, 0, 117, 161, 251, 231, 23, 48, 88, 10, 26, 120, 204, 49, 76, 112, 251, 51, - 241, 92, 42, 0, 175, 39, 11, 238, 254, 1, 179, 193, 177, 252, 130, 173, 32, 172, 145, 25, 128, - 205, 95, 40, 239, 37, 5, 13, 48, 103, 120, 156, 219, 192, 184, 129, 113, 66, 181, 72, 95, 193, 78, - 15, 155, 135, 7, 210, 124, 95, 180, 243, 55, 89, 173, 139, 153, 94, 42, 165, 55, 177, 95, 17, 0, - 203, 243, 12, 234, 166, 9, 120, 156, 51, 52, 48, 48, 51, 49, 81, 200, 204, 75, 73, 173, 208, 43, - 41, 102, 136, 168, 106, 126, 242, 175, 223, 38, 98, 70, 233, 150, 78, 150, 124, 14, 231, 143, 7, - 62, 94, 51, 49, 0, 2, 133, 130, 162, 84, 221, 130, 162, 252, 228, 212, 226, 226, 252, 34, 134, - 208, 157, 186, 31, 47, 168, 153, 190, 187, 95, 115, 108, 227, 228, 83, 235, 117, 121, 131, 174, - 139, 64, 213, 149, 22, 103, 232, 38, 38, 151, 100, 230, 231, 49, 120, 186, 158, 186, 240, 178, - 233, 194, 73, 102, 190, 226, 174, 151, 186, 110, 98, 38, 139, 254, 174, 54, 132, 216, 88, 82, 89, - 144, 90, 12, 178, 113, 183, 90, 205, 75, 54, 177, 89, 121, 201, 63, 152, 99, 2, 84, 150, 73, 249, - 91, 22, 138, 0, 0, 241, 97, 60, 0, 240, 2, 29, 168, 35, 56, 248, 127, 160, 194, 136, 30, 255, 23, - 237, 239, 105, 74, 117, 135, 167, 30, 120, 156, 1, 32, 0, 223, 255, 144, 5, 144, 5, 176, 219, 1, - 20, 127, 49, 60, 26, 210, 115, 209, 143, 23, 37, 16, 222, 210, 126, 210, 106, 226, 169, 206, 174, - 147, 239, 1, 161, 221, 228, 15, 87, 235, 2, 161, 85, 120, 156, 155, 98, 245, 205, 114, 194, 78, - 233, 204, 220, 130, 252, 162, 18, 5, 45, 133, 196, 98, 133, 210, 146, 204, 28, 133, 180, 162, 252, - 92, 5, 117, 16, 115, 227, 182, 35, 108, 155, 183, 177, 199, 137, 2, 0, 124, 181, 17, 6, 240, 2, - 224, 16, 152, 60, 5, 50, 165, 6, 153, 5, 155, 56, 162, 8, 96, 31, 156, 9, 238, 140, 120, 156, 1, - 32, 0, 223, 255, 209, 12, 209, 12, 176, 169, 5, 20, 74, 65, 131, 50, 107, 113, 230, 114, 231, 216, - 102, 25, 202, 231, 246, 248, 64, 46, 179, 105, 147, 189, 5, 148, 248, 3, 15, 241, 254, 1, 137, - 133, 157, 21, 251, 180, 160, 188, 206, 135, 191, 105, 98, 239, 115, 191, 202, 198, 146, 137, 120, - 156, 1, 30, 0, 225, 255, 179, 2, 179, 2, 144, 186, 20, 89, 17, 51, 252, 210, 252, 47, 239, 152, - 111, 118, 189, 211, 171, 86, 247, 39, 115, 216, 188, 145, 206, 101, 231, 103, 16, 69, 254, 1, 179, - 193, 177, 252, 130, 173, 32, 172, 145, 25, 128, 205, 95, 40, 239, 37, 5, 13, 48, 103, 120, 156, - 219, 192, 184, 129, 113, 66, 181, 200, 76, 253, 153, 162, 149, 114, 17, 45, 5, 123, 236, 213, 75, - 27, 95, 69, 95, 87, 191, 160, 57, 177, 95, 17, 0, 183, 85, 12, 112, 166, 9, 120, 156, 51, 52, 48, - 48, 51, 49, 81, 200, 204, 75, 73, 173, 208, 43, 41, 102, 136, 168, 106, 126, 242, 175, 223, 38, - 98, 70, 233, 150, 78, 150, 124, 14, 231, 143, 7, 62, 94, 51, 49, 0, 2, 133, 130, 162, 84, 221, - 130, 162, 252, 228, 212, 226, 226, 252, 34, 134, 208, 157, 186, 31, 47, 168, 153, 190, 187, 95, - 115, 108, 227, 228, 83, 235, 117, 121, 131, 174, 139, 64, 213, 149, 22, 103, 232, 38, 38, 151, - 100, 230, 231, 49, 164, 103, 172, 223, 223, 241, 226, 254, 99, 158, 21, 50, 31, 222, 124, 93, 101, - 95, 244, 124, 249, 113, 67, 136, 141, 37, 149, 5, 169, 197, 32, 27, 119, 171, 213, 188, 100, 19, - 155, 149, 151, 252, 131, 57, 38, 64, 101, 153, 148, 191, 101, 161, 8, 0, 99, 166, 62, 150, 240, 2, - 29, 168, 35, 56, 248, 127, 160, 194, 136, 30, 255, 23, 237, 239, 105, 74, 117, 135, 167, 30, 120, - 156, 1, 32, 0, 223, 255, 144, 5, 144, 5, 176, 219, 1, 20, 71, 34, 183, 94, 158, 203, 170, 37, 254, - 94, 138, 249, 65, 34, 11, 142, 121, 227, 207, 19, 147, 239, 1, 161, 227, 254, 14, 190, 230, 4, - 165, 33, 120, 156, 155, 98, 245, 208, 98, 194, 206, 141, 187, 218, 25, 181, 173, 98, 242, 212, 21, - 180, 21, 188, 130, 253, 253, 244, 138, 75, 138, 50, 243, 210, 51, 211, 42, 53, 138, 82, 11, 53, - 129, 162, 234, 49, 121, 137, 201, 37, 153, 249, 121, 86, 10, 64, 69, 147, 39, 178, 139, 49, 106, - 110, 254, 201, 212, 200, 178, 121, 27, 123, 156, 40, 0, 128, 199, 23, 236, 249, 133, 51, 41, 123, - 134, 199, 29, 111, 223, 229, 205, 27, 146, 86, 56, 160, 112, 117, 16, -]); - -const actionData2 = { - id: '1746612610060', - type: 'push', - method: 'POST', - timestamp: 1746612610060, - repo: 'kriswest/git-proxy.git', -}; - -// push with a commit message not terminated by a newline -const reqBodyNoNewline = Buffer.from([ - 48, 48, 99, 56, 101, 100, 50, 48, 51, 55, 101, 52, 102, 98, 54, 102, 97, 48, 51, 54, 50, 101, 53, - 50, 99, 101, 99, 48, 51, 102, 55, 98, 53, 48, 51, 102, 56, 49, 101, 50, 54, 48, 49, 48, 32, 101, - 97, 55, 100, 97, 55, 98, 56, 100, 100, 49, 51, 52, 99, 97, 49, 57, 102, 100, 99, 57, 97, 55, 52, - 55, 48, 52, 56, 48, 99, 54, 100, 57, 101, 51, 53, 99, 49, 52, 54, 32, 114, 101, 102, 115, 47, 104, - 101, 97, 100, 115, 47, 57, 55, 49, 45, 112, 97, 114, 115, 105, 110, 103, 45, 98, 117, 103, 45, - 102, 105, 120, 0, 32, 114, 101, 112, 111, 114, 116, 45, 115, 116, 97, 116, 117, 115, 45, 118, 50, - 32, 115, 105, 100, 101, 45, 98, 97, 110, 100, 45, 54, 52, 107, 32, 111, 98, 106, 101, 99, 116, 45, - 102, 111, 114, 109, 97, 116, 61, 115, 104, 97, 49, 32, 97, 103, 101, 110, 116, 61, 103, 105, 116, - 47, 50, 46, 51, 57, 46, 53, 46, 40, 65, 112, 112, 108, 101, 46, 71, 105, 116, 45, 49, 53, 52, 41, - 48, 48, 48, 48, 80, 65, 67, 75, 0, 0, 0, 2, 0, 0, 0, 6, 145, 18, 120, 156, 101, 143, 203, 106, - 196, 48, 12, 69, 247, 249, 10, 237, 11, 193, 143, 196, 15, 24, 134, 46, 166, 180, 165, 80, 10, 93, - 116, 237, 56, 50, 113, 167, 142, 131, 163, 62, 230, 239, 171, 206, 182, 90, 136, 123, 133, 238, - 65, 162, 134, 8, 163, 182, 222, 169, 52, 57, 84, 210, 121, 107, 210, 48, 141, 209, 122, 45, 133, - 155, 180, 84, 198, 88, 239, 189, 25, 98, 183, 133, 134, 43, 1, 206, 74, 104, 139, 67, 154, 76, 10, - 66, 27, 133, 163, 138, 24, 133, 78, 118, 26, 185, 59, 137, 202, 8, 41, 186, 240, 73, 75, 109, 240, - 128, 37, 112, 236, 117, 9, 173, 4, 56, 44, 87, 219, 239, 87, 43, 111, 215, 64, 223, 184, 83, 31, - 107, 57, 130, 180, 131, 180, 140, 28, 44, 220, 8, 174, 142, 167, 37, 19, 97, 131, 167, 150, 119, - 120, 227, 77, 56, 156, 89, 82, 221, 22, 108, 253, 95, 244, 31, 194, 24, 111, 197, 168, 24, 33, 25, - 209, 221, 205, 153, 32, 68, 202, 95, 120, 202, 13, 35, 213, 118, 233, 223, 119, 72, 124, 219, 125, - 166, 151, 86, 127, 46, 240, 92, 87, 96, 53, 195, 99, 217, 62, 176, 240, 159, 129, 114, 93, 127, 1, - 25, 61, 91, 97, 240, 2, 164, 79, 73, 234, 21, 174, 135, 115, 17, 30, 139, 192, 106, 142, 44, 173, - 216, 34, 72, 124, 120, 156, 1, 32, 0, 223, 255, 209, 12, 209, 12, 176, 169, 5, 20, 139, 208, 221, - 207, 118, 231, 169, 95, 67, 7, 214, 241, 232, 108, 55, 226, 13, 45, 89, 224, 147, 189, 5, 148, 16, - 197, 16, 115, 249, 3, 13, 7, 76, 69, 119, 226, 211, 188, 105, 170, 104, 199, 140, 64, 68, 33, 37, - 254, 75, 57, 120, 156, 219, 204, 180, 153, 105, 3, 19, 163, 97, 124, 194, 180, 123, 101, 137, 81, - 171, 213, 30, 70, 108, 62, 187, 255, 230, 227, 235, 27, 139, 67, 77, 12, 128, 64, 161, 52, 147, - 97, 235, 211, 3, 165, 250, 39, 54, 100, 58, 157, 206, 218, 161, 249, 225, 179, 126, 79, 152, 124, - 0, 0, 240, 135, 26, 148, 254, 1, 49, 119, 187, 89, 234, 50, 192, 75, 235, 7, 147, 84, 91, 104, 20, - 225, 71, 237, 3, 184, 120, 156, 219, 202, 184, 149, 113, 66, 172, 72, 133, 224, 2, 246, 27, 33, - 201, 11, 46, 45, 80, 119, 63, 119, 109, 97, 87, 127, 166, 183, 250, 196, 66, 23, 0, 196, 49, 13, - 44, 254, 1, 135, 220, 195, 128, 128, 52, 128, 76, 108, 225, 120, 117, 124, 134, 173, 157, 134, 26, - 70, 104, 120, 156, 59, 196, 120, 136, 113, 130, 148, 136, 129, 27, 243, 3, 127, 185, 242, 5, 181, - 49, 63, 11, 119, 86, 239, 220, 127, 202, 51, 90, 109, 162, 222, 20, 0, 185, 11, 12, 248, 254, 10, - 70, 111, 87, 177, 109, 10, 146, 22, 186, 250, 240, 85, 191, 249, 137, 133, 208, 176, 108, 20, 120, - 156, 123, 41, 240, 66, 104, 67, 13, 227, 230, 106, 198, 119, 76, 76, 250, 250, 147, 51, 89, 226, - 38, 23, 178, 152, 179, 232, 40, 36, 166, 76, 222, 200, 34, 54, 249, 20, 139, 244, 228, 151, 44, - 54, 147, 181, 89, 101, 39, 251, 179, 26, 128, 212, 212, 179, 38, 0, 177, 3, 68, 205, 9, 214, 251, - 44, 37, 69, 165, 169, 147, 215, 177, 117, 79, 182, 102, 151, 182, 210, 215, 87, 64, 2, 137, 229, - 137, 153, 37, 10, 41, 73, 122, 201, 69, 169, 137, 37, 169, 161, 197, 169, 69, 26, 5, 69, 249, 105, - 153, 57, 169, 122, 165, 64, 78, 94, 98, 110, 170, 142, 122, 98, 82, 114, 138, 186, 206, 228, 131, - 108, 194, 18, 234, 25, 169, 185, 137, 121, 37, 197, 25, 137, 69, 185, 137, 150, 6, 234, 58, 153, - 197, 142, 41, 185, 153, 121, 155, 131, 217, 69, 25, 1, 68, 104, 56, 92, 171, 18, 32, 137, 219, - 241, 114, 55, 57, 171, 216, 65, 107, 36, 144, 242, 200, 130, 86, 149, -]); - -const actionDataNoNewLine = { - id: '1746697059624', - type: 'push', - method: 'POST', - timestamp: 1746697059624, - repo: 'kriswest/git-proxy.git', -}; - -describe('Check that pushes can be parsed', async () => { - it('Should report an error when request body is missing', async () => { - const action = new actions.Action( - actionData.id, - actionData.type, - actionData.method, - actionData.timestamp, - actionData.repo, - ); - const req = {}; - const result = await processor.exec(req, action); - expect(result.error).to.be.true; - }); - - it('Should report an error when request body is truncated', async () => { - const action = new actions.Action( - actionData.id, - actionData.type, - actionData.method, - actionData.timestamp, - actionData.repo, - ); - const req = { body: truncatedReqBody }; - const result = await processor.exec(req, action); - expect(result.error).to.be.true; - }); - - it('Should NOT report an error for a valid push with a single commit and commitData should be parsed out', async () => { - const action = new actions.Action( - actionData.id, - actionData.type, - actionData.method, - actionData.timestamp, - actionData.repo, - ); - const req = { body: reqBody }; - const result = await processor.exec(req, action); - expect(result.error).to.be.false; - expect(result.blocked).to.be.false; - expect(result.steps[0].stepName).to.equal('parsePackFile'); - expect(result.project).to.equal('kriswest'); - expect(result.url).to.equal('https://github.com/kriswest/git-proxy.git'); - expect(result.branch).to.equal('refs/heads/971-test'); - expect(result.user).to.equal('Kris West'); - expect(result.commitTo).to.equal('583e02096f1c6bdd441068fe6eefa5c6546c89d9'); - expect(result.commitFrom).to.equal('ae570e6301e8cdcecf17e9c4d859129e0da3c42c'); - expect(result.commitData[0].parent).to.equal('ae570e6301e8cdcecf17e9c4d859129e0da3c42c'); - expect(result.commitData[0].tree).to.equal('24859f1ecf415a78e514184dd81349d4ccccb6a0'); - expect(result.commitData[0].author).to.equal('Kris West'); - expect(result.commitData[0].committer).to.equal('Kris West'); - expect(result.commitData[0].authorEmail).to.equal('kristopher.west@natwest.com'); - expect(result.commitData[0].message).to.equal( - 'test: adjust data capture with commit msg from console', - ); - expect(result.commitData[0].commitTimestamp).to.equal('1746541853'); - }); - - it('Should NOT report an error for a valid push with a single commit, but no newline at the end of the commit message, and commitData should be parsed out', async () => { - const action = new actions.Action( - actionDataNoNewLine.id, - actionDataNoNewLine.type, - actionDataNoNewLine.method, - actionDataNoNewLine.timestamp, - actionDataNoNewLine.repo, - ); - const req = { body: reqBodyNoNewline }; - const result = await processor.exec(req, action); - expect(result.error).to.be.false; - expect(result.blocked).to.be.false; - expect(result.steps[0].stepName).to.equal('parsePackFile'); - expect(result.project).to.equal('kriswest'); - expect(result.url).to.equal('https://github.com/kriswest/git-proxy.git'); - expect(result.branch).to.equal('refs/heads/971-parsing-bug-fix'); - expect(result.user).to.equal('Kris West'); - expect(result.commitTo).to.equal('ea7da7b8dd134ca19fdc9a7470480c6d9e35c146'); - expect(result.commitFrom).to.equal('ed2037e4fb6fa0362e52cec03f7b503f81e26010'); - expect(result.commitData[0].parent).to.equal('ed2037e4fb6fa0362e52cec03f7b503f81e26010'); - expect(result.commitData[0].tree).to.equal('537982fb8e218976f4b5c793108b31266799964c'); - expect(result.commitData[0].author).to.equal('Hemant Sharma'); - expect(result.commitData[0].committer).to.equal('Kris West'); - expect(result.commitData[0].authorEmail).to.equal('hemant.sharma1@natwest.com'); - expect(result.commitData[0].message).to.equal( - 'Edit activeDirectory.js for GitProxy Non Prod Implementation', - ); - expect(result.commitData[0].commitTimestamp).to.equal('1746697052'); - }); - - it('Should NOT report an error for a valid push with 2 commits and commitData should be parsed out', async () => { - const action2 = new actions.Action( - actionData2.id, - actionData2.type, - actionData2.method, - actionData2.timestamp, - actionData2.repo, - ); - const req2 = { body: reqBody2 }; - const result = await processor.exec(req2, action2); - expect(result.error).to.be.false; - expect(result.blocked).to.be.false; - expect(result.steps[0].stepName).to.equal('parsePackFile'); - expect(result.project).to.equal('kriswest'); - expect(result.url).to.equal('https://github.com/kriswest/git-proxy.git'); - expect(result.branch).to.equal('refs/heads/971-test'); - expect(result.user).to.equal('Kris West'); - expect(result.commitTo).to.equal('872509e67f23518ec859df34ddc5cf0e129106be'); - expect(result.commitFrom).to.equal('583e02096f1c6bdd441068fe6eefa5c6546c89d9'); - expect(result.commitData[0].parent).to.equal('583e02096f1c6bdd441068fe6eefa5c6546c89d9'); - expect(result.commitData[0].tree).to.equal('fd45a2c1c29c233ce59805d37743a2aab0f523fe'); - expect(result.commitData[0].author).to.equal('Kris West'); - expect(result.commitData[0].committer).to.equal('Kris West'); - expect(result.commitData[0].authorEmail).to.equal('kristopher.west@natwest.com'); - expect(result.commitData[0].message).to.equal('test: parsePush'); - expect(result.commitData[0].commitTimestamp).to.equal('1746612538'); - expect(result.commitData[0].message).to.equal('test: parsePush'); - }); -}); diff --git a/test/testParsePush.test.js b/test/testParsePush.test.js index 65c745ec1..a3e438e62 100644 --- a/test/testParsePush.test.js +++ b/test/testParsePush.test.js @@ -7,7 +7,7 @@ const { getCommitData, getPackMeta, parsePacketLines, - unpack + unpack, } = require('../src/proxy/processors/push-action/parsePush'); import { EMPTY_COMMIT_HASH, FLUSH_PACKET, PACK_SIGNATURE } from '../src/proxy/processors/constants'; @@ -18,7 +18,7 @@ import { EMPTY_COMMIT_HASH, FLUSH_PACKET, PACK_SIGNATURE } from '../src/proxy/pr * @param {string} commitContent - Content of the commit object. * @param {number} type - Type of the object (1 for commit). * @return {Buffer} - The generated PACK buffer. - */ + */ function createSamplePackBuffer( numEntries = 1, commitContent = 'tree 123\nparent 456\nauthor A 123 +0000\ncommitter C 456 +0000\n\nmessage', @@ -55,7 +55,7 @@ function createSamplePackBuffer( */ function createPacketLineBuffer(lines) { let buffer = Buffer.alloc(0); - lines.forEach(line => { + lines.forEach((line) => { const lengthInHex = (line.length + 4).toString(16).padStart(4, '0'); buffer = Buffer.concat([buffer, Buffer.from(lengthInHex, 'ascii'), Buffer.from(line, 'ascii')]); }); @@ -124,7 +124,7 @@ describe('parsePackFile', () => { const step = action.steps[0]; expect(step.stepName).to.equal('parsePackFile'); expect(step.error).to.be.true; - expect(step.errorMessage).to.include('No data received'); + expect(step.errorMessage).to.include('No body found in request'); }); it('should add error step if req.body is empty', async () => { @@ -135,7 +135,7 @@ describe('parsePackFile', () => { const step = action.steps[0]; expect(step.stepName).to.equal('parsePackFile'); expect(step.error).to.be.true; - expect(step.errorMessage).to.include('No data received'); + expect(step.errorMessage).to.include('No body found in request'); }); it('should add error step if no ref updates found', async () => { @@ -194,12 +194,13 @@ describe('parsePackFile', () => { const ref = 'refs/heads/main'; const packetLine = `${oldCommit} ${newCommit} ${ref}\0capabilities\n`; - const commitContent = "tree 1234567890abcdef1234567890abcdef12345678\n" + - "parent abcdef1234567890abcdef1234567890abcdef12\n" + - "author Test Author 1234567890 +0000\n" + - "committer Test Committer 1234567890 +0000\n\n" + - "feat: Add new feature\n\n" + - "This is the commit body."; + const commitContent = + 'tree 1234567890abcdef1234567890abcdef12345678\n' + + 'parent abcdef1234567890abcdef1234567890abcdef12\n' + + 'author Test Author 1234567890 +0000\n' + + 'committer Test Committer 1234567890 +0000\n\n' + + 'feat: Add new feature\n\n' + + 'This is the commit body.'; const commitContentBuffer = Buffer.from(commitContent, 'utf8'); zlibInflateStub.returns(commitContentBuffer); @@ -212,7 +213,7 @@ describe('parsePackFile', () => { expect(result).to.equal(action); // Check step and action properties - const step = action.steps.find(s => s.stepName === 'parsePackFile'); + const step = action.steps.find((s) => s.stepName === 'parsePackFile'); expect(step).to.exist; expect(step.error).to.be.false; expect(step.errorMessage).to.be.null; @@ -224,7 +225,7 @@ describe('parsePackFile', () => { expect(action.user).to.equal('Test Committer'); // Check parsed commit data - const commitMessages = action.commitData.map(commit => commit.message); + const commitMessages = action.commitData.map((commit) => commit.message); expect(action.commitData).to.be.an('array').with.lengthOf(1); expect(commitMessages[0]).to.equal('feat: Add new feature\n\nThis is the commit body.'); @@ -251,10 +252,11 @@ describe('parsePackFile', () => { const packetLine = `${oldCommit} ${newCommit} ${ref}\0capabilities\n`; // Commit content without a parent line - const commitContent = "tree 1234567890abcdef1234567890abcdef12345678\n" + - "author Test Author 1234567890 +0000\n" + - "committer Test Committer 1234567890 +0100\n\n" + - "feat: Initial commit"; + const commitContent = + 'tree 1234567890abcdef1234567890abcdef12345678\n' + + 'author Test Author 1234567890 +0000\n' + + 'committer Test Committer 1234567890 +0100\n\n' + + 'feat: Initial commit'; const parentFromCommit = '0'.repeat(40); // Expected parent hash const commitContentBuffer = Buffer.from(commitContent, 'utf8'); @@ -266,7 +268,7 @@ describe('parsePackFile', () => { const result = await exec(req, action); expect(result).to.equal(action); - const step = action.steps.find(s => s.stepName === 'parsePackFile'); + const step = action.steps.find((s) => s.stepName === 'parsePackFile'); expect(step).to.exist; expect(step.error).to.be.false; @@ -290,11 +292,12 @@ describe('parsePackFile', () => { const parent1 = 'b1'.repeat(20); const parent2 = 'b2'.repeat(20); - const commitContent = "tree 1234567890abcdef1234567890abcdef12345678\n" + + const commitContent = + 'tree 1234567890abcdef1234567890abcdef12345678\n' + `parent ${parent1}\n` + `parent ${parent2}\n` + - "author Test Author 1234567890 +0000\n" + - "committer Test Committer 1234567890 +0100\n\n" + + 'author Test Author 1234567890 +0000\n' + + 'committer Test Committer 1234567890 +0100\n\n' + "Merge branch 'feature'"; const commitContentBuffer = Buffer.from(commitContent, 'utf8'); @@ -307,7 +310,7 @@ describe('parsePackFile', () => { expect(result).to.equal(action); // Check step and action properties - const step = action.steps.find(s => s.stepName === 'parsePackFile'); + const step = action.steps.find((s) => s.stepName === 'parsePackFile'); expect(step).to.exist; expect(step.error).to.be.false; @@ -327,10 +330,11 @@ describe('parsePackFile', () => { const packetLine = `${oldCommit} ${newCommit} ${ref}\0capabilities\n`; // Malformed commit content - missing tree line - const commitContent = "parent abcdef1234567890abcdef1234567890abcdef12\n" + - "author Test Author 1678886400 +0000\n" + - "committer Test Committer 1678886460 +0100\n\n" + - "feat: Missing tree"; + const commitContent = + 'parent abcdef1234567890abcdef1234567890abcdef12\n' + + 'author Test Author 1678886400 +0000\n' + + 'committer Test Committer 1678886460 +0100\n\n' + + 'feat: Missing tree'; const commitContentBuffer = Buffer.from(commitContent, 'utf8'); zlibInflateStub.returns(commitContentBuffer); @@ -340,7 +344,7 @@ describe('parsePackFile', () => { const result = await exec(req, action); expect(result).to.equal(action); - const step = action.steps.find(s => s.stepName === 'parsePackFile'); + const step = action.steps.find((s) => s.stepName === 'parsePackFile'); expect(step).to.exist; expect(step.error).to.be.true; expect(step.errorMessage).to.include('Invalid commit data: Missing tree'); @@ -351,7 +355,7 @@ describe('parsePackFile', () => { const newCommit = 'b'.repeat(40); const ref = 'refs/heads/main'; const packetLines = [`${oldCommit} ${newCommit} ${ref}\0capa\n`]; - + const packetLineBuffer = createPacketLineBuffer(packetLines); const garbageData = Buffer.from('NOT PACK DATA'); req.body = Buffer.concat([packetLineBuffer, garbageData]); @@ -378,11 +382,12 @@ describe('parsePackFile', () => { 'some other data containing PACK keyword', // Include "PACK" within a packet line's content ]; - const commitContent = "tree 1234567890abcdef1234567890abcdef12345678\n" + + const commitContent = + 'tree 1234567890abcdef1234567890abcdef12345678\n' + `parent ${oldCommit}\n` + - "author Test Author 1234567890 +0000\n" + - "committer Test Committer 1234567890 +0000\n\n" + - "Test commit message with PACK inside"; + 'author Test Author 1234567890 +0000\n' + + 'committer Test Committer 1234567890 +0000\n\n' + + 'Test commit message with PACK inside'; const samplePackBuffer = createSamplePackBuffer(1, commitContent, 1); zlibInflateStub.returns(Buffer.from(commitContent, 'utf8')); @@ -417,11 +422,12 @@ describe('parsePackFile', () => { const ref = 'refs/heads/master'; const packetLines = [`${oldCommit} ${newCommit} ${ref}\0`]; - const commitContent = "tree 1234567890abcdef1234567890abcdef12345678\n" + + const commitContent = + 'tree 1234567890abcdef1234567890abcdef12345678\n' + `parent ${oldCommit}\n` + - "author Test Author 1234567890 +0000\n" + - "committer Test Committer 1234567890 +0000\n\n" + - "Commit A"; + 'author Test Author 1234567890 +0000\n' + + 'committer Test Committer 1234567890 +0000\n\n' + + 'Commit A'; const samplePackBuffer = createSamplePackBuffer(1, commitContent, 1); zlibInflateStub.returns(Buffer.from(commitContent, 'utf8')); @@ -496,13 +502,13 @@ describe('parsePackFile', () => { it('should handle buffer exactly 12 bytes long', () => { const buffer = createSamplePackBuffer(1).slice(0, 12); // Only header - const [meta, contentBuff] = getPackMeta(buffer); + const [meta, contentBuff] = getPackMeta(buffer); - expect(meta).to.deep.equal({ - sig: PACK_SIGNATURE, - version: 2, - entries: 1, - }); + expect(meta).to.deep.equal({ + sig: PACK_SIGNATURE, + version: 2, + entries: 1, + }); expect(contentBuff.length).to.equal(0); // No content left }); }); @@ -548,7 +554,10 @@ describe('parsePackFile', () => { describe('getCommitData', () => { it('should return empty array if no type 1 contents', () => { - const contents = [{ type: 2, content: 'blob' }, { type: 3, content: 'tree' }]; + const contents = [ + { type: 2, content: 'blob' }, + { type: 3, content: 'tree' }, + ]; expect(getCommitData(contents)).to.deep.equal([]); }); @@ -563,6 +572,7 @@ describe('parsePackFile', () => { parent: '456', author: 'Au Thor', committer: 'Com Itter', + committerEmail: 'c@e.com', commitTimestamp: '222', message: 'Commit message here', authorEmail: 'a@e.com', @@ -644,37 +654,41 @@ describe('parsePackFile', () => { }); it('should correctly parse a commit with a GPG signature header', () => { - const gpgSignedCommit = "tree b4d3c0ffee1234567890abcdef1234567890aabbcc\n" + - "parent 01dbeef9876543210fedcba9876543210fedcba\n" + - "author Test Author 1744814600 +0100\n" + - "committer Test Committer 1744814610 +0200\n" + - "gpgsig -----BEGIN PGP SIGNATURE-----\n \n" + - " wsFcBAABCAAQBQJn/8ISCRC1aQ7uu5UhlAAAntAQACeyQd6IykNXiN6m9DfVp8DJ\n" + - " UsY64ws+Td0inrEee+cHXVI9uJn15RJYQkICwlM4TZsVGav7nYaVqO+gfAg2ORAH\n" + - " ghUnwSFFs7ucN/p0a47ItkJmt04+jQIFlZIC+wy1u2H3aKJwqaF+kGP5SA33ahgV\n" + - " ZWviKodXFki8/G+sKB63q1qrDw6aELtftEgeAPQUcuLzj+vu/m3dWrDbatfUXMkC\n" + - " JC6PbFajqrJ5pEtFwBqqRE+oIsOM9gkNAti1yDD5eoS+bNXACe0hT0+UoIzn5a34\n" + - " xcElXTSdAK/MRjGiLN91G2nWvlbpM5wAEqr5Bl5ealCc6BbWfPxbP46slaE5DfkD\n" + - " u0+RkVX06MSSPqzOmEV14ZWKap5C19FpF9o/rY8vtLlCxjWMhtUvvdR4OQfQpEDY\n" + - " eTqzCHRnM3+7r3ABAWt9v7cG99bIMEs3sGcMy11HMeaoBpye6vCIP4ghNnoB1hUJ\n" + - " D7MD77jzk4Kbf4IzS5omExyMu3AiNZecZX4+1w/527yPhv3s/HB1Gfz0oCUned+6\n" + - " b9Kkle+krsQ/EK/4gPcb/Kb1cTcm3HhjaOSYwA+JpApJQ0mrduH34AT5MZJuIPFe\n" + - " QheLzQI1d2jmFs11GRC5hc0HBk1WmGm6U8+FBuxCX0ECZPdYeQJjUeWjnNeUoE6a\n" + - " 5lytZU4Onk57nUhIMSrx\n" + - " =IxZr\n" + - " -----END PGP SIGNATURE-----\n\n" + - "This is the commit message.\n" + - "It can span multiple lines.\n\n" + - "And include blank lines internally."; + const gpgSignedCommit = + 'tree b4d3c0ffee1234567890abcdef1234567890aabbcc\n' + + 'parent 01dbeef9876543210fedcba9876543210fedcba\n' + + 'author Test Author 1744814600 +0100\n' + + 'committer Test Committer 1744814610 +0200\n' + + 'gpgsig -----BEGIN PGP SIGNATURE-----\n \n' + + ' wsFcBAABCAAQBQJn/8ISCRC1aQ7uu5UhlAAAntAQACeyQd6IykNXiN6m9DfVp8DJ\n' + + ' UsY64ws+Td0inrEee+cHXVI9uJn15RJYQkICwlM4TZsVGav7nYaVqO+gfAg2ORAH\n' + + ' ghUnwSFFs7ucN/p0a47ItkJmt04+jQIFlZIC+wy1u2H3aKJwqaF+kGP5SA33ahgV\n' + + ' ZWviKodXFki8/G+sKB63q1qrDw6aELtftEgeAPQUcuLzj+vu/m3dWrDbatfUXMkC\n' + + ' JC6PbFajqrJ5pEtFwBqqRE+oIsOM9gkNAti1yDD5eoS+bNXACe0hT0+UoIzn5a34\n' + + ' xcElXTSdAK/MRjGiLN91G2nWvlbpM5wAEqr5Bl5ealCc6BbWfPxbP46slaE5DfkD\n' + + ' u0+RkVX06MSSPqzOmEV14ZWKap5C19FpF9o/rY8vtLlCxjWMhtUvvdR4OQfQpEDY\n' + + ' eTqzCHRnM3+7r3ABAWt9v7cG99bIMEs3sGcMy11HMeaoBpye6vCIP4ghNnoB1hUJ\n' + + ' D7MD77jzk4Kbf4IzS5omExyMu3AiNZecZX4+1w/527yPhv3s/HB1Gfz0oCUned+6\n' + + ' b9Kkle+krsQ/EK/4gPcb/Kb1cTcm3HhjaOSYwA+JpApJQ0mrduH34AT5MZJuIPFe\n' + + ' QheLzQI1d2jmFs11GRC5hc0HBk1WmGm6U8+FBuxCX0ECZPdYeQJjUeWjnNeUoE6a\n' + + ' 5lytZU4Onk57nUhIMSrx\n' + + ' =IxZr\n' + + ' -----END PGP SIGNATURE-----\n\n' + + 'This is the commit message.\n' + + 'It can span multiple lines.\n\n' + + 'And include blank lines internally.'; const contents = [ { type: 1, content: gpgSignedCommit }, - { type: 1, content: `tree 111\nparent 000\nauthor A1 1744814600 +0200\ncommitter C1 1744814610 +0200\n\nMsg1` } + { + type: 1, + content: `tree 111\nparent 000\nauthor A1 1744814600 +0200\ncommitter C1 1744814610 +0200\n\nMsg1`, + }, ]; const result = getCommitData(contents); expect(result).to.be.an('array').with.lengthOf(2); - + // Check the GPG signed commit data const gpgResult = result[0]; expect(gpgResult.tree).to.equal('b4d3c0ffee1234567890abcdef1234567890aabbcc'); @@ -683,8 +697,10 @@ describe('parsePackFile', () => { expect(gpgResult.committer).to.equal('Test Committer'); expect(gpgResult.authorEmail).to.equal('test.author@example.com'); expect(gpgResult.commitTimestamp).to.equal('1744814610'); - expect(gpgResult.message).to.equal(`This is the commit message.\nIt can span multiple lines.\n\nAnd include blank lines internally.`); - + expect(gpgResult.message).to.equal( + `This is the commit message.\nIt can span multiple lines.\n\nAnd include blank lines internally.`, + ); + // Sanity check: the second commit should be the simple commit const simpleResult = result[1]; expect(simpleResult.message).to.equal('Msg1'); @@ -698,11 +714,7 @@ describe('parsePackFile', () => { describe('parsePacketLines', () => { it('should parse multiple valid packet lines correctly and return the correct offset', () => { - const lines = [ - 'line1 content', - 'line2 more content\nwith newline', - 'line3', - ]; + const lines = ['line1 content', 'line2 more content\nwith newline', 'line3']; const buffer = createPacketLineBuffer(lines); // Helper adds "0000" at the end const expectedOffset = buffer.length; // Should indicate the end of the buffer after flush packet const [parsedLines, offset] = parsePacketLines(buffer); @@ -746,7 +758,7 @@ describe('parsePackFile', () => { buffer = Buffer.concat([buffer, extraData]); const expectedOffset = buffer.length - extraData.length; - const [parsedLines, offset] = parsePacketLines(buffer); + const [parsedLines, offset] = parsePacketLines(buffer); expect(parsedLines).to.deep.equal(lines); expect(offset).to.equal(expectedOffset); @@ -755,7 +767,9 @@ describe('parsePackFile', () => { it('should throw an error if a packet line length exceeds buffer bounds', () => { // 000A -> length 10, but actual line length is only 3 bytes const invalidLengthBuffer = Buffer.from('000Aabc'); - expect(() => parsePacketLines(invalidLengthBuffer)).to.throw(/Invalid packet line length 000A/); + expect(() => parsePacketLines(invalidLengthBuffer)).to.throw( + /Invalid packet line length 000A/, + ); }); it('should throw an error for non-hex length prefix (all non-hex)', () => { @@ -769,7 +783,7 @@ describe('parsePackFile', () => { expect(() => parsePacketLines(invalidHexBuffer)).to.throw(/Invalid packet line length 000z/); }); - it('should handle buffer ending exactly after a valid line length without content', () => { + it('should handle buffer ending exactly after a valid line length without content', () => { // 0008 -> length 8, but buffer ends after header (no content) const incompleteBuffer = Buffer.from('0008'); expect(() => parsePacketLines(incompleteBuffer)).to.throw(/Invalid packet line length 0008/); diff --git a/test/testPush.test.js b/test/testPush.test.js index f4e09a4a5..4681f3de5 100644 --- a/test/testPush.test.js +++ b/test/testPush.test.js @@ -8,32 +8,120 @@ chai.use(chaiHttp); chai.should(); const expect = chai.expect; +// dummy repo +const TEST_ORG = 'finos'; +const TEST_REPO = 'test-push'; +const TEST_URL = 'https://github.com/finos/test-push.git'; +// approver user +const TEST_USERNAME_1 = 'push-test'; +const TEST_EMAIL_1 = 'push-test@test.com'; +const TEST_PASSWORD_1 = 'test1234'; +// committer user +const TEST_USERNAME_2 = 'push-test-2'; +const TEST_EMAIL_2 = 'push-test-2@test.com'; +const TEST_PASSWORD_2 = 'test5678'; +// unknown user +const TEST_USERNAME_3 = 'push-test-3'; +const TEST_EMAIL_3 = 'push-test-3@test.com'; + +const TEST_PUSH = { + steps: [], + error: false, + blocked: false, + allowPush: false, + authorised: false, + canceled: false, + rejected: false, + autoApproved: false, + autoRejected: false, + commitData: [], + id: '0000000000000000000000000000000000000000__1744380874110', + type: 'push', + method: 'get', + timestamp: 1744380903338, + project: TEST_ORG, + repoName: TEST_REPO + '.git', + url: TEST_REPO, + repo: TEST_ORG + '/' + TEST_REPO + '.git', + user: TEST_USERNAME_2, + userEmail: TEST_EMAIL_2, + lastStep: null, + blockedMessage: + '\n\n\nGitProxy has received your push:\n\nhttp://localhost:8080/requests/0000000000000000000000000000000000000000__1744380874110\n\n\n', + _id: 'GIMEz8tU2KScZiTz', + attestation: null, +}; + describe('auth', async () => { let app; let cookie; - before(async function () { - app = await service.start(); - await db.deleteUser('login-test-user'); + const setCookie = function (res) { + res.headers['set-cookie'].forEach((x) => { + if (x.startsWith('connect')) { + const value = x.split(';')[0]; + cookie = value; + } + }); + }; + const login = async function (username, password) { + console.log(`logging in as ${username}...`); const res = await chai.request(app).post('/api/auth/login').send({ - username: 'admin', - password: 'admin', + username: username, + password: password, }); - + res.should.have.status(200); expect(res).to.have.cookie('connect.sid'); + setCookie(res); + }; + + const loginAsApprover = () => login(TEST_USERNAME_1, TEST_PASSWORD_1); + const loginAsCommitter = () => login(TEST_USERNAME_2, TEST_PASSWORD_2); + const loginAsAdmin = () => login('admin', 'admin'); + + const logout = async function () { + const res = await chai.request(app).post('/api/auth/logout').set('Cookie', `${cookie}`); res.should.have.status(200); + cookie = null; + }; - // Get the connect cooie - res.headers['set-cookie'].forEach((x) => { - if (x.startsWith('connect')) { - cookie = x.split(';')[0]; - } + before(async function () { + app = await service.start(); + await loginAsAdmin(); + + // set up a repo, user and push to test against + await db.deleteRepo(TEST_REPO); + await db.deleteUser(TEST_USERNAME_1); + await db.createRepo({ + project: TEST_ORG, + name: TEST_REPO, + url: TEST_URL, }); + + // Create a new user for the approver + console.log('creating approver'); + await db.createUser(TEST_USERNAME_1, TEST_PASSWORD_1, TEST_EMAIL_1, TEST_USERNAME_1, false); + await db.addUserCanAuthorise(TEST_REPO, TEST_USERNAME_1); + + // create a new user for the committer + console.log('creating committer'); + await db.createUser(TEST_USERNAME_2, TEST_PASSWORD_2, TEST_EMAIL_2, TEST_USERNAME_2, false); + await db.addUserCanPush(TEST_REPO, TEST_USERNAME_2); + + // logout of admin account + await logout(); }); describe('test push API', async function () { + afterEach(async function () { + await db.deletePush(TEST_PUSH.id); + await logout(); + }); + it('should get 404 for unknown push', async function () { + await loginAsApprover(); + const commitId = '0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f'; // eslint-disable-line max-len const res = await chai @@ -42,6 +130,177 @@ describe('auth', async () => { .set('Cookie', `${cookie}`); res.should.have.status(404); }); + + it('should allow an authorizer to approve a push', async function () { + await db.writeAudit(TEST_PUSH); + await loginAsApprover(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/authorise`) + .set('Cookie', `${cookie}`) + .set('content-type', 'application/x-www-form-urlencoded') + .send({ + params: { + attestation: [ + { + label: 'I am happy for this to be pushed to the upstream repository', + tooltip: { + text: 'Are you happy for this contribution to be pushed upstream?', + links: [], + }, + checked: true, + }, + ], + }, + }); + res.should.have.status(200); + }); + + it('should NOT allow an authorizer to approve if attestation is incomplete', async function () { + // make the approver also the committer + const testPush = { ...TEST_PUSH }; + testPush.user = TEST_USERNAME_1; + testPush.userEmail = TEST_EMAIL_1; + await db.writeAudit(testPush); + await loginAsApprover(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/authorise`) + .set('Cookie', `${cookie}`) + .set('content-type', 'application/x-www-form-urlencoded') + .send({ + params: { + attestation: [ + { + label: 'I am happy for this to be pushed to the upstream repository', + tooltip: { + text: 'Are you happy for this contribution to be pushed upstream?', + links: [], + }, + checked: false, + }, + ], + }, + }); + res.should.have.status(401); + }); + + it('should NOT allow an authorizer to approve if committer is unknown', async function () { + // make the approver also the committer + const testPush = { ...TEST_PUSH }; + testPush.user = TEST_USERNAME_3; + testPush.userEmail = TEST_EMAIL_3; + await db.writeAudit(testPush); + await loginAsApprover(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/authorise`) + .set('Cookie', `${cookie}`) + .set('content-type', 'application/x-www-form-urlencoded') + .send({ + params: { + attestation: [ + { + label: 'I am happy for this to be pushed to the upstream repository', + tooltip: { + text: 'Are you happy for this contribution to be pushed upstream?', + links: [], + }, + checked: true, + }, + ], + }, + }); + res.should.have.status(401); + }); + + it('should NOT allow an authorizer to approve their own push', async function () { + // make the approver also the committer + const testPush = { ...TEST_PUSH }; + testPush.user = TEST_USERNAME_1; + testPush.userEmail = TEST_EMAIL_1; + await db.writeAudit(testPush); + await loginAsApprover(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/authorise`) + .set('Cookie', `${cookie}`) + .set('content-type', 'application/x-www-form-urlencoded') + .send({ + params: { + attestation: [ + { + label: 'I am happy for this to be pushed to the upstream repository', + tooltip: { + text: 'Are you happy for this contribution to be pushed upstream?', + links: [], + }, + checked: true, + }, + ], + }, + }); + res.should.have.status(401); + }); + + it('should NOT allow a non-authorizer to approve a push', async function () { + await db.writeAudit(TEST_PUSH); + await loginAsCommitter(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/authorise`) + .set('Cookie', `${cookie}`) + .set('content-type', 'application/x-www-form-urlencoded') + .send({ + params: { + attestation: [ + { + label: 'I am happy for this to be pushed to the upstream repository', + tooltip: { + text: 'Are you happy for this contribution to be pushed upstream?', + links: [], + }, + checked: true, + }, + ], + }, + }); + res.should.have.status(401); + }); + + it('should allow an authorizer to reject a push', async function () { + await db.writeAudit(TEST_PUSH); + await loginAsApprover(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/reject`) + .set('Cookie', `${cookie}`); + res.should.have.status(200); + }); + + it('should NOT allow an authorizer to reject their own push', async function () { + // make the approver also the committer + const testPush = { ...TEST_PUSH }; + testPush.user = TEST_USERNAME_1; + testPush.userEmail = TEST_EMAIL_1; + await db.writeAudit(testPush); + await loginAsApprover(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/reject`) + .set('Cookie', `${cookie}`); + res.should.have.status(401); + }); + + it('should NOT allow a non-authorizer to reject a push', async function () { + await db.writeAudit(TEST_PUSH); + await loginAsCommitter(); + const res = await chai + .request(app) + .post(`/api/v1/push/${TEST_PUSH.id}/reject`) + .set('Cookie', `${cookie}`); + res.should.have.status(401); + }); }); after(async function () { @@ -49,5 +308,10 @@ describe('auth', async () => { res.should.have.status(200); await service.httpServer.close(); + + await db.deleteRepo(TEST_REPO); + await db.deleteUser(TEST_USERNAME_1); + await db.deleteUser(TEST_USERNAME_2); + await db.deletePush(TEST_PUSH.id); }); });