From 084bbc359fa2f49e8490b2832211e4a69aea0855 Mon Sep 17 00:00:00 2001 From: Sabarivasan-Velayutham Date: Sat, 16 Nov 2024 03:17:57 +0530 Subject: [PATCH 1/2] feat: add email validation --- .../processors/push-action/checkAuthorEmails.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/proxy/processors/push-action/checkAuthorEmails.js b/src/proxy/processors/push-action/checkAuthorEmails.js index 70c973d05..efa8ddda7 100644 --- a/src/proxy/processors/push-action/checkAuthorEmails.js +++ b/src/proxy/processors/push-action/checkAuthorEmails.js @@ -4,7 +4,19 @@ const config = require('../../../config'); const commitConfig = config.getCommitConfig(); function isEmailAllowed(email) { + if (!email) { + console.log('Invalid email address...'); + return false; // If the email is null, undefined, or an empty string, return false + } + const [emailLocal, emailDomain] = email.split('@'); + + // Check if split was successful + if (!emailLocal || !emailDomain) { + console.log('Invalid email format (missing local or domain part)...'); + return false; // If either part is missing, return false + } + console.log({ emailLocal, emailDomain }); // E-mail address is not a permissible domain name From d9fff6695649860b1f0ebc758613871e89de136c Mon Sep 17 00:00:00 2001 From: Sabarivasan-Velayutham Date: Sat, 16 Nov 2024 03:25:29 +0530 Subject: [PATCH 2/2] test: increase test coverage for push action --- src/service/routes/push.js | 2 +- test/checkGetDiff.test.js | 100 +++++++ test/push.test.js | 252 ++++++++++++++++++ test/testBlockForAuth.test.js | 44 +++ test/testCheckAuthorEmails.test.js | 81 ++++++ test/testCheckCommitMessages.test.js | 92 +++++++ test/testCheckIfWaitingAuth.test.js | 80 ++++++ test/testCheckUserPushPermission.test.js | 69 +++++ ...stProxyRoute.js => testProxyRoute.test.js} | 0 test/testScanDiff.test.js | 92 +++++++ 10 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 test/checkGetDiff.test.js create mode 100644 test/push.test.js create mode 100644 test/testBlockForAuth.test.js create mode 100644 test/testCheckAuthorEmails.test.js create mode 100644 test/testCheckCommitMessages.test.js create mode 100644 test/testCheckIfWaitingAuth.test.js create mode 100644 test/testCheckUserPushPermission.test.js rename test/{testProxyRoute.js => testProxyRoute.test.js} (100%) create mode 100644 test/testScanDiff.test.js diff --git a/src/service/routes/push.js b/src/service/routes/push.js index 9750375ca..e9145defa 100644 --- a/src/service/routes/push.js +++ b/src/service/routes/push.js @@ -171,7 +171,7 @@ router.post('/:id/cancel', async (req, res) => { console.log(`user ${req.user.username} not authorised to cancel push request for ${id}`); res.status(401).send({ message: - 'User ${req.user.username)} not authorised to cancel push requests on this project.', + `User ${req.user.username} not authorised to cancel push requests on this project.`, }); } } else { diff --git a/test/checkGetDiff.test.js b/test/checkGetDiff.test.js new file mode 100644 index 000000000..d3be732c1 --- /dev/null +++ b/test/checkGetDiff.test.js @@ -0,0 +1,100 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const child = require('child_process'); +const { exec } = require('../src/proxy/processors/push-action/getDiff'); +const { Step } = require('../src/proxy/actions'); + +describe('getDiff.exec', () => { + let req; + let action; + let spawnSyncStub; + + beforeEach(() => { + req = {} + action = { + proxyGitPath: '/path/to/git', + repoName: 'my-repo', + commitFrom: 'commit-from', + commitTo: 'commit-to', + commitData: [{ parent: 'parent-commit' }], + addStep: sinon.stub(), + }; + spawnSyncStub = sinon.stub(child, 'spawnSync').returns({ stdout: 'diff content' }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should execute git diff command and set the content', async () => { + const expectedContent = 'diff content'; + + await exec(req, action); + + expect( + spawnSyncStub.calledOnceWithExactly( + 'git', + ['diff', 'commit-from', 'commit-to'], + { + cwd: '/path/to/git/my-repo', + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + } + ) + ).to.be.true; + + expect(action.addStep.calledOnce).to.be.true; + const step = action.addStep.getCall(0).args[0]; + expect(step).to.be.instanceOf(Step); + expect(step.content).to.equal(expectedContent); + }); + + it('should handle error and set the error message', async () => { + const errorMessage = 'some error'; + spawnSyncStub.throws(new Error(errorMessage)); + + await exec(req, action); + + expect(action.addStep.calledOnce).to.be.true; + const step = action.addStep.getCall(0).args[0]; + expect(step).to.be.instanceOf(Step); + }); + + it('should handle commitFrom as all zeros and set the correct commitFrom', async () => { + action.commitFrom = '0000000000000000000000000000000000000000'; + action.commitData = [{ parent: 'parent-commit' }]; + + await exec(req, action); + + expect( + spawnSyncStub.calledOnceWithExactly( + 'git', + ['diff', 'parent-commit', 'commit-to'], + { + cwd: '/path/to/git/my-repo', + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + } + ) + ).to.be.true; + }); + + it('should handle commitFrom as all zeros and no parent commit', async () => { + action.commitFrom = '0000000000000000000000000000000000000000'; + action.commitData = [{ parent: '0000000000000000000000000000000000000000' }]; + + await exec(req, action); + + expect( + spawnSyncStub.calledOnceWithExactly( + 'git', + ['diff', '4b825dc642cb6eb9a060e54bf8d69288fbee4904', 'commit-to'], + { + cwd: '/path/to/git/my-repo', + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + } + ) + ).to.be.true; + }); +}); diff --git a/test/push.test.js b/test/push.test.js new file mode 100644 index 000000000..e2c22eb2a --- /dev/null +++ b/test/push.test.js @@ -0,0 +1,252 @@ +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const sinon = require('sinon'); +const db = require('../src/db'); +const service = require('../src/service'); + +chai.use(chaiHttp); +chai.should(); +const { expect } = chai; + +describe('Push Routes', function () { + let app; + let cookie; + + before(async function () { + // Start the service and clean up test data + app = await service.start(); + await db.deleteUser('login-test-user'); + + // Login to get session cookie + const res = await chai.request(app).post('/api/auth/login').send({ + username: 'admin', + password: 'admin', + }); + + expect(res).to.have.cookie('connect.sid'); + cookie = res.headers['set-cookie'].find((x) => x.startsWith('connect.sid')); + expect(res.status).to.equal(200); + + // Get the connect cookie + res.headers['set-cookie'].forEach((x) => { + if (x.startsWith('connect')) { + cookie = x.split(";")[0]; + } + }); + }) + + after(async function () { + // Logout and stop the service + const res = await chai.request(app).post('/api/auth/logout').set('Cookie', `${cookie}`); + res.should.have.status(200); + await service.httpServer.close(); + }); + + afterEach(function () { + sinon.restore(); // Restore stubs after each test + }); + + describe('GET /push', function () { + it('should return pushes with query parameters', async function () { + sinon.stub(db, 'getPushes').resolves([{ id: 1, type: 'push' }]); + + const res = await chai + .request(app) + .get('/api/v1/push') + .query({ limit: 10, skip: 0, active: 'true' }); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal([{ id: 1, type: 'push' }]); + }); + }); + + describe('GET /push/:id', function () { + it('should return a push by ID', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, type: 'push' }); + + const res = await chai.request(app).get('/api/v1/push/1'); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ id: 1, type: 'push' }); + }); + + it('should return 404 if push not found', async function () { + sinon.stub(db, 'getPush').resolves(null); + + const res = await chai.request(app).get('/api/v1/push/1'); + + expect(res.status).to.equal(404); + expect(res.body.message).to.equal('not found'); + }); + }); + + describe('POST /push/:id/reject', function () { + it('should reject a push request', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'author' }); + sinon.stub(db, 'getUsers').resolves([{ username: ' author', admin: false }]); + sinon.stub(db, 'canUserApproveRejectPush').resolves(true); + sinon.stub(db, 'reject').resolves({ success: true }); + + const res = await chai + .request(app) + .post('/api/v1/push/1/reject') + .set('Cookie', cookie); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ success: true }); + }); + + it('should return 401 if user is not logged in', async function () { + const res = await chai.request(app).post('/api/v1/push/1/reject'); + expect(res.status).to.equal(401); + expect(res.body).to.deep.equal({ message: 'not logged in' }); + }); + + it('should return 401 if user is the author and not admin', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'admin' }); + sinon.stub(db, 'getUsers').resolves([{ username: 'admin', admin: false }]); + + const res = await chai + .request(app) + .post('/api/v1/push/1/reject') + .set('Cookie', cookie); + + expect(res.status).to.equal(401); + expect(res.body).to.deep.equal({ message: 'Cannot reject your own changes' }); + }); + + it('should return 401 if user is unauthorised to reject', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'author' }); + sinon.stub(db, 'getUsers').resolves([{ username: 'author', admin: false }]); + sinon.stub(db, 'canUserApproveRejectPush').resolves(false); + + const res = await chai + .request(app) + .post('/api/v1/push/1/reject') + .set('Cookie', cookie); + + expect(res.status).to.equal(401); + expect(res.body.message).to.equal('User is not authorised to reject changes'); + }); + }); + + describe('POST /push/:id/authorise', function () { + it('should authorise a push request', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'user1' }); + sinon.stub(db, 'getUsers').resolves([{ username: 'userl', gitAccount: 'userl', admin: false }]) + sinon.stub(db, 'canUserApproveRejectPush').resolves(true); + sinon.stub(db, 'authorise').resolves({ success: true }); + + const res = await chai + .request(app) + .post('/api/v1/push/1/authorise') + .set('Cookie', cookie) + .send({ params: { attestation: [{ checked: true }] } }); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ success: true }); + }); + + it('should return 401 if user is not logged in', async () => { + const res = await chai.request(app).post('/api/v1/push/1/authorise'); + expect(res.status).to.be.equal(401); + expect(res.body).to.deep.equal({ message: 'You are unauthorized to perform this action...' }); + }); + + it('should return 401 if attestation is incomplete', async () => { + const res = await chai + .request(app) + .post('/api/v1/push/1/authorise') + .set('Cookie', cookie) + .send({ params: { attestation: [{ checked: false }] } }); + expect(res.status).to.be.equal(401); + expect(res.body).to.deep.equal({ message: 'You are unauthorized to perform this action...' }); + }); + + it('should return 401 if user is the author and not admin', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'admin' }); + sinon.stub(db, 'getUsers').resolves([{ username: 'admin', admin: false }]); + + const res = await chai + .request(app) + .post('/api/v1/push/1/authorise') + .set('Cookie', cookie) + .send({ params: { attestation: [{ checked: true }] } }); + + expect(res.status).to.be.equal(401); + expect(res.body).to.deep.equal({ message: 'Cannot approve your own changes' }); + }); + + + it('should return 401 if user is unauthorised to authorise', async function () { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'user1' }); + sinon.stub(db, 'getUsers').resolves([{ username: 'author', admin: false }]); + sinon.stub(db, 'canUserApproveRejectPush').resolves(false); + + const res = await chai + .request(app) + .post('/api/v1/push/1/authorise') + .set('Cookie', cookie) + .send({ params: { attestation: [{ checked: true }] } }); + + expect(res.status).to.equal(401); + expect(res.body.message).to.equal("user admin not authorised to approve push's on this project"); + }); + + it('should return 401 if user has no associated GitHub account', async () => { + sinon.stub(db, 'getPush').resolves({ id: 1, user: 'author' }); + sinon.stub(db, 'getUsers').resolves([{ username: 'author', admin: false }]); + sinon.stub(db, 'canUserApproveRejectPush').resolves(true); + + const res = await chai + .request(app) + .post('/api/v1/push/1/authorise') + .set('Cookie', cookie) + .send({ params: { attestation: [{ checked: true }] } }); + + expect(res.status).to.be.equal(401); + expect(res.body).to.deep.equal({ + message: 'You must associate a GitHub account with your user before approving...', + }); + }); + + + }); + + describe('POST /push/:id/cancel', function () { + it('should cancel a push request', async function () { + sinon.stub(db, 'canUserCancelPush').resolves(true); + sinon.stub(db, 'cancel').resolves({ success: true }); + + const res = await chai + .request(app) + .post('/api/v1/push/1/cancel') + .set('Cookie', cookie); + + expect(res.status).to.equal(200); + expect(res.body).to.deep.equal({ success: true }); + }); + + it('should return 401 if user is not logged in', async () => { + const res = await chai + .request(app) + .post('/api/v1/push/1/cancel'); + + expect(res.status).to.be.equal(401); + expect(res.body).to.deep.equal({ message: 'not logged in' }); + }); + + + it('should return 401 if user is unauthorised to cancel', async function () { + sinon.stub(db, 'canUserCancelPush').resolves(false); + + const res = await chai + .request(app) + .post('/api/v1/push/1/cancel') + .set('Cookie', cookie); + + expect(res.status).to.equal(401); + expect(res.body).to.deep.equal({ message: 'User admin not authorised to cancel push requests on this project.' }); + }); + }); +}) diff --git a/test/testBlockForAuth.test.js b/test/testBlockForAuth.test.js new file mode 100644 index 000000000..3a70b22a4 --- /dev/null +++ b/test/testBlockForAuth.test.js @@ -0,0 +1,44 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const { Step } = require('../src/proxy/actions'); +const { getServiceUIURL } = require('../src/service/urls'); +const { exec } = require('../src/proxy/processors/push-action/blockForAuth'); + +describe('exec', () => { + let req; + let action; + let actionAddStepSpy; + + beforeEach(() => { + req = { + protocol: 'http', + headers: { + host: 'localhost:3000', + }, + }; // Mock request object + + action = { + id: '123', + addStep: sinon.spy(), + }; // Mock action object + + actionAddStepSpy = action.addStep; // Assign the spy directly + }); + + it('should add a step with the correct message', async () => { + const url = getServiceUIURL(req); + const expectedMessage = + '\n\n\n' + + `\x1B[32mGitProxy has received your push ✅\x1B[0m\n\n` + + '🔗 Shareable Link\n\n' + + `\x1B[34m${url}/admin/push/${action.id}\x1B[0m` + + '\n\n\n'; + + await exec(req, action); + + expect(actionAddStepSpy.calledOnce).to.be.true; + expect(actionAddStepSpy.firstCall.args[0]).to.be.instanceof(Step); + expect(actionAddStepSpy.firstCall.args[0].stepName).to.equal('authBlock'); + expect(actionAddStepSpy.firstCall.args[0].blockedMessage).to.equal(expectedMessage); + }); +}); diff --git a/test/testCheckAuthorEmails.test.js b/test/testCheckAuthorEmails.test.js new file mode 100644 index 000000000..9445ee85e --- /dev/null +++ b/test/testCheckAuthorEmails.test.js @@ -0,0 +1,81 @@ +const { exec } = require('../src/proxy/processors/push-action/checkAuthorEmails'); // Import exec function + +const assert = require('assert'); // Use Node.js assert module for assertions + +describe('checkAuthorEmails', () => { + let action; + + // Before each test, set up mock data for action and req + beforeEach(() => { + action = { + commitData: [ + { authorEmail: 'valid.email@example.com' }, + { authorEmail: 'blockedUser@example.com' }, + { authorEmail: 'user@blocked-domain.com' }, + { authorEmail: 'invalid.email@' }, + { authorEmail: 'invalidemail.com' }, + { authorEmail: '' }, + { authorEmail: null }, + { authorEmail: undefined }, + ], + addStep: function () { this.calls.push('addStep called'); }, // Mock the addStep function with a call counter + calls: [] // Array to track calls to addStep + }; + }); + + it('should return true for a valid email', async () => { + const req = {}; // Mock request object + const result = await exec(req, action); // Execute the function + // Check that addStep was called (simulating a step addition) + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for an invalid email with no domain', async () => { + const req = {}; // Mock request object + action.commitData[3] = { authorEmail: 'invalid.email@' }; // Invalid email with no domain + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for an invalid email with no "@" symbol', async () => { + const req = {}; + action.commitData[4] = { authorEmail: 'invalidemail.com' }; // Invalid email with no '@' + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for an email with a forbidden username', async () => { + const req = {}; + action.commitData[1] = { authorEmail: 'blockedUser@example.com' }; // Email with blocked username + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for an email with a forbidden domain', async () => { + const req = {}; + action.commitData[2] = { authorEmail: 'user@blocked-domain.com' }; // Email with blocked domain + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for an empty email string', async () => { + const req = {}; + action.commitData[5] = { authorEmail: '' }; // Empty email string + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for a null email', async () => { + const req = {}; + action.commitData[6] = { authorEmail: null }; // Null email + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); + + it('should return false for an undefined email', async () => { + const req = {}; + action.commitData[7] = { authorEmail: undefined }; // Undefined email + const result = await exec(req, action); + assert.strictEqual(result.calls.length, 1, 'addStep should have been called once'); + }); +}); diff --git a/test/testCheckCommitMessages.test.js b/test/testCheckCommitMessages.test.js new file mode 100644 index 000000000..d984cf573 --- /dev/null +++ b/test/testCheckCommitMessages.test.js @@ -0,0 +1,92 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const { exec } = require('../src/proxy/processors/push-action/checkCommitMessages'); +const config = require('../src/config'); + +describe('exec', () => { + let action; + let consoleLogStub; + + beforeEach(() => { + action = { + commitData: [], + steps: [], + addStep: function (step) { + this.steps = this.steps || []; + this.steps.push(step); + }, + }; + + // Mock console.log + consoleLogStub = sinon.stub(console, 'log'); + + // Mock commitConfig + sinon.stub(config, 'getCommitConfig').returns({ + literals: ['blocked', 'forbidden'], + patterns: ['blocked pattern', 'forbidden pattern'], + }); + }); + + afterEach(() => { + consoleLogStub.restore(); + config.getCommitConfig.restore(); + }); + + it('should return false if commit message is empty', async () => { + action.commitData = [{ message: '' }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.true; + }); + + it('should return false if commit message is not a string', async () => { + action.commitData = [{ message: 12345 }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.true; + }); + + it('should return false if commit message contains blocked literals', async () => { + action.commitData = [{ message: 'This commit is blocked' }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.false; + }); + + it('should return false if commit message matches blocked patterns', async () => { + action.commitData = [{ message: 'This message contains a blocked pattern' }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.false; + }); + + it('should return false if commit message contains a forbidden literal', async () => { + action.commitData = [{ message: 'This commit is forbidden' }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.false; + }); + + it('should return false if commit message matches a forbidden pattern', async () => { + action.commitData = [{ message: 'This commit contains a forbidden pattern' }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.false; + }); + + it('should handle mixed valid and invalid commit messages', async () => { + action.commitData = [ + { message: 'This is a valid commit message' }, + { message: 'This commit is blocked' }, + ]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.false; + }); + + it('should return true if all commit messages are valid', async () => { + action.commitData = [{ message: 'Valid commit message' }]; + const result = await exec({}, action); + expect(result.steps[0].error).to.be.false; + }); + + it('should log the request and action', async () => { + action.commitData = [{ message: 'Valid commit message' }]; + const req = { some: 'request' }; + await exec(req, action); + expect(consoleLogStub.calledWith({ req, action })).to.be.true; + }); +}); diff --git a/test/testCheckIfWaitingAuth.test.js b/test/testCheckIfWaitingAuth.test.js new file mode 100644 index 000000000..951387d5f --- /dev/null +++ b/test/testCheckIfWaitingAuth.test.js @@ -0,0 +1,80 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const { exec } = require('../src/proxy/processors/push-action/checkIfWaitingAuth'); +const data = require('../src/db'); + +describe('checkIfWaitingAuth.exec', () => { + let req; + let action; + let existingAction; + let step; + + beforeEach(() => { + req = {}; // Mock request object + action = { + id: '123', + setAllowPush: sinon.stub(), + addStep: sinon.stub(), + error: false, // Ensure error is included + }; + step = { + log: sinon.stub(), + setError: sinon.stub(), + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should set allow push if existing action is found and not error', async () => { + existingAction = { authorised: true }; + sinon.stub(data, 'getPush').resolves(existingAction); + + await exec(req, action); + + expect(action.setAllowPush.calledOnce).to.be.true; + expect(action.addStep.calledOnce).to.be.true; + }); + + it('should handle action without error property', async () => { + existingAction = { authorised: true }; + action.error = false; // Ensure error is false + sinon.stub(data, 'getPush').resolves(existingAction); + + await exec(req, action); + + expect(action.setAllowPush.calledOnce).to.be.true; + expect(action.addStep.calledOnce).to.be.true; + }); + + it('should not set allow push if existing action is found but has error', async () => { + existingAction = { authorised: false, error: 'Some error' }; + sinon.stub(data, 'getPush').resolves(existingAction); + + await exec(req, action); + + expect(action.setAllowPush.notCalled).to.be.true; + expect(action.addStep.calledOnce).to.be.true; + }); + + it('should not set allow push if existing action is not found', async () => { + sinon.stub(data, 'getPush').resolves(null); + + await exec(req, action); + + expect(action.setAllowPush.notCalled).to.be.true; + expect(action.addStep.calledOnce).to.be.true; + }); + + it('should set step error if an exception occurs', async () => { + sinon.stub(data, 'getPush').throws(new Error('Database error')); + + try { + await exec(req, action); + } catch (error) { + expect(action.addStep.calledOnce).to.be.true; + expect(step.setError.calledOnce).to.be.true; + } + }); +}); diff --git a/test/testCheckUserPushPermission.test.js b/test/testCheckUserPushPermission.test.js new file mode 100644 index 000000000..12baf36c2 --- /dev/null +++ b/test/testCheckUserPushPermission.test.js @@ -0,0 +1,69 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const db = require('../src/db'); +const { Step } = require('../src/proxy/actions'); +const { exec } = require('../src/proxy/processors/push-action/checkUserPushPermission'); + +describe('checkUserPushPermission.exec', () => { + let req; + let action; + + beforeEach(() => { + req = {} + action = { + repo: 'owner/repo.git', + user: 'test123', + addStep: sinon.stub(), + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should allow user to push if user is associated with the Git account and has permission', async () => { + const getUsersStub = sinon.stub(db, 'getUsers').resolves([{ username: 'test123' }]); + const isUserPushAllowedStub = sinon.stub(db, 'isUserPushAllowed').resolves(true); + const stepLogStub = sinon.stub(Step.prototype, 'log'); + + const result = await exec(req, action); + + expect(getUsersStub.calledOnceWithExactly({ gitAccount: 'test123' })).to.be.true; + expect(isUserPushAllowedStub.calledOnceWithExactly('repo', 'test123')).to.be.true; + expect(stepLogStub.calledOnceWithExactly('User test123 is allowed to push on repo owner/repo.git')).to.be.true; + expect(action.addStep.calledOnceWithExactly(sinon.match.instanceOf(Step))).to.be.true; + expect(result).to.deep.equal(action); + }); + + it('should reject push if user is associated with the Git account but does not have permission', async () => { + const getUsersStub = sinon.stub(db, 'getUsers').resolves([{ username: 'test123' }]); + const isUserPushAllowedStub = sinon.stub(db, 'isUserPushAllowed').resolves(false); + const stepLogStub = sinon.stub(Step.prototype, 'log'); + const stepSetErrorStub = sinon.stub(Step.prototype, 'setError'); + + const result = await exec(req, action); + + expect(getUsersStub.calledOnceWithExactly({ gitAccount: 'test123' })).to.be.true; + expect(isUserPushAllowedStub.calledOnceWithExactly('repo', 'test123')).to.be.true; + expect(stepLogStub.calledOnceWithExactly('User test123 is not allowed to push on repo owner/repo.git, ending')).to.be.true; + expect(stepSetErrorStub.calledOnceWithExactly('Rejecting push as user test123 is not allowed to push on repo owner/repo.git')).to.be.true; + expect(action.addStep.calledOnceWithExactly(sinon.match.instanceOf(Step))).to.be.true; + expect(result).to.deep.equal(action); + }); + + it('should reject push if user is not associated with the Git account', async () => { + const getUsersStub = sinon.stub(db, 'getUsers').resolves([]); + const isUserPushAllowedStub = sinon.stub(db, 'isUserPushAllowed').resolves(false); + const stepLogStub = sinon.stub(Step.prototype, 'log'); + const stepSetErrorStub = sinon.stub(Step.prototype, 'setError'); + + const result = await exec(req, action); + + expect(getUsersStub.calledOnceWithExactly({ gitAccount: 'test123' })).to.be.true; + expect(isUserPushAllowedStub.notCalled).to.be.true; + expect(stepLogStub.calledOnceWithExactly('User test123 is not allowed to push on repo owner/repo.git, ending')).to.be.true; + expect(stepSetErrorStub.calledOnceWithExactly('Rejecting push as user test123 is not allowed to push on repo owner/repo.git')).to.be.true; + expect(action.addStep.calledOnceWithExactly(sinon.match.instanceOf(Step))).to.be.true; + expect(result).to.deep.equal(action); + }); +}); diff --git a/test/testProxyRoute.js b/test/testProxyRoute.test.js similarity index 100% rename from test/testProxyRoute.js rename to test/testProxyRoute.test.js diff --git a/test/testScanDiff.test.js b/test/testScanDiff.test.js new file mode 100644 index 000000000..a2ad95d75 --- /dev/null +++ b/test/testScanDiff.test.js @@ -0,0 +1,92 @@ +const { expect } = require('chai'); +const proxyquire = require('proxyquire').noCallThru(); + +const mockConfig = { + getCommitConfig: () => ({ + diff: { + block: { + literals: ['blocked literal'], + patterns: ['blocked pattern'], + providers: { provider: 'blocked provider' } + } + } + }), + getPrivateOrganizations: () => ['privateOrg'] +}; +const { exec } = proxyquire('../src/proxy/processors/push-action/scanDiff', { + '../../../config': mockConfig +}); + +describe('exec', () => { + let req; + let action; + + beforeEach(() => { + req = {} + action = { + steps: [], + commitFrom: 'commitFromHash', + commitTo: 'commitToHash', + project: 'testproject', + addStep: function (step) { + this.steps.push(step); + } + }; + }); + + it('should block the push if diff is empty', async () => { + action.steps.push({ stepName: 'diff', content: '' }); + const result = await exec(req, action); + expect(result.steps[1].error).to.be.true; + }); + + it('should block the push if diff is not a string', async () => { + action.steps.push({ stepName: 'diff', content: null }); + const result = await exec(req, action); + expect(result.steps[1].error).to.be.true; + }); + + it('should block the push if diff is undefined', async () => { + action.steps.push({ stepName: 'diff', content: undefined }); + const result = await exec(req, action); + expect(result.steps[1].error).to.be.true; + }); + + it('should block the push if diff contains blocked literals', async () => { + action.steps.push({ stepName: 'diff', content: 'blocked literal' }); + const result = await exec(req, action); + expect(result.steps[1].error).to.be.true; + }); + + it('should block the push if diff contains blocked patterns', async () => { + action.steps.push({ stepName: 'diff', content: 'blocked pattern' }); + const result = await exec(req, action); + expect(result.steps[1].error).to.be.true; + }); + + it('should block the push if diff contains blocked providers and organization is not private', async () => { + action.steps.push({ stepName: 'diff', content: 'blocked provider' }); + action.project = 'nonPrivateOrganization'; + const result = await exec(req, action); + expect(result.steps[1].error).to.be.true; + }); + + it('should block the push if diff contains blocked providers and organization is private', async () => { + action.steps.push({ stepName: 'diff', content: 'This contains a blocked provider' }); + action.project = 'privateOrg'; + const result = await exec(req, action); + expect(result.steps[1].error).to.be.false; + }); + + it('should allow the push if diff does not contain blocked literals, patterns, or providers', async () => { + action.steps.push({ stepName: 'diff', content: 'This is a safe diff' }); + const result = await exec(req, action); + expect(result.steps[1].error).to.be.false; + }); + + it('should return the action object if the diff is legal', async () => { + action.steps.push({ stepName: 'diff', content: 'This is a safe diff' }); + const result = await exec(req, action); + expect(result).to.equal(action); + }); +});