Skip to content

Commit aaea93a

Browse files
authored
Merge pull request #1115 from jescalada/push-action-fuzz-tests
test: Implement fuzz tests for processors
2 parents ddff723 + b28010a commit aaea93a

8 files changed

+295
-3
lines changed

package-lock.json

Lines changed: 45 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"eslint-plugin-react": "^7.37.5",
112112
"eslint-plugin-standard": "^5.0.0",
113113
"eslint-plugin-typescript": "^0.14.0",
114+
"fast-check": "^4.2.0",
114115
"husky": "^9.1.7",
115116
"mocha": "^10.8.2",
116117
"nyc": "^17.1.0",

test/processors/blockForAuth.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const fc = require('fast-check');
12
const chai = require('chai');
23
const sinon = require('sinon');
34
const proxyquire = require('proxyquire').noCallThru();
@@ -92,4 +93,41 @@ describe('blockForAuth', () => {
9293
expect(message).to.include('/push/push@special#chars!');
9394
});
9495
});
96+
97+
describe('fuzzing', () => {
98+
it('should create a step with correct parameters regardless of action ID', () => {
99+
fc.assert(
100+
fc.asyncProperty(fc.string(), async (actionId) => {
101+
action.id = actionId;
102+
103+
const freshStepInstance = new Step('temp');
104+
const setAsyncBlockStub = sinon.stub(freshStepInstance, 'setAsyncBlock');
105+
106+
const StepSpyLocal = sinon.stub().returns(freshStepInstance);
107+
const getServiceUIURLStubLocal = sinon.stub().returns('http://localhost:8080');
108+
109+
const blockForAuth = proxyquire('../../src/proxy/processors/push-action/blockForAuth', {
110+
'../../../service/urls': { getServiceUIURL: getServiceUIURLStubLocal },
111+
'../../actions': { Step: StepSpyLocal }
112+
});
113+
114+
const result = await blockForAuth.exec(req, action);
115+
116+
expect(StepSpyLocal.calledOnce).to.be.true;
117+
expect(StepSpyLocal.calledWithExactly('authBlock')).to.be.true;
118+
expect(setAsyncBlockStub.calledOnce).to.be.true;
119+
120+
const message = setAsyncBlockStub.firstCall.args[0];
121+
expect(message).to.include(`http://localhost:8080/dashboard/push/${actionId}`);
122+
expect(message).to.include('\x1B[32mGitProxy has received your push ✅\x1B[0m');
123+
expect(message).to.include(`\x1B[34mhttp://localhost:8080/dashboard/push/${actionId}\x1B[0m`);
124+
expect(message).to.include('🔗 Shareable Link');
125+
expect(result).to.equal(action);
126+
}),
127+
{
128+
numRuns: 100
129+
}
130+
);
131+
});
132+
});
95133
});

test/processors/checkAuthorEmails.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const sinon = require('sinon');
22
const proxyquire = require('proxyquire').noCallThru();
33
const { expect } = require('chai');
4+
const fc = require('fast-check');
45

56
describe('checkAuthorEmails', () => {
67
let action;
@@ -169,4 +170,72 @@ describe('checkAuthorEmails', () => {
169170
).to.be.true;
170171
});
171172
});
173+
174+
describe('fuzzing', () => {
175+
it('should not crash on random string in commit email', () => {
176+
fc.assert(
177+
fc.property(fc.string(), (commitEmail) => {
178+
action.commitData = [
179+
{ authorEmail: commitEmail }
180+
];
181+
exec({}, action);
182+
}),
183+
{
184+
numRuns: 100
185+
}
186+
);
187+
188+
expect(action.step.error).to.be.true;
189+
expect(stepSpy.calledWith(
190+
'The following commit author e-mails are illegal: '
191+
)).to.be.true;
192+
});
193+
194+
it('should handle valid emails with random characters', () => {
195+
fc.assert(
196+
fc.property(fc.emailAddress(), (commitEmail) => {
197+
action.commitData = [
198+
{ authorEmail: commitEmail }
199+
];
200+
exec({}, action);
201+
}),
202+
{
203+
numRuns: 100
204+
}
205+
);
206+
expect(action.step.error).to.be.undefined;
207+
});
208+
209+
it('should handle invalid types in commit email', () => {
210+
fc.assert(
211+
fc.property(fc.anything(), (commitEmail) => {
212+
action.commitData = [
213+
{ authorEmail: commitEmail }
214+
];
215+
exec({}, action);
216+
}),
217+
{
218+
numRuns: 100
219+
}
220+
);
221+
222+
expect(action.step.error).to.be.true;
223+
expect(stepSpy.calledWith(
224+
'The following commit author e-mails are illegal: '
225+
)).to.be.true;
226+
});
227+
228+
it('should handle arrays of valid emails', () => {
229+
fc.assert(
230+
fc.property(fc.array(fc.emailAddress()), (commitEmails) => {
231+
action.commitData = commitEmails.map(email => ({ authorEmail: email }));
232+
exec({}, action);
233+
}),
234+
{
235+
numRuns: 100
236+
}
237+
);
238+
expect(action.step.error).to.be.undefined;
239+
});
240+
});
172241
});

test/processors/checkCommitMessages.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const chai = require('chai');
22
const sinon = require('sinon');
33
const proxyquire = require('proxyquire');
44
const { Action, Step } = require('../../src/proxy/actions');
5+
const fc = require('fast-check');
56

