diff --git a/CHANGES.md b/CHANGES.md index f6942fedeacc..5b6d2a60354e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,9 +9,10 @@ Change Log * For all classes/functions that can now take a `Resource` instance, all additional parameters that are part of the `Resource` class have been deprecated and will be removed in Cesium 1.44. This generally includes `proxy`, `headers` and `query` parameters. * Major refactor of URL handling. All classes that take a url parameter, can now take a Resource or a String. This includes all imagery providers, all terrain providers, `Cesium3DTileset`, `KMLDataSource`, `CZMLDataSource`, `GeoJsonDataSource`, `Model`, `Billboard`, along with all the low level `load*()` functions. * Added `ClippingPlaneCollection.isSupported` function for checking if rendering with clipping planes is supported. +* Added new `CesiumIon` utility class for working with the Cesium ion beta API. * Improved CZML Custom Properties sandcastle example [#6086](https://github.com/AnalyticalGraphicsInc/cesium/pull/6086) -* Added `Plane.projectPointOntoPlane` for projecting a `Cartesian3` position onto a `Plane` [#6092](https://github.com/AnalyticalGraphicsInc/cesium/pull/6092) -* Added `Cartesian3.projectVector` for projecting one vector to another [#6093](https://github.com/AnalyticalGraphicsInc/cesium/pull/6093) +* Added `Plane.projectPointOntoPlane` for projecting a `Cartesian3` position onto a `Plane` [#6092](https://github.com/AnalyticalGraphicsInc/cesium/pull/6092) +* Added `Cartesian3.projectVector` for projecting one vector to another [#6093](https://github.com/AnalyticalGraphicsInc/cesium/pull/6093) * Added `Cesium3DTileset.tileFailed` event that will be raised when a tile fails to load. The object passed to the event listener will have a url and message property. If there are no event listeners, error messages will be logged to the console. [#6088](https://github.com/AnalyticalGraphicsInc/cesium/pull/6088) * Added `AttributeCompression.zigZagDeltaDecode` which will decode delta and ZigZag encoded buffers in place. * Added `pack` and `unpack` functions to `OrientedBoundingBox` for packing to and unpacking from a flat buffer. @@ -28,7 +29,7 @@ Change Log * Only one node is supported. * Only one mesh per node is supported. * Only one primitive per mesh is supported. -* Updated documentation links to reflect new locations on cesiumjs.org and cesium.com. +* Updated documentation links to reflect new locations on cesiumjs.org and cesium.com. * Updated 'Viewer.zoomTo' and 'Viewer.flyTo' to take in Cesium3DTilesets as a target and updated sandcastle 3DTileset examples to reflect this change * Fixed a glTF animation bug that caused certain animations to jitter. [#5740](https://github.com/AnalyticalGraphicsInc/cesium/pull/5740) * Fixed a bug when creating billboard and model entities without a globe. [#6109](https://github.com/AnalyticalGraphicsInc/cesium/pull/6109) diff --git a/Source/Core/loadImage.js b/Source/Core/loadImage.js index d624658a0042..1427f6bc8ed8 100644 --- a/Source/Core/loadImage.js +++ b/Source/Core/loadImage.js @@ -79,11 +79,11 @@ define([ } function makeRequest(resource, allowCrossOrigin) { - var url = resource.url; var request = resource.request; - request.url = url; + request.url = resource.url; request.requestFunction = function() { var crossOrigin; + var url = resource.url; // data URIs can't have allowCrossOrigin set. if (isDataUri(url) || isBlobUri(url)) { @@ -106,6 +106,11 @@ define([ return promise .otherwise(function(e) { + //Don't retry cancelled or otherwise aborted requests + if (request.state !== RequestState.FAILED) { + return when.reject(e); + } + return resource.retryOnError(e) .then(function(retry) { if (retry) { @@ -113,9 +118,8 @@ define([ request.state = RequestState.UNISSUED; request.deferred = undefined; - return makeRequest(resource); + return makeRequest(resource, allowCrossOrigin); } - return when.reject(e); }); }); diff --git a/Source/Core/loadJsonp.js b/Source/Core/loadJsonp.js index bcec3f2ea49c..65e3e4bc1d7a 100644 --- a/Source/Core/loadJsonp.js +++ b/Source/Core/loadJsonp.js @@ -97,8 +97,7 @@ define([ resource.addQueryParameters(callbackQuery); var request = resource.request; - var url = resource.url; - request.url = url; + request.url = resource.url; request.requestFunction = function() { var deferred = when.defer(); @@ -113,7 +112,7 @@ define([ } }; - loadJsonp.loadAndExecuteScript(url, functionName, deferred); + loadJsonp.loadAndExecuteScript(resource.url, functionName, deferred); return deferred.promise; }; @@ -124,22 +123,21 @@ define([ return promise .otherwise(function(e) { - if (request.state === RequestState.FAILED) { - return resource.retryOnError(e) - .then(function(retry) { - if (retry) { - // Reset request so it can try again - request.state = RequestState.UNISSUED; - request.deferred = undefined; - - return makeRequest(resource, callbackParameterName, functionName); - } - - return when.reject(e); - }); + if (request.state !== RequestState.FAILED) { + return when.reject(e); } - - return when.reject(e); + return resource.retryOnError(e) + .then(function(retry) { + if (retry) { + // Reset request so it can try again + request.state = RequestState.UNISSUED; + request.deferred = undefined; + + return makeRequest(resource, callbackParameterName, functionName); + } + + return when.reject(e); + }); }); } diff --git a/Source/Core/loadWithXhr.js b/Source/Core/loadWithXhr.js index a7cede31c615..f7067be65f42 100644 --- a/Source/Core/loadWithXhr.js +++ b/Source/Core/loadWithXhr.js @@ -85,19 +85,17 @@ define([ } function makeRequest(optionsOrResource) { - var url = optionsOrResource.url; var request = optionsOrResource.request; - request.url = url; - - var responseType = optionsOrResource.responseType; - var method = optionsOrResource.method; - var data = optionsOrResource.data; - var headers = optionsOrResource.headers; - var overrideMimeType = optionsOrResource.overrideMimeType; + request.url = optionsOrResource.url; request.requestFunction = function() { + var responseType = optionsOrResource.responseType; + var method = optionsOrResource.method; + var data = optionsOrResource.data; + var headers = optionsOrResource.headers; + var overrideMimeType = optionsOrResource.overrideMimeType; var deferred = when.defer(); - var xhr = loadWithXhr.load(url, responseType, method, data, headers, deferred, overrideMimeType); + var xhr = loadWithXhr.load(optionsOrResource.url, responseType, method, data, headers, deferred, overrideMimeType); if (defined(xhr) && defined(xhr.abort)) { request.cancelFunction = function() { xhr.abort(); @@ -116,22 +114,22 @@ define([ return data; }) .otherwise(function(e) { - if ((request.state === RequestState.FAILED) && defined(optionsOrResource.retryOnError)) { - return optionsOrResource.retryOnError(e) - .then(function(retry) { - if (retry) { - // Reset request so it can try again - request.state = RequestState.UNISSUED; - request.deferred = undefined; - - return makeRequest(optionsOrResource); - } - - return when.reject(e); - }); + if ((request.state !== RequestState.FAILED) || !defined(optionsOrResource.retryOnError)) { + return when.reject(e); } - return when.reject(e); + return optionsOrResource.retryOnError(e) + .then(function(retry) { + if (retry) { + // Reset request so it can try again + request.state = RequestState.UNISSUED; + request.deferred = undefined; + + return makeRequest(optionsOrResource); + } + + return when.reject(e); + }); }); } diff --git a/Source/Scene/CesiumIon.js b/Source/Scene/CesiumIon.js new file mode 100644 index 000000000000..4df460849336 --- /dev/null +++ b/Source/Scene/CesiumIon.js @@ -0,0 +1,283 @@ +define([ + './ArcGisMapServerImageryProvider', + './BingMapsImageryProvider', + './createTileMapServiceImageryProvider', + './GoogleEarthEnterpriseMapsProvider', + './MapboxImageryProvider', + './SingleTileImageryProvider', + './UrlTemplateImageryProvider', + './WebMapServiceImageryProvider', + './WebMapTileServiceImageryProvider', + '../Core/Check', + '../Core/defaultValue', + '../Core/defined', + '../Core/loadJson', + '../Core/Resource', + '../Core/RuntimeError', + '../ThirdParty/when' + ], function( + ArcGisMapServerImageryProvider, + BingMapsImageryProvider, + createTileMapServiceImageryProvider, + GoogleEarthEnterpriseMapsProvider, + MapboxImageryProvider, + SingleTileImageryProvider, + UrlTemplateImageryProvider, + WebMapServiceImageryProvider, + WebMapTileServiceImageryProvider, + Check, + defaultValue, + defined, + loadJson, + Resource, + RuntimeError, + when) { + 'use strict'; + + /** + * Utility object for working with the Cesium ion API. + * + * @see https://cesium.com + * + * @exports CesiumIon + * + * @experimental This class is part of Cesium ion beta functionality and may change without our normal deprecation policy. + */ + var CesiumIon = {}; + + /** + * The default Cesium ion access token to use. + * + * @type {String} + */ + CesiumIon.defaultAccessToken = undefined; + + /** + * The default Cesium ion server to use. + * + * @type {String} + * @default https://api.cesium.com + */ + CesiumIon.defaultServerUrl = 'https://api.cesium.com'; + + /** + * Asynchronously creates a {@link Resource} representing a Cesium ion asset. + * + * @param {Number} assetId The Cesium ion asset id. + * @param {Object} [options] An object with the following properties: + * @param {String} [options.accessToken=CesiumIon.defaultAccessToken] The access token to use. + * @param {String} [options.serverUrl=CesiumIon.defaultServerUrl] The url to the Cesium ion API server. + * @returns {Promise.} A Promise to a Resource representing the Cesium ion Asset. + * + * @example + * //Load a Cesium3DTileset with asset ID of 124624234 + * Cesium.CesiumIon.createResource(124624234) + * .then(function (resource) { + * viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ url: resource })); + * }); + * + * @example + * //Load a CZML file with asset ID of 10890 + * Cesium.CesiumIon.createResource(10890) + * .then(function (resource) { + * viewer.dataSources.add(Cesium.CzmlDataSource.load(resource)); + * }); + * + * @example + * //Load an ImageryProvider with asset ID of 2347923 + * Cesium.CesiumIon.createResource(2347923) + * .then(function (resource) { + * viewer.imageryLayers.addProvider(Cesium.createTileMapServiceImageryProvider({url : resource })); + * }); + */ + CesiumIon.createResource = function(assetId, options) { + Check.defined('assetId', assetId); + + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var serverUrl = defaultValue(options.serverUrl, CesiumIon.defaultServerUrl); + var accessToken = defaultValue(options.accessToken, CesiumIon.defaultAccessToken); + + var resourceOptions = { + url: serverUrl + '/v1/assets/' + assetId + '/endpoint' + }; + + if (defined(accessToken)) { + resourceOptions.queryParameters = { access_token: accessToken }; + } + + var endpointResource = new Resource(resourceOptions); + return CesiumIon._loadJson(endpointResource) + .then(function(endpoint) { + return CesiumIonResource.create(endpoint, endpointResource); + }); + }; + + /** + * Creates an {@link ImageryProvider} representing a Cesium ion imagery asset. + * Unlike {@link CesiumIon.createResource}, this function supports external asset functionality. + * + * @param {Number} assetId The Cesium ion asset id. + * @param {Object} [options] An object with the following properties: + * @param {String} [options.accessToken=CesiumIon.defaultAccessToken] The access token to use. + * @param {String} [options.serverUrl=CesiumIon.defaultServerUrl] The url to the Cesium ion API server. + * @returns {Promise} A promise to an imagery provider presenting the requested Cesium ion Asset. + * + * @example + * //Load an ImageryProvider with asset ID of 2347923 + * Cesium.CesiumIon.createImageryProvider(2347923) + * .then(function (imageryProvider) { + * viewer.imageryLayers.addProvider(imageryProvider); + * }); + */ + CesiumIon.createImageryProvider = function(assetId, options) { + return CesiumIon.createResource(assetId, options) + .then(function(resource) { + return resource.createImageryProvider(); + }); + }; + + /** + * A {@link Resource} instance that encapsulates Cesium ion asset + * creation and automatic refresh token handling. This object + * should not be created directly, use CesiumIonResource.create + * + * @private + */ + function CesiumIonResource(options, endpoint, endpointResource) { + Resource.call(this, options); + + // The asset endpoint data returned from ion. + this.ionEndpoint = endpoint; + + // The endpoint resource to fetch when a new token is needed + this.ionEndpointResource = endpointResource; + + // The primary CesiumIonResource from which an instance is derived + this.ionRoot = undefined; + } + + CesiumIonResource.create = function (endpoint, endpointResource) { + var options = { + url: endpoint.url, + retryCallback: createRetryCallback(endpoint, endpointResource), + retryAttempts: 1 + }; + + if (defined(endpoint.accessToken)) { + options.queryParameters = { access_token: endpoint.accessToken }; + } + + return new CesiumIonResource(options, endpoint, endpointResource); + }; + + if (defined(Object.create)) { + CesiumIonResource.prototype = Object.create(Resource.prototype); + CesiumIonResource.prototype.constructor = CesiumIonResource; + } + + CesiumIonResource.prototype.clone = function(result) { + var ionRoot = defaultValue(this.ionRoot, this); + + if (!defined(result)) { + // We always want to use the root's information because it's the most up-to-date + result = new CesiumIonResource({ url: this._url }, ionRoot.ionEndpoint, ionRoot.ionEndpointResource); + } + + result = Resource.prototype.clone.call(this, result); + result.ionRoot = ionRoot; + + // Same comment as above, use the root's access_token + result.queryParameters.access_token = ionRoot.queryParameters.access_token; + return result; + }; + + function createRetryCallback(endpoint, endpointResource) { + // We use a shared pending promise for all derived assets, since they share + // a common access_token. If we're already requesting a new token for this + // asset, we wait on the same promise. + var pendingPromise; + + var retryCallback = function(that, error) { + // We only want to retry in the case of invalid credentials (401) or image + // requests(since Image failures can not provide a status code) + if (!defined(error) || (error.statusCode !== 401 && !(error.target instanceof Image))) { + return when.resolve(false); + } + + if (!defined(pendingPromise)) { + pendingPromise = CesiumIon._loadJson(endpointResource) + .then(function(newEndpoint) { + //Set the token for root resource so derived resources automatically pick it up + var ionRoot = that.ionRoot; + if (defined(ionRoot)) { + ionRoot.ionEndpoint = newEndpoint; + ionRoot.queryParameters.access_token = newEndpoint.accessToken; + } + return newEndpoint; + }) + .always(function(newEndpoint) { + // Pass or fail, we're done with this promise, the next failure should use a new one. + pendingPromise = undefined; + + // We need this return because our old busted version of when + // doesn't conform to spec of returning the result of the above `then`. + return newEndpoint; + }); + } + + return pendingPromise.then(function(newEndpoint) { + // Set the new token and endpoint for this resource + that.ionEndpoint = newEndpoint; + that.queryParameters.access_token = newEndpoint.accessToken; + return true; + }); + }; + + //Exposed for testing + retryCallback._pendingPromise = pendingPromise; + + return retryCallback; + } + + function createFactory(Type) { + return function(options) { + return new Type(options); + }; + } + + // These values are the unofficial list of supported external imagery + // assets in the Cesium ion beta. They are subject to change. + var ImageryProviderMapping = { + ARCGIS_MAPSERVER: createFactory(ArcGisMapServerImageryProvider), + BING: createFactory(BingMapsImageryProvider), + GOOGLE_EARTH: createFactory(GoogleEarthEnterpriseMapsProvider), + MAPBOX: createFactory(MapboxImageryProvider), + SINGLE_TILE: createFactory(SingleTileImageryProvider), + TMS: createTileMapServiceImageryProvider, + URL_TEMPLATE: createFactory(UrlTemplateImageryProvider), + WMS: createFactory(WebMapServiceImageryProvider), + WMTS: createFactory(WebMapTileServiceImageryProvider) + }; + + CesiumIonResource.prototype.createImageryProvider = function() { + var type = this.ionEndpoint.type; + if (type === 'IMAGERY') { + return createTileMapServiceImageryProvider({ url: this }); + } + + var factory = ImageryProviderMapping[type]; + + if (!defined(factory)) { + throw new RuntimeError('Unrecognized Cesium ion imagery type: ' + type); + } + + return factory(this.ionEndpoint); + }; + + //Exposed for testing + CesiumIon._CesiumIonResource = CesiumIonResource; + CesiumIon._loadJson = loadJson; + CesiumIon._createRetryCallback = createRetryCallback; + + return CesiumIon; +}); diff --git a/Specs/Scene/CesiumIonSpec.js b/Specs/Scene/CesiumIonSpec.js new file mode 100644 index 000000000000..97823d6a0f8f --- /dev/null +++ b/Specs/Scene/CesiumIonSpec.js @@ -0,0 +1,222 @@ +defineSuite([ + 'Scene/CesiumIon', + 'Core/RequestErrorEvent', + 'Core/Resource', + 'Scene/ArcGisMapServerImageryProvider', + 'Scene/BingMapsImageryProvider', + 'Scene/GoogleEarthEnterpriseMapsProvider', + 'Scene/MapboxImageryProvider', + 'Scene/SingleTileImageryProvider', + 'Scene/UrlTemplateImageryProvider', + 'Scene/WebMapServiceImageryProvider', + 'Scene/WebMapTileServiceImageryProvider', + 'ThirdParty/when' + ], function( + CesiumIon, + RequestErrorEvent, + Resource, + ArcGisMapServerImageryProvider, + BingMapsImageryProvider, + GoogleEarthEnterpriseMapsProvider, + MapboxImageryProvider, + SingleTileImageryProvider, + UrlTemplateImageryProvider, + WebMapServiceImageryProvider, + WebMapTileServiceImageryProvider, + when) { + 'use strict'; + + var assetId; + var endpoint; + + beforeEach(function() { + assetId = 123890213; + endpoint = { + type: '3DTILES', + url: 'https://assets.cesium.com/' + assetId, + accessToken: 'not_really_a_refresh_token' + }; + }); + + it('createResource calls CesiumIonResource.create with expected default parameters', function() { + var mockResource = {}; + var loadJson = spyOn(CesiumIon, '_loadJson').and.returnValue(when.resolve(endpoint)); + var create = spyOn(CesiumIon._CesiumIonResource, 'create').and.returnValue(mockResource); + + return CesiumIon.createResource(assetId).then(function(resource) { + var loadArgs = loadJson.calls.argsFor(0); + var endpointResource = loadArgs[0]; + expect(endpointResource).toBeInstanceOf(Resource); + expect(endpointResource.getUrlComponent()).toEqual(CesiumIon.defaultServerUrl + '/v1/assets/' + assetId + '/endpoint'); + expect(create).toHaveBeenCalledWith(endpoint, endpointResource); + expect(resource).toBe(mockResource); + }); + }); + + it('createResource calls CesiumIonResource.create with expected parameters', function() { + var mockResource = {}; + var options = { accessToken: 'not_a_token', serverUrl: 'https://test.invalid' }; + var loadJson = spyOn(CesiumIon, '_loadJson').and.returnValue(when.resolve(endpoint)); + var create = spyOn(CesiumIon._CesiumIonResource, 'create').and.returnValue(mockResource); + + return CesiumIon.createResource(assetId, options).then(function(resource) { + var loadArgs = loadJson.calls.argsFor(0); + var endpointResource = loadArgs[0]; + expect(endpointResource).toBeInstanceOf(Resource); + expect(endpointResource.getUrlComponent()).toEqual(options.serverUrl + '/v1/assets/' + assetId + '/endpoint'); + expect(endpointResource.queryParameters).toEqual({ access_token: options.accessToken }); + expect(create).toHaveBeenCalledWith(endpoint, endpointResource); + expect(resource).toBe(mockResource); + }); + }); + + it('createImageryProvider calls createResource and returns createImageryProvider result', function() { + var mockImageryProvider = {}; + var mockResource = { createImageryProvider: jasmine.createSpy('createImageryProvider').and.returnValue(mockImageryProvider) }; + + spyOn(CesiumIon, 'createResource').and.returnValue(when.resolve(mockResource)); + + var options = {}; + return CesiumIon.createImageryProvider(assetId, options) + .then(function(imageryProvider) { + expect(CesiumIon.createResource).toHaveBeenCalledWith(assetId, options); + expect(mockResource.createImageryProvider).toHaveBeenCalledWith(); + expect(imageryProvider).toBe(mockImageryProvider); + }); + }); + + describe('CesiumIonResource', function() { + it('constructs with expected values', function() { + spyOn(Resource, 'call').and.callThrough(); + + var endpointResource = new Resource({ url: 'https://api.test.invalid' }); + var resource = CesiumIon._CesiumIonResource.create(endpoint, endpointResource); + expect(resource).toBeInstanceOf(Resource); + expect(resource.ionEndpoint).toEqual(endpoint); + expect(Resource.call).toHaveBeenCalledWith(resource, { + url: endpoint.url, + retryCallback: resource.retryCallback, + retryAttempts: 1, + queryParameters: { access_token: endpoint.accessToken } + }); + }); + + it('clone works', function() { + var endpointResource = new Resource({ url: 'https://api.test.invalid' }); + var resource = CesiumIon._CesiumIonResource.create(endpoint, endpointResource); + var cloned = resource.clone(); + expect(cloned).not.toBe(resource); + expect(cloned.ionRoot).toBe(resource); + cloned.ionRoot = undefined; + expect(cloned).toEqual(resource); + }); + + it('create creates the expected resource', function() { + var endpointResource = new Resource({ url: 'https://api.test.invalid', access_token: 'not_the_token' }); + var resource = CesiumIon._CesiumIonResource.create(endpoint, endpointResource); + expect(resource.getUrlComponent()).toEqual('https://assets.cesium.com/123890213'); + expect(resource.queryParameters).toEqual({ access_token: 'not_really_a_refresh_token' }); + expect(resource.ionEndpoint).toBe(endpoint); + expect(resource.ionEndpointResource).toEqual(endpointResource); + expect(resource.retryCallback).toBeDefined(); + expect(resource.retryAttempts).toBe(1); + }); + + function testImageryAsset(endpoint, ImageryClass) { + var endpointResource = new Resource({ url: 'https://api.test.invalid' }); + var resource = CesiumIon._CesiumIonResource.create(endpoint, endpointResource); + var imageryProvider = resource.createImageryProvider(); + expect(imageryProvider).toBeInstanceOf(ImageryClass); + } + + it('createImageryProvider works', function() { + var url = 'https://test.invalid'; + testImageryAsset({ type: 'IMAGERY', url: url }, UrlTemplateImageryProvider); + testImageryAsset({ type: 'ARCGIS_MAPSERVER', url: url }, ArcGisMapServerImageryProvider); + testImageryAsset({ type: 'BING', url: url }, BingMapsImageryProvider); + testImageryAsset({ type: 'GOOGLE_EARTH', url: url, channel: 1 }, GoogleEarthEnterpriseMapsProvider); + testImageryAsset({ type: 'MAPBOX', url: url, mapId: 1 }, MapboxImageryProvider); + testImageryAsset({ type: 'SINGLE_TILE', url: url }, SingleTileImageryProvider); + testImageryAsset({ type: 'TMS', url: url }, UrlTemplateImageryProvider); + testImageryAsset({ type: 'URL_TEMPLATE', url: url }, UrlTemplateImageryProvider); + testImageryAsset({ type: 'WMS', url: url, layers: [] }, WebMapServiceImageryProvider); + testImageryAsset({ type: 'WMTS', url: url, layer: '', style: '', tileMatrixSetID: 1 }, WebMapTileServiceImageryProvider); + }); + + it('createImageryProvider throws with unknown asset type', function() { + endpoint.type = 'ADSASDS'; + var endpointResource = new Resource({ url: 'https://api.test.invalid' }); + var resource = CesiumIon._CesiumIonResource.create(endpoint, endpointResource); + expect(function() { resource.createImageryProvider(); }).toThrowRuntimeError(); + }); + }); + + describe('retryCallback', function() { + var endpointResource; + var resource; + var retryCallback; + + beforeEach(function() { + endpointResource = new Resource({ url: 'https://api.test.invalid', access_token: 'not_the_token' }); + resource = CesiumIon._CesiumIonResource.create(endpoint, endpointResource); + retryCallback = CesiumIon._createRetryCallback(endpoint, endpointResource, resource); + }); + + it('returns false when error is undefined', function() { + return retryCallback(resource, undefined).then(function(result) { + expect(result).toBe(false); + }); + }); + + it('returns false when error is non-401', function() { + var error = new RequestErrorEvent(404); + return retryCallback(resource, error).then(function(result) { + expect(result).toBe(false); + }); + }); + + it('returns false when error is event with non-Image target', function() { + var event = { target: {} }; + return retryCallback(resource, event).then(function(result) { + expect(result).toBe(false); + }); + }); + + function testCallback(resource, event) { + var deferred = when.defer(); + spyOn(CesiumIon, '_loadJson').and.returnValue(deferred.promise); + + var newEndpoint = { + type: '3DTILES', + url: 'https://assets.cesium.com/' + assetId, + accessToken: 'not_not_really_a_refresh_token' + }; + + var promise = retryCallback(resource, event); + var resultPromise = promise.then(function(result) { + expect(resource.queryParameters.access_token).toEqual(newEndpoint.accessToken); + expect(result).toBe(true); + }); + + expect(CesiumIon._loadJson).toHaveBeenCalledWith(endpointResource); + + //A second retry should re-use the same pending promise + var promise2 = retryCallback(resource, event); + expect(promise._pendingPromise).toBe(promise2._pendingPromise); + + deferred.resolve(newEndpoint); + + return resultPromise; + } + + it('works when error is a 401', function() { + var error = new RequestErrorEvent(401); + return testCallback(resource, error); + }); + + it('works when error is event with Image target', function() { + var event = { target: new Image() }; + return testCallback(resource, event); + }); + }); +});