diff --git a/buildjsdocs.sh b/buildjsdocs.sh
index 90562558f66ac8943148bc07108606ba479f8535..57eefb29fd046e11c82e472089dd87291f23fddd 100755
--- a/buildjsdocs.sh
+++ b/buildjsdocs.sh
@@ -11,7 +11,7 @@ NPM="$(which npm 2>/dev/null)"
 PREFIX="build"
 OUTPUT_DIR="build/jsdocs"
 
-JS_FILES="core/js/*.js apps/*/js/*.js"
+JS_FILES="core/js/*.js core/js/**/*.js apps/*/js/*.js"
 
 if test -z "$NPM"
 then
diff --git a/core/js/files/client.js b/core/js/files/client.js
new file mode 100644
index 0000000000000000000000000000000000000000..9bb7bb999fd3752cf890e20fb72b2f886e1704e3
--- /dev/null
+++ b/core/js/files/client.js
@@ -0,0 +1,673 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global dav */
+
+(function(OC, FileInfo) {
+	/**
+	 * @class OC.Files.Client
+	 * @classdesc Client to access files on the server
+	 *
+	 * @param {Object} options
+	 * @param {String} options.host host name
+	 * @param {int} [options.port] port
+	 * @param {boolean} [options.useHTTPS] whether to use https
+	 * @param {String} [options.root] root path
+	 * @param {String} [options.userName] user name
+	 * @param {String} [options.password] password
+	 *
+	 * @since 8.2
+	 */
+	var Client = function(options) {
+		this._root = options.root;
+		if (this._root.charAt(this._root.length - 1) === '/') {
+			this._root = this._root.substr(0, this._root.length - 1);
+		}
+
+		if (!options.port) {
+			// workaround in case port is null or empty
+			options.port = undefined;
+		}
+		var url = '';
+		var port = '';
+		if (options.useHTTPS) {
+			url += 'https://';
+			if (options.port && options.port !== 443) {
+				port = ':' + options.port;
+			}
+		} else {
+			url += 'http://';
+			if (options.port && options.port !== 80) {
+				port = ':' + options.port;
+			}
+		}
+		var credentials = '';
+		if (options.userName) {
+			credentials += encodeURIComponent(options.userName);
+		}
+		if (options.password) {
+			credentials += ':' + encodeURIComponent(options.password);
+		}
+		if (credentials.length > 0) {
+			url += credentials + '@';
+		}
+
+		url += options.host + port + this._root;
+		this._defaultHeaders = options.defaultHeaders || {'X-Requested-With': 'XMLHttpRequest'};
+		this._baseUrl = url;
+		this._client = new dav.Client({
+			baseUrl: this._baseUrl,
+			xmlNamespaces: {
+				'DAV:': 'd',
+				'http://owncloud.org/ns': 'oc'
+			}
+		});
+		this._client.xhrProvider = _.bind(this._xhrProvider, this);
+	};
+
+	Client.NS_OWNCLOUD = 'http://owncloud.org/ns';
+	Client.NS_DAV = 'DAV:';
+	Client._PROPFIND_PROPERTIES = [
+		/**
+		 * Modified time
+		 */
+		[Client.NS_DAV, 'getlastmodified'],
+		/**
+		 * Etag
+		 */
+		[Client.NS_DAV, 'getetag'],
+		/**
+		 * Mime type
+		 */
+		[Client.NS_DAV, 'getcontenttype'],
+		/**
+		 * Resource type "collection" for folders, empty otherwise
+		 */
+		[Client.NS_DAV, 'resourcetype'],
+		/**
+		 * Compound file id, contains fileid + server instance id
+		 */
+		[Client.NS_OWNCLOUD, 'id'],
+		/**
+		 * Letter-coded permissions
+		 */
+		[Client.NS_OWNCLOUD, 'permissions'],
+		//[Client.NS_OWNCLOUD, 'downloadURL'],
+		/**
+		 * Folder sizes
+		 */
+		[Client.NS_OWNCLOUD, 'size'],
+		/**
+		 * File sizes
+		 */
+		[Client.NS_DAV, 'getcontentlength']
+	];
+
+	/**
+	 * @memberof OC.Files
+	 */
+	Client.prototype = {
+
+		/**
+		 * Root path of the Webdav endpoint
+		 *
+		 * @type string
+		 */
+		_root: null,
+
+		/**
+		 * Client from the library
+		 *
+		 * @type nl.sara.webdav.Client
+		 */
+		_client: null,
+
+		/**
+		 * Returns the configured XHR provider for davclient
+		 * @return {XMLHttpRequest}
+		 */
+		_xhrProvider: function() {
+			var headers = this._defaultHeaders;
+			var xhr = new XMLHttpRequest();
+			var oldOpen = xhr.open;
+			// override open() method to add headers
+			xhr.open = function() {
+				var result = oldOpen.apply(this, arguments);
+				_.each(headers, function(value, key) {
+					xhr.setRequestHeader(key, value);
+				});
+				return result;
+			};
+			return xhr;
+		},
+
+		/**
+		 * Prepends the base url to the given path sections
+		 *
+		 * @param {...String} path sections
+		 *
+		 * @return {String} base url + joined path, any leading or trailing slash
+		 * will be kept
+		 */
+		_buildUrl: function() {
+			var path = this._buildPath.apply(this, arguments);
+			if (path.charAt([path.length - 1]) === '/') {
+				path = path.substr(0, path.length - 1);
+			}
+			if (path.charAt(0) === '/') {
+				path = path.substr(1);
+			}
+			return this._baseUrl + '/' + path;
+		},
+
+		/**
+		 * Append the path to the root and also encode path
+		 * sections
+		 *
+		 * @param {...String} path sections
+		 *
+		 * @return {String} joined path, any leading or trailing slash
+		 * will be kept
+		 */
+		_buildPath: function() {
+			var path = OC.joinPaths.apply(this, arguments);
+			var sections = path.split('/');
+			var i;
+			for (i = 0; i < sections.length; i++) {
+				sections[i] = encodeURIComponent(sections[i]);
+			}
+			path = sections.join('/');
+			return path;
+		},
+
+		/**
+		 * Parse headers string into a map
+		 *
+		 * @param {string} headersString headers list as string
+		 *
+		 * @return {Object.<String,Array>} map of header name to header contents
+		 */
+		_parseHeaders: function(headersString) {
+			var headerRows = headersString.split('\n');
+			var headers = {};
+			for (var i = 0; i < headerRows.length; i++) {
+				var sepPos = headerRows[i].indexOf(':');
+				if (sepPos < 0) {
+					continue;
+				}
+
+				var headerName = headerRows[i].substr(0, sepPos);
+				var headerValue = headerRows[i].substr(sepPos + 2);
+
+				if (!headers[headerName]) {
+					// make it an array
+					headers[headerName] = [];
+				}
+
+				headers[headerName].push(headerValue);
+			}
+			return headers;
+		},
+
+		/**
+		 * Parses the compound file id
+		 *
+		 * @param {string} compoundFileId compound file id as returned by the server
+		 *
+		 * @return {int} local file id, stripped of the instance id
+		 */
+		_parseFileId: function(compoundFileId) {
+			if (!compoundFileId || compoundFileId.length < 8) {
+				return null;
+			}
+			return parseInt(compoundFileId.substr(0, 8), 10);
+		},
+
+		/**
+		 * Parses the etag response which is in double quotes.
+		 *
+		 * @param {string} etag etag value in double quotes
+		 *
+		 * @return {string} etag without double quotes
+		 */
+		_parseEtag: function(etag) {
+			if (etag.charAt(0) === '"') {
+				return etag.split('"')[1];
+			}
+			return etag;
+		},
+
+		/**
+		 * Parse Webdav result
+		 *
+		 * @param {Object} response XML object
+		 *
+		 * @return {Array.<FileInfo>} array of file info
+		 */
+		_parseFileInfo: function(response) {
+			var path = response.href;
+			if (path.substr(0, this._root.length) === this._root) {
+				path = path.substr(this._root.length);
+			}
+
+			if (path.charAt(path.length - 1) === '/') {
+				path = path.substr(0, path.length - 1);
+			}
+
+			path = '/' + decodeURIComponent(path);
+
+			if (response.propStat.length === 1 && response.propStat[0].status !== 200) {
+				return null;
+			}
+
+			var props = response.propStat[0].properties;
+
+			var data = {
+				id: this._parseFileId(props['{' + Client.NS_OWNCLOUD + '}id']),
+				path: OC.dirname(path) || '/',
+				name: OC.basename(path),
+				mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']),
+				_props: props
+			};
+
+			var etagProp = props['{' + Client.NS_DAV + '}getetag'];
+			if (!_.isUndefined(etagProp)) {
+				data.etag = this._parseEtag(etagProp);
+			}
+
+			var sizeProp = props['{' + Client.NS_DAV + '}getcontentlength'];
+			if (!_.isUndefined(sizeProp)) {
+				data.size = parseInt(sizeProp, 10);
+			}
+
+			sizeProp = props['{' + Client.NS_OWNCLOUD + '}size'];
+			if (!_.isUndefined(sizeProp)) {
+				data.size = parseInt(sizeProp, 10);
+			}
+
+			var contentType = props['{' + Client.NS_DAV + '}getcontenttype'];
+			if (!_.isUndefined(contentType)) {
+				data.mimetype = contentType;
+			}
+
+			var resType = props['{' + Client.NS_DAV + '}resourcetype'];
+			var isFile = true;
+			if (!data.mimetype && resType) {
+				var xmlvalue = resType[0];
+				if (xmlvalue.namespaceURI === Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'collection') {
+					data.mimetype = 'httpd/unix-directory';
+					isFile = false;
+				}
+			}
+
+			data.permissions = OC.PERMISSION_READ;
+			var permissionProp = props['{' + Client.NS_OWNCLOUD + '}permissions'];
+			if (!_.isUndefined(permissionProp)) {
+				var permString = permissionProp || '';
+				data.mountType = null;
+				for (var i = 0; i < permString.length; i++) {
+					var c = permString.charAt(i);
+					switch (c) {
+						// FIXME: twisted permissions
+						case 'C':
+						case 'K':
+							data.permissions |= OC.PERMISSION_CREATE;
+							if (!isFile) {
+								data.permissions |= OC.PERMISSION_UPDATE;
+							}
+							break;
+						case 'W':
+							if (isFile) {
+								// also add create permissions
+								data.permissions |= OC.PERMISSION_CREATE;
+							}
+							data.permissions |= OC.PERMISSION_UPDATE;
+							break;
+						case 'D':
+							data.permissions |= OC.PERMISSION_DELETE;
+							break;
+						case 'R':
+							data.permissions |= OC.PERMISSION_SHARE;
+							break;
+						case 'M':
+							if (!data.mountType) {
+								// TODO: how to identify external-root ?
+								data.mountType = 'external';
+							}
+							break;
+						case 'S':
+							// TODO: how to identify shared-root ?
+							data.mountType = 'shared';
+							break;
+					}
+				}
+			}
+
+			return new FileInfo(data);
+		},
+
+		/**
+		 * Parse Webdav multistatus
+		 *
+		 * @param {Array} responses
+		 */
+		_parseResult: function(responses) {
+			var self = this;
+			return _.map(responses, function(response) {
+				return self._parseFileInfo(response);
+			});
+		},
+
+		/**
+		 * Returns whether the given status code means success
+		 *
+		 * @param {int} status status code
+		 *
+		 * @return true if status code is between 200 and 299 included
+		 */
+		_isSuccessStatus: function(status) {
+			return status >= 200 && status <= 299;
+		},
+
+		/**
+		 * Returns the default PROPFIND properties to use during a call.
+		 *
+		 * @return {Array.<Object>} array of properties
+		 */
+		_getPropfindProperties: function() {
+			if (!this._propfindProperties) {
+				this._propfindProperties = _.map(Client._PROPFIND_PROPERTIES, function(propDef) {
+					return '{' + propDef[0] + '}' + propDef[1];
+				});
+			}
+			return this._propfindProperties;
+		},
+
+		/**
+		 * Lists the contents of a directory
+		 *
+		 * @param {String} path path to retrieve
+		 * @param {Object} [options] options
+		 * @param {boolean} [options.includeParent=false] set to true to keep
+		 * the parent folder in the result list
+		 *
+		 * @return {Promise} promise
+		 */
+		getFolderContents: function(path, options) {
+			if (!path) {
+				path = '';
+			}
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+
+			// TODO: headers
+			this._client.propFind(
+				this._buildUrl(path),
+				this._getPropfindProperties(),
+				1
+			).then(function(result) {
+				if (self._isSuccessStatus(result.status)) {
+					var results = self._parseResult(result.body);
+					if (!options || !options.includeParent) {
+						// remove root dir, the first entry
+						results.shift();
+					}
+					deferred.resolve(result.status, results);
+				} else {
+					deferred.reject(result.status);
+				}
+			});
+			return promise;
+		},
+
+		/**
+		 * Returns the file info of a given path.
+		 *
+		 * @param {String} path path
+		 * @param {Array} [properties] list of webdav properties to
+		 * retrieve
+		 *
+		 * @return {Promise} promise
+		 */
+		getFileInfo: function(path) {
+			if (!path) {
+				path = '';
+			}
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+
+			// TODO: headers
+			this._client.propFind(
+				this._buildUrl(path),
+				this._getPropfindProperties(),
+				0
+			).then(
+				function(result) {
+					if (self._isSuccessStatus(result.status)) {
+						deferred.resolve(result.status, self._parseResult([result.body])[0]);
+					} else {
+						deferred.reject(result.status);
+					}
+				}
+			);
+			return promise;
+		},
+
+		/**
+		 * Returns the contents of the given file.
+		 *
+		 * @param {String} path path to file
+		 *
+		 * @return {Promise}
+		 */
+		getFileContents: function(path) {
+			if (!path) {
+				throw 'Missing argument "path"';
+			}
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+
+			this._client.request(
+				'GET',
+				this._buildUrl(path),
+				this._defaultHeaders
+			).then(
+				function(result) {
+					if (self._isSuccessStatus(result.status)) {
+						deferred.resolve(result.status, result.body);
+					} else {
+						deferred.reject(result.status);
+					}
+				}
+			);
+			return promise;
+		},
+
+		/**
+		 * Puts the given data into the given file.
+		 *
+		 * @param {String} path path to file
+		 * @param {String} body file body
+		 * @param {Object} [options]
+		 * @param {String} [options.contentType='text/plain'] content type
+		 * @param {bool} [options.overwrite=true] whether to overwrite an existing file
+		 *
+		 * @return {Promise}
+		 */
+		putFileContents: function(path, body, options) {
+			if (!path) {
+				throw 'Missing argument "path"';
+			}
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+			options = options || {};
+			var headers = _.extend({}, this._defaultHeaders);
+			var contentType = 'text/plain';
+			if (options.contentType) {
+				contentType = options.contentType;
+			}
+
+			headers['Content-Type'] = contentType;
+
+			if (_.isUndefined(options.overwrite) || options.overwrite) {
+				// will trigger 412 precondition failed if a file already exists
+				headers['If-None-Match'] = '*';
+			}
+
+			this._client.request(
+				'PUT',
+				this._buildUrl(path),
+				headers,
+				body || ''
+			).then(
+				function(result) {
+					if (self._isSuccessStatus(result.status)) {
+						deferred.resolve(result.status);
+					} else {
+						deferred.reject(result.status);
+					}
+				}
+			);
+			return promise;
+		},
+
+		_simpleCall: function(method, path) {
+			if (!path) {
+				throw 'Missing argument "path"';
+			}
+
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+
+			this._client.request(
+				method,
+				this._buildUrl(path),
+				this._defaultHeaders
+			).then(
+				function(result) {
+					if (self._isSuccessStatus(result.status)) {
+						deferred.resolve(result.status);
+					} else {
+						deferred.reject(result.status);
+					}
+				}
+			);
+			return promise;
+		},
+
+		/**
+		 * Creates a directory
+		 *
+		 * @param {String} path path to create
+		 *
+		 * @return {Promise}
+		 */
+		createDirectory: function(path) {
+			return this._simpleCall('MKCOL', path);
+		},
+
+		/**
+		 * Deletes a file or directory
+		 *
+		 * @param {String} path path to delete
+		 *
+		 * @return {Promise}
+		 */
+		remove: function(path) {
+			return this._simpleCall('DELETE', path);
+		},
+
+		/**
+		 * Moves path to another path
+		 *
+		 * @param {String} path path to move
+		 * @param {String} destinationPath destination path
+		 * @param {boolean} [allowOverwrite=false] true to allow overwriting,
+		 * false otherwise
+		 *
+		 * @return {Promise} promise
+		 */
+		move: function(path, destinationPath, allowOverwrite) {
+			if (!path) {
+				throw 'Missing argument "path"';
+			}
+			if (!destinationPath) {
+				throw 'Missing argument "destinationPath"';
+			}
+
+			var self = this;
+			var deferred = $.Deferred();
+			var promise = deferred.promise();
+			var headers =
+				_.extend({
+					'Destination' : this._buildUrl(destinationPath)
+				}, this._defaultHeaders);
+
+			if (!allowOverwrite) {
+				headers['Overwrite'] = 'F';
+			}
+
+			this._client.request(
+				'MOVE',
+				this._buildUrl(path),
+				headers
+			).then(
+				function(response) {
+					if (self._isSuccessStatus(response.status)) {
+						deferred.resolve(response.status);
+					} else {
+						deferred.reject(response.status);
+					}
+				}
+			);
+			return promise;
+		}
+
+	};
+
+	if (!OC.Files) {
+		/**
+		 * @namespace OC.Files
+		 *
+		 * @since 8.2
+		 */
+		OC.Files = {};
+	}
+
+	/**
+	 * Returns the default instance of the files client
+	 *
+	 * @return {OC.Files.Client} default client
+	 *
+	 * @since 8.2
+	 */
+	OC.Files.getClient = function() {
+		if (OC.Files._defaultClient) {
+			return OC.Files._defaultClient;
+		}
+
+		var client = new OC.Files.Client({
+			host: OC.getHost(),
+			port: OC.getPort(),
+			root: OC.linkToRemoteBase('webdav'),
+			useHTTPS: OC.getProtocol() === 'https'
+		});
+		OC.Files._defaultClient = client;
+		return client;
+	};
+
+	OC.Files.Client = Client;
+})(OC, OC.Files.FileInfo);
+
diff --git a/core/js/files/fileinfo.js b/core/js/files/fileinfo.js
new file mode 100644
index 0000000000000000000000000000000000000000..95af07b79924e030bfad6a2d812311f5bc498a5a
--- /dev/null
+++ b/core/js/files/fileinfo.js
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC) {
+
+	/**
+	 * @class OC.Files.FileInfo
+	 * @classdesc File information
+	 *
+	 * @param {Object} data file data, see attributes for details
+	 *
+	 * @since 8.2
+	 */
+	var FileInfo = function(data) {
+		if (!_.isUndefined(data.id)) {
+			this.id = parseInt(data.id, 10);
+		}
+
+		// TODO: normalize path
+		this.path = data.path || '';
+		this.name = data.name;
+
+		this.mtime = data.mtime;
+		this.etag = data.etag;
+		this.permissions = data.permissions;
+		this.size = data.size;
+		this.mimetype = data.mimetype || 'application/octet-stream';
+		this.mountType = data.mountType;
+		this.icon = data.icon;
+		this._props = data._props;
+
+		if (data.type) {
+			this.type = data.type;
+		} else if (this.mimetype === 'httpd/unix-directory') {
+			this.type = 'dir';
+		} else {
+			this.type = 'file';
+		}
+
+		if (!this.mimetype) {
+			if (this.type === 'dir') {
+				this.mimetype = 'httpd/unix-directory';
+			} else {
+				this.mimetype = 'application/octet-stream';
+			}
+		}
+	};
+
+	/**
+	 * @memberof OC.Files
+	 */
+	FileInfo.prototype = {
+		/**
+		 * File id
+		 *
+		 * @type int
+		 */
+		id: null,
+
+		/**
+		 * File name
+		 *
+		 * @type String
+		 */
+		name: null,
+
+		/**
+		 * Path leading to the file, without the file name,
+		 * and with a leading slash.
+		 *
+		 * @type String
+		 */
+		path: null,
+
+		/**
+		 * Mime type
+		 *
+		 * @type String
+		 */
+		mimetype: null,
+
+		/**
+		 * Icon URL.
+		 *
+		 * Can be used to override the mime type icon.
+		 *
+		 * @type String
+		 */
+		icon: null,
+
+		/**
+		 * File type. 'file'  for files, 'dir' for directories.
+		 *
+		 * @type String
+		 * @deprecated rely on mimetype instead
+		 */
+		type: 'file',
+
+		/**
+		 * Permissions.
+		 *
+		 * @see OC#PERMISSION_ALL for permissions
+		 * @type int
+		 */
+		permissions: null,
+
+		/**
+		 * Modification time
+		 *
+		 * @type int
+		 */
+		mtime: null,
+
+		/**
+		 * Etag
+		 *
+		 * @type String
+		 */
+		etag: null,
+
+		/**
+		 * Mount type.
+		 *
+		 * One of null, "external-root", "shared" or "shared-root"
+		 *
+		 * @type string
+		 */
+		mountType: null
+	};
+
+	if (!OC.Files) {
+		OC.Files = {};
+	}
+	OC.Files.FileInfo = FileInfo;
+})(OC);
+
diff --git a/core/js/js.js b/core/js/js.js
index 57c9871233b4efb02140625c79ace399617f9cba..ce552bb8ea2c1a76cbfc5bc8c931a2944fdf2ad2 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -1428,7 +1428,6 @@ function initCore() {
 		$('body').delegate('#app-content', 'apprendered appresized', adjustControlsWidth);
 
 	}
