Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for matching form-urlencoded http request body with schema mentioned in spec #76

Merged
merged 5 commits into from
Jul 27, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions lib/content-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
var ContentTypeChecker = function(contentType) {
function match(regex, contentType) {
return contentType ? regex.test(contentType) : false;
}

return {
isJson: function() {
return match(/json/i, contentType);
},
isMultipart: function() {
return match(/multipart\/form-data/i, contentType);
},
isFormUrlEncoded: function() {
return match(/application\/x-www-form-urlencoded/i, contentType);
}
};
};

module.exports = ContentTypeChecker;
35 changes: 13 additions & 22 deletions lib/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var logger = require('./logger');
var lodash = require('lodash');
var urlParser = require('./url-parser');
var specSchema= require('./spec-schema');
var ContentTypeChecker = require('./content-type');

var mediaTypeRe = /^\s*([^;]+)/i;

Expand Down Expand Up @@ -48,25 +49,13 @@ function getHeaderFromHttpReq( httpReq, header ) {
return null;
}

function isJsonBody(contentType) {
return contentType ? /json/i.test(contentType) : false;
}

function isMultipartContentType(contentType) {
if(/multipart\/form-data/i.test(contentType)) {
return true;
}

return false;
}

function getBodyContent(req, contentType){
function getBodyContent(req, parseToJson){
var body = null;
if (req && req.body) {
body = req.body.trim();
}

if (isJsonBody(contentType)){
if (parseToJson){
try {
body = JSON.parse(body);
} catch (e) {
Expand All @@ -89,25 +78,27 @@ function isBodyEqual( httpReq, specReq, contentType ) {
return true;
}

var reqBody = getBodyContent(httpReq, contentType);
var specBody = getBodyContent(specReq, contentType);
var reqBody = getBodyContent(httpReq, ContentTypeChecker(contentType).isJson());
var specBody = getBodyContent(specReq, (ContentTypeChecker(contentType).isJson() || ContentTypeChecker(contentType).isFormUrlEncoded()));

if (reqBody === specBody){
return true;
}

if(isMultipartContentType(contentType)) {
if(ContentTypeChecker(contentType).isMultipart()) {
return true;
}

if (/application\/x-www-form-urlencoded/i.test(contentType)) {
var jsonEncodedSpecBody = JSON.parse(specBody);
return urlParser.jsonToFormEncodedString(jsonEncodedSpecBody) === reqBody;
if (ContentTypeChecker(contentType).isFormUrlEncoded()) {
var httpBodyInJson = urlParser.formEncodedStringToJson(reqBody);
return validateJson(httpBodyInJson, specBody, specReq.schema);
}

if (isJsonBody(contentType)){
if (ContentTypeChecker(contentType).isJson()) {
return validateJson(reqBody, specBody, specReq.schema);
}

return false;
}

function hasHeaders( httpReq, specReq ){
Expand Down Expand Up @@ -137,7 +128,7 @@ function areContentTypesSame(httpMediaType, specMediaType) {
return true;
}

if(isMultipartContentType(httpMediaType) && isMultipartContentType(specMediaType)) {
if(ContentTypeChecker(httpMediaType).isMultipart() && ContentTypeChecker(specMediaType).isMultipart()) {
return true;
}

Expand Down
37 changes: 37 additions & 0 deletions lib/url-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,40 @@ exports.jsonToFormEncodedString = function(objectData) {
}
return arrayStr.join('&');
};

exports.formEncodedStringToJson = function(keyValue) {
var obj = {},
key_value,
key;

function isDefined(value) {
return typeof value !== 'undefined';
}

function tryDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
} catch(e) {
// Ignore any invalid uri component
}
}

(keyValue || '').split('&').forEach(function(keyValue) {
if ( keyValue ) {
key_value = keyValue.replace(/\+/g,'%20').split('=');
key = tryDecodeURIComponent(key_value[0]);
if ( isDefined(key) ) {
var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = val;
} else if(Array.isArray(obj[key])) {
obj[key].push(val);
} else {
obj[key] = [obj[key],val];
}
}
}
});

return obj;
};
56 changes: 56 additions & 0 deletions test/api/form-urlencoded-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
var helper = require('../lib');
var request = helper.getRequest();

describe('UrlEncoded Requests', function() {
before(function (done) {
helper.drakov.run({sourceFiles: 'test/example/md/form-urlencoded.md'}, done);
});

after(function (done) {
helper.drakov.stop(done);
});

describe('/api/urlencoded', function() {
describe('if http request body matches exactly with spec request body', function() {
it('should respond with success response', function(done) {
request.post('/api/urlencoded')
.set('Content-type', 'application/x-www-form-urlencoded')
.send('random_number=4&static=not_random')

.expect(200)
.expect('Content-type', 'application/json;charset=UTF-8')
.expect({success: true})
.end(helper.endCb(done));
});
});

describe('if request body does not match with spec request body', function() {
describe('but schema matches', function() {
it('should respond with success response', function(done) {
request.post('/api/urlencoded')
.set('Content-type', 'application/x-www-form-urlencoded')
.send('random_number=100&static=not_random')

.expect(200)
.expect('Content-type', 'application/json;charset=UTF-8')
.expect({success: true})
.end(helper.endCb(done));
});
});

describe('and schema also does not match', function() {
it('should respond with error response', function(done) {
request.post('/api/urlencoded')
.set('Content-type', 'application/x-www-form-urlencoded')
.send('test=false')

.expect(404)
.expect('Content-type', 'text/html; charset=utf-8')
.end(helper.endCb(done));
});
});
});

});

});
49 changes: 49 additions & 0 deletions test/example/md/form-urlencoded.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
FORMAT: 1A

# Accept Form Urlencoded
Accept form urlencoded request type

## Things [/api/urlencoded]

### Accept urlencoded request using schema [POST]

+ Request (application/x-www-form-urlencoded)

+ Body

{
"random_number": "4",
"static": "not_random"
}

+ Schema

{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://jsonschema.net",
"type": "object",
"properties": {
"random_number": {
"id": "http://jsonschema.net/random_number",
"type": "string",
"description": "chosen by fair dice roll. guaranteed to be random"
},
"static": {
"id": "http://jsonschema.net/static",
"type": "string",
"description": "not random"
}
},
"required": [
"random_number",
"static"
]
}

+ Response 200 (application/json;charset=UTF-8)

+ Body

{
"success": true
}
25 changes: 25 additions & 0 deletions test/unit/url-parser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,29 @@ describe('URL Parser', function() {
});
});

describe('Form Encoded string to JSON', function() {
it('should parse a string into key-value pairs', function() {
assert.deepEqual(urlParser.formEncodedStringToJson(''), {});
assert.deepEqual(urlParser.formEncodedStringToJson('simple=pair'), {simple: 'pair'});
assert.deepEqual(urlParser.formEncodedStringToJson('first=1&second=2'), {first: '1', second: '2'});
assert.deepEqual(urlParser.formEncodedStringToJson('escaped%20key=escaped%20value'), {'escaped key': 'escaped value'});
assert.deepEqual(urlParser.formEncodedStringToJson('emptyKey='), {emptyKey: ''});
assert.deepEqual(urlParser.formEncodedStringToJson('flag1&key=value&flag2'), {flag1: true, key: 'value', flag2: true});
});
it('should ignore key values that are not valid URI components', function() {
assert.doesNotThrow(function() { urlParser.formEncodedStringToJson('%'); });
assert.deepEqual(urlParser.formEncodedStringToJson('%'), {});
assert.deepEqual(urlParser.formEncodedStringToJson('invalid=%'), { invalid: undefined });
assert.deepEqual(urlParser.formEncodedStringToJson('invalid=%&valid=good'), { invalid: undefined, valid: 'good' });
});
it('should parse a string into key-value pairs with duplicates grouped in an array', function() {
assert.deepEqual(urlParser.formEncodedStringToJson(''), {});
assert.deepEqual(urlParser.formEncodedStringToJson('duplicate=pair'), {duplicate: 'pair'});
assert.deepEqual(urlParser.formEncodedStringToJson('first=1&first=2'), {first: ['1','2']});
assert.deepEqual(urlParser.formEncodedStringToJson('escaped%20key=escaped%20value&&escaped%20key=escaped%20value2'), {'escaped key': ['escaped value','escaped value2']});
assert.deepEqual(urlParser.formEncodedStringToJson('flag1&key=value&flag1'), {flag1: [true,true], key: 'value'});
assert.deepEqual(urlParser.formEncodedStringToJson('flag1&flag1=value&flag1=value2&flag1'), {flag1: [true,'value','value2',true]});
});
});

});