Repository: finos/git-proxy

Issue:
Fix failing plugin tests - `ts-mocha` compatibility issue
These three tests from `test/plugin/plugin.test.js` started failing since we upgraded all our files from CommonJS + JS to ESM + TS. This is due to `ts-mocha` tripping up with the way our `PluginLoader` works. Note that **this issue is only reproducible in Node 18 to 21**, the problem is gone in Node 22 due to interoperability improvements.

<details>
<summary>Problematic tests</summary

```js
  it('should load plugins that are the default export (module.exports = pluginObj)', async function () {
    const loader = new PluginLoader([join(testPackagePath, 'default-export.js')]);
    await loader.load();
    expect(loader.pushPlugins.length).to.equal(1);
    expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).to.be.true;
    expect(loader.pushPlugins[0]).to.be.an.instanceOf(PushActionPlugin);
  }).timeout(10000);

  it('should load multiple plugins from a module that match the plugin class (module.exports = { pluginFoo, pluginBar })', async function () {
    const loader = new PluginLoader([join(testPackagePath, 'multiple-export.js')]);
    await loader.load();
    expect(loader.pushPlugins.length).to.equal(1);
    expect(loader.pullPlugins.length).to.equal(1);
    expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).to.be.true;
    expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p, 'isGitProxyPushActionPlugin'))).to
      .be.true;
    expect(loader.pullPlugins.every((p) => isCompatiblePlugin(p, 'isGitProxyPullActionPlugin'))).to
      .be.true;
    expect(loader.pushPlugins[0]).to.be.instanceOf(PushActionPlugin);
    expect(loader.pullPlugins[0]).to.be.instanceOf(PullActionPlugin);
  }).timeout(10000);

  it('should load plugins that are subclassed from plugin classes', async function () {
    const loader = new PluginLoader([join(testPackagePath, 'subclass.js')]);
    await loader.load();
    expect(loader.pushPlugins.length).to.equal(1);
    expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p))).to.be.true;
    expect(loader.pushPlugins.every((p) => isCompatiblePlugin(p, 'isGitProxyPushActionPlugin'))).to
      .be.true;
    expect(loader.pushPlugins[0]).to.be.instanceOf(PushActionPlugin);
  }).timeout(10000);
```

</details>

<details>
<summary>Test Execution Output</summary>

```bash
$ nvm use 18
Now using node v18.20.7 (npm v10.8.2)
$ npm run test-plugins

> @finos/git-proxy@1.17.1 test-plugins
> NODE_ENV=test ts-mocha './test/plugin/*.test.js' --exit



  loading plugins from packages
Failed to load plugin: Error [ERR_REQUIRE_ESM]: require() of ES Module /home/juan/Desktop/Juan/Projects/git-proxy/node_modules/load-plugin/index.js from /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts not supported.
Instead change the require of index.js in /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts to a dynamic import() which is available in all CommonJS modules.
Found 0 plugin modules
    1) should load plugins that are the default export (module.exports = pluginObj)
(node:747153) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
(Use `node --trace-warnings ...` to show where the warning was created)
Failed to load plugin: Error [ERR_REQUIRE_ESM]: require() of ES Module /home/juan/Desktop/Juan/Projects/git-proxy/node_modules/load-plugin/index.js from /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts not supported.
Instead change the require of index.js in /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts to a dynamic import() which is available in all CommonJS modules.
Found 0 plugin modules
    2) should load multiple plugins from a module that match the plugin class (module.exports = { pluginFoo, pluginBar })
Failed to load plugin: Error [ERR_REQUIRE_ESM]: require() of ES Module /home/juan/Desktop/Juan/Projects/git-proxy/node_modules/load-plugin/index.js from /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts not supported.
Instead change the require of index.js in /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts to a dynamic import() which is available in all CommonJS modules.
Found 0 plugin modules
    3) should load plugins that are subclassed from plugin classes
Failed to load plugin: Error [ERR_REQUIRE_ESM]: require() of ES Module /home/juan/Desktop/Juan/Projects/git-proxy/node_modules/load-plugin/index.js from /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts not supported.
Instead change the require of index.js in /home/juan/Desktop/Juan/Projects/git-proxy/src/plugin.ts to a dynamic import() which is available in all CommonJS modules
```

</details>


### Possible solutions:
- Run the CI in Node 22, and enforce Node 22 usage
  - May cause problems with users that cannot upgrade Node for certain reasons
  - By far the easiest fix

### Things I've tried: 
- Fixing the plugin implementation so that the `require` in the culprit code is avoided 
  - Spent a few hours trying to refactor the import (by delaying imports, etc.) without luck
- Fixing the `ts-mocha` execution so that it uses `tsx` loader under the hood
  - Also didn't work for me, however, I can't remember why

