8
0
Maciej Gołaszewski 8 лет назад
Сommit
bd4dbd6883

+ 12 - 0
packages/ckeditor-cloud-services-core/.eslintrc.js

@@ -0,0 +1,12 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* eslint-env node */
+
+'use strict';
+
+module.exports = {
+	extends: 'ckeditor5'
+};

+ 4 - 0
packages/ckeditor-cloud-services-core/.gitignore

@@ -0,0 +1,4 @@
+node_modules
+coverage
+
+package-lock.json

+ 18 - 0
packages/ckeditor-cloud-services-core/.travis.yml

@@ -0,0 +1,18 @@
+sudo: required
+dist: trusty
+addons:
+    apt:
+        sources:
+            - google-chrome
+        packages:
+            - google-chrome-stable
+language: node_js
+node_js:
+    - "6"
+before_install:
+    - export DISPLAY=:99.0
+    - sh -e /etc/init.d/xvfb start
+install:
+    - npm install
+script:
+    - npm run lint && npm test && cat ./coverage/text.txt

+ 23 - 0
packages/ckeditor-cloud-services-core/LICENSE.md

@@ -0,0 +1,23 @@
+Software License Agreement
+==========================
+
+**CKEditor Cloud Services Core** – https://github.com/ckeditor/ckeditor-cloudservices-core <br>
+Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved.
+
+Licensed under the terms of any of the following licenses at your choice:
+
+* [GNU General Public License Version 2 or later (the "GPL")](http://www.gnu.org/licenses/gpl.html)
+* [GNU Lesser General Public License Version 2.1 or later (the "LGPL")](http://www.gnu.org/licenses/lgpl.html)
+* [Mozilla Public License Version 1.1 or later (the "MPL")](http://www.mozilla.org/MPL/MPL-1.1.html)
+
+You are not required to, but if you want to explicitly declare the license you have chosen to be bound to when using, reproducing, modifying and distributing this software, just include a text file titled "legal.txt" in your version of this software, indicating your license choice. In any case, your choice will not restrict any recipient of your version of this software to use, reproduce, modify and distribute this software under any of the above licenses.
+
+Sources of Intellectual Property Included in CKEditor Cloud Services Core
+-------------------------------------------------------------------------
+
+Where not otherwise indicated, all CKEditor Cloud Services Core content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor Cloud Services Core will incorporate work done by developers outside of CKSource with their express permission.
+
+Trademarks
+----------
+
+**CKEditor** is a trademark of [CKSource](http://cksource.com) Frederico Knabben. All other brand and product names are trademarks, registered trademarks or service marks of their respective holders.

+ 4 - 0
packages/ckeditor-cloud-services-core/README.md

@@ -0,0 +1,4 @@
+# CKEditor Cloud Services Core
+
+CKEditor Cloud Services Core API:
+* Upload Gateway - API for file uploads to CKEditor Cloud Services. 

+ 120 - 0
packages/ckeditor-cloud-services-core/karma.conf.js

@@ -0,0 +1,120 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* eslint-env node */
+
+'use strict';
+
+const path = require( 'path' );
+
+const webpack = require( 'webpack' );
+
+module.exports = function( config ) {
+	config.set( {
+		// base path that will be used to resolve all patterns (eg. files, exclude)
+		basePath: '',
+
+		// frameworks to use. Available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+		frameworks: [ 'mocha', 'chai', 'sinon' ],
+
+		// list of files / patterns to load in the browser
+		files: [
+			'tests/**/*.js'
+		],
+
+		// list of files to exclude
+		exclude: [
+			'tests/**/@(_utils|_assets)/**'
+		],
+
+		// preprocess matching files before serving them to the browser
+		// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+		preprocessors: {
+			'tests/**/*.js': [ 'webpack', 'sourcemap' ],
+			'src/**/*.js': [ 'webpack', 'sourcemap' ]
+		},
+
+		webpack: {
+			resolve: {
+				modules: [
+					path.join( __dirname, 'src' ),
+					'node_modules'
+				]
+			},
+
+			module: {
+				loaders: [
+					{
+						test: /\.js$/,
+						exclude: /(node_modules\/((?!ckeditor)[a-z-]+))/,
+						loader: 'babel-loader',
+						enforce: 'pre',
+						query: {
+							plugins: [
+								'transform-es2015-modules-commonjs',
+								[
+									'istanbul',
+									{ 'exclude': [ 'tests/**/*.js', 'node_modules/**' ] }
+								]
+							]
+						}
+					}
+				]
+			},
+
+			plugins: [
+				new webpack.DefinePlugin( {
+					VERSION: JSON.stringify( require( './package.json' ).version )
+				} )
+			],
+
+			devtool: 'inline-source-map'
+		},
+
+		webpackMiddleware: {
+			noInfo: true,
+			stats: 'errors-only'
+		},
+
+		// test results reporter to use
+		// possible values: 'dots', 'progress'
+		// available reporters: https://npmjs.org/browse/keyword/karma-reporter
+		reporters: [ 'mocha', 'coverage' ],
+
+		coverageReporter: {
+			dir: 'coverage/',
+			reporters: [
+				{ type: 'html', subdir: 'report-html' },
+				{ type: 'lcov', subdir: 'report-lcov' },
+				{ type: 'text', subdir: '.', file: 'text.txt' }
+			]
+		},
+
+		// web server port
+		port: 9876,
+
+		// enable / disable colors in the output (reporters and logs)
+		colors: true,
+
+		// level of logging
+		// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+		logLevel: config.LOG_INFO,
+
+		// enable / disable watching file and executing tests whenever any file changes
+		autoWatch: true,
+
+		// start these browsers
+		// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+		browsers: [ 'Chrome' ],
+
+		// Continuous Integration mode
+		// if true, Karma captures browsers, runs the tests and exits
+		singleRun: false,
+
+		// Concurrency level
+		// how many browser should be started simultaneous
+		concurrency: Infinity
+	} );
+};

+ 43 - 0
packages/ckeditor-cloud-services-core/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "@ckeditor/ckeditor-cloudservices-core",
+  "version": "0.1.0",
+  "description": "CKEditor Cloud Services Core API",
+  "license": "(GPL-2.0 OR LGPL-2.1 OR MPL-1.1)",
+  "repository": {
+    "type": "git",
+    "url": "git@github.com:ckeditor/ckeditor-cloudservices-core.git"
+  },
+  "author": "CKSource (http://cksource.com/)",
+  "bugs": "https://github.com/ckeditor/ckeditor-cloudservices-core/issues",
+  "url": "https://github.com/ckeditor/ckeditor-cloudservices-core/issues",
+  "homepage": "https://github.com/ckeditor/ckeditor-cloudservices-core#readme",
+  "scripts": {
+    "test": "karma start --single-run",
+    "lint": "eslint src tests"
+  },
+  "dependencies": {
+    "@ckeditor/ckeditor5-utils": "^0.10.0"
+  },
+  "devDependencies": {
+    "babel-core": "^6.26.0",
+    "babel-loader": "^7.1.2",
+    "babel-plugin-istanbul": "^4.1.5",
+    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
+    "chai": "^4.1.2",
+    "eslint": "^4.7.1",
+    "eslint-config-ckeditor5": "^1.0.6",
+    "karma": "^1.7.1",
+    "karma-chai": "^0.1.0",
+    "karma-chrome-launcher": "^2.2.0",
+    "karma-coverage": "^1.1.1",
+    "karma-firefox-launcher": "^1.0.1",
+    "karma-mocha": "^1.3.0",
+    "karma-mocha-reporter": "^2.2.4",
+    "karma-sinon": "^1.0.5",
+    "karma-sourcemap-loader": "^0.3.7",
+    "karma-webpack": "^2.0.4",
+    "mocha": "^3.5.3",
+    "sinon": "^3.3.0",
+    "webpack": "^3.6.0"
+  }
+}

+ 257 - 0
packages/ckeditor-cloud-services-core/src/uploadgateway/fileuploader.js

@@ -0,0 +1,257 @@
+/**
+ * @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 EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+
+const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/;
+
+/**
+ * FileUploader class used to upload single file.
+ */
+class FileUploader {
+	/**
+	 * Creates `FileUploader` instance.
+	 *
+	 * @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
+	 * @param {String} token Token used for authentication.
+	 * @param {String} apiAddress API address.
+	 */
+	constructor( fileOrData, token, apiAddress ) {
+		if ( !fileOrData ) {
+			throw new Error( 'File must be provided' );
+		}
+
+		if ( !token ) {
+			throw new Error( 'Token must be provided' );
+		}
+
+		if ( !apiAddress ) {
+			throw new Error( 'Api address must be provided' );
+		}
+
+		/**
+		 * A file that is being uploaded.
+		 *
+		 * @type {Blob}
+		 */
+		this.file = _isBase64( fileOrData ) ? _base64ToBlob( fileOrData ) : fileOrData;
+
+		/**
+		 * CKEditor Cloud Services access token.
+		 *
+		 * @type {String}
+		 * @private
+		 */
+		this._token = token;
+
+		/**
+		 * CKEditor Cloud Services API address.
+		 *
+		 * @type {String}
+		 * @private
+		 */
+		this._apiAddress = apiAddress;
+	}
+
+	/**
+	 * Registers callback on `progress` event.
+	 *
+	 * @chainable
+	 * @param {Function} callback
+	 * @returns {FileUploader}
+	 */
+	onProgress( callback ) {
+		this.on( 'progress', ( event, data ) => callback( data ) );
+
+		return this;
+	}
+
+	/**
+	 * Registers callback on `error` event. Event is called once when error occurs.
+	 *
+	 * @chainable
+	 * @param {Function} callback
+	 * @returns {FileUploader}
+	 */
+	onError( callback ) {
+		this.once( 'error', ( event, data ) => callback( data ) );
+
+		return this;
+	}
+
+	/**
+	 * Aborts upload process.
+	 */
+	abort() {
+		this.xhr.abort();
+	}
+
+	/**
+	 * Sends XHR request to API.
+	 *
+	 * @chainable
+	 * @returns {Promise.<Object>}
+	 */
+	send() {
+		this._prepareRequest();
+		this._attachXHRListeners();
+
+		return this._sendRequest();
+	}
+
+	/**
+	 * Prepares XHR request.
+	 *
+	 * @private
+	 */
+	_prepareRequest() {
+		const xhr = new XMLHttpRequest();
+
+		xhr.open( 'POST', this._apiAddress );
+		xhr.setRequestHeader( 'Authorization', this._token );
+		xhr.responseType = 'json';
+
+		this.xhr = xhr;
+	}
+
+	/**
+	 * Attaches listeners to the XHR.
+	 *
+	 * @private
+	 */
+	_attachXHRListeners() {
+		const that = this;
+		const xhr = this.xhr;
+
+		xhr.addEventListener( 'error', onError( 'Network Error' ) );
+		xhr.addEventListener( 'abort', onError( 'Abort' ) );
+
+		/* istanbul ignore else */
+		if ( xhr.upload ) {
+			xhr.upload.addEventListener( 'progress', event => {
+				if ( event.lengthComputable ) {
+					this.fire( 'progress', {
+						total: event.total,
+						uploaded: event.loaded
+					} );
+				}
+			} );
+		}
+
+		xhr.addEventListener( 'load', () => {
+			const statusCode = xhr.status;
+			const xhrResponse = xhr.response;
+
+			if ( statusCode < 200 || statusCode > 299 ) {
+				return this.fire( 'error', xhrResponse.message || xhrResponse.error );
+			}
+		} );
+
+		function onError( message ) {
+			return () => that.fire( 'error', message );
+		}
+	}
+
+	/**
+	 * Sends XHR request.
+	 *
+	 * @private
+	 */
+	_sendRequest() {
+		const formData = new FormData();
+		const xhr = this.xhr;
+
+		formData.append( 'file', this.file );
+
+		return new Promise( ( resolve, reject ) => {
+			xhr.addEventListener( 'load', () => {
+				const statusCode = xhr.status;
+				const xhrResponse = xhr.response;
+
+				if ( statusCode < 200 || statusCode > 299 ) {
+					return reject( xhrResponse.message || xhrResponse.error );
+				}
+
+				return resolve( xhrResponse );
+			} );
+
+			xhr.addEventListener( 'error', () => reject( 'Network Error' ) );
+			xhr.addEventListener( 'abort', () => reject( 'Abort' ) );
+
+			xhr.send( formData );
+		} );
+	}
+
+	/**
+	 * Fired when error occurs.
+	 *
+	 * @event error
+	 * @param {String} error Error message
+	 */
+
+	/**
+	 * Fired on upload progress.
+	 *
+	 * @event progress
+	 * @param {Object} status Total and uploaded status
+	 */
+}
+
+mix( FileUploader, EmitterMixin );
+
+/**
+ * Transforms Base64 string data into file.
+ *
+ * @param {String} base64 String data.
+ * @param {Number} [sliceSize=512]
+ * @returns {Blob}
+ * @private
+ */
+function _base64ToBlob( base64, sliceSize = 512 ) {
+	try {
+		const contentType = base64.match( BASE64_HEADER_REG_EXP )[ 1 ];
+		const base64Data = atob( base64.replace( BASE64_HEADER_REG_EXP, '' ) );
+
+		const byteArrays = [];
+
+		for ( let offset = 0; offset < base64Data.length; offset += sliceSize ) {
+			const slice = base64Data.slice( offset, offset + sliceSize );
+			const byteNumbers = new Array( slice.length );
+
+			for ( let i = 0; i < slice.length; i++ ) {
+				byteNumbers[ i ] = slice.charCodeAt( i );
+			}
+
+			byteArrays.push( new Uint8Array( byteNumbers ) );
+		}
+
+		return new Blob( byteArrays, { type: contentType } );
+	} catch ( error ) {
+		throw new Error( 'Problem with decoding Base64 image data.' );
+	}
+}
+
+/**
+ * Checks that string is Base64.
+ *
+ * @param {String} string
+ * @returns {Boolean}
+ * @private
+ */
+function _isBase64( string ) {
+	if ( typeof string !== 'string' ) {
+		return false;
+	}
+
+	const match = string.match( BASE64_HEADER_REG_EXP );
+	return !!( match && match.length );
+}
+
+export default FileUploader;

+ 72 - 0
packages/ckeditor-cloud-services-core/src/uploadgateway/uploadgateway.js

@@ -0,0 +1,72 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import FileUploader from './fileuploader';
+
+/**
+ * UploadGateway abstracts file uploads to CKEditor Cloud Services.
+ */
+export default class UploadGateway {
+	/**
+	 * Creates `UploadGateway` instance.
+	 *
+	 * @param {String} token Token used for authentication.
+	 * @param {String} apiAddress API address.
+	 */
+	constructor( token, apiAddress ) {
+		if ( !token ) {
+			throw new Error( 'Token must be provided' );
+		}
+
+		if ( !apiAddress ) {
+			throw new Error( 'Api address must be provided' );
+		}
+
+		/**
+		 * CKEditor Cloud Services access token.
+		 *
+		 * @type {String}
+		 * @private
+		 */
+		this._token = token;
+
+		/**
+		 * CKEditor Cloud Services API address.
+		 *
+		 * @type {String}
+		 * @private
+		 */
+		this._apiAddress = apiAddress;
+	}
+
+	/**
+	 * Creates a {@link FileUploader} instance that wraps file upload process.
+	 * The file is being sent at a time when the method {@link FileUploader#then then} is called
+	 * or when {@link FileUploader#send send} method is called.
+	 *
+	 *     new UploadGateway( 'JSON_WEB_TOKEN_FROM_SERVER', 'https://example.org' )
+	 *        .upload( 'FILE' )
+	 *        .onProgress( ( data ) => console.log( data ) )
+	 *        .send()
+	 *        .then( ( response ) => console.log( response ) );
+	 *
+	 *     // OR
+	 *
+	 *     new UploadGateway( 'JSON_WEB_TOKEN_FROM_SERVER', 'https://example.org' )
+	 *         .upload( 'FILE' )
+	 *         .onProgress( ( data ) => console.log( data ) )
+	 *         .send()
+	 *         .then( ( response ) => console.log( response ) );
+	 *
+	 * @param {Blob/String} fileOrData A blob object or a data string encoded with Base64.
+	 * @returns {FileUploader} Returns `FileUploader` instance.
+	 */
+	upload( fileOrData ) {
+		return new FileUploader( fileOrData, this._token, this._apiAddress );
+	}
+}
+

+ 218 - 0
packages/ckeditor-cloud-services-core/tests/uploadgateway/fileuploader.js

@@ -0,0 +1,218 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* eslint-env commonjs, browser */
+
+'use strict';
+
+import FileUploader from '../../src/uploadgateway/fileuploader';
+
+const API_ADDRESS = 'https://example.dev';
+const TOKEN = 'token';
+const BASE_64_FILE = 'data:image/gif;base64,R0lGODlhCQAJAPIAAGFhYZXK/1FRUf///' +
+	'9ra2gD/AAAAAAAAACH5BAEAAAUALAAAAAAJAAkAAAMYWFqwru2xERcYJLSNNWNBVimC5wjfaTkJADs=';
+
+describe( 'FileUploader', () => {
+	let fileUploader;
+
+	beforeEach( () => {
+		fileUploader = new FileUploader( BASE_64_FILE, TOKEN, API_ADDRESS );
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should throw error when no fileOrData provided', () => {
+			expect( () => new FileUploader() ).to.throw( 'File must be provided' );
+		} );
+
+		it( 'should throw error when no token provided', () => {
+			expect( () => new FileUploader( 'file' ) ).to.throw( 'Token must be provided' );
+		} );
+
+		it( 'should throw error when no api address provided', () => {
+			expect( () => new FileUploader( 'file', TOKEN ) ).to.throw( 'Api address must be provided' );
+		} );
+
+		it( 'should throw error when wrong Base64 file is provided', () => {
+			expect( () => new FileUploader( 'data:image/gif;base64,R', TOKEN, API_ADDRESS ) )
+				.to.throw( 'Problem with decoding Base64 image data.' );
+		} );
+
+		it( 'should convert base64 to file', done => {
+			const fileReader = new FileReader();
+
+			fileReader.readAsDataURL( fileUploader.file );
+			fileReader.onloadend = () => {
+				expect( fileReader.result ).to.be.equal( BASE_64_FILE );
+
+				done();
+			};
+		} );
+
+		it( 'should set `file` field', () => {
+			const file = new File( [], 'test.jpg' );
+
+			const fileUploader = new FileUploader( file, TOKEN, API_ADDRESS );
+
+			expect( fileUploader.file.name ).to.be.equal( 'test.jpg' );
+		} );
+	} );
+
+	describe( 'onProgress()', () => {
+		it( 'should register callback for `progress` event', done => {
+			fileUploader.onProgress( data => {
+				expect( data ).to.be.deep.equal( { total: 12345, loaded: 123 } );
+				done();
+			} );
+
+			fileUploader.fire( 'progress', { total: 12345, loaded: 123 } );
+		} );
+	} );
+
+	describe( 'onError()', () => {
+		it( 'should register callback for `error` event', done => {
+			fileUploader.onError( data => {
+				expect( data ).to.be.instanceOf( Error );
+				expect( data.message ).to.be.equal( 'TEST' );
+
+				done();
+			} );
+
+			fileUploader.fire( 'error', new Error( 'TEST' ) );
+		} );
+
+		it( 'should call registered callback for `error` event once', () => {
+			const spy = sinon.spy();
+			fileUploader.onError( spy );
+
+			fileUploader.fire( 'error', new Error( 'TEST' ) );
+			fileUploader.fire( 'error', new Error( 'TEST' ) );
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+
+	describe( 'send()', () => {
+		let request;
+
+		beforeEach( () => {
+			global.xhr = sinon.useFakeXMLHttpRequest();
+
+			global.xhr.onCreate = xhr => {
+				request = xhr;
+			};
+		} );
+
+		afterEach( () => global.xhr.restore() );
+
+		it( 'should sent request with correct data (url, method, type, headers)', done => {
+			fileUploader
+				.send()
+				.then( () => {
+					expect( request.url ).to.be.equal( API_ADDRESS );
+					expect( request.method ).to.be.equal( 'POST' );
+					expect( request.responseType ).to.be.equal( 'json' );
+					expect( request.requestHeaders ).to.be.deep.equal( { Authorization: 'token' } );
+
+					done();
+				} );
+
+			request.respond( 200, { 'Content-Type': 'application/json' },
+				JSON.stringify( { 'default': 'https://test.dev' } )
+			);
+		} );
+
+		it( 'should fire `error` event with error message when response is failed', done => {
+			fileUploader
+				.onError( error => {
+					expect( error ).to.be.equal( 'Message' );
+
+					done();
+				} )
+				.send();
+
+			request.respond( 400, { 'Content-Type': 'application/json' },
+				JSON.stringify( {
+					error: 'Error',
+					message: 'Message'
+				} )
+			);
+		} );
+
+		it( 'should fire `error` event with error when response is failed', done => {
+			fileUploader
+				.onError( error => {
+					expect( error ).to.be.equal( 'Error' );
+
+					done();
+				} )
+				.send();
+
+			request.respond( 400, { 'Content-Type': 'application/json' },
+				JSON.stringify( {
+					error: 'Error'
+				} )
+			);
+		} );
+
+		it( 'should fire `error` event when response is aborted', done => {
+			fileUploader
+				.onError( error => {
+					expect( error ).to.be.equal( 'Abort' );
+
+					done();
+				} )
+				.send();
+
+			request.abort();
+		} );
+
+		it( 'should fire `error` event when network error occurs', done => {
+			fileUploader
+				.onError( error => {
+					expect( error ).to.be.equal( 'Network Error' );
+
+					done();
+				} )
+				.send();
+
+			request.error();
+		} );
+
+		it( 'should fire `progress` event', done => {
+			fileUploader
+				.onProgress( data => {
+					expect( data.total ).to.be.equal( 1 );
+					expect( data.uploaded ).to.be.equal( 10 );
+
+					done();
+				} )
+				.send();
+
+			request.uploadProgress( { total: 1, loaded: 10 } );
+			request.respond( 200 );
+		} );
+	} );
+
+	describe( 'abort()', () => {
+		let request;
+
+		beforeEach( () => {
+			global.xhr = sinon.useFakeXMLHttpRequest();
+
+			global.xhr.onCreate = xhr => {
+				request = xhr;
+			};
+		} );
+
+		afterEach( () => global.xhr.restore() );
+
+		it( 'should abort xhr request', () => {
+			fileUploader.send();
+			fileUploader.abort();
+
+			expect( request.aborted ).to.be.true;
+		} );
+	} );
+} );

+ 29 - 0
packages/ckeditor-cloud-services-core/tests/uploadgateway/uploadgateway.js

@@ -0,0 +1,29 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import FileUploader from '../../src/uploadgateway/fileuploader';
+import UploadGateway from '../../src/uploadgateway/uploadgateway';
+
+describe( 'UploadGateway', () => {
+	describe( 'constructor()', () => {
+		it( 'should throw error when no token provided', () => {
+			expect( () => new UploadGateway( undefined, 'test' ) ).to.throw( 'Token must be provided' );
+		} );
+
+		it( 'should throw error when no apiAddress provided', () => {
+			expect( () => new UploadGateway( 'token' ) ).to.throw( 'Api address must be provided' );
+		} );
+	} );
+
+	describe( 'upload()', () => {
+		it( 'should return `FileUploader` instance', () => {
+			const uploader = new UploadGateway( 'token', 'test' );
+
+			expect( uploader.upload( 'file' ) ).to.be.instanceOf( FileUploader );
+		} );
+	} );
+} );