Explorar o código

Merge pull request #17 from ckeditor/t/16

Feature: Allowed custom refreshToken callback in the Token constructor. Closes #16.
Piotr Jasiun %!s(int64=7) %!d(string=hai) anos
pai
achega
e142567b5f

+ 60 - 37
packages/ckeditor-cloud-services-core/src/token/token.js

@@ -23,15 +23,16 @@ class Token {
 	 * Creates `Token` instance.
 	 * Method `init` should be called after using the constructor or use `create` method instead.
 	 *
-	 * @param {String} tokenUrl Endpoint address to download the token.
+	 * @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
+	 * value is a function it has to match the {@link ~refreshToken} interface.
 	 * @param {Object} options
 	 * @param {String} [options.initValue] Initial value of the token.
 	 * @param {Number} [options.refreshInterval=3600000] Delay between refreshes. Default 1 hour.
 	 * @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
 	 */
-	constructor( tokenUrl, options = DEFAULT_OPTIONS ) {
-		if ( !tokenUrl ) {
-			throw new Error( '`tokenUrl` must be provided' );
+	constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
+		if ( !tokenUrlOrRefreshToken ) {
+			throw new Error( 'A `tokenUrl` must be provided as the first constructor argument.' );
 		}
 
 		/**
@@ -40,18 +41,23 @@ class Token {
 		 * `create` method creates token with initialized value from url.
 		 *
 		 * @name value
-		 * @type {String}
+		 * @member {String} #value
 		 * @observable
 		 * @readonly
-		 * @memberOf Token#
 		 */
 		this.set( 'value', options.initValue );
 
 		/**
-		 * @type {String}
+		 * Base refreshing function.
+		 *
 		 * @private
+		 * @member {String|Function} #_refresh
 		 */
-		this._tokenUrl = tokenUrl;
+		if ( typeof tokenUrlOrRefreshToken === 'function' ) {
+			this._refresh = tokenUrlOrRefreshToken;
+		} else {
+			this._refresh = () => defaultRefreshToken( tokenUrlOrRefreshToken );
+		}
 
 		/**
 		 * @type {Object}
@@ -84,35 +90,14 @@ class Token {
 	}
 
 	/**
-	 * Gets the new token.
+	 * Refresh token method. Useful in a method form as it can be override in tests.
 	 *
 	 * @protected
-	 * @returns {Promise.<Token>}
 	 */
 	_refreshToken() {
-		return new Promise( ( resolve, reject ) => {
-			const xhr = new XMLHttpRequest();
-
-			xhr.open( 'GET', this._tokenUrl );
-
-			xhr.addEventListener( 'load', () => {
-				const statusCode = xhr.status;
-				const xhrResponse = xhr.response;
-
-				if ( statusCode < 200 || statusCode > 299 ) {
-					return reject( 'Cannot download new token!' );
-				}
-
-				this.set( 'value', xhrResponse );
-
-				return resolve( this );
-			} );
-
-			xhr.addEventListener( 'error', () => reject( 'Network Error' ) );
-			xhr.addEventListener( 'abort', () => reject( 'Abort' ) );
-
-			xhr.send();
-		} );
+		return this._refresh()
+				.then( value => this.set( 'value', value ) )
+				.then( () => this );
 	}
 
 	/**
@@ -121,7 +106,7 @@ class Token {
 	 * @protected
 	 */
 	_startRefreshing() {
-		this._refreshInterval = setInterval( this._refreshToken.bind( this ), this._options.refreshInterval );
+		this._refreshInterval = setInterval( this._refreshToken, this._options.refreshInterval );
 	}
 
 	/**
@@ -136,15 +121,16 @@ class Token {
 	/**
 	 * Creates a initialized {@link Token} instance.
 	 *
-	 * @param {String} tokenUrl Endpoint address to download the token.
+	 * @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
+	 * value is a function it has to match the {@link ~refreshToken} interface.
 	 * @param {Object} options
 	 * @param {String} [options.initValue] Initial value of the token.
 	 * @param {Number} [options.refreshInterval=3600000] Delay between refreshes. Default 1 hour.
 	 * @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically.
 	 * @returns {Promise.<Token>}
 	 */
-	static create( tokenUrl, options = DEFAULT_OPTIONS ) {
-		const token = new Token( tokenUrl, options );
+	static create( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) {
+		const token = new Token( tokenUrlOrRefreshToken, options );
 
 		return token.init();
 	}
@@ -152,4 +138,41 @@ class Token {
 
 mix( Token, ObservableMixin );
 
+/**
+ * This function is called in a defined interval by the {@link ~Token} class.
+ * It should return a promise, which resolves with the new token value.
+ * If any error occurs it should return a rejected promise with an error message.
+ *
+ * @function refreshToken
+ * @returns {Promise.<String>}
+ */
+
+/**
+ * @private
+ * @param {String} tokenUrl
+ */
+function defaultRefreshToken( tokenUrl ) {
+	return new Promise( ( resolve, reject ) => {
+		const xhr = new XMLHttpRequest();
+
+		xhr.open( 'GET', tokenUrl );
+
+		xhr.addEventListener( 'load', () => {
+			const statusCode = xhr.status;
+			const xhrResponse = xhr.response;
+
+			if ( statusCode < 200 || statusCode > 299 ) {
+				return reject( 'Cannot download new token!' );
+			}
+
+			return resolve( xhrResponse );
+		} );
+
+		xhr.addEventListener( 'error', () => reject( 'Network Error' ) );
+		xhr.addEventListener( 'abort', () => reject( 'Abort' ) );
+
+		xhr.send();
+	} );
+};
+
 export default Token;

+ 40 - 4
packages/ckeditor-cloud-services-core/tests/token/token.js

@@ -26,7 +26,9 @@ describe( 'Token', () => {
 
 	describe( 'constructor()', () => {
 		it( 'should throw error when no tokenUrl provided', () => {
-			expect( () => new Token() ).to.throw( '`tokenUrl` must be provided' );
+			expect( () => new Token() ).to.throw(
+				'A `tokenUrl` must be provided as the first constructor argument.'
+			);
 		} );
 
 		it( 'should set a init token value', () => {
@@ -48,6 +50,13 @@ describe( 'Token', () => {
 
 			requests[ 0 ].respond( 200, '', 'token-value' );
 		} );
+
+		it( 'should accept the callback in the constructor', () => {
+			expect( () => {
+				// eslint-disable-next-line
+				const token = new Token( () => Promise.resolve( 'token' ) );
+			} ).to.not.throw();
+		} );
 	} );
 
 	describe( 'init()', () => {
@@ -64,6 +73,15 @@ describe( 'Token', () => {
 			requests[ 0 ].respond( 200, '', 'token-value' );
 		} );
 
+		it( 'should get a token from the refreshToken function when is provided', () => {
+			const token = new Token( () => Promise.resolve( 'token-value' ), { autoRefresh: false } );
+
+			return token.init()
+				.then( () => {
+					expect( token.value ).to.equal( 'token-value' );
+				} );
+		} );
+
 		it( 'should start token refresh every 1 hour', done => {
 			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );
 
@@ -100,7 +118,16 @@ describe( 'Token', () => {
 			requests[ 0 ].respond( 200, '', 'token-value' );
 		} );
 
-		it( 'should throw error when cannot download new token ', done => {
+		it( 'should get a token from the specified callback function', () => {
+			const token = new Token( () => Promise.resolve( 'token-value' ), { initValue: 'initValue', autoRefresh: false } );
+
+			return token._refreshToken()
+				.then( newToken => {
+					expect( newToken.value ).to.equal( 'token-value' );
+				} );
+		} );
+
+		it( 'should throw an error when cannot download new token', done => {
 			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
 
 			token._refreshToken()
@@ -113,7 +140,7 @@ describe( 'Token', () => {
 			requests[ 0 ].respond( 401 );
 		} );
 
-		it( 'should throw error when response is aborted', done => {
+		it( 'should throw an error when the response is aborted', done => {
 			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
 
 			token._refreshToken()
@@ -126,7 +153,7 @@ describe( 'Token', () => {
 			requests[ 0 ].abort();
 		} );
 
-		it( 'should throw error event when network error occurs', done => {
+		it( 'should throw an error when network error occurs', done => {
 			const token = new Token( 'http://token-endpoint', { initValue: 'initValue', autoRefresh: false } );
 
 			token._refreshToken()
@@ -138,6 +165,15 @@ describe( 'Token', () => {
 
 			requests[ 0 ].error();
 		} );
+
+		it( 'should throw an error when the callback throws error', () => {
+			const token = new Token( () => Promise.reject( 'Custom error occurred' ), { initValue: 'initValue', autoRefresh: false } );
+
+			token._refreshToken()
+				.catch( error => {
+					expect( error ).to.equal( 'Custom error occurred' );
+				} );
+		} );
 	} );
 
 	describe( '_startRefreshing()', () => {