From 7e004cf60daeae11f2e6a630c220c8c444753704 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Thu, 9 Jul 2015 20:11:27 -0400 Subject: [PATCH 1/3] npm: Add dev-deps + scripts for integration tests --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e3b10f..96f1cf3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "coveralls": "npm run coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage", "coverage": "istanbul cover node_modules/.bin/_mocha -- test/unit/index.test.js", "lint": "jshint .", - "test": "mocha test/unit/index.test.js", + "test": "npm run test-unit && npm run test-integration", + "test-integration": "karma start", + "test-unit": "mocha test/unit/index.test.js", "validate": "npm run lint && npm test && npm run coveralls" }, "repository": { @@ -30,8 +32,14 @@ "devDependencies": { "coveralls": "^2.11.2", "istanbul": "^0.3.16", + "jquery": "^2.1.4", "jshint": "^2.8.0", + "karma": "^0.12.37", + "karma-browserify": "^4.2.1", + "karma-mocha": "^0.2.0", + "karma-phantomjs-launcher": "^0.2.0", "mocha": "^2.2.5", + "phantomjs": "^1.9.17", "sinon": "^1.15.3" } } From f2039b1be5c70c93ef9b020df64dea54f9371fa9 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Thu, 9 Jul 2015 20:12:10 -0400 Subject: [PATCH 2/3] test: Add karma config for integration tests --- karma.conf.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 karma.conf.js diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..b5a1370 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,66 @@ +/* jshint node:true */ +'use strict'; + +module.exports = function(config) { + config.set({ + // base path, that will be used to resolve files and exclude + basePath: '', + + frameworks: ['browserify', 'mocha'], + + // list of files / patterns to load in the browser + files: [ + 'test/integration/*.test.js' + ], + + // list of files to exclude + exclude: [], + + preprocessors: { + 'test/**/*.js': ['browserify'] + }, + + browserify: { + debug: true + }, + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit' + reporters: ['dots'], + + // web server port + port: 9876, + + // cli runner port + runnerPort: 9100, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_{DISABLE,ERROR,WARN,INFO,DEBUG} + logLevel: config.LOG_INFO, + + // watch files and execute tests whenever any file changes + autoWatch: false, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: [ + 'PhantomJS' + ], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: true + }); +}; From 036e95ad06321886987d356964b6f3d9f3846921 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Thu, 9 Jul 2015 20:12:27 -0400 Subject: [PATCH 3/3] lib: Fix data for form-urlencoded content type Update the `data` option to properly handle both application/json and application/x-www-form-urlencoded content types. --- index.js | 12 ++- test/integration/index.test.js | 140 +++++++++++++++++++++++++++++++++ test/unit/index.test.js | 36 ++++++++- 3 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 test/integration/index.test.js diff --git a/index.js b/index.js index e725ef7..3440f38 100644 --- a/index.js +++ b/index.js @@ -15,9 +15,15 @@ function injectHeader(header, tokenValue, jqXHR) { } function injectData(tokenValue, options, key) { - var data = options.data ? JSON.parse(options.data) : {}; - data[key] = tokenValue; - options.data = JSON.stringify(data); + var data; + if (~options.contentType.indexOf('application/json')) { + data = options.data ? JSON.parse(options.data) : {}; + data[key] = tokenValue; + options.data = JSON.stringify(data); + } else { + options.data += options.data ? '&' : ''; + options.data += key + '=' + tokenValue; + } } function injectQuery(tokenValue, options, param) { diff --git a/test/integration/index.test.js b/test/integration/index.test.js new file mode 100644 index 0000000..5e2176d --- /dev/null +++ b/test/integration/index.test.js @@ -0,0 +1,140 @@ +'use strict'; + +var $ = require('jquery'), + assert = require('assert'), + sinon = require('sinon'), + createCsrfPrefilter = require('../..'); + +describe('Requests prefiltered with CSRF token injection', function() { + var xhr, requests, prefilter, prefilters = $.Callbacks(); + + $.ajaxPrefilter(prefilters.fire); + + describe('POST with header prefilter', function() { + + beforeEach(function() { + prefilter = createCsrfPrefilter('my-token-value'); + prefilters.add(prefilter); + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = function(req) { requests.push(req); }; + + $.ajax({ + type: 'POST', + url: '/some/url', + dataType: 'json', + data: { + foo: 'bar', + baz: 'qux' + } + }); + }); + + afterEach(function() { + xhr.restore(); + prefilters.remove(prefilter); + }); + + it('should inject expected header', function() { + assert.strictEqual(requests[0].requestHeaders['X-CSRF-Token'], 'my-token-value'); + }); + }); + + describe('POST with data prefilter and application/json content-type', function() { + + beforeEach(function() { + prefilter = createCsrfPrefilter('my-token-value', { + data: '_csrf' + }); + prefilters.add(prefilter); + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = function(req) { requests.push(req); }; + + $.ajax({ + type: 'POST', + url: '/some/url', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify({ + foo: 'bar', + baz: 'qux' + }) + }); + }); + + afterEach(function() { + xhr.restore(); + prefilters.remove(prefilter); + }); + + it('should inject expected header', function() { + var requestBody = JSON.parse(requests[0].requestBody); + assert.strictEqual(requestBody._csrf, 'my-token-value'); + }); + }); + + describe('POST with data prefilter and application/x-www-form-urlencoded content-type', function() { + + beforeEach(function() { + prefilter = createCsrfPrefilter('my-token-value', { + data: '_csrf' + }); + prefilters.add(prefilter); + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = function(req) { requests.push(req); }; + + $.ajax({ + type: 'POST', + url: '/some/url', + dataType: 'json', + data: { + foo: 'bar', + baz: 'qux' + } + }); + }); + + afterEach(function() { + xhr.restore(); + prefilters.remove(prefilter); + }); + + it('should inject expected header', function() { + assert.strictEqual(requests[0].requestBody, 'foo=bar&baz=qux&_csrf=my-token-value'); + }); + }); + + describe('POST with query prefilter', function() { + + beforeEach(function() { + prefilter = createCsrfPrefilter('my-token-value', { + query: '_csrf' + }); + prefilters.add(prefilter); + xhr = sinon.useFakeXMLHttpRequest(); + requests = []; + xhr.onCreate = function(req) { requests.push(req); }; + + $.ajax({ + type: 'POST', + url: '/some/url', + dataType: 'json', + data: { + foo: 'bar', + baz: 'qux' + } + }); + }); + + afterEach(function() { + xhr.restore(); + prefilters.remove(prefilter); + }); + + it('should inject expected header', function() { + assert.strictEqual(requests[0].url, '/some/url?_csrf=my-token-value'); + }); + }); +}); diff --git a/test/unit/index.test.js b/test/unit/index.test.js index 5264174..8d63c80 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -77,10 +77,11 @@ describe('jQuery CSRF prefilter factory', function() { }); }); - describe('when prefilter created with data option', function() { + describe('when prefilter created with data option and content type application/json', function() { beforeEach(function() { opts.data = '_csrf'; + options.contentType = 'application/json'; prefilter = createCsrfPrefilter(tokenValue, opts); }); @@ -108,6 +109,38 @@ describe('jQuery CSRF prefilter factory', function() { }); }); + describe('when prefilter created with data option and content type application/x-www-form-urlencoded', function() { + + beforeEach(function() { + opts.data = '_csrf'; + options.contentType = 'application/x-www-form-urlencoded'; + prefilter = createCsrfPrefilter(tokenValue, opts); + }); + + describe('and ajax request is safe', function() { + + it('should not inject specified data into request', function() { + safeMethods.forEach(function(method) { + options.type = method; + prefilter(options, originalOptions, jqXHR); + assert(!options.data); + }); + }); + }); + + describe('and ajax request is unsafe', function() { + + it('should inject specified data into request', function() { + unsafeMethods.forEach(function(method) { + options.type = method; + prefilter(options, originalOptions, jqXHR); + assert(~options.data.indexOf('_csrf=' + tokenValue), tokenValue); + assert(!jqXHR.setRequestHeader.calledWith('X-CSRF-Token', tokenValue)); + }); + }); + }); + }); + describe('when prefilter created with query option', function() { beforeEach(function() { @@ -152,6 +185,7 @@ describe('jQuery CSRF prefilter factory', function() { opts.header = 'X-CSRFToken'; opts.data = '_csrf'; opts.query = '_csrf'; + options.contentType = 'application/json'; prefilter = createCsrfPrefilter(tokenValue, opts); });