From 8857b80ae0dd7be54d0d731000c9f8edb0434336 Mon Sep 17 00:00:00 2001 From: James Lal Date: Fri, 26 Apr 2013 08:54:43 -0700 Subject: Bug 867747 - OAuth2 authentication support (particularly for google) r=kgrandon,gaye --- test/caldav/connection_test.js | 64 ++++++--- test/caldav/http/basic_auth_test.js | 57 ++++++++ test/caldav/http/oauth2_test.js | 194 +++++++++++++++++++++++++++ test/caldav/index_test.js | 2 + test/caldav/oauth2_test.js | 258 ++++++++++++++++++++++++++++++++++++ test/caldav/querystring_test.js | 20 +++ test/caldav/xhr_test.js | 2 +- test/helper.js | 36 ++++- test/support/fake_xhr.js | 15 ++- 9 files changed, 617 insertions(+), 31 deletions(-) create mode 100644 test/caldav/http/basic_auth_test.js create mode 100644 test/caldav/http/oauth2_test.js create mode 100644 test/caldav/oauth2_test.js create mode 100644 test/caldav/querystring_test.js (limited to 'test') diff --git a/test/caldav/connection_test.js b/test/caldav/connection_test.js index fb32e8e..b035072 100644 --- a/test/caldav/connection_test.js +++ b/test/caldav/connection_test.js @@ -1,20 +1,25 @@ testSupport.lib('xhr'); testSupport.lib('connection'); +testSupport.lib('http/basic_auth'); +testSupport.lib('http/oauth2'); suite('caldav/connection', function() { - var subject; var Connection; var XHR; - var user = 'foo'; - var password = 'bar'; - var domain = 'http://foo.com'; + var BasicAuth; suiteSetup(function() { Connection = Caldav.require('connection'); XHR = Caldav.require('xhr'); + BasicAuth = Caldav.require('http/basic_auth'); }); + var subject; + var user = 'foo'; + var password = 'bar'; + var domain = 'http://foo.com'; + setup(function() { subject = new Connection({ user: user, @@ -38,32 +43,53 @@ suite('caldav/connection', function() { assert.equal(subject.domain, domain, 'should remove trailing slash'); }); - }); + suite('#request', function() { - suite('request', function() { + function commonCases() { + test('url without domain', function() { + var request = subject.request({ + url: 'bar.json' + }); - test('credentails', function() { - var result = subject.request({ - url: domain + // we add slash + assert.equal(request.url, domain + '/bar.json'); }); + } - assert.instanceOf(result, XHR); - assert.equal(result.url, domain); - assert.equal(result.password, password); - assert.equal(result.user, user); - }); + suite('basic auth (default)', function() { + + test('credentails', function() { + var result = subject.request({ + url: domain + }); - test('url without domain', function() { - var request = subject.request({ - url: 'bar.json' + assert.instanceOf(result, BasicAuth); + assert.equal(result.url, domain); + assert.equal(result.password, password); + assert.equal(result.user, user); }); - // we add slash - assert.equal(request.url, domain + '/bar.json'); + commonCases(); }); }); + suite('#update', function() { + test('without .onupdate handler', function() { + subject.update({ x: true }); + assert.equal(subject.x, true); + }); + + test('with handler', function(done) { + subject.onupdate = function() { + assert.equal(subject.oauth, 'foo'); + done(); + }; + + subject.update({ oauth: 'foo' }); + }); + }); + }); diff --git a/test/caldav/http/basic_auth_test.js b/test/caldav/http/basic_auth_test.js new file mode 100644 index 0000000..566de08 --- /dev/null +++ b/test/caldav/http/basic_auth_test.js @@ -0,0 +1,57 @@ +testSupport.lib('xhr'); +testSupport.lib('connection'); +testSupport.lib('http/basic_auth'); +testSupport.helper('fake_xhr'); + +suite('http/basic_auth', function() { + + var XHR; + var FakeXhr; + var Connection; + var BasicAuth; + + suiteSetup(function() { + FakeXhr = Caldav.require('support/fake_xhr'); + XHR = Caldav.require('xhr'); + Connection = Caldav.require('connection'); + BasicAuth = Caldav.require('http/basic_auth'); + }); + + var subject; + var connection; + var url = 'http://foo.com/bar'; + + setup(function() { + connection = new Connection({ + user: 'jlal', + password: 'foo', + domain: 'google.com' + }); + + subject = new BasicAuth(connection, { + url: url, + xhrClass: FakeXhr + }); + }); + + test('initialization', function() { + assert.instanceOf(subject, XHR); + assert.equal(subject.url, url); + }); + + test('#send', function() { + var xhr = subject.send(); + + assert.deepEqual( + xhr.openArgs, + [ + 'GET', + url, + subject.async, + connection.user, + connection.password + ] + ); + }); + +}); diff --git a/test/caldav/http/oauth2_test.js b/test/caldav/http/oauth2_test.js new file mode 100644 index 0000000..809dfc0 --- /dev/null +++ b/test/caldav/http/oauth2_test.js @@ -0,0 +1,194 @@ +testSupport.lib('xhr'); +testSupport.lib('connection'); +testSupport.lib('querystring'); +testSupport.lib('oauth2'); +testSupport.lib('http/oauth2'); +testSupport.helper('fake_xhr'); + +suite('http/oauth2', function() { + var XHR; + var FakeXhr; + var Connection; + var GoogleOauth; + var OAuth; + var QueryString; + + suiteSetup(function() { + FakeXhr = Caldav.require('support/fake_xhr'); + XHR = Caldav.require('xhr'); + Connection = Caldav.require('connection'); + GoogleOauth = Caldav.require('http/oauth2'); + QueryString = Caldav.require('querystring'); + OAuth = Caldav.require('oauth2'); + }); + + var subject; + var connection; + + setup(function() { + connection = new Connection({ + domain: 'google.com', + oauth: { code: 'xxx' }, + apiCredentials: { + url: 'http://foobar.com/', + client_id: 'client_id', + client_secret: 'client_secret', + redirect_uri: 'redirect_uri' + } + }); + + subject = new GoogleOauth(connection, { + xhrClass: FakeXhr, + url: 'http://bar.com' + }); + }); + + test('initialization', function() { + assert.instanceOf(subject, XHR); + assert.deepEqual(subject.oauth.apiCredentials, connection.apiCredentials); + }); + + test('without oauth code/refresh_token', function() { + connection.oauth = {}; + assert.throws(function() { + new GoogleOauth(connection, {}); + }, /oauth/); + }); + + suite('.send', function() { + testSupport.mock.useFakeXHR(); + + var updatesConnection; + setup(function() { + updatesConnection = false; + connection.onupdate = function() { + updatesConnection = true; + }; + }); + + function buildRequest() { + return new GoogleOauth(connection, { + url: 'foo/bar', + xhrClass: FakeXhr + }); + } + + function buildResponse(data) { + var result = { + issued_at: Date.now(), + expires_in: 3600, + access_token: 'access_token', + token_type: 'Bearer' + }; + + var key; + for (key in data) { + result[key] = data[key]; + } + + return result; + } + + var request; + var response; + + suite('with code', function() { + setup(function() { + response = buildResponse({ + refresh_token: 'refresh', + user: 'gotuser' + }); + request = buildRequest(); + }); + + test('fetches access_token then sends', function(done) { + var isComplete = false; + var xhr; + + subject.oauth.authenticateCode = function(code, callback) { + assert.equal(code, connection.oauth.code, 'sends correct code'); + setTimeout(function() { + assert.ok(!xhr.sendArgs, 'has not sent request yet'); + callback(null, response); + assert.ok(updatesConnection, 'sends connection update event'); + assert.deepEqual(connection.oauth, response, 'updates connection'); + assert.equal(connection.user, 'gotuser', 'updates user'); + + assert.ok(xhr.sendArgs, 'sent request'); + isComplete = true; + xhr.respond(); + }); + }; + + xhr = subject.send(function() { + assert.ok(isComplete, 'is complete'); + done(); + }); + }); + }); + + suite('with expired access_token', function() { + + var expectedOauth; + setup(function() { + // no refresh_token intentionally + response = buildResponse(); + + connection.oauth = buildResponse({ + issued_at: Date.now() - 10000, + expires_in: 3600, + refresh_token: 'refresh_me' + }); + + request = buildRequest(); + + expectedOauth = {}; + for (var key in response) { + expectedOauth[key] = response[key]; + } + + expectedOauth.refresh_token = connection.oauth.refresh_token; + }); + + test('refreshes access_token', function(done) { + var isComplete = false; + var xhr; + + subject.oauth.refreshToken = function(refreshToken, callback) { + assert.equal( + refreshToken, + connection.oauth.refresh_token, + 'sends correct refresh token' + ); + + + setTimeout(function() { + assert.ok(!xhr.sendArgs, 'has not sent request yet'); + callback(null, response); + assert.ok( + updatesConnection, 'sends connection update event' + ); + + assert.deepEqual( + connection.oauth, expectedOauth, 'updates connection' + ); + + assert.ok(xhr.sendArgs, 'sent request'); + isComplete = true; + xhr.respond(); + }); + }; + + xhr = subject.send(function() { + assert.ok(isComplete, 'is complete'); + done(); + }); + + }); + }); + + suite('with 401 response', function() { + }); + }); + +}); diff --git a/test/caldav/index_test.js b/test/caldav/index_test.js index 59012a4..baad822 100644 --- a/test/caldav/index_test.js +++ b/test/caldav/index_test.js @@ -31,6 +31,8 @@ suite('caldav', function() { assert.ok(root.Connection, 'Caldav.Connection'); assert.ok(root.Resources, 'Caldav.Resources'); assert.ok(root.Resources.Calendar, 'Calendar.Resources.Calendar'); + assert.ok(root.OAuth2, 'OAuth2'); + assert.ok(root.Http, 'Http'); }); }); diff --git a/test/caldav/oauth2_test.js b/test/caldav/oauth2_test.js new file mode 100644 index 0000000..9c3024c --- /dev/null +++ b/test/caldav/oauth2_test.js @@ -0,0 +1,258 @@ +testSupport.lib('querystring'); +testSupport.lib('xhr'); +testSupport.lib('oauth2'); +testSupport.helper('fake_xhr'); + +suite('oauth', function() { + var XHR; + var FakeXhr; + var OAuth; + var QueryString; + + suiteSetup(function() { + FakeXhr = Caldav.require('support/fake_xhr'); + XHR = Caldav.require('xhr'); + QueryString = Caldav.require('querystring'); + OAuth = Caldav.require('oauth2'); + }); + + function mockTime() { + setup(function() { + this.clock = this.sinon.useFakeTimers(); + }); + + teardown(function() { + this.clock.restore(); + }); + } + + var subject; + var apiCredentials = { + url: 'http://foobar.com/', + client_id: 'client_id', + client_secret: 'client_secret', + redirect_uri: 'redirect_uri' + }; + + setup(function() { + subject = new OAuth(apiCredentials); + }); + + test('initialization', function() { + assert.deepEqual(subject.apiCredentials, apiCredentials); + }); + + test('invalid credentials', function() { + assert.throws(function() { + new OAuth({ foo: 'bar' }); + }, /apiCredentials/); + }); + + suite('#authoriztionCode', function() { + testSupport.mock.useFakeXHR(); + + var code = 'codexxx'; + + test('without code', function(done) { + subject.authenticateCode(null, function(err) { + assert.instanceOf(err, Error, 'sends an error'); + done(); + }); + }); + + suite('success', function() { + mockTime(); + + var response = { + access_token: 'token', + refresh_token: 'refresh', + expires_in: 3600, + token_type: 'Bearer' + }; + + var expectedResponse; + var expectedRequest; + + setup(function() { + // copy expected properties over + expectedResponse = { + issued_at: Date.now() + }; + + for (var key in response) { + expectedResponse[key] = response[key]; + } + + // expected post data + expectedRequest = QueryString.stringify({ + code: code, + client_id: subject.apiCredentials.client_id, + client_secret: subject.apiCredentials.client_secret, + redirect_uri: subject.apiCredentials.redirect_uri, + grant_type: 'authorization_code' + }); + }); + + test('sending request', function(done) { + var isComplete = false; + var xhr = subject.authenticateCode(code, function(err, result) { + assert.isNull(err); + + assert.isTrue(isComplete, 'completed assertions'); + assert.deepEqual(result, expectedResponse); + done(); + }); + + + // verify xhr does the right thing + assert.equal(xhr.sendArgs[0], expectedRequest, 'sends correct params'); + assert.equal(xhr.openArgs[0], 'POST', 'is HTTP post verb'); + assert.equal(xhr.openArgs[1], apiCredentials.url, 'opened with url'); + isComplete = true; + + xhr.respond( + JSON.stringify(response), + 200, + { 'Content-Type': 'application/json' } + ); + }); + + test('with username_info', function(done) { + var userInfoData = { email: 'myfooba.com' }; + var userInfo = subject.apiCredentials.user_info = { + url: 'http://google.com/', + field: 'email' + }; + + var isComplete = false; + var xhr = subject.authenticateCode(code, function(err, data) { + assert.isNull(err, 'no error'); + assert.ok(isComplete, 'completed testing'); + expectedResponse.user = userInfoData.email; + assert.deepEqual(data, expectedResponse); + done(); + }); + + xhr.respond( + JSON.stringify(response), + 200, + { 'Content-Type': 'application/json' } + ); + + var userInfoXhr = + FakeXhr.instances[FakeXhr.instances.length - 1]; + + assert.notEqual(userInfoXhr, xhr, 'issued userinfo'); + + assert.equal( + userInfoXhr.openArgs[1], + userInfo.url + ); + + assert.equal( + userInfoXhr.headers['Authorization'], + response.token_type + ' ' + response.access_token, + 'sets access token' + ); + + isComplete = true; + + userInfoXhr.respond( + JSON.stringify(userInfoData), + 200, + { 'Content-Type': 'application/json' } + ); + }); + + }); + }); + + suite('refreshToken', function() { + test('without .refresh_token', function() { + assert.throws(function() { + subject.refreshToken(null, function() {}); + }, /token/); + }); + + suite('success', function() { + testSupport.mock.useFakeXHR(); + mockTime(); + + var refreshToken = 'mytokenfoo'; + var response = { + access_token: 'newcode', + expires_in: 3600, + token_type: 'Bearer' + }; + + var expectedResponse; + var expectedRequest; + setup(function() { + expectedResponse = { + access_token: response.access_token, + expires_in: response.expires_in, + token_type: response.token_type, + issued_at: Date.now() + }; + + expectedRequest = QueryString.stringify({ + refresh_token: refreshToken, + client_id: subject.apiCredentials.client_id, + client_secret: subject.apiCredentials.client_secret, + grant_type: 'refresh_token' + }); + }); + + + test('send request', function(done) { + var isComplete = false; + var xhr = subject.refreshToken(refreshToken, function(err, result) { + assert.isNull(err); + assert.isTrue(isComplete, 'assertions complete'); + assert.deepEqual(result, expectedResponse); + done(); + }); + + assert.deepEqual(xhr.sendArgs[0], expectedRequest, 'sent formdata'); + assert.equal(xhr.openArgs[1], apiCredentials.url, 'opened with url'); + isComplete = true; + + xhr.respond( + JSON.stringify(response), + 200, + { 'Content-Type': 'application/json' } + ); + }); + }); + }); + + suite('#accessTokenValid', function() { + test('no access_token', function() { + assert.isFalse(subject.accessTokenValid({ code: 'xxx' })); + }); + + test('access_token present but time invalid', function() { + var oauth = { + access_token: 'xxx', + expires_in: 3600, + issued_at: Date.now() - 3700 + }; + + assert.isFalse(subject.accessTokenValid(oauth)); + }); + + + test('access_token present and not expired', function() { + var oauth = { + access_token: 'xxx', + expires_in: 3600, + issued_at: Date.now() + }; + + assert.isTrue(subject.accessTokenValid(oauth)); + }); + }); + + +}); + diff --git a/test/caldav/querystring_test.js b/test/caldav/querystring_test.js new file mode 100644 index 0000000..e614b6f --- /dev/null +++ b/test/caldav/querystring_test.js @@ -0,0 +1,20 @@ +testSupport.lib('querystring'); + +suite('caldav/querystring', function() { + var QueryString; + + suiteSetup(function() { + QueryString = Caldav.require('querystring'); + }); + + /* + * quick sanity check we just copied node's version so we expect it to work. + */ + test('stringify', function() { + var input = { foo: 'bar', baz: 'qux' }; + var expected = 'foo=bar&baz=qux'; + + assert.equal(QueryString.stringify(input), expected); + }); + +}); diff --git a/test/caldav/xhr_test.js b/test/caldav/xhr_test.js index a3e6b66..6e93337 100644 --- a/test/caldav/xhr_test.js +++ b/test/caldav/xhr_test.js @@ -1,7 +1,7 @@ testSupport.lib('xhr'); testSupport.helper('fake_xhr'); -suite('webacls/xhr', function() { +suite('xhr', function() { var subject, Xhr, FakeXhr; diff --git a/test/helper.js b/test/helper.js index 2a8c772..5cf8d5f 100644 --- a/test/helper.js +++ b/test/helper.js @@ -23,7 +23,7 @@ } requireBak.apply(this, arguments); - } + }; } /* cross require */ @@ -45,7 +45,7 @@ } else { window.require(file, callback); } - } + }; /* sinon */ if (testSupport.isNode) { @@ -103,7 +103,7 @@ cb(null, xhr.responseText); } } - } + }; xhr.send(null); } }; @@ -121,6 +121,26 @@ }; testSupport.mock = { + useFakeXHR: function() { + testSupport.helper('fake_xhr'); + testSupport.lib('xhr'); + + var realXHR; + var XHR; + var FakeXhr; + + suiteSetup(function() { + XHR = Caldav.require('xhr'); + FakeXhr = Caldav.require('support/fake_xhr'); + + realXHR = XHR.prototype.xhrClass; + XHR.prototype.xhrClass = FakeXhr; + }); + + suiteTeardown(function() { + XHR.prototype.xhrClass = realXHR; + }); + }, /** * Mocks out a method @@ -165,7 +185,7 @@ testSupport.helper = function(lib) { testSupport.require('/test/support/' + lib); - } + }; Caldav = require('../lib/caldav/caldav.js'); @@ -179,12 +199,15 @@ return require(path); } return oldRequire(path); - } + }; } + /* since we have global mocks easier to just include these globally */ requireRequest = function(callback) { testSupport.lib('responder'); - testSupport.lib('xhr'); + testSupport.lib('oauth2'); + testSupport.lib('http/basic_auth'); + testSupport.lib('http/oauth2'); testSupport.lib('connection'); testSupport.lib('sax'); testSupport.lib('sax/base'); @@ -192,7 +215,6 @@ testSupport.lib('request/errors'); testSupport.lib('request/abstract'); testSupport.lib('template'); - testSupport.helper('fake_xhr'); testSupport.lib('request/propfind'); //in the future we need a callback for browser support. diff --git a/test/support/fake_xhr.js b/test/support/fake_xhr.js index 0ffcff8..3f6cea0 100644 --- a/test/support/fake_xhr.js +++ b/test/support/fake_xhr.js @@ -1,4 +1,5 @@ (function(module) { + console.log('I HAZ LOADED'); function FakeXhr() { this.openArgs = null; @@ -14,7 +15,7 @@ FakeXhr.prototype = { open: function() { - this.openArgs = arguments; + this.openArgs = Array.prototype.slice.call(arguments); }, getResponseHeader: function(key) { @@ -26,18 +27,24 @@ }, send: function() { - this.sendArgs = arguments; + this.sendArgs = Array.prototype.slice.call(arguments); }, - respond: function(data, code) { + respond: function(data, code, headers) { + if (headers) { + this.responseHeaders = headers; + } else { + this.responseHeaders['content-type'] = 'text/xml'; + } + this.readyState = 4; - this.responseHeaders['content-type'] = 'text/xml'; this.responseText = data; this.status = code || 200; this.onreadystatechange(); } }; + console.log('EXPORTS ME', FakeXhr); module.exports = FakeXhr; }.apply( -- cgit