diff --git a/packages/common/src/grpc-operation.js b/packages/common/src/grpc-operation.js index 217dcaab04b..7972d1e6500 100644 --- a/packages/common/src/grpc-operation.js +++ b/packages/common/src/grpc-operation.js @@ -20,20 +20,19 @@ 'use strict'; -var events = require('events'); var modelo = require('modelo'); /** - * @type {module:common/grpcService} + * @type {module:common/grpcServiceObject} * @private */ -var GrpcService = require('./grpc-service.js'); +var GrpcServiceObject = require('./grpc-service-object.js'); /** - * @type {module:common/grpcServiceObject} + * @type {module:common/operation} * @private */ -var GrpcServiceObject = require('./grpc-service-object.js'); +var Operation = require('./operation.js'); /** * @type {module:common/util} @@ -101,16 +100,11 @@ function GrpcOperation(parent, name) { methods: methods }; + Operation.call(this, config); GrpcServiceObject.call(this, config); - events.EventEmitter.call(this); - - this.completeListeners = 0; - this.hasActiveListeners = false; - - this.listenForEvents_(); } -modelo.inherits(GrpcOperation, GrpcServiceObject, events.EventEmitter); +modelo.inherits(GrpcOperation, GrpcServiceObject, Operation); /** * Cancel the operation. @@ -133,83 +127,4 @@ GrpcOperation.prototype.cancel = function(callback) { this.request(protoOpts, reqOpts, callback || util.noop); }; -/** - * Wraps the `complete` and `error` events in a Promise. - * - * @return {promise} - */ -GrpcOperation.prototype.promise = function() { - var self = this; - - return new self.Promise(function(resolve, reject) { - self - .on('error', reject) - .on('complete', function(metadata) { - resolve([metadata]); - }); - }); -}; - -/** - * Begin listening for events on the operation. This method keeps track of how - * many "complete" listeners are registered and removed, making sure polling is - * handled automatically. - * - * As long as there is one active "complete" listener, the connection is open. - * When there are no more listeners, the polling stops. - * - * @private - */ -GrpcOperation.prototype.listenForEvents_ = function() { - var self = this; - - this.on('newListener', function(event) { - if (event === 'complete') { - self.completeListeners++; - - if (!self.hasActiveListeners) { - self.hasActiveListeners = true; - self.startPolling_(); - } - } - }); - - this.on('removeListener', function(event) { - if (event === 'complete' && --self.completeListeners === 0) { - self.hasActiveListeners = false; - } - }); -}; - -/** - * Poll `getMetadata` to check the operation's status. This runs a loop to ping - * the API on an interval. - * - * Note: This method is automatically called once a "complete" event handler is - * registered on the operation. - * - * @private - */ -GrpcOperation.prototype.startPolling_ = function() { - var self = this; - - if (!this.hasActiveListeners) { - return; - } - - this.getMetadata(function(err, resp) { - if (err || resp.error) { - self.emit('error', err || GrpcService.decorateStatus_(resp.result)); - return; - } - - if (!resp.done) { - setTimeout(self.startPolling_.bind(self), 500); - return; - } - - self.emit('complete', resp); - }); -}; - module.exports = GrpcOperation; diff --git a/packages/common/src/index.js b/packages/common/src/index.js index 754a25f0068..a56915b435c 100644 --- a/packages/common/src/index.js +++ b/packages/common/src/index.js @@ -32,6 +32,12 @@ exports.GrpcService = require('./grpc-service.js'); */ exports.GrpcServiceObject = require('./grpc-service-object.js'); +/** + * @type {module:common/operation} + * @private + */ +exports.Operation = require('./operation.js'); + /** * @type {module:common/paginator} * @private diff --git a/packages/common/src/operation.js b/packages/common/src/operation.js new file mode 100644 index 00000000000..65666e62f8a --- /dev/null +++ b/packages/common/src/operation.js @@ -0,0 +1,198 @@ +/*! + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module common/operation + */ + +'use strict'; + +var events = require('events'); +var extend = require('extend'); +var modelo = require('modelo'); + +/** + * @type {module:common/service} + * @private + */ +var GrpcService = require('./grpc-service.js'); + +/** + * @type {module:common/serviceObject} + * @private + */ +var ServiceObject = require('./service-object.js'); + +// jscs:disable maximumLineLength +/** + * An Operation object allows you to interact with APIs that take longer to + * process things. + * + * @constructor + * @alias module:common/operation + * + * @param {object} config - Configuration object. + * @param {module:common/service|module:common/serviceObject|module:common/grpcService|module:common/grpcServiceObject} config.parent - The + * parent object. + * @param {string} id - The operation ID. + */ +// jscs:enable maximumLineLength +function Operation(config) { + var methods = { + /** + * Checks to see if an operation exists. + */ + exists: true, + + /** + * Retrieves the operation. + */ + get: true, + + /** + * Retrieves metadata for the operation. + */ + getMetadata: { + reqOpts: { + name: config.id + } + } + }; + + config = extend({ + baseUrl: '' + }, config); + + config.methods = config.methods || methods; + + ServiceObject.call(this, config); + events.EventEmitter.call(this); + + this.completeListeners = 0; + this.hasActiveListeners = false; + + this.listenForEvents_(); +} + +modelo.inherits(Operation, ServiceObject, events.EventEmitter); + +/** + * Wraps the `complete` and `error` events in a Promise. + * + * @return {promise} + */ +Operation.prototype.promise = function() { + var self = this; + + return new self.Promise(function(resolve, reject) { + self + .on('error', reject) + .on('complete', function(metadata) { + resolve([metadata]); + }); + }); +}; + +/** + * Begin listening for events on the operation. This method keeps track of how + * many "complete" listeners are registered and removed, making sure polling is + * handled automatically. + * + * As long as there is one active "complete" listener, the connection is open. + * When there are no more listeners, the polling stops. + * + * @private + */ +Operation.prototype.listenForEvents_ = function() { + var self = this; + + this.on('newListener', function(event) { + if (event === 'complete') { + self.completeListeners++; + + if (!self.hasActiveListeners) { + self.hasActiveListeners = true; + self.startPolling_(); + } + } + }); + + this.on('removeListener', function(event) { + if (event === 'complete' && --self.completeListeners === 0) { + self.hasActiveListeners = false; + } + }); +}; + +/** + * Poll for a status update. Execute the callback: + * + * - callback(err): Operation failed + * - callback(): Operation incomplete + * - callback(null, metadata): Operation complete + * + * @private + * + * @param {function} callback + */ +Operation.prototype.poll_ = function(callback) { + this.getMetadata(function(err, resp) { + if (err || resp.error) { + callback(err || GrpcService.decorateGrpcStatus_(resp.error)); + return; + } + + if (!resp.done) { + callback(); + return; + } + + callback(null, resp); + }); +}; + +/** + * Poll `getMetadata` to check the operation's status. This runs a loop to ping + * the API on an interval. + * + * Note: This method is automatically called once a "complete" event handler is + * registered on the operation. + * + * @private + */ +Operation.prototype.startPolling_ = function() { + var self = this; + + if (!this.hasActiveListeners) { + return; + } + + this.poll_(function(err, metadata) { + if (err) { + self.emit('error', err); + return; + } + + if (!metadata) { + setTimeout(self.startPolling_.bind(self), 500); + return; + } + + self.emit('complete', metadata); + }); +}; + +module.exports = Operation; diff --git a/packages/common/test/grpc-operation.js b/packages/common/test/grpc-operation.js index 0f06d166bb8..037073115e4 100644 --- a/packages/common/test/grpc-operation.js +++ b/packages/common/test/grpc-operation.js @@ -18,33 +18,21 @@ var assert = require('assert'); var proxyquire = require('proxyquire'); -var modelo = require('modelo'); -var EventEmitter = require('events').EventEmitter; -var util = require('../src/util.js'); -var nodeutil = require('util'); - -var GrpcServiceObject = require('../src/grpc-service-object.js'); -var GrpcService = require('../src/grpc-service.js'); - -function createFake(Class) { - function Fake() { - this.calledWith_ = arguments; - Class.apply(this, arguments); - } - nodeutil.inherits(Fake, Class); - return Fake; -} +var util = require('../src/util.js'); var fakeModelo = { inherits: function() { this.calledWith_ = arguments; - modelo.inherits.apply(modelo, arguments); } }; -var FakeGrpcServiceObject = createFake(GrpcServiceObject); -var FakeGrpcService = createFake(GrpcService); +function FakeGrpcServiceObject() { + this.grpcServiceObjectArguments_ = arguments; +} +function FakeOperation() { + this.operationArguments_ = arguments; +} describe('GrpcOperation', function() { var FAKE_SERVICE = { @@ -59,8 +47,7 @@ describe('GrpcOperation', function() { GrpcOperation = proxyquire('../src/grpc-operation.js', { modelo: fakeModelo, './grpc-service-object.js': FakeGrpcServiceObject, - './grpc-service.js': FakeGrpcService, - './util.js': util + './operation.js': FakeOperation }); }); @@ -69,21 +56,10 @@ describe('GrpcOperation', function() { }); describe('instantiation', function() { - it('should extend GrpcServiceObject and EventEmitter', function() { - var args = fakeModelo.calledWith_; - - assert.strictEqual(args[0], GrpcOperation); - assert.strictEqual(args[1], FakeGrpcServiceObject); - assert.strictEqual(args[2], EventEmitter); - }); - - it('should pass GrpcServiceObject the correct config', function() { - var config = grpcOperation.calledWith_[0]; - - assert.strictEqual(config.parent, FAKE_SERVICE); - assert.strictEqual(config.id, OPERATION_ID); - - assert.deepEqual(config.methods, { + var EXPECTED_CONFIG = { + parent: FAKE_SERVICE, + id: OPERATION_ID, + methods: { delete: { protoOpts: { service: 'Operations', @@ -104,30 +80,32 @@ describe('GrpcOperation', function() { name: OPERATION_ID } } - }); - }); + } + }; - it('should localize listener variables', function() { - assert.strictEqual(grpcOperation.completeListeners, 0); - assert.strictEqual(grpcOperation.hasActiveListeners, false); - }); + it('should extend GrpcServiceObject and Operation', function() { + var args = fakeModelo.calledWith_; - it('should call listenForEvents_', function() { - var listenForEvents = GrpcOperation.prototype.listenForEvents_; - var called = false; + assert.strictEqual(args[0], GrpcOperation); + assert.strictEqual(args[1], FakeGrpcServiceObject); + assert.strictEqual(args[2], FakeOperation); + }); - GrpcOperation.prototype.listenForEvents_ = function() { - called = true; - }; + it('should pass Operation the correct config', function() { + var config = grpcOperation.operationArguments_[0]; + assert.deepEqual(config, EXPECTED_CONFIG); + }); - new GrpcOperation(FAKE_SERVICE, OPERATION_ID); - assert.strictEqual(called, true); - GrpcOperation.prototype.listenForEvents_ = listenForEvents; + it('should pass GrpcServiceObject the correct config', function() { + var config = grpcOperation.grpcServiceObjectArguments_[0]; + assert.deepEqual(config, EXPECTED_CONFIG); }); }); describe('cancel', function() { it('should provide the proper request options', function(done) { + grpcOperation.id = OPERATION_ID; + grpcOperation.request = function(protoOpts, reqOpts, callback) { assert.deepEqual(protoOpts, { service: 'Operations', @@ -135,7 +113,7 @@ describe('GrpcOperation', function() { }); assert.strictEqual(reqOpts.name, OPERATION_ID); - callback(); + callback(); // done() }; grpcOperation.cancel(done); @@ -150,239 +128,4 @@ describe('GrpcOperation', function() { grpcOperation.cancel(); }); }); - - describe('promise', function() { - beforeEach(function() { - grpcOperation.startPolling_ = util.noop; - }); - - it('should return an instance of the localized Promise', function() { - var FakePromise = grpcOperation.Promise = function() {}; - var promise = grpcOperation.promise(); - - assert(promise instanceof FakePromise); - }); - - it('should reject the promise if an error occurs', function() { - var error = new Error('err'); - - setImmediate(function() { - grpcOperation.emit('error', error); - }); - - return grpcOperation.promise().then(function() { - throw new Error('Promise should have been rejected.'); - }, function(err) { - assert.strictEqual(err, error); - }); - }); - - it('should resolve the promise on complete', function() { - var metadata = {}; - - setImmediate(function() { - grpcOperation.emit('complete', metadata); - }); - - return grpcOperation.promise().then(function(data) { - assert.deepEqual(data, [metadata]); - }); - }); - }); - - describe('listenForEvents_', function() { - beforeEach(function() { - grpcOperation.startPolling_ = util.noop; - }); - - it('should start polling when complete listener is bound', function(done) { - grpcOperation.startPolling_ = function() { - done(); - }; - - grpcOperation.on('complete', util.noop); - }); - - it('should track the number of listeners', function() { - assert.strictEqual(grpcOperation.completeListeners, 0); - - grpcOperation.on('complete', util.noop); - assert.strictEqual(grpcOperation.completeListeners, 1); - - grpcOperation.removeListener('complete', util.noop); - assert.strictEqual(grpcOperation.completeListeners, 0); - }); - - it('should only run a single pulling loop', function() { - var startPollingCallCount = 0; - - grpcOperation.startPolling_ = function() { - startPollingCallCount++; - }; - - grpcOperation.on('complete', util.noop); - grpcOperation.on('complete', util.noop); - - assert.strictEqual(startPollingCallCount, 1); - }); - - it('should close when no more message listeners are bound', function() { - grpcOperation.on('complete', util.noop); - grpcOperation.on('complete', util.noop); - assert.strictEqual(grpcOperation.hasActiveListeners, true); - - grpcOperation.removeListener('complete', util.noop); - assert.strictEqual(grpcOperation.hasActiveListeners, true); - - grpcOperation.removeListener('complete', util.noop); - assert.strictEqual(grpcOperation.hasActiveListeners, false); - }); - }); - - describe('startPolling_', function() { - var listenForEvents_; - - before(function() { - listenForEvents_ = GrpcOperation.prototype.listenForEvents_; - }); - - after(function() { - GrpcOperation.prototype.listenForEvents_ = listenForEvents_; - }); - - beforeEach(function() { - GrpcOperation.prototype.listenForEvents_ = util.noop; - grpcOperation.hasActiveListeners = true; - }); - - afterEach(function() { - grpcOperation.hasActiveListeners = false; - }); - - it('should not call getMetadata if no listeners', function(done) { - grpcOperation.hasActiveListeners = false; - - grpcOperation.getMetadata = done; // if called, test will fail. - - grpcOperation.startPolling_(); - done(); - }); - - it('should call getMetadata if listeners are registered', function(done) { - grpcOperation.hasActiveListeners = true; - - grpcOperation.getMetadata = function() { - done(); - }; - - grpcOperation.startPolling_(); - }); - - describe('API error', function() { - var error = new Error('Error.'); - - beforeEach(function() { - grpcOperation.getMetadata = function(callback) { - callback(error); - }; - }); - - it('should emit the error', function(done) { - grpcOperation.on('error', function(err) { - assert.strictEqual(err, error); - done(); - }); - - grpcOperation.startPolling_(); - }); - }); - - describe('operation failure', function() { - var formattedError = { status: 'a' }; - - var apiResponse = { - error: true, - result: 'b' - }; - - beforeEach(function() { - grpcOperation.getMetadata = function(callback) { - callback(null, apiResponse, apiResponse); - }; - }); - - it('should emit the operation error', function(done) { - FakeGrpcService.decorateStatus_ = function(status) { - assert.strictEqual(status, apiResponse.result); - return formattedError; - }; - - grpcOperation.on('error', function(err) { - assert.strictEqual(err, formattedError); - done(); - }); - - grpcOperation.startPolling_(); - }); - }); - - describe('operation pending', function() { - var apiResponse = { done: false }; - var setTimeoutCached = global.setTimeout; - - beforeEach(function() { - grpcOperation.getMetadata = function(callback) { - callback(null, apiResponse, apiResponse); - }; - }); - - after(function() { - global.setTimeout = setTimeoutCached; - }); - - it('should call startPolling_ after 500 ms', function(done) { - var startPolling_ = grpcOperation.startPolling_; - var startPollingCalled = false; - - global.setTimeout = function(fn, timeoutMs) { - fn(); // should call startPolling_ - assert.strictEqual(timeoutMs, 500); - }; - - grpcOperation.startPolling_ = function() { - if (!startPollingCalled) { - // Call #1. - startPollingCalled = true; - startPolling_.apply(this, arguments); - return; - } - - // This is from the setTimeout call. - assert.strictEqual(this, grpcOperation); - done(); - }; - - grpcOperation.startPolling_(); - }); - }); - - describe('operation complete', function() { - var apiResponse = { done: true }; - - beforeEach(function() { - grpcOperation.getMetadata = function(callback) { - callback(null, apiResponse, apiResponse); - }; - }); - - it('should emit complete with metadata', function(done) { - grpcOperation.on('complete', function(metadata) { - assert.strictEqual(metadata, apiResponse); - done(); - }); - - grpcOperation.startPolling_(); - }); - }); - }); }); diff --git a/packages/common/test/index.js b/packages/common/test/index.js index 51bed6aa953..076f6bc88d6 100644 --- a/packages/common/test/index.js +++ b/packages/common/test/index.js @@ -22,6 +22,7 @@ var proxyquire = require('proxyquire'); var fakeGrpcOperation = {}; var fakeGrpcService = {}; var fakeGrpcServiceObject = {}; +var fakeOperation = {}; var fakePaginator = {}; var fakeService = {}; var fakeServiceObject = {}; @@ -35,6 +36,7 @@ describe('common', function() { './grpc-operation.js': fakeGrpcOperation, './grpc-service.js': fakeGrpcService, './grpc-service-object.js': fakeGrpcServiceObject, + './operation.js': fakeOperation, './paginator.js': fakePaginator, './service.js': fakeService, './service-object.js': fakeServiceObject, @@ -47,6 +49,7 @@ describe('common', function() { GrpcOperation: fakeGrpcOperation, GrpcService: fakeGrpcService, GrpcServiceObject: fakeGrpcServiceObject, + Operation: fakeOperation, Service: fakeService, ServiceObject: fakeServiceObject, paginator: fakePaginator, diff --git a/packages/common/test/operation.js b/packages/common/test/operation.js new file mode 100644 index 00000000000..cd4933dd1d4 --- /dev/null +++ b/packages/common/test/operation.js @@ -0,0 +1,355 @@ +/*! + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var assert = require('assert'); +var EventEmitter = require('events').EventEmitter; +var proxyquire = require('proxyquire'); + +var util = require('../src/util.js'); + +var fakeModelo = { + inherits: function() { + this.calledWith_ = arguments; + return require('modelo').inherits.apply(this, arguments); + } +}; + +var decorateGrpcStatusOverride_; +function FakeGrpcService() {} +FakeGrpcService.decorateGrpcStatus_ = function() { + return (decorateGrpcStatusOverride_ || util.noop).apply(null, arguments); +}; + +function FakeServiceObject() { + this.serviceObjectArguments_ = arguments; +} + +describe('Operation', function() { + var FAKE_SERVICE = {}; + var OPERATION_ID = '/a/b/c/d'; + + var Operation; + var operation; + + before(function() { + Operation = proxyquire('../src/operation.js', { + modelo: fakeModelo, + './grpc-service.js': FakeGrpcService, + './service-object.js': FakeServiceObject + }); + }); + + beforeEach(function() { + operation = new Operation({ + parent: FAKE_SERVICE, + id: OPERATION_ID + }); + operation.Promise = Promise; + decorateGrpcStatusOverride_ = null; + }); + + describe('instantiation', function() { + it('should extend ServiceObject and EventEmitter', function() { + var args = fakeModelo.calledWith_; + + assert.strictEqual(args[0], Operation); + assert.strictEqual(args[1], FakeServiceObject); + assert.strictEqual(args[2], EventEmitter); + }); + + it('should pass ServiceObject the correct config', function() { + var config = operation.serviceObjectArguments_[0]; + + assert.strictEqual(config.baseUrl, ''); + assert.strictEqual(config.parent, FAKE_SERVICE); + assert.strictEqual(config.id, OPERATION_ID); + + assert.deepEqual(config.methods, { + exists: true, + get: true, + getMetadata: { + reqOpts: { + name: OPERATION_ID + } + } + }); + }); + + it('should allow overriding baseUrl', function() { + var baseUrl = 'baseUrl'; + + var operation = new Operation({ + baseUrl: baseUrl + }); + + assert.strictEqual(operation.serviceObjectArguments_[0].baseUrl, baseUrl); + }); + + it('should localize listener variables', function() { + assert.strictEqual(operation.completeListeners, 0); + assert.strictEqual(operation.hasActiveListeners, false); + }); + + it('should call listenForEvents_', function() { + var listenForEvents = Operation.prototype.listenForEvents_; + var called = false; + + Operation.prototype.listenForEvents_ = function() { + called = true; + }; + + new Operation(FAKE_SERVICE, OPERATION_ID); + assert.strictEqual(called, true); + Operation.prototype.listenForEvents_ = listenForEvents; + }); + }); + + describe('promise', function() { + beforeEach(function() { + operation.startPolling_ = util.noop; + }); + + it('should return an instance of the localized Promise', function() { + var FakePromise = operation.Promise = function() {}; + var promise = operation.promise(); + + assert(promise instanceof FakePromise); + }); + + it('should reject the promise if an error occurs', function() { + var error = new Error('err'); + + setImmediate(function() { + operation.emit('error', error); + }); + + return operation.promise().then(function() { + throw new Error('Promise should have been rejected.'); + }, function(err) { + assert.strictEqual(err, error); + }); + }); + + it('should resolve the promise on complete', function() { + var metadata = {}; + + setImmediate(function() { + operation.emit('complete', metadata); + }); + + return operation.promise().then(function(data) { + assert.deepEqual(data, [metadata]); + }); + }); + }); + + describe('listenForEvents_', function() { + beforeEach(function() { + operation.startPolling_ = util.noop; + }); + + it('should start polling when complete listener is bound', function(done) { + operation.startPolling_ = function() { + done(); + }; + + operation.on('complete', util.noop); + }); + + it('should track the number of listeners', function() { + assert.strictEqual(operation.completeListeners, 0); + + operation.on('complete', util.noop); + assert.strictEqual(operation.completeListeners, 1); + + operation.removeListener('complete', util.noop); + assert.strictEqual(operation.completeListeners, 0); + }); + + it('should only run a single pulling loop', function() { + var startPollingCallCount = 0; + + operation.startPolling_ = function() { + startPollingCallCount++; + }; + + operation.on('complete', util.noop); + operation.on('complete', util.noop); + + assert.strictEqual(startPollingCallCount, 1); + }); + + it('should close when no more message listeners are bound', function() { + operation.on('complete', util.noop); + operation.on('complete', util.noop); + assert.strictEqual(operation.hasActiveListeners, true); + + operation.removeListener('complete', util.noop); + assert.strictEqual(operation.hasActiveListeners, true); + + operation.removeListener('complete', util.noop); + assert.strictEqual(operation.hasActiveListeners, false); + }); + }); + + describe('startPolling_', function() { + var listenForEvents_; + + before(function() { + listenForEvents_ = Operation.prototype.listenForEvents_; + }); + + after(function() { + Operation.prototype.listenForEvents_ = listenForEvents_; + }); + + beforeEach(function() { + Operation.prototype.listenForEvents_ = util.noop; + operation.hasActiveListeners = true; + }); + + afterEach(function() { + operation.hasActiveListeners = false; + }); + + it('should not call getMetadata if no listeners', function(done) { + operation.hasActiveListeners = false; + + operation.getMetadata = done; // if called, test will fail. + + operation.startPolling_(); + done(); + }); + + it('should call getMetadata if listeners are registered', function(done) { + operation.hasActiveListeners = true; + + operation.getMetadata = function() { + done(); + }; + + operation.startPolling_(); + }); + + describe('API error', function() { + var error = new Error('Error.'); + + beforeEach(function() { + operation.getMetadata = function(callback) { + callback(error); + }; + }); + + it('should emit the error', function(done) { + operation.on('error', function(err) { + assert.strictEqual(err, error); + done(); + }); + + operation.startPolling_(); + }); + }); + + describe('operation failure', function() { + var apiResponse = { + error: {} + }; + + beforeEach(function() { + operation.getMetadata = function(callback) { + callback(null, apiResponse, apiResponse); + }; + }); + + it('should emit the operation error', function(done) { + var decoratedGrpcStatus = {}; + + decorateGrpcStatusOverride_ = function(status) { + assert.strictEqual(status, apiResponse.error); + return decoratedGrpcStatus; + }; + + operation.on('error', function(err) { + assert.strictEqual(err, decoratedGrpcStatus); + done(); + }); + + operation.startPolling_(); + }); + }); + + describe('operation pending', function() { + var apiResponse = { done: false }; + var setTimeoutCached = global.setTimeout; + + beforeEach(function() { + operation.getMetadata = function(callback) { + callback(null, apiResponse, apiResponse); + }; + }); + + after(function() { + global.setTimeout = setTimeoutCached; + }); + + it('should call startPolling_ after 500 ms', function(done) { + var startPolling_ = operation.startPolling_; + var startPollingCalled = false; + + global.setTimeout = function(fn, timeoutMs) { + fn(); // should call startPolling_ + assert.strictEqual(timeoutMs, 500); + }; + + operation.startPolling_ = function() { + if (!startPollingCalled) { + // Call #1. + startPollingCalled = true; + startPolling_.apply(this, arguments); + return; + } + + // This is from the setTimeout call. + assert.strictEqual(this, operation); + done(); + }; + + operation.startPolling_(); + }); + }); + + describe('operation complete', function() { + var apiResponse = { done: true }; + + beforeEach(function() { + operation.getMetadata = function(callback) { + callback(null, apiResponse, apiResponse); + }; + }); + + it('should emit complete with metadata', function(done) { + operation.on('complete', function(metadata) { + assert.strictEqual(metadata, apiResponse); + done(); + }); + + operation.startPolling_(); + }); + }); + }); +});