67
chai.should();
78
const expect = chai.expect;
@@ -149,5 +150,53 @@ describe('checkCommitMessages', () => {
149150
expect(logStub.calledWith('The following commit messages are illegal: secret password here'))
150151
.to.be.true;
151152
});
153+
154+
describe('fuzzing', () => {
155+
it('should not crash on arbitrary commit messages', async () => {
156+
await fc.assert(
157+
fc.asyncProperty(
158+
fc.array(
159+
fc.record({
160+
message: fc.oneof(
161+
fc.string(),
162+
fc.constant(null),
163+
fc.constant(undefined),
164+
fc.integer(),
165+
fc.double(),
166+
fc.boolean(),
167+
fc.object(),
168+
),
169+
author: fc.string()
170+
}),
171+
{ maxLength: 20 }
172+
),
173+
async (fuzzedCommits) => {
174+
const fuzzAction = new Action(
175+
'fuzz',
176+
'push',
177+
'POST',
178+
Date.now(),
179+
'fuzz/repo'
180+
);
181+
fuzzAction.commitData = Array.isArray(fuzzedCommits) ? fuzzedCommits : [];
182+
183+
const result = await exec({}, fuzzAction);
184+
185+
expect(result).to.have.property('steps');
186+
expect(result.steps[0]).to.have.property('error').that.is.a('boolean');
187+
}
188+
),
189+
{
190+
examples: [
191+
[{ message: '', author: 'me' }],
192+
[{ message: '1234-5678-9012-3456', author: 'me' }],
193+
[{ message: null, author: 'me' }],
194+
[{ message: {}, author: 'me' }],
195+
[{ message: 'SeCrEt', author: 'me' }]
196+
]
197+
}
198+
);
199+
});
200+
});
152201
});
153202
});

test/processors/checkUserPushPermission.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const chai = require('chai');
22
const sinon = require('sinon');
33
const proxyquire = require('proxyquire');
4+
const fc = require('fast-check');
45
const { Action, Step } = require('../../src/proxy/actions');
56

67
chai.should();
@@ -116,5 +117,26 @@ describe('checkUserPushPermission', () => {
116117
'Push blocked: User not found. Please contact an administrator for support.',
117118
);
118119
});
120+
121+
describe('fuzzing', () => {
122+
it('should not crash on arbitrary getUsers return values (fuzzing)', async () => {
123+
const userList = fc.sample(
124+
fc.array(
125+
fc.record({
126+
username: fc.string(),
127+
gitAccount: fc.string()
128+
}),
129+
{ maxLength: 5 }
130+
),
131+
1
132+
)[0];
133+
getUsersStub.resolves(userList);
134+
135+
const result = await exec(req, action);
136+
137+
expect(result.steps).to.have.lengthOf(1);
138+
expect(result.steps[0].error).to.be.true;
139+
});
140+
});
119141
});
120142
});

test/processors/getDiff.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const path = require('path');
22
const simpleGit = require('simple-git');
33
const fs = require('fs').promises;
4+
const fc = require('fast-check');
45
const { Action } = require('../../src/proxy/actions');
56
const { exec } = require('../../src/proxy/processors/push-action/getDiff');
67

@@ -116,4 +117,57 @@ describe('getDiff', () => {
116117
expect(result.steps[0].content).to.not.be.null;
117118
expect(result.steps[0].content.length).to.be.greaterThan(0);
118119
});
120+
121+
describe('fuzzing', () => {
122+
it('should handle random action inputs without crashing', async function () {
123+
// Not comprehensive but helps prevent crashing on bad input
124+
await fc.assert(
125+
fc.asyncProperty(
126+
fc.string({ minLength: 0, maxLength: 40 }),
127+
fc.string({ minLength: 0, maxLength: 40 }),
128+
fc.array(fc.record({ parent: fc.string({ minLength: 0, maxLength: 40 }) }), { maxLength: 3 }),
129+
async (from, to, commitData) => {
130+
const action = new Action('id', 'push', 'POST', Date.now(), 'test/repo');
131+
action.proxyGitPath = __dirname;
132+
action.repoName = 'temp-test-repo';
133+
action.commitFrom = from;
134+
action.commitTo = to;
135+
action.commitData = commitData;
136+
137+
const result = await exec({}, action);
138+
139+
expect(result).to.have.property('steps');
140+
expect(result.steps[0]).to.have.property('error');
141+
expect(result.steps[0]).to.have.property('content');
142+
}
143+
),
144+
{ numRuns: 10 }
145+
);
146+
});
147+
148+
it('should handle randomized commitFrom and commitTo of proper length', async function () {
149+
await fc.assert(
150+
fc.asyncProperty(
151+
fc.stringMatching(/^[0-9a-fA-F]{40}$/),
152+
fc.stringMatching(/^[0-9a-fA-F]{40}$/),
153+
async (from, to) => {
154+
const action = new Action('id', 'push', 'POST', Date.now(), 'test/repo');
155+
action.proxyGitPath = __dirname;
156+
action.repoName = 'temp-test-repo';
157+
action.commitFrom = from;
158+
action.commitTo = to;
159+
action.commitData = [
160+
{ parent: '0000000000000000000000000000000000000000' }
161+
];
162+
163+
const result = await exec({}, action);
164+
165+
expect(result.steps[0].error).to.be.true;
166+
expect(result.steps[0].errorMessage).to.contain('Invalid revision range');
167+
}
168+
),
169+
{ numRuns: 10 }
170+
);
171+
});
172+
});
119173
});

0 commit comments

Comments
 (0)