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 2 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
46 changes: 24 additions & 22 deletions lib/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ var specSchema= require('./spec-schema');

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

var ContentType = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this 👍

Could we though move it into a separate content-type.js module and export the functions? 💭

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

function getMediaType( contentType ) {
return contentType.match( mediaTypeRe )[0].toLowerCase();
}
Expand Down Expand Up @@ -48,25 +60,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 +89,27 @@ function isBodyEqual( httpReq, specReq, contentType ) {
return true;
}

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

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

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

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

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

return false;
}

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

if(isMultipartContentType(httpMediaType) && isMultipartContentType(specMediaType)) {
if(ContentType.isMultipart(httpMediaType) && ContentType.isMultipart(specMediaType)) {
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]});
});
});

});