diff --git a/lib/apirequest.js b/lib/apirequest.js index 5680042f50..72392d1844 100644 --- a/lib/apirequest.js +++ b/lib/apirequest.js @@ -20,7 +20,12 @@ var Multipart = require('multipart-stream'); var utils = require('./utils.js'); var DefaultTransporter = require('./transporters.js'); var transporter = new DefaultTransporter(); +var stream = require('stream'); +function isReadableStream(obj) { + return obj instanceof stream.Stream && typeof obj._read == 'function' && + typeof obj._readableState == 'object'; +} function logErrorOnly(err) { if (err) { @@ -49,7 +54,7 @@ function isValidParams(params, keys, callback) { * @return {Request} Returns Request object or null */ function createAPIRequest(parameters, callback) { - var req; + var req, body; var mediaUrl = parameters.mediaUrl; var context = parameters.context; var params = parameters.params; @@ -78,6 +83,7 @@ function createAPIRequest(parameters, callback) { var media = params.media || {}; var resource = params.resource; var authClient = params.auth || context._options.auth || context.google._options.auth; + var defaultMime = typeof media.body === 'string' ? 'text/plain' : 'application/octet-stream'; delete params.media; delete params.resource; delete params.auth; @@ -95,37 +101,45 @@ function createAPIRequest(parameters, callback) { if (mediaUrl && media && media.body) { options.url = mediaUrl; - // Create a boundary identifier and multipart read stream - var boundary = Math.random().toString(36).slice(2); - var mp = new Multipart(boundary); + if (resource) { + // Create a boundary identifier and multipart read stream + var boundary = Math.random().toString(36).slice(2); + body = new Multipart(boundary); - // Always a multipart upload - params.uploadType = 'multipart'; + // Use multipart upload + params.uploadType = 'multipart'; - options.headers = { - 'Content-Type': 'multipart/related; boundary="' + boundary + '"' - }; + options.headers = { + 'Content-Type': 'multipart/related; boundary="' + boundary + '"' + }; - // Add parts to multipart request - if (resource) { - mp.addPart({ + // Add parts to multipart request + body.addPart({ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(resource) }); - } - - var defaultMime = typeof media.body === 'string' ? 'text/plain' : 'application/octet-stream'; - mp.addPart({ - headers: { - 'Content-Type': media.mimeType || resource && resource.mimeType || defaultMime - }, - body: media.body // can be a readable stream or raw string! - }); - } - else { + body.addPart({ + headers: { + 'Content-Type': media.mimeType || resource && resource.mimeType || defaultMime + }, + body: media.body // can be a readable stream or raw string! + }); + } else { + params.uploadType = 'media'; + options.headers = { + 'Content-Type': media.mimeType || defaultMime + }; + + if (isReadableStream(media.body)) { + body = media.body; + } else { + options.body = media.body; + } + } + } else { options.json = resource || ((method === 'GET' || method === 'DELETE') ? true : {}); } @@ -140,7 +154,7 @@ function createAPIRequest(parameters, callback) { req = transporter.request(options, callback); } - if (mp) mp.pipe(req); + if (body) body.pipe(req); return req; } diff --git a/lib/auth/computeclient.js b/lib/auth/computeclient.js index 057d9d6e6d..e2389aad1f 100644 --- a/lib/auth/computeclient.js +++ b/lib/auth/computeclient.js @@ -70,7 +70,13 @@ Compute.prototype.refreshToken_ = function(ignored_, opt_callback) { method: 'GET', uri: uri, json: true - }, opt_callback); + }, function(err, tokens) { + if (!err && tokens && tokens.expires_in) { + tokens.expiry_date = ((new Date()).getTime() + (tokens.expires_in * 1000)); + delete tokens.expires_in; + } + opt_callback && opt_callback(err, tokens); + }); }; /** diff --git a/lib/auth/jwtclient.js b/lib/auth/jwtclient.js index 55545a934b..977a9c9840 100644 --- a/lib/auth/jwtclient.js +++ b/lib/auth/jwtclient.js @@ -86,7 +86,7 @@ JWT.prototype.refreshToken_ = function(ignored_, opt_callback) { opt_callback && opt_callback(err, { access_token: token, token_type: 'Bearer', - expires_in: that.gapi.token_expires + expiry_date: ((new Date()).getTime() + (that.gapi.token_expires * 1000)) }); }); }; diff --git a/lib/auth/oauth2client.js b/lib/auth/oauth2client.js index 0fcad9b26d..75d3407780 100644 --- a/lib/auth/oauth2client.js +++ b/lib/auth/oauth2client.js @@ -135,7 +135,13 @@ OAuth2Client.prototype.getToken = function(code, opt_callback) { uri: uri, form: values, json: true - }, opt_callback); + }, function(err, tokens) { + if (!err && tokens && tokens.expires_in) { + tokens.expiry_date = ((new Date()).getTime() + (tokens.expires_in * 1000)); + delete tokens.expires_in; + } + opt_callback && opt_callback(err, tokens); + }); }; /** @@ -159,7 +165,13 @@ OAuth2Client.prototype.refreshToken_ = function(refresh_token, opt_callback) { uri: uri, form: values, json: true - }, opt_callback); + }, function(err, tokens) { + if (!err && tokens && tokens.expires_in) { + tokens.expiry_date = ((new Date()).getTime() + (tokens.expires_in * 1000)); + delete tokens.expires_in; + } + opt_callback && opt_callback(err, tokens); + }); }; /** @@ -199,6 +211,20 @@ OAuth2Client.prototype.revokeToken = function(token, opt_callback) { }, opt_callback); }; +/** + * Revokes access token and clears the credentials object + * @param {Function=} callback callback + */ +OAuth2Client.prototype.revokeCredentials = function(callback) { + var token = this.credentials.access_token; + this.credentials = {}; + if (token) { + this.revokeToken(token, callback); + } else { + callback(new Error('No access token to revoke.'), null); + } +}; + /** * Provides a request implementation with OAuth 2.0 flow. * If credentials have a refresh_token, in cases of HTTP @@ -206,45 +232,53 @@ OAuth2Client.prototype.revokeToken = function(token, opt_callback) { * access token and replays the unsuccessful request. * @param {object} opts Request options. * @param {function} callback callback. - * @param {boolean=} opt_dontForceRefresh If set, don't ask for a new token - * with refresh_token. * @return {Request} Request object */ -OAuth2Client.prototype.request = function(opts, callback, opt_dontForceRefresh) { +OAuth2Client.prototype.request = function(opts, callback) { var that = this; var credentials = this.credentials; + var expiryDate = credentials.expiry_date; + // if no expiry time, assume it's not expired + var isTokenExpired = expiryDate ? expiryDate <= (new Date()).getTime() : false; if (!credentials.access_token && !credentials.refresh_token) { callback(new Error('No access or refresh token is set.'), null); return; } + var shouldRefresh = !credentials.access_token || isTokenExpired; + + if (shouldRefresh && credentials.refresh_token) { + this.refreshAccessToken(function(err, tokens) { + if (err) { + callback(err, null); + } else if (!tokens || (tokens && !tokens.access_token)) { + callback(new Error('Could not refresh access token.'), null); + } else { + return that._makeRequest(opts, callback); + } + }); + } else { + return this._makeRequest(opts, callback); + } +}; + +/** + * Makes a request without paying attention to refreshing or anything + * Assumes that all credentials are set correctly. + * @param {object} opts Options for request + * @param {Function} callback callback function + * @return {Request} The request object created + */ +OAuth2Client.prototype._makeRequest = function(opts, callback) { + var that = this; + var credentials = this.credentials; credentials.token_type = credentials.token_type || 'Bearer'; opts.headers = opts.headers || {}; opts.headers['Authorization'] = credentials.token_type + ' ' + credentials.access_token; - return this.transporter.request(opts, function(err, body, res) { - // TODO: Check if it's not userRateLimitExceeded - var hasAuthError = res && (res.statusCode === 401 || res.statusCode === 403); - // if there is an auth error, refresh the token - // and make the request again - if (!opt_dontForceRefresh && hasAuthError && credentials.refresh_token) { - // refresh access token and re-request - that.refreshAccessToken(function(err, result) { - if (err || (result && !result.access_token)) { - callback(err, result, res); - } else { - var tokens = result; - tokens.refresh_token = credentials.refresh_token; - that.credentials = tokens; - that.request(opts, callback, true); - } - }); - } else { - callback(err, body, res); - } - }); + return this.transporter.request(opts, callback); }; /** diff --git a/test/fixtures/media-response.txt b/test/fixtures/media-response.txt index 8b6f6aba0a..2aafbd0bce 100644 --- a/test/fixtures/media-response.txt +++ b/test/fixtures/media-response.txt @@ -3,7 +3,7 @@ Content-Type: application/json $resource --$boundary -Content-Type: text/plain +Content-Type: $mimeType $media --$boundary-- diff --git a/test/fixtures/mediabody.txt b/test/fixtures/mediabody.txt new file mode 100644 index 0000000000..18dc9d8d38 --- /dev/null +++ b/test/fixtures/mediabody.txt @@ -0,0 +1 @@ +hello world abc123 diff --git a/test/test.compute.js b/test/test.compute.js index 9e0d3cf8cf..6c417f663a 100644 --- a/test/test.compute.js +++ b/test/test.compute.js @@ -18,6 +18,9 @@ var assert = require('assert'); var googleapis = require('../lib/googleapis.js'); +var nock = require('nock'); + +nock.disableNetConnect(); describe('Compute auth client', function() { @@ -27,7 +30,8 @@ describe('Compute auth client', function() { request: function(opts, opt_callback) { opt_callback(null, { 'access_token': 'initial-access-token', - 'token_type': 'Bearer' + 'token_type': 'Bearer', + 'expires_in': 3600 }, {}); } }; @@ -38,25 +42,36 @@ describe('Compute auth client', function() { }); }); - it('should refresh token when request fails', function(done) { + it('should refresh if access token has expired', function(done) { + var scope = nock('http://metadata') + .get('/computeMetadata/v1beta1/instance/service-accounts/default/token') + .reply(200, { access_token: 'abc123', expires_in: 10000 }); var compute = new googleapis.auth.Compute(); compute.credentials = { access_token: 'initial-access-token', - refresh_token: 'compute-placeholder' - }; - compute.transporter = { - request: function(opts, opt_callback) { - opt_callback({}, {}, { statusCode: 401 }); - } + refresh_token: 'compute-placeholder', + expiry_date: (new Date()).getTime() - 2000 }; - compute.refreshToken_ = function(token, callback) { - callback(null, { - 'access_token': 'another-access-token', - 'token_type': 'Bearer' - }); + compute.request({}, function() { + assert.equal(compute.credentials.access_token, 'abc123'); + scope.done(); + done(); + }); + }); + + it('should not refresh if access token has expired', function(done) { + var scope = nock('http://metadata') + .get('/computeMetadata/v1beta1/instance/service-accounts/default/token') + .reply(200, { access_token: 'abc123', expires_in: 10000 }); + var compute = new googleapis.auth.Compute(); + compute.credentials = { + access_token: 'initial-access-token', + refresh_token: 'compute-placeholder' }; compute.request({}, function() { - assert.equal('another-access-token', compute.credentials.access_token); + assert.equal(compute.credentials.access_token, 'initial-access-token'); + assert.equal(false, scope.isDone()); + nock.cleanAll(); done(); }); }); diff --git a/test/test.jwt.js b/test/test.jwt.js index 89b8e3ad73..ad2430a9e4 100644 --- a/test/test.jwt.js +++ b/test/test.jwt.js @@ -18,6 +18,9 @@ var assert = require('assert'); var googleapis = require('../lib/googleapis.js'); +var nock = require('nock'); + +nock.disableNetConnect(); describe('JWT auth client', function() { @@ -49,7 +52,7 @@ describe('JWT auth client', function() { }); }); - it('should accept scope as string', function (done) { + it('should accept scope as string', function(done) { var jwt = new googleapis.auth.JWT( 'foo@serviceaccount.com', '/path/to/key.pem', @@ -60,34 +63,106 @@ describe('JWT auth client', function() { jwt.GAPI = function(opts, callback) { assert.equal('http://foo', opts.scope); done(); - } + }; jwt.authorize(); }); - it('should refresh token when request fails', function(done) { + it('should refresh token if missing access token', function(done) { var jwt = new googleapis.auth.JWT( 'foo@serviceaccount.com', '/path/to/key.pem', ['http://bar', 'http://foo'], 'bar@subjectaccount.com'); + jwt.credentials = { - access_token: 'initial-access-token', refresh_token: 'jwt-placeholder' }; - jwt.transporter = { - request: function(opts, opt_callback) { - opt_callback(null, null, {statusCode: 401}); + + jwt.gapi = { + getToken: function(callback) { + callback(null, 'abc123'); } }; - jwt.refreshToken_ = function(token, callback) { - callback(null, { - 'access_token': 'another-access-token', - 'token_type': 'Bearer' - }); + + jwt.request({}, function() { + assert.equal('abc123', jwt.credentials.access_token); + done(); + }); + }); + + it('should refresh token if expired', function(done) { + var jwt = new googleapis.auth.JWT( + 'foo@serviceaccount.com', + '/path/to/key.pem', + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com'); + + jwt.credentials = { + access_token: 'woot', + refresh_token: 'jwt-placeholder', + expiry_date: (new Date()).getTime() - 1000 }; + + jwt.gapi = { + getToken: function(callback) { + callback(null, 'abc123'); + } + }; + jwt.request({}, function() { - assert.equal('another-access-token', jwt.credentials.access_token); + assert.equal('abc123', jwt.credentials.access_token); + done(); + }); + }); + + it('should not refresh if not expired', function(done) { + var scope = nock('https://accounts.google.com') + .log(console.log) + .post('/o/oauth2/token', '*') + .reply(200, { access_token: 'abc123', expires_in: 10000 }); + + var jwt = new googleapis.auth.JWT( + 'foo@serviceaccount.com', + '/path/to/key.pem', + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com'); + + jwt.credentials = { + access_token: 'initial-access-token', + refresh_token: 'jwt-placeholder', + expiry_date: (new Date()).getTime() + 5000 + }; + + jwt.request({}, function() { + assert.equal('initial-access-token', jwt.credentials.access_token); + assert.equal(false, scope.isDone()); + nock.cleanAll(); + done(); + }); + }); + + it('should assume access token is not expired', function(done) { + var scope = nock('https://accounts.google.com') + .log(console.log) + .post('/o/oauth2/token', '*') + .reply(200, { access_token: 'abc123', expires_in: 10000 }); + + var jwt = new googleapis.auth.JWT( + 'foo@serviceaccount.com', + '/path/to/key.pem', + ['http://bar', 'http://foo'], + 'bar@subjectaccount.com'); + + jwt.credentials = { + access_token: 'initial-access-token', + refresh_token: 'jwt-placeholder' + }; + + jwt.request({}, function() { + assert.equal('initial-access-token', jwt.credentials.access_token); + assert.equal(false, scope.isDone()); + nock.cleanAll(); done(); }); }); diff --git a/test/test.media.js b/test/test.media.js index e17458f9b9..e6940a5403 100644 --- a/test/test.media.js +++ b/test/test.media.js @@ -33,10 +33,21 @@ describe('Media', function() { drive = google.drive('v2'); }); - it('should post with uploadType=multipart', function(done) { + it('should post with uploadType=multipart if resource and media set', function(done) { var scope = nock('https://www.googleapis.com') .post('/upload/drive/v2/files?uploadType=multipart') .reply(200, { fileId: 'abc123' }); + var req = drive.files.insert({ resource: {}, media: { body: 'hello' }}, function(err, body) { + assert.equal(JSON.stringify(body), JSON.stringify({ fileId: 'abc123' })); + scope.done(); + done(); + }); + }); + + it('should post with uploadType=media media set but not resource', function(done) { + var scope = nock('https://www.googleapis.com') + .post('/upload/drive/v2/files?uploadType=media') + .reply(200, { fileId: 'abc123' }); var req = drive.files.insert({ media: { body: 'hello' }}, function(err, body) { assert.equal(JSON.stringify(body), JSON.stringify({ fileId: 'abc123' })); scope.done(); @@ -44,27 +55,17 @@ describe('Media', function() { }); }); - it('should generate a valid upload if media is set, metadata is not set', function(done) { + it('should generate a valid media upload if media is set, metadata is not set', function(done) { var scope = nock('https://www.googleapis.com') - .post('/upload/drive/v2/files?uploadType=multipart') + .post('/upload/drive/v2/files?uploadType=media') .reply(201, function(uri, reqBody) { return reqBody; // return request body as response for testing purposes }); - var resource = { mimeType: 'text/plain' }; var media = { body: 'hey' }; - var expectedResp = fs.readFileSync(__dirname + '/fixtures/media-response.txt', { encoding: 'utf8' }); - var req = drive.files.insert({ resource: resource, media: media }, function(err, body) { + var req = drive.files.insert({ media: media }, function(err, body) { assert.equal(req.method, 'POST'); - assert.equal(req.uri.href, 'https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart'); - assert.equal(req.headers['Content-Type'].indexOf('multipart/related;'), 0); - var boundary = req.src.boundary; - expectedResp = expectedResp - .replace(/\n/g, '\r\n') - .replace(/\$boundary/g, boundary) - .replace('$media', media.body) - .replace('$resource', JSON.stringify(resource)) - .trim(); - assert.strictEqual(expectedResp, body); + assert.equal(req.uri.href, 'https://www.googleapis.com/upload/drive/v2/files?uploadType=media'); + assert.strictEqual(media.body, body); scope.done(); done(); }); @@ -89,6 +90,7 @@ describe('Media', function() { .replace(/\$boundary/g, boundary) .replace('$media', media.body) .replace('$resource', JSON.stringify(resource)) + .replace('$mimeType', 'text/plain') .trim(); assert.strictEqual(expectedResp, body); scope.done(); @@ -98,7 +100,7 @@ describe('Media', function() { it('should not require parameters for insertion requests', function() { var req = drive.files.insert({ someAttr: 'someValue', media: { body: 'wat' } }, noop); - assert.equal(req.uri.query, 'someAttr=someValue&uploadType=multipart'); + assert.equal(req.uri.query, 'someAttr=someValue&uploadType=media'); }); it('should not multipart upload if no media body given', function() { @@ -125,6 +127,7 @@ describe('Media', function() { .replace(/\$boundary/g, boundary) .replace('$media', media.body) .replace('$resource', JSON.stringify(resource)) + .replace('$mimeType', 'text/plain') .trim(); assert.strictEqual(expectedResp, body); scope.done(); @@ -146,4 +149,59 @@ describe('Media', function() { done(); }); }); + + it('should accept readable stream as media body without metadata', function(done) { + var scope = nock('https://www.googleapis.com') + .post('/upload/gmail/v1/users/me/drafts?uploadType=media') + .reply(201, function(uri, reqBody) { + return reqBody; // return request body as response for testing purposes + }); + + var gmail = google.gmail('v1'); + var body = fs.createReadStream(__dirname + '/fixtures/mediabody.txt'); + var expectedBody = fs.readFileSync(__dirname + '/fixtures/mediabody.txt'); + var req = gmail.users.drafts.create({ + userId: 'me', + media: { + mimeType: 'message/rfc822', + body: body + } + }, function(err, resp) { + assert.equal(resp, expectedBody); + scope.done(); + done(); + }); + }); + + it('should accept readable stream as media body with metadata', function(done) { + var scope = nock('https://www.googleapis.com') + .post('/upload/gmail/v1/users/me/drafts?uploadType=multipart') + .reply(201, function(uri, reqBody) { + return reqBody; // return request body as response for testing purposes + }); + + var gmail = google.gmail('v1'); + var resource = { message: { raw: (new Buffer('hello', 'binary')).toString('base64') } }; + var body = fs.createReadStream(__dirname + '/fixtures/mediabody.txt'); + var bodyString = fs.readFileSync(__dirname + '/fixtures/mediabody.txt', { encoding: 'utf8' }); + var media = { mimeType: 'message/rfc822', body: body }; + var expectedBody = fs.readFileSync(__dirname + '/fixtures/media-response.txt', { encoding: 'utf8' }); + var req = gmail.users.drafts.create({ + userId: 'me', + resource: resource, + media: media + }, function(err, resp) { + var boundary = req.src.boundary; + expectedBody = expectedBody + .replace(/\n/g, '\r\n') + .replace(/\$boundary/g, boundary) + .replace('$media', bodyString) + .replace('$resource', JSON.stringify(resource)) + .replace('$mimeType', 'message/rfc822') + .trim(); + assert.strictEqual(expectedBody, resp); + scope.done(); + done(); + }); + }); }); diff --git a/test/test.oauth2.js b/test/test.oauth2.js index 374f43f2ed..fb1a0bc25f 100644 --- a/test/test.oauth2.js +++ b/test/test.oauth2.js @@ -125,24 +125,6 @@ describe('OAuth2 client', function() { }); }); - it('should replay the request with a refreshed token if auth failed', function(done) { - var i = 0; - var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); - oauth2client.credentials = { access_token: 'foo', refresh_token: 'bar' }; - var google = new googleapis.GoogleApis(); - oauth2client.transporter = { - request: function(opts, callback) { - if (i === 1) { - assert.equal(opts.uri, 'https://accounts.google.com/o/oauth2/token'); - return done(); - } - i++; - callback(null, null, { statusCode: 401 }); - } - }; - google.urlshortener('v1').url.list({ auth: oauth2client }, noop); - }); - it('should verify a valid certificate against a jwt', function(done) { var publicKey = fs.readFileSync('./test/fixtures/public.pem', 'utf-8'); var privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8'); @@ -828,4 +810,121 @@ describe('OAuth2 client', function() { done(); }); }); + + it('should refresh if access token is expired', function(done) { + var scope = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply(200, { access_token: 'abc123', expires_in: 1 }); + var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + var google = new googleapis.GoogleApis(); + var drive = google.drive({ version: 'v2', auth: oauth2client }); + var now = (new Date()).getTime(); + var twoSecondsAgo = now - 2000; + oauth2client.credentials = { refresh_token: 'abc', expiry_date: twoSecondsAgo }; + drive.files.get({ fileId: 'wat' }, function(err, result) { + var expiry_date = oauth2client.credentials.expiry_date; + assert.notEqual(expiry_date, undefined); + assert(expiry_date > now); + assert(expiry_date < now + 5000); + assert.equal(oauth2client.credentials.refresh_token, 'abc'); + assert.equal(oauth2client.credentials.access_token, 'abc123'); + assert.equal(oauth2client.credentials.token_type, 'Bearer'); + scope.done(); + done(); + }); + }); + + it('should make request if access token not expired', function(done) { + var scope = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply(200, { access_token: 'abc123', expires_in: 10000 }); + var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + var google = new googleapis.GoogleApis(); + var drive = google.drive({ version: 'v2', auth: oauth2client }); + var now = (new Date()).getTime(); + var tenSecondsFromNow = now + 10000; + oauth2client.credentials = { access_token: 'abc123', refresh_token: 'abc', expiry_date: tenSecondsFromNow }; + drive.files.get({ fileId: 'wat' }, function(err, result) { + assert.equal(JSON.stringify(oauth2client.credentials), JSON.stringify({ + access_token: 'abc123', + refresh_token: 'abc', + expiry_date: tenSecondsFromNow, + token_type: 'Bearer' + })); + + assert.throws(function() { + scope.done(); + }, 'AssertionError'); + nock.cleanAll(); + done(); + }); + }); + + it('should refresh if have refresh token but no access token', function(done) { + var scope = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply(200, { access_token: 'abc123', expires_in: 1 }); + var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + var google = new googleapis.GoogleApis(); + var drive = google.drive({ version: 'v2', auth: oauth2client }); + var now = (new Date()).getTime(); + oauth2client.credentials = { refresh_token: 'abc' }; + drive.files.get({ fileId: 'wat' }, function(err, result) { + var expiry_date = oauth2client.credentials.expiry_date; + assert.notEqual(expiry_date, undefined); + assert(expiry_date > now); + assert(expiry_date < now + 4000); + assert.equal(oauth2client.credentials.refresh_token, 'abc'); + assert.equal(oauth2client.credentials.access_token, 'abc123'); + assert.equal(oauth2client.credentials.token_type, 'Bearer'); + scope.done(); + done(); + }); + }); + + describe('revokeCredentials()', function() { + it('should revoke credentials if access token present', function(done) { + var scope = nock('https://accounts.google.com') + .get('/o/oauth2/revoke?token=abc') + .reply(200, { success: true }); + var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + var google = new googleapis.GoogleApis(); + oauth2client.credentials = { access_token: 'abc', refresh_token: 'abc' }; + oauth2client.revokeCredentials(function(err, result) { + assert.equal(err, null); + assert.equal(result.success, true); + assert.equal(JSON.stringify(oauth2client.credentials), '{}'); + scope.done(); + done(); + }); + }); + + it('should clear credentials and return error if no access token to revoke', function(done) { + var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + var google = new googleapis.GoogleApis(); + oauth2client.credentials = { refresh_token: 'abc' }; + oauth2client.revokeCredentials(function(err, result) { + assert.equal(err.message, 'No access token to revoke.'); + assert.equal(result, null); + assert.equal(JSON.stringify(oauth2client.credentials), '{}'); + done(); + }); + }); + }); + + describe('getToken()', function() { + it('should return expiry_date', function(done) { + var now = (new Date()).getTime(); + var scope = nock('https://accounts.google.com') + .post('/o/oauth2/token') + .reply(200, { access_token: 'abc', refresh_token: '123', expires_in: 10 }); + var oauth2client = new googleapis.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); + oauth2client.getToken('code here', function(err, tokens) { + assert(tokens.expiry_date > now + (10 * 1000)); + assert(tokens.expiry_date < now + (15 * 1000)); + scope.done(); + done(); + }); + }); + }); });