-
 }
 
 $(document).ready(initCore);
diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js
index cd387d76ce8f81b6991e5a136bf82810cd6f2ef8..f09a7054c9f968d0c95e50c19fdf8b294df99679 100644
--- a/core/js/tests/specHelper.js
+++ b/core/js/tests/specHelper.js
@@ -86,6 +86,7 @@ window.firstDay = 0;
 // setup dummy webroots
 /* jshint camelcase: false */
 window.oc_debug = true;
+// FIXME: oc_webroot is supposed to be only the path!!!
 window.oc_webroot = location.href + '/';
 window.oc_appswebroots = {
 	"files": window.oc_webroot + '/apps/files/'
diff --git a/core/js/tests/specs/files/clientSpec.js b/core/js/tests/specs/files/clientSpec.js
new file mode 100644
index 0000000000000000000000000000000000000000..67815d93f0565224b3005e6b1171104b3f003240
--- /dev/null
+++ b/core/js/tests/specs/files/clientSpec.js
@@ -0,0 +1,712 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+*
+* You should have received a copy of the GNU Affero General Public
+* License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+describe('OC.Files.Client tests', function() {
+	var Client = OC.Files.Client;
+	var baseUrl;
+	var client;
+
+	beforeEach(function() {
+		baseUrl = 'https://testhost:999/owncloud/remote.php/webdav/';
+
+		client = new Client({
+			host: 'testhost',
+			port: 999,
+			root: '/owncloud/remote.php/webdav',
+			useHTTPS: true
+		});
+	});
+	afterEach(function() {
+		client = null;
+	});
+
+	/**
+	 * Send an status response and check that the given
+	 * promise gets its success handler called with the error
+	 * status code
+	 *
+	 * @param {Promise} promise promise
+	 * @param {int} status status to test
+	 */
+	function respondAndCheckStatus(promise, status) {
+		var successHandler = sinon.stub();
+		var failHandler = sinon.stub();
+		promise.done(successHandler);
+		promise.fail(failHandler);
+
+		fakeServer.requests[0].respond(
+			status,
+			{'Content-Type': 'application/xml'},
+			''
+		);
+
+		promise.then(function() {
+			expect(successHandler.calledOnce).toEqual(true);
+			expect(successHandler.getCall(0).args[0]).toEqual(status);
+
+			expect(failHandler.notCalled).toEqual(true);
+		});
+
+		return promise;
+	}
+
+	/**
+	 * Send an error response and check that the given
+	 * promise gets its fail handler called with the error
+	 * status code
+	 *
+	 * @param {Promise} promise promise object
+	 * @param {int} status error status to test
+	 */
+	function respondAndCheckError(promise, status) {
+		var successHandler = sinon.stub();
+		var failHandler = sinon.stub();
+		promise.done(successHandler);
+		promise.fail(failHandler);
+
+		fakeServer.requests[0].respond(
+			status,
+			{'Content-Type': 'application/xml'},
+			''
+		);
+
+		promise.then(function() {
+			expect(failHandler.calledOnce).toEqual(true);
+			expect(failHandler.calledWith(status)).toEqual(true);
+
+			expect(successHandler.notCalled).toEqual(true);
+
+			fulfill();
+		});
+
+		return promise;
+	}
+
+	/**
+	 * Returns a list of request properties parsed from the given request body.
+	 *
+	 * @param {string} requestBody request XML
+	 *
+	 * @return {Array.<String>} array of request properties in the format
+	 * "{NS:}propname"
+	 */
+	function getRequestedProperties(requestBody) {
+		var doc = (new window.DOMParser()).parseFromString(
+				requestBody,
+				'application/xml'
+		);
+		var propRoots = doc.getElementsByTagNameNS('DAV:', 'prop');
+		var propsList = propRoots.item(0).childNodes;
+		return _.map(propsList, function(propNode) {
+			return '{' + propNode.namespaceURI + '}' + propNode.localName;
+		});
+	}
+
+	function makePropBlock(props) {
+		var s = '<d:prop>\n';
+
+		_.each(props, function(value, key) {
+			s += '<' + key + '>' + value + '</' + key + '>\n';
+		});
+
+		return s + '</d:prop>\n';
+	}
+
+	function makeResponseBlock(href, props, failedProps) {
+		var s = '<d:response>\n';
+		s += '<d:href>' + href + '</d:href>\n';
+		s += '<d:propstat>\n';
+		s += makePropBlock(props);
+		s += '<d:status>HTTP/1.1 200 OK</d:status>';
+		s += '</d:propstat>\n';
+		if (failedProps) {
+			s += '<d:propstat>\n';
+			_.each(failedProps, function(prop) {
+				s += '<' + prop + '/>\n';
+			});
+			s += '<d:status>HTTP/1.1 404 Not Found</d:status>\n';
+			s += '</d:propstat>\n';
+		}
+		return s + '</d:response>\n';
+	}
+
+	describe('file listing', function() {
+
+		var folderContentsXml =
+			'<?xml version="1.0" encoding="utf-8"?>' +
+			'<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' +
+			makeResponseBlock(
+			'/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/',
+			{
+				'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT',
+				'd:getetag': '"56cfcabd79abb"',
+				'd:resourcetype': '<d:collection/>',
+				'oc:id': '00000011oc2d13a6a068',
+				'oc:permissions': 'RDNVCK',
+				'oc:size': 120
+			},
+			[
+				'd:getcontenttype',
+				'd:getcontentlength'
+			]
+			) +
+			makeResponseBlock(
+			'/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt',
+			{
+				'd:getlastmodified': 'Fri, 10 Jul 2015 13:38:05 GMT',
+				'd:getetag': '"559fcabd79a38"',
+				'd:getcontenttype': 'text/plain',
+				'd:getcontentlength': 250,
+				'd:resourcetype': '',
+				'oc:id': '00000051oc2d13a6a068',
+				'oc:permissions': 'RDNVW'
+			},
+			[
+				'oc:size',
+			]
+			) +
+			makeResponseBlock(
+			'/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/sub',
+			{
+				'd:getlastmodified': 'Fri, 10 Jul 2015 14:00:00 GMT',
+				'd:getetag': '"66cfcabd79abb"',
+				'd:resourcetype': '<d:collection/>',
+				'oc:id': '00000015oc2d13a6a068',
+				'oc:permissions': 'RDNVCK',
+				'oc:size': 100
+			},
+			[
+				'd:getcontenttype',
+				'd:getcontentlength'
+			]
+			) +
+			'</d:multistatus>';
+
+		it('sends PROPFIND with explicit properties to get file list', function() {
+			client.getFolderContents('path/to space/文件夹');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('PROPFIND');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9');
+			expect(fakeServer.requests[0].requestHeaders.Depth).toEqual(1);
+
+			var props = getRequestedProperties(fakeServer.requests[0].requestBody);
+			expect(props).toContain('{DAV:}getlastmodified');
+			expect(props).toContain('{DAV:}getcontentlength');
+			expect(props).toContain('{DAV:}getcontenttype');
+			expect(props).toContain('{DAV:}getetag');
+			expect(props).toContain('{DAV:}resourcetype');
+			expect(props).toContain('{http://owncloud.org/ns}id');
+			expect(props).toContain('{http://owncloud.org/ns}size');
+			expect(props).toContain('{http://owncloud.org/ns}permissions');
+		});
+		it('sends PROPFIND to base url when empty path given', function() {
+			client.getFolderContents('');
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].url).toEqual(baseUrl);
+		});
+		it('sends PROPFIND to base url when root path given', function() {
+			client.getFolderContents('/');
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].url).toEqual(baseUrl);
+		});
+		it('parses the result list into a FileInfo array', function() {
+			var promise = client.getFolderContents('path/to space/文件夹');
+
+			expect(fakeServer.requests.length).toEqual(1);
+
+			fakeServer.requests[0].respond(
+				207,
+				{'Content-Type': 'application/xml'},
+				folderContentsXml
+			);
+
+			promise.then(function(status, response) {
+				expect(status).toEqual(207);
+				expect(_.isArray(response)).toEqual(true);
+
+				expect(response.length).toEqual(2);
+
+				// file entry
+				var info = response[0];
+				expect(info instanceof OC.Files.FileInfo).toEqual(true);
+				expect(info.id).toEqual(51);
+				expect(info.path).toEqual('/path/to space/文件夹');
+				expect(info.name).toEqual('One.txt');
+				expect(info.permissions).toEqual(31);
+				expect(info.size).toEqual(250);
+				expect(info.mtime.getTime()).toEqual(1436535485000);
+				expect(info.mimetype).toEqual('text/plain');
+				expect(info.etag).toEqual('559fcabd79a38');
+
+				// sub entry
+				info = response[1];
+				expect(info instanceof OC.Files.FileInfo).toEqual(true);
+				expect(info.id).toEqual(15);
+				expect(info.path).toEqual('/path/to space/文件夹');
+				expect(info.name).toEqual('sub');
+				expect(info.permissions).toEqual(31);
+				expect(info.size).toEqual(100);
+				expect(info.mtime.getTime()).toEqual(1436536800000);
+				expect(info.mimetype).toEqual('httpd/unix-directory');
+				expect(info.etag).toEqual('66cfcabd79abb');
+			});
+			return promise.promise();
+		});
+		it('returns parent node in result if specified', function() {
+			var promise = client.getFolderContents('path/to space/文件夹', {includeParent: true});
+
+			expect(fakeServer.requests.length).toEqual(1);
+
+			fakeServer.requests[0].respond(
+				207,
+				{'Content-Type': 'application/xml'},
+				folderContentsXml
+			);
+
+			promise.then(function(status, response) {
+				expect(status).toEqual(207);
+				expect(_.isArray(response)).toEqual(true);
+
+				expect(response.length).toEqual(3);
+
+				// root entry
+				var info = response[0];
+				expect(info instanceof OC.Files.FileInfo).toEqual(true);
+				expect(info.id).toEqual(11);
+				expect(info.path).toEqual('/path/to space');
+				expect(info.name).toEqual('文件夹');
+				expect(info.permissions).toEqual(31);
+				expect(info.size).toEqual(120);
+				expect(info.mtime.getTime()).toEqual(1436522405000);
+				expect(info.mimetype).toEqual('httpd/unix-directory');
+				expect(info.etag).toEqual('56cfcabd79abb');
+
+				// the two other entries follow
+				expect(response[1].id).toEqual(51);
+				expect(response[2].id).toEqual(15);
+			});
+
+			return promise;
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.getFolderContents('path/to space/文件夹', {includeParent: true});
+			return respondAndCheckError(promise, 404);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+
+	describe('file info', function() {
+		var responseXml =
+			'<?xml version="1.0" encoding="utf-8"?>' +
+			'<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' +
+			makeResponseBlock(
+			'/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/',
+			{
+				'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT',
+				'd:getetag': '"56cfcabd79abb"',
+				'd:resourcetype': '<d:collection/>',
+				'oc:id': '00000011oc2d13a6a068',
+				'oc:permissions': 'RDNVCK',
+				'oc:size': 120
+			},
+			[
+				'd:getcontenttype',
+				'd:getcontentlength'
+			]
+			) +
+			'</d:multistatus>';
+
+		it('sends PROPFIND with zero depth to get single file info', function() {
+			client.getFileInfo('path/to space/文件夹');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('PROPFIND');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9');
+			expect(fakeServer.requests[0].requestHeaders.Depth).toEqual(0);
+
+			var props = getRequestedProperties(fakeServer.requests[0].requestBody);
+			expect(props).toContain('{DAV:}getlastmodified');
+			expect(props).toContain('{DAV:}getcontentlength');
+			expect(props).toContain('{DAV:}getcontenttype');
+			expect(props).toContain('{DAV:}getetag');
+			expect(props).toContain('{DAV:}resourcetype');
+			expect(props).toContain('{http://owncloud.org/ns}id');
+			expect(props).toContain('{http://owncloud.org/ns}size');
+			expect(props).toContain('{http://owncloud.org/ns}permissions');
+		});
+		it('parses the result into a FileInfo', function() {
+			var promise = client.getFileInfo('path/to space/文件夹');
+
+			expect(fakeServer.requests.length).toEqual(1);
+
+			fakeServer.requests[0].respond(
+				207,
+				{'Content-Type': 'application/xml'},
+				responseXml
+			);
+
+			promise.then(function(status, response) {
+				expect(status).toEqual(207);
+				expect(_.isArray(response)).toEqual(false);
+
+				var info = response;
+				expect(info instanceof OC.Files.FileInfo).toEqual(true);
+				expect(info.id).toEqual(11);
+				expect(info.path).toEqual('/path/to space');
+				expect(info.name).toEqual('文件夹');
+				expect(info.permissions).toEqual(31);
+				expect(info.size).toEqual(120);
+				expect(info.mtime.getTime()).toEqual(1436522405000);
+				expect(info.mimetype).toEqual('httpd/unix-directory');
+				expect(info.etag).toEqual('56cfcabd79abb');
+			});
+
+			return promise;
+		});
+		it('properly parses entry inside root', function() {
+			var responseXml =
+				'<?xml version="1.0" encoding="utf-8"?>' +
+				'<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' +
+				makeResponseBlock(
+				'/owncloud/remote.php/webdav/in%20root',
+				{
+					'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT',
+					'd:getetag': '"56cfcabd79abb"',
+					'd:resourcetype': '<d:collection/>',
+					'oc:id': '00000011oc2d13a6a068',
+					'oc:permissions': 'RDNVCK',
+					'oc:size': 120
+				},
+				[
+					'd:getcontenttype',
+					'd:getcontentlength'
+				]
+				) +
+				'</d:multistatus>';
+
+			var promise = client.getFileInfo('in root');
+
+			expect(fakeServer.requests.length).toEqual(1);
+
+			fakeServer.requests[0].respond(
+				207,
+				{'Content-Type': 'application/xml'},
+				responseXml
+			);
+
+			promise.then(function(status, response) {
+				expect(status).toEqual(207);
+				expect(_.isArray(response)).toEqual(false);
+
+				var info = response;
+				expect(info instanceof OC.Files.FileInfo).toEqual(true);
+				expect(info.id).toEqual(11);
+				expect(info.path).toEqual('/');
+				expect(info.name).toEqual('in root');
+				expect(info.permissions).toEqual(31);
+				expect(info.size).toEqual(120);
+				expect(info.mtime.getTime()).toEqual(1436522405000);
+				expect(info.mimetype).toEqual('httpd/unix-directory');
+				expect(info.etag).toEqual('56cfcabd79abb');
+			});
+
+			return promise;
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.getFileInfo('path/to space/文件夹');
+			return respondAndCheckError(promise, 404);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+
+	describe('permissions', function() {
+
+		function getFileInfoWithPermission(webdavPerm, isFile) {
+			var props = {
+				'd:getlastmodified': 'Fri, 10 Jul 2015 13:38:05 GMT',
+				'd:getetag': '"559fcabd79a38"',
+				'd:getcontentlength': 250,
+				'oc:id': '00000051oc2d13a6a068',
+				'oc:permissions': webdavPerm,
+			};
+
+			if (isFile) {
+				props['d:getcontenttype'] = 'text/plain';
+			} else {
+				props['d:resourcetype'] = '<d:collection/>';
+			}
+
+			var responseXml =
+				'<?xml version="1.0" encoding="utf-8"?>' +
+				'<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' +
+				makeResponseBlock(
+					'/owncloud/remote.php/webdav/file.txt',
+					props
+				) +
+				'</d:multistatus>';
+			var promise = client.getFileInfo('file.txt');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			fakeServer.requests[0].respond(
+				207,
+				{'Content-Type': 'application/xml'},
+				responseXml
+			);
+
+			fakeServer.restore();
+			fakeServer = sinon.fakeServer.create();
+
+			return promise;
+		}
+
+		function testPermission(permission, isFile, expectedPermissions) {
+			var promise = getFileInfoWithPermission(permission, isFile);
+			promise.then(function(result) {
+				expect(result.permissions).toEqual(expectedPermissions);
+			});
+			return promise;
+		}
+
+		function testMountType(permission, isFile, expectedMountType) {
+			var promise = getFileInfoWithPermission(permission, isFile);
+			promise.then(function(result) {
+				expect(result.mountType).toEqual(expectedMountType);
+			});
+			return promise;
+		}
+
+		it('properly parses file permissions', function() {
+			// permission, isFile, expectedPermissions
+			var testCases = [
+				['', true, OC.PERMISSION_READ],
+				['C', true, OC.PERMISSION_READ | OC.PERMISSION_CREATE],
+				['K', true, OC.PERMISSION_READ | OC.PERMISSION_CREATE],
+				['W', true, OC.PERMISSION_READ | OC.PERMISSION_CREATE | OC.PERMISSION_UPDATE],
+				['D', true, OC.PERMISSION_READ | OC.PERMISSION_DELETE],
+				['R', true, OC.PERMISSION_READ | OC.PERMISSION_SHARE],
+				['CKWDR', true, OC.PERMISSION_ALL]
+			];
+			return Promise.all(
+				_.map(testCases, function(testCase) {
+					return testPermission.apply(testCase);
+				})
+			);
+		});
+		it('properly parses folder permissions', function() {
+			var testCases = [
+				['', false, OC.PERMISSION_READ],
+				['C', false, OC.PERMISSION_READ | OC.PERMISSION_CREATE | OC.PERMISSION_UPDATE],
+				['K', false, OC.PERMISSION_READ | OC.PERMISSION_CREATE | OC.PERMISSION_UPDATE],
+				['W', false, OC.PERMISSION_READ | OC.PERMISSION_UPDATE],
+				['D', false, OC.PERMISSION_READ | OC.PERMISSION_DELETE],
+				['R', false, OC.PERMISSION_READ | OC.PERMISSION_SHARE],
+				['CKWDR', false, OC.PERMISSION_ALL]
+			];
+
+			return Promise.all(
+				_.map(testCases, function(testCase) {
+					return testPermission.apply(testCase);
+				})
+			);
+		});
+		it('properly parses mount types', function() {
+			var testCases = [
+				['CKWDR', false, null],
+				['M', false, 'external'],
+				['S', false, 'shared'],
+				['SM', false, 'shared']
+			];
+
+			return Promise.all(
+				_.map(testCases, function(testCase) {
+					return testMountType.apply(testCase);
+				})
+			);
+		});
+	});
+
+	describe('get file contents', function() {
+		it('returns file contents', function() {
+			var promise = client.getFileContents('path/to space/文件夹/One.txt');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('GET');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt');
+
+			fakeServer.requests[0].respond(
+				200,
+				{'Content-Type': 'text/plain'},
+				'some contents'
+			);
+
+			promise.then(function(status, response) {
+				expect(status).toEqual(200);
+				expect(response).toEqual('some contents');
+			});
+
+			return promise;
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.getFileContents('path/to space/文件夹/One.txt');
+			return respondAndCheckError(promise, 409);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+
+	describe('put file contents', function() {
+		it('sends PUT with file contents', function() {
+			var promise = client.putFileContents(
+					'path/to space/文件夹/One.txt',
+					'some contents'
+			);
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('PUT');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt');
+			expect(fakeServer.requests[0].requestBody).toEqual('some contents');
+			expect(fakeServer.requests[0].requestHeaders['If-None-Match']).toEqual('*');
+			expect(fakeServer.requests[0].requestHeaders['Content-Type']).toEqual('text/plain;charset=utf-8');
+
+			return respondAndCheckStatus(promise, 201);
+		});
+		it('sends PUT with file contents with headers matching options', function() {
+			var promise = client.putFileContents(
+					'path/to space/文件夹/One.txt',
+					'some contents',
+					{
+						overwrite: false,
+						contentType: 'text/markdown'
+					}
+			);
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('PUT');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt');
+			expect(fakeServer.requests[0].requestBody).toEqual('some contents');
+			expect(fakeServer.requests[0].requestHeaders['If-None-Match']).not.toBeDefined();
+			expect(fakeServer.requests[0].requestHeaders['Content-Type']).toEqual('text/markdown;charset=utf-8');
+
+			return respondAndCheckStatus(promise, 201);
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.putFileContents(
+					'path/to space/文件夹/One.txt',
+					'some contents'
+			);
+			return respondAndCheckError(promise, 409);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+
+	describe('create directory', function() {
+		it('sends MKCOL with specified path', function() {
+			var promise = client.createDirectory('path/to space/文件夹/new dir');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('MKCOL');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/new%20dir');
+
+			return respondAndCheckStatus(promise, 201);
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.createDirectory('path/to space/文件夹/new dir');
+			return respondAndCheckError(promise, 404);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+
+	describe('deletion', function() {
+		it('sends DELETE with specified path', function() {
+			var promise = client.remove('path/to space/文件夹');
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('DELETE');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9');
+
+			return respondAndCheckStatus(promise, 201);
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.remove('path/to space/文件夹');
+			return respondAndCheckError(promise, 404);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+
+	describe('move', function() {
+		it('sends MOVE with specified paths with fail on overwrite by default', function() {
+			var promise = client.move(
+					'path/to space/文件夹',
+					'path/to space/anotherdir/文件夹'
+			);
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('MOVE');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9');
+			expect(fakeServer.requests[0].requestHeaders.Destination)
+				.toEqual(baseUrl + 'path/to%20space/anotherdir/%E6%96%87%E4%BB%B6%E5%A4%B9');
+			expect(fakeServer.requests[0].requestHeaders.Overwrite)
+				.toEqual('F');
+
+			return respondAndCheckStatus(promise, 201);
+		});
+		it('sends MOVE with silent overwrite mode when specified', function() {
+			var promise = client.move(
+					'path/to space/文件夹',
+					'path/to space/anotherdir/文件夹',
+					{allowOverwrite: true}
+			);
+
+			expect(fakeServer.requests.length).toEqual(1);
+			expect(fakeServer.requests[0].method).toEqual('MOVE');
+			expect(fakeServer.requests[0].url).toEqual(baseUrl + 'path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9');
+			expect(fakeServer.requests[0].requestHeaders.Destination)
+				.toEqual(baseUrl + 'path/to%20space/anotherdir/%E6%96%87%E4%BB%B6%E5%A4%B9');
+			expect(fakeServer.requests[0].requestHeaders.Overwrite)
+				.not.toBeDefined();
+
+			return respondAndCheckStatus(promise, 201);
+		});
+		it('rejects promise when an error occurred', function() {
+			var promise = client.move(
+					'path/to space/文件夹',
+					'path/to space/anotherdir/文件夹',
+					{allowOverwrite: true}
+			);
+			return respondAndCheckError(promise, 404);
+		});
+		it('throws exception if arguments are missing', function() {
+			// TODO
+		});
+	});
+});
diff --git a/tests/karma.config.js b/tests/karma.config.js
index 64a94ef230bcd83014dd85e841dd0d8e107129e8..dc621ae0f743fb3cc7ab85f74287e6ade2a403f8 100644
--- a/tests/karma.config.js
+++ b/tests/karma.config.js
@@ -164,15 +164,15 @@ module.exports = function(config) {
 	// need to test the core app as well ?
 	if (testCore) {
 		// core tests
-		files.push(corePath + 'tests/specs/*.js');
+		files.push(corePath + 'tests/specs/**/*.js');
 	}
 
 	function addApp(app) {
 		// if only a string was specified, expand to structure
 		if (typeof(app) === 'string') {
 			app = {
-				srcFiles: 'apps/' + app + '/js/*.js',
-				testFiles: 'apps/' + app + '/tests/js/*.js'
+				srcFiles: 'apps/' + app + '/js/**/*.js',
+				testFiles: 'apps/' + app + '/tests/js/**/*.js'
 			};
 		}