### Current solution:
Right now, we're simply skipping the three tests, since they do pass when executed in Node 22 (and they don't throw the `ERR_REQUIRE_ESM` error when executed via `tsx`).

Discussion:
@kriswest: Node 18 is EOL (last month?), however 20 has a year to go in maintenance unfortunately...

![Image](https://github.com/user-attachments/assets/5781b988-1f7f-4f3b-90fc-750e5d68eee6)

Solution patch:
diff --git a/package.json b/package.json
index 94d78c4a..941c6576 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,7 @@
     "build-lib": "./scripts/build-for-publish.sh",
     "restore-lib": "./scripts/undo-build.sh",
     "check-types": "tsc",
-    "test": "NODE_ENV=test ts-mocha './test/*.js' --exit",
+    "test": "NODE_ENV=test ts-mocha './test/**/*.test.js' --exit",
     "test-coverage": "nyc npm run test",
     "test-coverage-ci": "nyc --reporter=lcovonly --reporter=text npm run test",
     "prepare": "node ./scripts/prepare.js",
diff --git a/src/proxy/processors/push-action/checkAuthorEmails.ts b/src/proxy/processors/push-action/checkAuthorEmails.ts
index 367514e1..9462ed4e 100644
--- a/src/proxy/processors/push-action/checkAuthorEmails.ts
+++ b/src/proxy/processors/push-action/checkAuthorEmails.ts
@@ -5,24 +5,27 @@ import { Commit } from '../../actions/Action';
 const commitConfig = getCommitConfig();
 
 const isEmailAllowed = (email: string): boolean => {
+  if (!email) {
+    return false;
+  }
+
   const [emailLocal, emailDomain] = email.split('@');
-  console.log({ emailLocal, emailDomain });
 
-  // E-mail address is not a permissible domain name
+  if (!emailLocal || !emailDomain) {
+    return false;
+  }
+
   if (
     commitConfig.author.email.domain.allow &&
     !emailDomain.match(new RegExp(commitConfig.author.email.domain.allow, 'g'))
   ) {
-    console.log('Bad e-mail address domain...');
     return false;
   }
 
-  // E-mail username is not a permissible form
   if (
     commitConfig.author.email.local.block &&
     emailLocal.match(new RegExp(commitConfig.author.email.local.block, 'g'))
   ) {
-    console.log('Bad e-mail address username...');
     return false;
   }
 
diff --git a/src/proxy/processors/push-action/getDiff.ts b/src/proxy/processors/push-action/getDiff.ts
index 89ac0afb..460a9156 100644
--- a/src/proxy/processors/push-action/getDiff.ts
+++ b/src/proxy/processors/push-action/getDiff.ts
@@ -10,7 +10,7 @@ const exec = async (req: any, action: Action): Promise<Action> => {
     // https://stackoverflow.com/questions/40883798/how-to-get-git-diff-of-the-first-commit
     let commitFrom = `4b825dc642cb6eb9a060e54bf8d69288fbee4904`;
 
-    if (!action.commitData) {
+    if (!action.commitData || action.commitData.length === 0) {
       throw new Error('No commit data found');
     }
 
diff --git a/src/proxy/processors/push-action/gitleaks.ts b/src/proxy/processors/push-action/gitleaks.ts
index 73aabe55..af28a499 100644
--- a/src/proxy/processors/push-action/gitleaks.ts
+++ b/src/proxy/processors/push-action/gitleaks.ts
@@ -123,6 +123,12 @@ const exec = async (req: any, action: Action): Promise<Action> => {
     return action;
   }
 
+  if (!config.enabled) {
+    console.log('gitleaks is disabled, skipping');
+    action.addStep(step);
+    return action;
+  }
+
   const { commitFrom, commitTo } = action;
   const workingDir = `${action.proxyGitPath}/${action.repoName}`;
   console.log(`Scanning range with gitleaks: ${commitFrom}:${commitTo}`, workingDir);



Test patch:
diff --git a/test/fixtures/gitleaks-config.toml b/test/fixtures/gitleaks-config.toml
new file mode 100644
index 00000000..eb09a983
--- /dev/null
+++ b/test/fixtures/gitleaks-config.toml
@@ -0,0 +1,25 @@
+title = "sample gitleaks config"
+
+[[rules]]
+id = "generic-api-key"
+description = "Generic API Key"
+regex = '''(?i)(?:key|api|token|secret)[\s:=]+([a-z0-9]{32,})'''
+tags = ["key", "api-key"]
+
+[[rules]]
+id = "aws-access-key-id"
+description = "AWS Access Key ID"
+regex = '''AKIA[0-9A-Z]{16}'''
+tags = ["aws", "key"]
+
+[[rules]]
+id = "basic-auth"
+description = "Auth Credentials"
+regex = '''(?i)(https?://)[a-z0-9]+:[a-z0-9]+@'''
+tags = ["auth", "password"]
+
+[[rules]]
+id = "jwt-token"
+description = "JSON Web Token"
+regex = '''eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.?[A-Za-z0-9._-]*'''
+tags = ["jwt", "token"]
diff --git a/test/plugin/plugin.test.js b/test/plugin/plugin.test.js
index 1180ba9b..cee46699 100644
--- a/test/plugin/plugin.test.js
+++ b/test/plugin/plugin.test.js
@@ -23,7 +23,7 @@ describe('loading plugins from packages', function () {
     spawnSync('npm', ['install'], { cwd: testPackagePath, timeout: 5000 });
   });
 
-  it('should load plugins that are the default export (module.exports = pluginObj)', async function () {
+  it.skip('should load plugins that are the default export (module.exports = pluginObj)', async function () {
     const loader = new PluginLoader([join(testPackagePath, 'default-export.js')]);
     await loader.load();
     expect(loader.pushPlugins.length).to.equal(1);
@@ -31,7 +31,7 @@ describe('loading plugins from packages', function () {
     expect(loader.pushPlugins[0]).to.be.an.instanceOf(PushActionPlugin);
   }).timeout(10000);
 
-  it('should load multiple plugins from a module that match the plugin class (module.exports = { pluginFoo, pluginBar })', async function () {
+  it.skip('should load multiple plugins from a module that match the plugin class (module.exports = { pluginFoo, pluginBar })', async function () {
     const loader = new PluginLoader([join(testPackagePath, 'multiple-export.js')]);
     await loader.load();
     expect(loader.pushPlugins.length).to.equal(1);
@@ -45,7 +45,7 @@ describe('loading plugins from packages', function () {
     expect(loader.pullPlugins[0]).to.be.instanceOf(PullActionPlugin);
   }).timeout(10000);
 
-  it('should load plugins that are subclassed from plugin classes', async function () {
+  it.skip('should load plugins that are subclassed from plugin classes', async function () {
     const loader = new PluginLoader([join(testPackagePath, 'subclass.js')]);
     await loader.load();
     expect(loader.pushPlugins.length).to.equal(1);
diff --git a/test/processors/blockForAuth.test.js b/test/processors/blockForAuth.test.js
new file mode 100644
index 00000000..f566f1b2
--- /dev/null
+++ b/test/processors/blockForAuth.test.js
@@ -0,0 +1,96 @@
+const chai = require('chai');
+const sinon = require('sinon');
+const proxyquire = require('proxyquire').noCallThru();
+const { Step } = require('../../src/proxy/actions');
+
+chai.should();
+const expect = chai.expect;
+
+describe('blockForAuth', () => {
+  let action;
+  let exec;
+  let getServiceUIURLStub;
+  let req;
+  let stepInstance;
+  let StepSpy;
+
+  beforeEach(() => {
+    req = {
+      protocol: 'https',
+      headers: { host: 'example.com' }
+    };
+
+    action = {
+      id: 'push_123',
+      addStep: sinon.stub()
+    };
+
+    stepInstance = new Step('temp');
+    sinon.stub(stepInstance, 'setAsyncBlock');
+
+    StepSpy = sinon.stub().returns(stepInstance);
+
+    getServiceUIURLStub = sinon.stub().returns('http://localhost:8080');
+
+    const blockForAuth = proxyquire('../../src/proxy/processors/push-action/blockForAuth', {
+      '../../../service/urls': { getServiceUIURL: getServiceUIURLStub },
+      '../../actions': { Step: StepSpy }
+    });
+
+    exec = blockForAuth.exec;
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('exec', () => {
+
+    it('should generate a correct shareable URL', async () => {
+      await exec(req, action);
+      expect(getServiceUIURLStub.calledOnce).to.be.true;
+      expect(getServiceUIURLStub.calledWithExactly(req)).to.be.true;
+    });
+
+    it('should create step with correct parameters', async () => {
+      await exec(req, action);
+
+      expect(StepSpy.calledOnce).to.be.true;
+      expect(StepSpy.calledWithExactly('authBlock')).to.be.true;
+      expect(stepInstance.setAsyncBlock.calledOnce).to.be.true;
+
+      const message = stepInstance.setAsyncBlock.firstCall.args[0];
+      expect(message).to.include('http://localhost:8080/dashboard/push/push_123');
+      expect(message).to.include('\x1B[32mGitProxy has received your push ✅\x1B[0m');
+      expect(message).to.include('\x1B[34mhttp://localhost:8080/dashboard/push/push_123\x1B[0m');
+      expect(message).to.include('🔗 Shareable Link');
+    });
+
+    it('should add step to action exactly once', async () => {
+      await exec(req, action);
+      expect(action.addStep.calledOnce).to.be.true;
+      expect(action.addStep.calledWithExactly(stepInstance)).to.be.true;
+    });
+
+    it('should return action instance', async () => {
+      const result = await exec(req, action);
+      expect(result).to.equal(action);
+    });
+
+    it('should handle https URL format', async () => {
+      getServiceUIURLStub.returns('https://git-proxy-hosted-ui.com');
+      await exec(req, action);
+
+      const message = stepInstance.setAsyncBlock.firstCall.args[0];
+      expect(message).to.include('https://git-proxy-hosted-ui.com/dashboard/push/push_123');
+    });
+
+    it('should handle special characters in action ID', async () => {
+      action.id = 'push@special#chars!';
+      await exec(req, action);
+
+      const message = stepInstance.setAsyncBlock.firstCall.args[0];
+      expect(message).to.include('/push/push@special#chars!');
+    });
+  });
+});
diff --git a/test/processors/checkAuthorEmails.test.js b/test/processors/checkAuthorEmails.test.js
new file mode 100644
index 00000000..52d8ffc6
--- /dev/null
+++ b/test/processors/checkAuthorEmails.test.js
@@ -0,0 +1,171 @@
+const sinon = require('sinon');
+const proxyquire = require('proxyquire').noCallThru();
+const { expect } = require('chai');
+
+describe('checkAuthorEmails', () => {
+  let action;
+  let commitConfig
+  let exec;
+  let getCommitConfigStub;
+  let stepSpy;
+  let StepStub;
+
+  beforeEach(() => {
+    StepStub = class {
+      constructor() {
+        this.error = undefined;
+      }
+      log() {}
+      setError() {}
+    };
+    stepSpy = sinon.spy(StepStub.prototype, 'log');
+    sinon.spy(StepStub.prototype, 'setError');
+
+    commitConfig = {
+      author: {
+        email: {
+          domain: { allow: null },
+          local: { block: null }
+        }
+      }
+    };
+    getCommitConfigStub = sinon.stub().returns(commitConfig);
+
+    action = {
+      commitData: [],
+      addStep: sinon.stub().callsFake((step) => {
+        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 }
+    });
+
+    exec = checkAuthorEmails.exec;
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('exec', () => {
+    it('should allow valid emails when no restrictions', async () => {
+      action.commitData = [
+        { authorEmail: 'valid@example.com' },
+        { authorEmail: 'another.valid@test.org' }
+      ];
+
+      await exec({}, action);
+
+      expect(action.step.error).to.be.undefined;
+    });
+
+    it('should block emails from forbidden domains', async () => {
+      commitConfig.author.email.domain.allow = 'example\\.com$';
+      action.commitData = [
+        { authorEmail: 'valid@example.com' },
+        { 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;
+    });
+
+    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' }
+      ];
+
+      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;
+    });
+
+    it('should handle empty email strings', async () => {
+      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;
+    });
+
+    it('should allow emails when both checks pass', async () => {
+      commitConfig.author.email.domain.allow = 'example\\.com$';
+      commitConfig.author.email.local.block = 'forbidden';
+      action.commitData = [
+        { authorEmail: 'allowed@example.com' },
+        { authorEmail: 'also.allowed@example.com' }
+      ];
+
+      await exec({}, action);
+
+      expect(action.step.error).to.be.undefined;
+    });
+
+    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' }
+      ];
+
+      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;
+    });
+
+    it('should handle emails without domain', async () => {
+      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;
+    });
+
+    it('should handle multiple illegal emails', async () => {
+      commitConfig.author.email.domain.allow = 'example\\.com$';
+      action.commitData = [
+        { authorEmail: 'invalid1@bad.org' },
+        { authorEmail: 'invalid2@wrong.net' },
+        { 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;
+    });
+  });
+});
diff --git a/test/processors/checkCommitMessages.test.js b/test/processors/checkCommitMessages.test.js
new file mode 100644
index 00000000..75156e0a
--- /dev/null
+++ b/test/processors/checkCommitMessages.test.js
@@ -0,0 +1,154 @@
+const chai = require('chai');
+const sinon = require('sinon');
+const proxyquire = require('proxyquire');
+const { Action, Step } = require('../../src/proxy/actions');
+
+chai.should();
+const expect = chai.expect;
+
+describe('checkCommitMessages', () => {
+  let commitConfig;
+  let exec;
+  let getCommitConfigStub;
+  let logStub;
+
+  beforeEach(() => {
+    logStub = sinon.stub(console, 'log');
+
+    commitConfig = {
+      message: {
+        block: {
+          literals: ['secret', 'password'],
+          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 }
+    });
+
+    exec = checkCommitMessages.exec;
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('exec', () => {
+    let action;
+    let req;
+    let stepSpy;
+
+    beforeEach(() => {
+      req = {};
+      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' }
+      ];
+      stepSpy = sinon.spy(Step.prototype, 'log');
+    });
+
+    it('should allow commit with valid messages', async () => {
+      const result = await exec(req, action);
+
+      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;
+    });
+
+    it('should block commit with illegal messages', async () => {
+      action.commitData?.push({ 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(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;
+    });
+
+    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' }
+      ];
+
+      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;
+    });
+
+    it('should not error when commit data is empty', async () => {
+      // Empty commit data is a valid scenario that happens when making a branch from an unapproved commit
+      // This is remedied in the getMissingData.exec action  
+      action.commitData = [];
+      const result = await exec(req, action);
+
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(logStub.calledWith('The following commit messages are legal: ')).to.be.true;
+    });
+
+    it('should handle commit data with null values', async () => {
+      action.commitData = [
+        { message: null, author: 'test@example.com' },
+        { message: undefined, 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;
+    });
+
+    it('should handle commit messages of incorrect type', async () => {
+      action.commitData = [
+        { message: 123, 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;
+    });
+
+    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' }
+      ];
+
+      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;
+    });
+  });
+});
diff --git a/test/processors/checkIfWaitingAuth.test.js b/test/processors/checkIfWaitingAuth.test.js
new file mode 100644
index 00000000..f9a66a3a
--- /dev/null
+++ b/test/processors/checkIfWaitingAuth.test.js
@@ -0,0 +1,124 @@
+const chai = require('chai');
+const sinon = require('sinon');
+const proxyquire = require('proxyquire');
+const { Action } = require('../../src/proxy/actions');
+
+chai.should();
+const expect = chai.expect;
+
+describe('checkIfWaitingAuth', () => {
+  let exec;
+  let getPushStub;
+
+  beforeEach(() => {
+    getPushStub = sinon.stub();
+
+    const checkIfWaitingAuth = proxyquire('../../src/proxy/processors/push-action/checkIfWaitingAuth', {
+      '../../../db': { getPush: getPushStub }
+    });
+
+    exec = checkIfWaitingAuth.exec;
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('exec', () => {
+    let action;
+    let req;
+
+    beforeEach(() => {
+      req = {};
+      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'
+      );
+      authorizedAction.authorised = true;
+      getPushStub.resolves(authorizedAction);
+
+      const result = await exec(req, action);
+
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(result.allowPush).to.be.true;
+      expect(result).to.deep.equal(authorizedAction);
+    });
+
+    it('should not set allowPush when action exists but not authorized', async () => {
+      const unauthorizedAction = new Action(
+        '1234567890',
+        'push',
+        'POST',
+        1234567890,
+        'test/repo'
+      );
+      unauthorizedAction.authorised = false;
+      getPushStub.resolves(unauthorizedAction);
+
+      const result = await exec(req, action);
+
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(result.allowPush).to.be.false;
+    });
+
+    it('should not set allowPush when action does not exist', async () => {
+      getPushStub.resolves(null);
+
+      const result = await exec(req, action);
+
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(result.allowPush).to.be.false;
+    });
+
+    it('should not modify action when it has an error', async () => {
+      action.error = true;
+      const authorizedAction = new Action(
+        '1234567890',
+        'push',
+        'POST',
+        1234567890,
+        'test/repo'
+      );
+      authorizedAction.authorised = true;
+      getPushStub.resolves(authorizedAction);
+
+      const result = await exec(req, action);
+
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(result.allowPush).to.be.false;
+      expect(result.error).to.be.true;
+    });
+
+    it('should add step with error when getPush throws', async () => {
+      const error = new Error('DB error');
+      getPushStub.rejects(error);
+
+      try {
+        await exec(req, action);
+        throw new Error('Should have thrown');
+      } catch (e) {
+        expect(e).to.equal(error);
+        expect(action.steps).to.have.lengthOf(1);
+        expect(action.steps[0].error).to.be.true;
+        expect(action.steps[0].errorMessage).to.contain('DB error');
+      }
+    });
+  });
+});
diff --git a/test/processors/checkUserPushPermission.test.js b/test/processors/checkUserPushPermission.test.js
new file mode 100644
index 00000000..b140d383
--- /dev/null
+++ b/test/processors/checkUserPushPermission.test.js
@@ -0,0 +1,102 @@
+const chai = require('chai');
+const sinon = require('sinon');
+const proxyquire = require('proxyquire');
+const { Action, Step } = require('../../src/proxy/actions');
+
+chai.should();
+const expect = chai.expect;
+
+describe('checkUserPushPermission', () => {
+  let exec;
+  let getUsersStub;
+  let isUserPushAllowedStub;
+  let logStub;
+
+  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
+      }
+    });
+
+    exec = checkUserPushPermission.exec;
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('exec', () => {
+    let action;
+    let req;
+    let stepSpy;
+
+    beforeEach(() => {
+      req = {};
+      action = new Action(
+        '1234567890',
+        'push',
+        'POST',
+        1234567890,
+        'test/repo.git'
+      );
+      action.user = 'git-user';
+      stepSpy = sinon.spy(Step.prototype, 'log');
+    });
+
+    it('should allow push when user has permission', async () => {
+      getUsersStub.resolves([{ username: 'db-user', 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;
+    });
+
+    it('should reject push when user has no permission', async () => {
+      getUsersStub.resolves([{ username: 'db-user', 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;
+    });
+
+    it('should reject push when no user found for git account', async () => {
+      getUsersStub.resolves([]);
+
+      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 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');
+    });
+
+    it('should handle multiple users for git account by rejecting push', async () => {
+      getUsersStub.resolves([
+        { username: 'user1', gitAccount: 'git-user' },
+        { username: 'user2', 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;
+    });
+  });
+});
diff --git a/test/testClearBareClone.test.js b/test/processors/clearBareClone.test.js
similarity index 82%
rename from test/testClearBareClone.test.js
rename to test/processors/clearBareClone.test.js
index ce31f8a9..1ebcf85c 100644
--- a/test/testClearBareClone.test.js
+++ b/test/processors/clearBareClone.test.js
@@ -1,8 +1,8 @@
 const fs = require('fs');
 const chai = require('chai');
-const clearBareClone = require('../src/proxy/processors/push-action/clearBareClone').exec;
-const pullRemote = require('../src/proxy/processors/push-action/pullRemote').exec;
-const { Action } = require('../src/proxy/actions/Action');
+const clearBareClone = require('../../src/proxy/processors/push-action/clearBareClone').exec;
+const pullRemote = require('../../src/proxy/processors/push-action/pullRemote').exec;
+const { Action } = require('../../src/proxy/actions/Action');
 chai.should();
 
 const expect = chai.expect;
diff --git a/test/processors/getDiff.test.js b/test/processors/getDiff.test.js
new file mode 100644
index 00000000..5acbc83e
--- /dev/null
+++ b/test/processors/getDiff.test.js
@@ -0,0 +1,151 @@
+const path = require('path');
+const simpleGit = require('simple-git');
+const fs = require('fs').promises;
+const { Action } = require('../../src/proxy/actions');
+const { exec } = require('../../src/proxy/processors/push-action/getDiff');
+
+const chai = require('chai');
+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');
+
+    await fs.writeFile(path.join(tempDir, 'test.txt'), 'initial content');
+    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'
+    );
+    action.proxyGitPath = __dirname; // Temp dir parent path
+    action.repoName = 'temp-test-repo';
+    action.commitFrom = 'HEAD~1';
+    action.commitTo = 'HEAD';
+    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'
+    );
+    action.proxyGitPath = __dirname; // Temp dir parent path
+    action.repoName = 'temp-test-repo';
+    action.commitFrom = 'HEAD~1';
+    action.commitTo = 'HEAD';
+    action.commitData = [
+      { parent: '0000000000000000000000000000000000000000' }
+    ];
+
+    const result = await exec({}, action);
+
+    expect(result.steps[0].error).to.be.false;
+    expect(result.steps[0].content).to.include('initial content');
+  });
+
+  it('should throw an error if no commit data is provided', async () => {
+    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 = [];
+
+    const result = await exec({}, action);
+    expect(result.steps[0].error).to.be.true;
+    expect(result.steps[0].errorMessage).to.contain('No commit data found');
+  });
+
+  it('should throw an error if no commit data is provided', async () => {
+    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 = undefined;
+
+    const result = await exec({}, action);
+    expect(result.steps[0].error).to.be.true;
+    expect(result.steps[0].errorMessage).to.contain('No commit data found');
+  });
+
+  it('should handle empty commit hash in commitFrom', async () => {
+    await fs.writeFile(path.join(tempDir, 'test.txt'), 'new content for parent test');
+    await git.add('.');
+    await git.commit('commit for parent test');
+
+    const log = await git.log();
+    const parentCommit = log.all[1].hash;
+    const headCommit = log.all[0].hash;
+
+    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 }
+    ];
+
+    const result = await exec({}, action);
+
+    expect(result.steps[0].error).to.be.false;
+    expect(result.steps[0].content).to.not.be.null;
+    expect(result.steps[0].content.length).to.be.greaterThan(0);
+  });
+});
diff --git a/test/processors/gitLeaks.test.js b/test/processors/gitLeaks.test.js
new file mode 100644
index 00000000..eeed7f8e
--- /dev/null
+++ b/test/processors/gitLeaks.test.js
@@ -0,0 +1,312 @@
+const chai = require('chai');
+const sinon = require('sinon');
+const proxyquire = require('proxyquire');
+const { Action, Step } = require('../../src/proxy/actions');
+
+chai.should();
+const expect = chai.expect;
+
+describe('gitleaks', () => {
+  describe('exec', () => {
+    let exec;
+    let stubs;
+    let action;
+    let req;
+    let stepSpy;
+    let logStub;
+    let errorStub;
+
+    beforeEach(() => {
+      stubs = {
+        getAPIs: sinon.stub(),
+        fs: {
+          stat: sinon.stub(),
+          access: sinon.stub(),
+          constants: { R_OK: 0 }
+        },
+        spawn: sinon.stub()
+      };
+
+      logStub = sinon.stub(console, 'log');
+      errorStub = sinon.stub(console, 'error');
+
+      const gitleaksModule = proxyquire('../../src/proxy/processors/push-action/gitleaks', {
+        '../../../config': { getAPIs: stubs.getAPIs },
+        'node:fs/promises': stubs.fs,
+        'node:child_process': { spawn: stubs.spawn }
+      });
+
+      exec = gitleaksModule.exec;
+
+      req = {};
+      action = new Action(
+        '1234567890',
+        'push',
+        'POST',
+        1234567890,
+        'test/repo'
+      );
+      action.proxyGitPath = '/tmp';
+      action.repoName = 'test-repo';
+      action.commitFrom = 'abc123';
+      action.commitTo = 'def456';
+
+      stepSpy = sinon.spy(Step.prototype, 'setError');
+    });
+
+    afterEach(() => {
+      sinon.restore();
+    });
+
+    it('should handle config loading failure', async () => {
+      stubs.getAPIs.throws(new Error('Config error'));
+
+      const result = await exec(req, action);
+
+      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;
+    });
+
+    it('should skip scanning when plugin is disabled', async () => {
+      stubs.getAPIs.returns({ gitleaks: { enabled: false } });
+
+      const result = await exec(req, action);
+
+      expect(result.error).to.be.false;
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(logStub.calledWith('gitleaks is disabled, skipping')).to.be.true;
+    });
+
+    it('should handle successful scan with no findings', async () => {
+      stubs.getAPIs.returns({ gitleaks: { enabled: true } });
+
+      const gitRootCommitMock = {
+        exitCode: 0,
+        stdout: 'rootcommit123\n',
+        stderr: ''
+      };
+
+      const gitleaksMock = {
+        exitCode: 0,
+        stdout: '',
+        stderr: 'No leaks found'
+      };
+
+      stubs.spawn
+        .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) }
+        })
+        .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) }
+        });
+
+      const result = await exec(req, action);
+
+      expect(result.error).to.be.false;
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+      expect(logStub.calledWith('succeded')).to.be.true;
+      expect(logStub.calledWith('No leaks found')).to.be.true;
+    });
+
+    it('should handle scan with findings', async () => {
+      stubs.getAPIs.returns({ gitleaks: { enabled: true } });
+
+      const gitRootCommitMock = {
+        exitCode: 0,
+        stdout: 'rootcommit123\n',
+        stderr: ''
+      };
+
+      const gitleaksMock = {
+        exitCode: 99,
+        stdout: 'Found secret in file.txt\n',
+        stderr: 'Warning: potential leak'
+      };
+
+      stubs.spawn
+        .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) }
+        })
+        .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) }
+        });
+
+      const result = await exec(req, action);
+
+      expect(result.error).to.be.true;
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.true;
+      expect(stepSpy.calledWith('\nFound secret in file.txt\nWarning: potential leak')).to.be.true;
+    });
+
+    it('should handle gitleaks execution failure', async () => {
+      stubs.getAPIs.returns({ gitleaks: { enabled: true } });
+
+      const gitRootCommitMock = {
+        exitCode: 0,
+        stdout: 'rootcommit123\n',
+        stderr: ''
+      };
+
+      const gitleaksMock = {
+        exitCode: 1,
+        stdout: '',
+        stderr: 'Command failed'
+      };
+
+      stubs.spawn
+        .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) }
+        })
+        .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) }
+        });
+
+      const result = await exec(req, action);
+
+      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;
+    });
+
+    it('should handle gitleaks spawn failure', async () => {
+      stubs.getAPIs.returns({ gitleaks: { enabled: true } });
+      stubs.spawn.onFirstCall().throws(new Error('Spawn error'));
+
+      const result = await exec(req, action);
+
+      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;
+    });
+
+    it('should handle empty gitleaks entry in proxy.config.json', async () => {
+      stubs.getAPIs.returns({ gitleaks: {} });
+      const result = await exec(req, action);
+      expect(result.error).to.be.false;
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+    });
+
+    it('should handle invalid gitleaks entry in proxy.config.json', async () => {
+      stubs.getAPIs.returns({ gitleaks: 'invalid config' });
+      stubs.spawn.onFirstCall().returns({
+        on: (event, cb) => {
+          if (event === 'close') cb(0);
+          return { stdout: { on: () => {} }, stderr: { on: () => {} } };
+        },
+        stdout: { on: (_, cb) => cb('') },
+        stderr: { on: (_, cb) => cb('') }
+      });
+
+      const result = await exec(req, action);
+
+      expect(result.error).to.be.false;
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+    });
+
+    it('should handle custom config path', async () => {
+      stubs.getAPIs.returns({ 
+        gitleaks: { 
+          enabled: true,
+          configPath: `../fixtures/gitleaks-config.toml`
+        } 
+      });
+
+      stubs.fs.stat.resolves({ isFile: () => true });
+      stubs.fs.access.resolves();
+
+      const gitRootCommitMock = {
+        exitCode: 0,
+        stdout: 'rootcommit123\n',
+        stderr: ''
+      };
+
+      const gitleaksMock = {
+        exitCode: 0,
+        stdout: '',
+        stderr: 'No leaks found'
+      };
+
+      stubs.spawn
+        .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) }
+        })
+        .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) }
+        });
+
+      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');
+    });
+
+    it('should handle invalid custom config path', async () => {
+      stubs.getAPIs.returns({ 
+        gitleaks: { 
+          enabled: true,
+          configPath: '/invalid/path.toml'
+        } 
+      });
+
+      stubs.fs.stat.rejects(new Error('File not found'));
+
+      const result = await exec(req, action);
+
+      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;
+    });
+  });
+});
diff --git a/test/processors/writePack.test.js b/test/processors/writePack.test.js
new file mode 100644
index 00000000..a7580caa
--- /dev/null
+++ b/test/processors/writePack.test.js
@@ -0,0 +1,107 @@
+const chai = require('chai');
+const sinon = require('sinon');
+const proxyquire = require('proxyquire');
+const { Action, Step } = require('../../src/proxy/actions');
+
+chai.should();
+const expect = chai.expect;
+
+describe('writePack', () => {
+  let exec;
+  let spawnSyncStub;
+  let stepLogSpy;
+  let stepSetContentSpy;
+  let stepSetErrorSpy;
+
+  beforeEach(() => {
+    spawnSyncStub = sinon.stub().returns({
+      stdout: 'git receive-pack output',
+      stderr: '',
+      status: 0
+    });
+
+    stepLogSpy = sinon.spy(Step.prototype, 'log');
+    stepSetContentSpy = sinon.spy(Step.prototype, 'setContent');
+    stepSetErrorSpy = sinon.spy(Step.prototype, 'setError');
+
+    const writePack = proxyquire('../../src/proxy/processors/push-action/writePack', {
+      'child_process': { spawnSync: spawnSyncStub }
+    });
+
+    exec = writePack.exec;
+  });
+
+  afterEach(() => {
+    sinon.restore();
+  });
+
+  describe('exec', () => {
+    let action;
+    let req;
+
+    beforeEach(() => {
+      req = {
+        body: 'pack data'
+      };
+      action = new Action(
+        '1234567890',
+        'push',
+        'POST',
+        1234567890,
+        'test/repo'
+      );
+      action.proxyGitPath = '/path/to/repo';
+    });
+
+    it('should execute git receive-pack with correct parameters', async () => {
+      const result = await exec(req, action);
+
+      expect(spawnSyncStub.calledOnce).to.be.true;
+      expect(spawnSyncStub.firstCall.args[0]).to.equal('git');
+      expect(spawnSyncStub.firstCall.args[1]).to.deep.equal(['receive-pack', 'repo']);
+      expect(spawnSyncStub.firstCall.args[2]).to.deep.equal({
+        cwd: '/path/to/repo',
+        input: 'pack data',
+        encoding: 'utf-8'
+      });
+
+      expect(stepLogSpy.calledWith('executing git receive-pack repo')).to.be.true;
+      expect(stepLogSpy.calledWith('git receive-pack output')).to.be.true;
+
+      expect(stepSetContentSpy.calledWith('git receive-pack output')).to.be.true;
+
+      expect(result.steps).to.have.lengthOf(1);
+      expect(result.steps[0].error).to.be.false;
+    });
+
+    it('should handle errors from git receive-pack', async () => {
+      const error = new Error('git error');
+      spawnSyncStub.throws(error);
+
+      try {
+        await exec(req, action);
+        throw new Error('Expected error to be thrown');
+      } catch (e) {
+        expect(stepSetErrorSpy.calledOnce).to.be.true;
+        expect(stepSetErrorSpy.firstCall.args[0]).to.include('git error');
+
+        expect(action.steps).to.have.lengthOf(1);
+        expect(action.steps[0].error).to.be.true;
+      }
+    });
+
+    it('should always add the step to the action even if error occurs', async () => {
+      spawnSyncStub.throws(new Error('git error'));
+
+      try {
+        await exec(req, action);
+      } catch (e) {
+        expect(action.steps).to.have.lengthOf(1);
+      }
+    });
+
+    it('should have the correct displayName', () => {
+      expect(exec.displayName).to.equal('writePack.exec');
+    });
+  });
+});