From e5a7db62792e26052741826253fed2b7b4a268da Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Thu, 30 Aug 2018 17:00:35 -0400 Subject: [PATCH 1/4] Add default behavior for client_credentials grant type. --- README.md | 22 +++++--- samples/TwitterAppOnly.gs | 10 ++-- src/Service.js | 17 +++++- src/Utilities.js | 24 ++++++++ test/mocks/urlfetchapp.js | 2 +- test/test.js | 112 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 171 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 04bae888..53e49207 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,13 @@ in your manifest file, ensure that the following scope is included: ## Redirect URI Before you can start authenticating against an OAuth2 provider, you usually need -to register your application with that OAuth2 provider and obtain a client ID and secret. Often -a provider's registration screen requires you to enter a "Redirect URI", which is the -URL that the user's browser will be redirected to after they've authorized access to their account at that provider. +to register your application with that OAuth2 provider and obtain a client ID +and secret. Often a provider's registration screen requires you to enter a +"Redirect URI", which is the URL that the user's browser will be redirected to +after they've authorized access to their account at that provider. -For this library (and the Apps Script functionality in general) the URL will always -be in the following format: +For this library (and the Apps Script functionality in general) the URL will +always be in the following format: https://script.google.com/macros/d/{SCRIPT ID}/usercallback @@ -350,8 +351,15 @@ headers. The most common of these is the `client_credentials` grant type, which often requires that the client ID and secret are passed in the Authorization header. -See the sample [`TwitterAppOnly.gs`](samples/TwitterAppOnly.gs) for more -information. +When using this grant type, if you set a client ID and secret using +`setClientId()` and `setClientSecret()` respectively then an +`Authorization: Basic ...` header will be added to the token request +automatically, since this is what most OAuth2 providers require. If your +provider uses a different method of authorization then don't set the client ID +and secret and add an authorization header manually. + +See the sample [`TwitterAppOnly.gs`](samples/TwitterAppOnly.gs) for a working +example. ## Compatibility diff --git a/samples/TwitterAppOnly.gs b/samples/TwitterAppOnly.gs index ccf2c887..b39bf10a 100644 --- a/samples/TwitterAppOnly.gs +++ b/samples/TwitterAppOnly.gs @@ -43,15 +43,13 @@ function getService() { // Set the endpoint URLs. .setTokenUrl('https://api.twitter.com/oauth2/token') + // Set the client ID and secret. + .setClientId(CLIENT_ID) + .setClientSecret(CLIENT_SECRET) + // Sets the custom grant type to use. .setGrantType('client_credentials') - // Sets the required Authorization header. - .setTokenHeaders({ - Authorization: 'Basic ' + - Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET) - }) - // Set the property store where authorized tokens should be persisted. .setPropertyStore(PropertiesService.getUserProperties()); } diff --git a/src/Service.js b/src/Service.js index f322906a..4522f4c0 100644 --- a/src/Service.js +++ b/src/Service.js @@ -698,8 +698,8 @@ Service_.prototype.lockable_ = function(func) { /** * Obtain an access token using the custom grant type specified. Most often - * this will be "client_credentials", in which case make sure to also specify an - * Authorization header if required by your OAuth provider. + * this will be "client_credentials", and a client ID and secret are set an + * "Authorization: Baseic ..." header will be added using thos values. */ Service_.prototype.exchangeGrant_ = function() { validate_({ @@ -710,6 +710,19 @@ Service_.prototype.exchangeGrant_ = function() { grant_type: this.grantType_ }; payload = extend_(payload, this.params_); + + // For the client_credentials grant type, add a basic authorization header + // if the client ID and client secret are set and no authorization header has + // been set yet (AKA do the expected thing). + if (this.grantType_ === 'client_credentials' && + this.clientId_ && + this.clientSecret_ && + !getValueCaseInsensitive_(this.tokenHeaders_, 'Authorization')) { + this.tokenHeaders_ = this.tokenHeaders_ || {}; + this.tokenHeaders_.Authorization = 'Basic ' + + Utilities.base64Encode(this.clientId_ + ':' + this.clientSecret_); + } + var token = this.fetchToken_(payload); this.saveToken_(token); }; diff --git a/src/Utilities.js b/src/Utilities.js index 54b7fc28..1f5ac231 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -76,3 +76,27 @@ function extend_(destination, source) { } return destination; } + +/* exported getValueCaseInsensitive_ */ +/** + * Gets the value stored in the object under the given key, in a + * case-insensitive way. + * @param {Object} obj The object to search in. + * @param {string} key The key to search for. + * @return {Object} the value under that key, or undefined otherwise + */ +function getValueCaseInsensitive_(obj, key) { + if (obj == null || typeof obj !== 'object' || + key == null || !key.toString) { + return undefined; + } + if (key in obj) { + return obj[key]; + } + return Object.keys(obj).reduce(function(result, k) { + if (result) return result; + if (k.toLowerCase() === key.toLowerCase()) { + return obj[k]; + } + }, undefined); +} diff --git a/test/mocks/urlfetchapp.js b/test/mocks/urlfetchapp.js index 999ac636..743dbced 100644 --- a/test/mocks/urlfetchapp.js +++ b/test/mocks/urlfetchapp.js @@ -12,7 +12,7 @@ var MockUrlFetchApp = function() { MockUrlFetchApp.prototype.fetch = function(url, optOptions) { var delay = this.delayFunction(); - var result = this.resultFunction(); + var result = this.resultFunction(url, optOptions); if (delay) { sleep(delay).wait(); } diff --git a/test/test.js b/test/test.js index b5df01cd..5cbb2b6d 100644 --- a/test/test.js +++ b/test/test.js @@ -13,6 +13,11 @@ var mocks = { } }, UrlFetchApp: new MockUrlFetchApp(), + Utilities: { + base64Encode: function(data) { + return Buffer.from(data).toString('base64'); + } + }, __proto__: gas.globalMockDefault }; var OAuth2 = gas.require('./src', mocks); @@ -236,6 +241,82 @@ describe('Service', function() { }); }); }); + + describe('#exchangeGrant_()', function() { + var getValueCaseInsensitive_ = OAuth2.getValueCaseInsensitive_; + + it('should not set auth header if the grant type is not client_credentials', + function(done) { + mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { + assert.isUndefined( + getValueCaseInsensitive_(urlOptions.headers, 'Authorization')); + done(); + }; + var service = OAuth2.createService('test') + .setGrantType('fake') + .setTokenUrl('http://www.example.com'); + service.exchangeGrant_(); + }); + + it('should not set auth header if the client ID is not set', + function(done) { + mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { + assert.isUndefined( + getValueCaseInsensitive_(urlOptions.headers, 'Authorization')); + done(); + }; + var service = OAuth2.createService('test') + .setGrantType('client_credentials') + .setTokenUrl('http://www.example.com'); + service.exchangeGrant_(); + }); + + it('should not set auth header if the client secret is not set', + function(done) { + mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { + assert.isUndefined( + getValueCaseInsensitive_(urlOptions.headers, 'Authorization')); + done(); + }; + var service = OAuth2.createService('test') + .setGrantType('client_credentials') + .setTokenUrl('http://www.example.com') + .setClientId('abc'); + service.exchangeGrant_(); + }); + + it('should not set auth header if it is already set', + function(done) { + mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { + assert.equal(urlOptions.headers.Authorization, 'something'); + done(); + }; + var service = OAuth2.createService('test') + .setGrantType('client_credentials') + .setTokenUrl('http://www.example.com') + .setClientId('abc') + .setClientSecret('def') + .setTokenHeaders({ + Authorization: 'something' + }); + service.exchangeGrant_(); + }); + + it('should set the auth header for the client_credentials grant type, if ' + + 'the client ID and client secret are set and the authorization header' + + 'is not already set', function(done) { + mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { + assert.equal(urlOptions.headers.Authorization, 'Basic YWJjOmRlZg=='); + done(); + }; + var service = OAuth2.createService('test') + .setGrantType('client_credentials') + .setTokenUrl('http://www.example.com') + .setClientId('abc') + .setClientSecret('def'); + service.exchangeGrant_(); + }); + }); }); describe('Utilities', function() { @@ -255,4 +336,35 @@ describe('Utilities', function() { assert.deepEqual(o, {foo: [100], bar: 2, baz: {}}); }); }); + + describe('#getValueCaseInsensitive_()', function() { + var getValueCaseInsensitive_ = OAuth2.getValueCaseInsensitive_; + + it('should find identical keys', function() { + assert.isTrue(getValueCaseInsensitive_({'a': true}, 'a')); + assert.isTrue(getValueCaseInsensitive_({'A': true}, 'A')); + assert.isTrue(getValueCaseInsensitive_({'Ab': true}, 'Ab')); + }); + + it('should find matching keys of different cases', function() { + assert.isTrue(getValueCaseInsensitive_({'a': true}, 'A')); + assert.isTrue(getValueCaseInsensitive_({'A': true}, 'a')); + assert.isTrue(getValueCaseInsensitive_({'Ab': true}, 'aB')); + assert.isTrue(getValueCaseInsensitive_({'a2': true}, 'A2')); + }); + + it('should work with non-alphabetic keys', function() { + assert.isTrue(getValueCaseInsensitive_({'A2': true}, 'a2')); + assert.isTrue(getValueCaseInsensitive_({'2': true}, '2')); + assert.isTrue(getValueCaseInsensitive_({2: true}, 2)); + assert.isTrue(getValueCaseInsensitive_({'!@#': true}, '!@#')); + }); + + it('should work null and undefined', function() { + assert.isUndefined(getValueCaseInsensitive_(null, 'key')); + assert.isUndefined(getValueCaseInsensitive_(undefined, 'key')); + assert.isUndefined(getValueCaseInsensitive_({'a': true}, null)); + assert.isUndefined(getValueCaseInsensitive_({'a': true}, undefined)); + }); + }); }); From daa1258b58284280f6918fc914f2af3cf0a53ca8 Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Fri, 31 Aug 2018 10:31:35 -0400 Subject: [PATCH 2/4] Update the Domo samples to use the default client_credentials behavior. --- samples/Domo.gs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/samples/Domo.gs b/samples/Domo.gs index 98aa0c7c..85242bcf 100644 --- a/samples/Domo.gs +++ b/samples/Domo.gs @@ -41,15 +41,13 @@ function getService() { // Set the endpoint URLs. .setTokenUrl('https://api.domo.com/oauth/token') + // Set the client ID and secret. + .setClientId(CLIENT_ID) + .setClientSecret(CLIENT_SECRET) + // Sets the custom grant type to use. .setGrantType('client_credentials') - // Sets the required Authorization header. - .setTokenHeaders({ - Authorization: 'Basic ' + - Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET) - }) - // Set the property store where authorized tokens should be persisted. .setPropertyStore(PropertiesService.getUserProperties()); } From 313fb23fabeb997bbe395a5bb49c2b090c681671 Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Tue, 11 Sep 2018 11:23:39 -0400 Subject: [PATCH 3/4] Switch to witLowerCaseKeys_ method, and other small fixes. --- src/Service.js | 13 +++++---- src/Utilities.js | 31 +++++++++------------ test/test.js | 71 +++++++++++++++++++++++++----------------------- 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/Service.js b/src/Service.js index 4522f4c0..02041632 100644 --- a/src/Service.js +++ b/src/Service.js @@ -699,7 +699,7 @@ Service_.prototype.lockable_ = function(func) { /** * Obtain an access token using the custom grant type specified. Most often * this will be "client_credentials", and a client ID and secret are set an - * "Authorization: Baseic ..." header will be added using thos values. + * "Authorization: Basic ..." header will be added using those values. */ Service_.prototype.exchangeGrant_ = function() { validate_({ @@ -711,15 +711,16 @@ Service_.prototype.exchangeGrant_ = function() { }; payload = extend_(payload, this.params_); - // For the client_credentials grant type, add a basic authorization header - // if the client ID and client secret are set and no authorization header has - // been set yet (AKA do the expected thing). + // For the client_credentials grant type, add a basic authorization header: + // - If the client ID and client secret are set. + // - No authorization header has been set yet. + var lowerCaseHeaders = witLowerCaseKeys_(this.tokenHeaders_); if (this.grantType_ === 'client_credentials' && this.clientId_ && this.clientSecret_ && - !getValueCaseInsensitive_(this.tokenHeaders_, 'Authorization')) { + (!lowerCaseHeaders || !lowerCaseHeaders.authorization)) { this.tokenHeaders_ = this.tokenHeaders_ || {}; - this.tokenHeaders_.Authorization = 'Basic ' + + this.tokenHeaders_.authorization = 'Basic ' + Utilities.base64Encode(this.clientId_ + ':' + this.clientSecret_); } diff --git a/src/Utilities.js b/src/Utilities.js index 1f5ac231..24f616a8 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -77,26 +77,21 @@ function extend_(destination, source) { return destination; } -/* exported getValueCaseInsensitive_ */ +/* exported witLowerCaseKeys_ */ /** - * Gets the value stored in the object under the given key, in a - * case-insensitive way. - * @param {Object} obj The object to search in. - * @param {string} key The key to search for. - * @return {Object} the value under that key, or undefined otherwise + * Gets a copy of an object with all the keys converted to lower-case strings. + * + * @param {Object} obj The object to copy. + * @return {Object} a shallow copy of the object with all lower-case keys. */ -function getValueCaseInsensitive_(obj, key) { - if (obj == null || typeof obj !== 'object' || - key == null || !key.toString) { - return undefined; - } - if (key in obj) { - return obj[key]; +function witLowerCaseKeys_(obj) { + if (obj === null || typeof obj !== 'object') { + return obj; } + // For each key in the source object, add a lower-case version to a new + // object, and return it. return Object.keys(obj).reduce(function(result, k) { - if (result) return result; - if (k.toLowerCase() === key.toLowerCase()) { - return obj[k]; - } - }, undefined); + result[k.toLowerCase()] = obj[k]; + return result; + }, {}); } diff --git a/test/test.js b/test/test.js index 5cbb2b6d..a279f857 100644 --- a/test/test.js +++ b/test/test.js @@ -243,13 +243,12 @@ describe('Service', function() { }); describe('#exchangeGrant_()', function() { - var getValueCaseInsensitive_ = OAuth2.getValueCaseInsensitive_; + var witLowerCaseKeys_ = OAuth2.witLowerCaseKeys_; it('should not set auth header if the grant type is not client_credentials', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.isUndefined( - getValueCaseInsensitive_(urlOptions.headers, 'Authorization')); + assert.isUndefined(witLowerCaseKeys_(urlOptions.headers).authorization); done(); }; var service = OAuth2.createService('test') @@ -261,8 +260,7 @@ describe('Service', function() { it('should not set auth header if the client ID is not set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.isUndefined( - getValueCaseInsensitive_(urlOptions.headers, 'Authorization')); + assert.isUndefined(witLowerCaseKeys_(urlOptions.headers).authorization); done(); }; var service = OAuth2.createService('test') @@ -274,8 +272,7 @@ describe('Service', function() { it('should not set auth header if the client secret is not set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.isUndefined( - getValueCaseInsensitive_(urlOptions.headers, 'Authorization')); + assert.isUndefined(witLowerCaseKeys_(urlOptions.headers).authorization); done(); }; var service = OAuth2.createService('test') @@ -288,7 +285,8 @@ describe('Service', function() { it('should not set auth header if it is already set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.equal(urlOptions.headers.Authorization, 'something'); + assert.equal(witLowerCaseKeys_(urlOptions.headers).authorization, + 'something'); done(); }; var service = OAuth2.createService('test') @@ -297,7 +295,7 @@ describe('Service', function() { .setClientId('abc') .setClientSecret('def') .setTokenHeaders({ - Authorization: 'something' + authorization: 'something' }); service.exchangeGrant_(); }); @@ -306,7 +304,8 @@ describe('Service', function() { 'the client ID and client secret are set and the authorization header' + 'is not already set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.equal(urlOptions.headers.Authorization, 'Basic YWJjOmRlZg=='); + assert.equal(witLowerCaseKeys_(urlOptions.headers).authorization, + 'Basic YWJjOmRlZg=='); done(); }; var service = OAuth2.createService('test') @@ -337,34 +336,38 @@ describe('Utilities', function() { }); }); - describe('#getValueCaseInsensitive_()', function() { - var getValueCaseInsensitive_ = OAuth2.getValueCaseInsensitive_; - - it('should find identical keys', function() { - assert.isTrue(getValueCaseInsensitive_({'a': true}, 'a')); - assert.isTrue(getValueCaseInsensitive_({'A': true}, 'A')); - assert.isTrue(getValueCaseInsensitive_({'Ab': true}, 'Ab')); - }); - - it('should find matching keys of different cases', function() { - assert.isTrue(getValueCaseInsensitive_({'a': true}, 'A')); - assert.isTrue(getValueCaseInsensitive_({'A': true}, 'a')); - assert.isTrue(getValueCaseInsensitive_({'Ab': true}, 'aB')); - assert.isTrue(getValueCaseInsensitive_({'a2': true}, 'A2')); + describe('#witLowerCaseKeys_()', function() { + var witLowerCaseKeys_ = OAuth2.witLowerCaseKeys_; + var data = { + 'a': true, + 'A': true, + 'B': true, + 'Cc': true, + 'D2': true, + 'E!@#': true + }; + var lowerCaseData = witLowerCaseKeys_(data); + + it('should contain lower-case keys', function() { + assert.isTrue(lowerCaseData['a']); + assert.isTrue(lowerCaseData['b']); + assert.isTrue(lowerCaseData['cc']); + assert.isTrue(lowerCaseData['d2']); + assert.isTrue(lowerCaseData['e!@#']); }); - it('should work with non-alphabetic keys', function() { - assert.isTrue(getValueCaseInsensitive_({'A2': true}, 'a2')); - assert.isTrue(getValueCaseInsensitive_({'2': true}, '2')); - assert.isTrue(getValueCaseInsensitive_({2: true}, 2)); - assert.isTrue(getValueCaseInsensitive_({'!@#': true}, '!@#')); + it('should not contain upper-case keys', function() { + assert.isUndefined(lowerCaseData['A']); + assert.isUndefined(lowerCaseData['B']); + assert.isUndefined(lowerCaseData['Cc']); + assert.isUndefined(lowerCaseData['D2']); + assert.isUndefined(lowerCaseData['E!@#']); }); - it('should work null and undefined', function() { - assert.isUndefined(getValueCaseInsensitive_(null, 'key')); - assert.isUndefined(getValueCaseInsensitive_(undefined, 'key')); - assert.isUndefined(getValueCaseInsensitive_({'a': true}, null)); - assert.isUndefined(getValueCaseInsensitive_({'a': true}, undefined)); + it('should handle null, undefined, and empty objects', function() { + assert.isNull(witLowerCaseKeys_(null)); + assert.isUndefined(witLowerCaseKeys_(undefined)); + assert.isEmpty(witLowerCaseKeys_({})); }); }); }); From d8a5f45e36d98d048029ad624503d45bcd721db2 Mon Sep 17 00:00:00 2001 From: Eric Koleda Date: Tue, 11 Sep 2018 12:24:09 -0400 Subject: [PATCH 4/4] Change to toLowerCaseKeys_(), combine tests. --- src/Service.js | 2 +- src/Utilities.js | 4 +-- test/test.js | 64 ++++++++++++++++++++++-------------------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/Service.js b/src/Service.js index 02041632..c1cf6e12 100644 --- a/src/Service.js +++ b/src/Service.js @@ -714,7 +714,7 @@ Service_.prototype.exchangeGrant_ = function() { // For the client_credentials grant type, add a basic authorization header: // - If the client ID and client secret are set. // - No authorization header has been set yet. - var lowerCaseHeaders = witLowerCaseKeys_(this.tokenHeaders_); + var lowerCaseHeaders = toLowerCaseKeys_(this.tokenHeaders_); if (this.grantType_ === 'client_credentials' && this.clientId_ && this.clientSecret_ && diff --git a/src/Utilities.js b/src/Utilities.js index 24f616a8..998cf0bb 100644 --- a/src/Utilities.js +++ b/src/Utilities.js @@ -77,14 +77,14 @@ function extend_(destination, source) { return destination; } -/* exported witLowerCaseKeys_ */ +/* exported toLowerCaseKeys_ */ /** * Gets a copy of an object with all the keys converted to lower-case strings. * * @param {Object} obj The object to copy. * @return {Object} a shallow copy of the object with all lower-case keys. */ -function witLowerCaseKeys_(obj) { +function toLowerCaseKeys_(obj) { if (obj === null || typeof obj !== 'object') { return obj; } diff --git a/test/test.js b/test/test.js index a279f857..366250ec 100644 --- a/test/test.js +++ b/test/test.js @@ -243,12 +243,12 @@ describe('Service', function() { }); describe('#exchangeGrant_()', function() { - var witLowerCaseKeys_ = OAuth2.witLowerCaseKeys_; + var toLowerCaseKeys_ = OAuth2.toLowerCaseKeys_; it('should not set auth header if the grant type is not client_credentials', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.isUndefined(witLowerCaseKeys_(urlOptions.headers).authorization); + assert.isUndefined(toLowerCaseKeys_(urlOptions.headers).authorization); done(); }; var service = OAuth2.createService('test') @@ -260,7 +260,7 @@ describe('Service', function() { it('should not set auth header if the client ID is not set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.isUndefined(witLowerCaseKeys_(urlOptions.headers).authorization); + assert.isUndefined(toLowerCaseKeys_(urlOptions.headers).authorization); done(); }; var service = OAuth2.createService('test') @@ -272,7 +272,7 @@ describe('Service', function() { it('should not set auth header if the client secret is not set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.isUndefined(witLowerCaseKeys_(urlOptions.headers).authorization); + assert.isUndefined(toLowerCaseKeys_(urlOptions.headers).authorization); done(); }; var service = OAuth2.createService('test') @@ -285,7 +285,7 @@ describe('Service', function() { it('should not set auth header if it is already set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.equal(witLowerCaseKeys_(urlOptions.headers).authorization, + assert.equal(toLowerCaseKeys_(urlOptions.headers).authorization, 'something'); done(); }; @@ -304,7 +304,7 @@ describe('Service', function() { 'the client ID and client secret are set and the authorization header' + 'is not already set', function(done) { mocks.UrlFetchApp.resultFunction = function(url, urlOptions) { - assert.equal(witLowerCaseKeys_(urlOptions.headers).authorization, + assert.equal(toLowerCaseKeys_(urlOptions.headers).authorization, 'Basic YWJjOmRlZg=='); done(); }; @@ -336,38 +336,32 @@ describe('Utilities', function() { }); }); - describe('#witLowerCaseKeys_()', function() { - var witLowerCaseKeys_ = OAuth2.witLowerCaseKeys_; - var data = { - 'a': true, - 'A': true, - 'B': true, - 'Cc': true, - 'D2': true, - 'E!@#': true - }; - var lowerCaseData = witLowerCaseKeys_(data); - - it('should contain lower-case keys', function() { - assert.isTrue(lowerCaseData['a']); - assert.isTrue(lowerCaseData['b']); - assert.isTrue(lowerCaseData['cc']); - assert.isTrue(lowerCaseData['d2']); - assert.isTrue(lowerCaseData['e!@#']); - }); - - it('should not contain upper-case keys', function() { - assert.isUndefined(lowerCaseData['A']); - assert.isUndefined(lowerCaseData['B']); - assert.isUndefined(lowerCaseData['Cc']); - assert.isUndefined(lowerCaseData['D2']); - assert.isUndefined(lowerCaseData['E!@#']); + describe('#toLowerCaseKeys_()', function() { + var toLowerCaseKeys_ = OAuth2.toLowerCaseKeys_; + + it('should contain only lower-case keys', function() { + var data = { + 'a': true, + 'A': true, + 'B': true, + 'Cc': true, + 'D2': true, + 'E!@#': true + }; + var lowerCaseData = toLowerCaseKeys_(data); + assert.deepEqual(lowerCaseData, { + 'a': true, + 'b': true, + 'cc': true, + 'd2': true, + 'e!@#': true + }); }); it('should handle null, undefined, and empty objects', function() { - assert.isNull(witLowerCaseKeys_(null)); - assert.isUndefined(witLowerCaseKeys_(undefined)); - assert.isEmpty(witLowerCaseKeys_({})); + assert.isNull(toLowerCaseKeys_(null)); + assert.isUndefined(toLowerCaseKeys_(undefined)); + assert.isEmpty(toLowerCaseKeys_({})); }); }); });