浏览代码

Feature: Added token with refresh mechanism

Bartosz Czerwonka 8 年之前
父节点
当前提交
6a59c9a5cc

+ 117 - 0
packages/ckeditor-cloud-services-core/src/token/token.js

@@ -0,0 +1,117 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* eslint-env browser */
+
+'use strict';
+
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
+
+const DEFAULT_OPTIONS = { refreshIntervalTime: 3600000, startAutoRefresh: true };
+
+/**
+ * Class representing the token used for communication with CKEditor Cloud Services.
+ * Value of the token is retrieving from the specified URL and is refreshed every 1 hour by default.
+ *
+ * @mixes ObservableMixin
+ */
+class Token {
+	/**
+	 * Creates `Token` instance.
+	 *
+	 * @param {String} tokenUrl Endpoint address to download the token.
+	 * @param {Object} options
+	 * @param {Number} [options.refreshIntervalTime=3600000] Delay between refreshes. Default 1 hour.
+	 * @param {Boolean} [options.autoStart=true] Specifies whether to start the refresh automatically.
+	 */
+	constructor( tokenUrl, options = DEFAULT_OPTIONS ) {
+		/**
+		 * Value of the token.
+		 *
+		 * @readonly
+		 * @observable
+		 * @member {String} #value
+		 */
+		this.set( 'value', '' );
+
+		/**
+		 * @type {String}
+		 * @private
+		 */
+		this._tokenUrl = tokenUrl;
+
+		/**
+		 * @type {Object}
+		 * @private
+		 */
+		this._options = Object.assign( {}, DEFAULT_OPTIONS, options );
+
+		this._init();
+	}
+
+	/**
+	 * Gets the new token.
+	 *
+	 * @returns {Promise}
+	 */
+	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( xhrResponse );
+			} );
+
+			xhr.addEventListener( 'error', () => reject( 'Network Error' ) );
+			xhr.addEventListener( 'abort', () => reject( 'Abort' ) );
+
+			xhr.send();
+		} );
+	}
+
+	/**
+	 * Starts value refreshing every `refreshInterval` time.
+	 */
+	startRefreshing() {
+		this._refreshInterval = setInterval( this.refreshToken.bind( this ), this._options.refreshIntervalTime );
+	}
+
+	/**
+	 * Stops value refreshing.
+	 */
+	stopRefreshing() {
+		clearInterval( this._refreshInterval );
+	}
+
+	/**
+	 * Initializes the value of the token.
+	 *
+	 * @private
+	 */
+	_init() {
+		this.refreshToken()
+			.then( () => {
+				if ( this._options.startAutoRefresh ) {
+					this.startRefreshing();
+				}
+			} );
+	}
+}
+
+mix( Token, ObservableMixin );
+
+export default Token;

+ 189 - 0
packages/ckeditor-cloud-services-core/tests/token/token.js

@@ -0,0 +1,189 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* eslint-env commonjs, browser */
+
+'use strict';
+
+import Token from './../../src/token/token';
+
+describe( 'Token', () => {
+	let requests;
+
+	beforeEach( () => {
+		requests = [];
+
+		global.xhr = sinon.useFakeXMLHttpRequest();
+
+		global.xhr.onCreate = xhr => {
+			requests.push( xhr );
+		};
+	} );
+
+	afterEach( () => global.xhr.restore() );
+
+	describe( 'constructor()', () => {
+		it( 'should set a token value', () => {
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			requests[ 0 ].respond( 200, '', 'token-value' );
+
+			expect( token.value ).to.equal( 'token-value' );
+		} );
+
+		it( 'should fire `change:value` event if the value of the token has changed', done => {
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			token.on( 'change:value', ( event, name, newValue ) => {
+				expect( newValue ).to.equal( 'token-value' );
+
+				done();
+			} );
+
+			requests[ 0 ].respond( 200, '', 'token-value' );
+		} );
+
+		it( 'should start token refresh every 1 hour', done => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );
+
+			const token = new Token( 'http://token-endpoint' );
+
+			requests[ 0 ].respond( 200, '', 'token-value' );
+
+			// waiting for the first request
+			setTimeout( () => {
+				expect( token.value ).to.equal( 'token-value' );
+
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+
+				expect( requests.length ).to.equal( 6 );
+
+				clock.restore();
+
+				done();
+			}, 10 );
+		} );
+	} );
+
+	describe( 'refreshToken()', () => {
+		it( 'should get a token from the specified address', done => {
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			token.refreshToken()
+				.then( newValue => {
+					expect( newValue ).to.equal( 'token-value' );
+					expect( token.value ).to.equal( newValue );
+
+					token.stopRefreshing();
+
+					done();
+				} );
+
+			requests[ 1 ].respond( 200, '', 'token-value' );
+		} );
+
+		it( 'should throw error when cannot download new token ', done => {
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			token.refreshToken()
+				.catch( error => {
+					expect( error ).to.equal( 'Cannot download new token!' );
+
+					done();
+				} );
+
+			requests[ 1 ].respond( 401 );
+		} );
+
+		it( 'should throw error when response is aborted', done => {
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			token.refreshToken()
+				.catch( error => {
+					expect( error ).to.equal( 'Abort' );
+
+					done();
+				} );
+
+			requests[ 1 ].abort();
+		} );
+
+		it( 'should throw error event when network error occurs', done => {
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			token.refreshToken()
+				.catch( error => {
+					expect( error ).to.equal( 'Network Error' );
+
+					done();
+				} );
+
+			requests[ 1 ].error();
+		} );
+	} );
+
+	describe( 'startRefreshing()', () => {
+		it( 'should start refreshing', done => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval' ] } );
+
+			const token = new Token( 'http://token-endpoint', { startAutoRefresh: false } );
+
+			token.startRefreshing();
+
+			requests[ 0 ].respond( 200, '', 'token-value' );
+
+			// waiting for the first request
+			setTimeout( () => {
+				expect( token.value ).to.equal( 'token-value' );
+
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+
+				expect( requests.length ).to.equal( 6 );
+
+				clock.restore();
+
+				done();
+			}, 10 );
+		} );
+	} );
+
+	describe( 'stopRefreshing()', () => {
+		it( 'should stop refreshing', done => {
+			const clock = sinon.useFakeTimers( { toFake: [ 'setInterval', 'clearInterval' ] } );
+
+			const token = new Token( 'http://token-endpoint' );
+
+			requests[ 0 ].respond( 200, '', 'token-value' );
+
+			// waiting for the first request
+			setTimeout( () => {
+				expect( token.value ).to.equal( 'token-value' );
+
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+
+				token.stopRefreshing();
+
+				clock.tick( 3600000 );
+				clock.tick( 3600000 );
+
+				expect( requests.length ).to.equal( 4 );
+
+				clock.restore();
+
+				done();
+			}, 10 );
+		} );
+	} );
+} );