diff options
author | Thomas Müller <thomas.mueller@tmit.eu> | 2015-11-23 09:38:01 +0100 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2015-11-23 09:38:01 +0100 |
commit | 79bbda994bb8dd2231f68f57785237f79f86f6c7 (patch) | |
tree | 65585aed0d21cc679cdf7e2509efd6fa3d33b458 /core/js | |
parent | 2f89eef334bd445a7e046d845d5d5d1b3e4b6b8c (diff) | |
parent | 418fefc93c3332c77ec617ef108138efb6a34544 (diff) | |
download | nextcloud-server-79bbda994bb8dd2231f68f57785237f79f86f6c7.tar.gz nextcloud-server-79bbda994bb8dd2231f68f57785237f79f86f6c7.zip |
Merge pull request #16902 from owncloud/jsocclient
Web UI uses Webdav instead of ajax/* calls
Diffstat (limited to 'core/js')
-rw-r--r-- | core/js/core.json | 8 | ||||
-rw-r--r-- | core/js/files/client.js | 691 | ||||
-rw-r--r-- | core/js/files/fileinfo.js | 138 | ||||
-rw-r--r-- | core/js/files/iedavclient.js | 169 | ||||
-rw-r--r-- | core/js/js.js | 1 | ||||
-rw-r--r-- | core/js/oc-dialogs.js | 2 | ||||
-rw-r--r-- | core/js/tests/specHelper.js | 1 | ||||
-rw-r--r-- | core/js/tests/specs/files/clientSpec.js | 711 |
8 files changed, 1717 insertions, 4 deletions
diff --git a/core/js/core.json b/core/js/core.json index a80636e8463..c7621a08d62 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -8,7 +8,9 @@ "handlebars/handlebars.js", "blueimp-md5/js/md5.js", "bootstrap/js/tooltip.js", - "backbone/backbone.js" + "backbone/backbone.js", + "es6-promise/dist/es6-promise.js", + "davclient.js/lib/client.js" ], "libraries": [ "jquery-showpassword.js", @@ -39,6 +41,8 @@ "setupchecks.js", "../search/js/search.js", "mimetype.js", - "mimetypelist.js" + "mimetypelist.js", + "files/fileinfo.js", + "files/client.js" ] } diff --git a/core/js/files/client.js b/core/js/files/client.js new file mode 100644 index 00000000000..82cf3ff5121 --- /dev/null +++ b/core/js/files/client.js @@ -0,0 +1,691 @@ +/* + * 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); + } + + var url = 'http://'; + if (options.useHTTPS) { + url = 'https://'; + } + 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 + 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'], + /** + * File id + */ + [Client.NS_OWNCLOUD, 'fileid'], + /** + * 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 dav.Client + */ + _client: null, + + /** + * Array of file info parsing functions. + * + * @type Array<OC.Files.Client~parseFileInfo> + */ + _fileInfoParsers: [], + + /** + * 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 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: props['{' + Client.NS_OWNCLOUD + '}fileid'], + path: OC.dirname(path) || '/', + name: OC.basename(path), + mtime: new Date(props['{' + Client.NS_DAV + '}getlastmodified']) + }; + + 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; + } + } + } + + // extend the parsed data using the custom parsers + _.each(this._fileInfoParsers, function(parserFunction) { + _.extend(data, parserFunction(response) || {}); + }); + + 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 + * @param {Array} [options.properties] list of Webdav properties to retrieve + * + * @return {Promise} promise + */ + getFolderContents: function(path, options) { + if (!path) { + path = ''; + } + options = options || {}; + var self = this; + var deferred = $.Deferred(); + var promise = deferred.promise(); + var properties; + if (_.isUndefined(options.properties)) { + properties = this.getPropfindProperties(); + } else { + properties = options.properties; + } + + // TODO: headers + this._client.propFind( + this._buildUrl(path), + properties, + 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} [options.properties] list of Webdav properties to retrieve + * + * @return {Promise} promise + */ + getFileInfo: function(path, options) { + if (!path) { + path = ''; + } + options = options || {}; + var self = this; + var deferred = $.Deferred(); + var promise = deferred.promise(); + var properties; + if (_.isUndefined(options.properties)) { + properties = this.getPropfindProperties(); + } else { + properties = options.properties; + } + + // TODO: headers + this._client.propFind( + this._buildUrl(path), + properties, + 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; + }, + + /** + * Add a file info parser function + * + * @param {OC.Files.Client~parseFileInfo>} + */ + addFileInfoParser: function(parserFunction) { + this._fileInfoParsers.push(parserFunction); + } + + }; + + /** + * File info parser function + * + * This function receives a list of Webdav properties as input and + * should return a hash array of parsed properties, if applicable. + * + * @callback OC.Files.Client~parseFileInfo + * @param {Object} XML Webdav properties + * @return {Array} array of parsed property values + */ + + 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 00000000000..3bf68d88b15 --- /dev/null +++ b/core/js/files/fileinfo.js @@ -0,0 +1,138 @@ +/* + * 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) { + var self = this; + _.each(data, function(value, key) { + if (!_.isFunction(value)) { + self[key] = value; + } + }); + + if (!_.isUndefined(this.id)) { + this.id = parseInt(data.id, 10); + } + + // TODO: normalize path + this.path = data.path || ''; + + if (this.type === 'dir') { + this.mimetype = 'httpd/unix-directory'; + } else { + this.mimetype = this.mimetype || 'application/octet-stream'; + } + + if (!this.type) { + if (this.mimetype === 'httpd/unix-directory') { + this.type = 'dir'; + } else { + this.type = 'file'; + } + } + }; + + /** + * @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: null, + + /** + * 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/files/iedavclient.js b/core/js/files/iedavclient.js new file mode 100644 index 00000000000..bc6bce2f9ae --- /dev/null +++ b/core/js/files/iedavclient.js @@ -0,0 +1,169 @@ +/* + * 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(dav) { + + /** + * Override davclient.js methods with IE-compatible logic + */ + dav.Client.prototype = _.extend({}, dav.Client.prototype, { + + /** + * Generates a propFind request. + * + * @param {string} url Url to do the propfind request on + * @param {Array} properties List of properties to retrieve. + * @return {Promise} + */ + propFind : function(url, properties, depth) { + + if(typeof depth == "undefined") { + depth = 0; + } + + var headers = { + Depth : depth, + 'Content-Type' : 'application/xml; charset=utf-8' + }; + + var body = + '<?xml version="1.0"?>\n' + + '<d:propfind '; + + var namespace; + for (namespace in this.xmlNamespaces) { + body += ' xmlns:' + this.xmlNamespaces[namespace] + '="' + namespace + '"'; + } + body += '>\n' + + ' <d:prop>\n'; + + for(var ii in properties) { + var propText = properties[ii]; + if (typeof propText !== 'string') { + // can happen on IE8 + continue; + } + var property = this.parseClarkNotation(properties[ii]); + if (this.xmlNamespaces[property.namespace]) { + body+=' <' + this.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n'; + } else { + body+=' <x:' + property.name + ' xmlns:x="' + property.namespace + '" />\n'; + } + + } + body+=' </d:prop>\n'; + body+='</d:propfind>'; + + return this.request('PROPFIND', url, headers, body).then( + function(result) { + var elements = this.parseMultiStatus(result.xhr.responseXML); + var response; + if (depth===0) { + response = { + status: result.status, + body: elements[0] + }; + } else { + response = { + status: result.status, + body: elements + }; + } + return response; + + }.bind(this) + ); + + }, + + + _getElementsByTagName: function(node, name, resolver) { + var parts = name.split(':'); + var tagName = parts[1]; + var namespace = resolver(parts[0]); + if (node.getElementsByTagNameNS) { + return node.getElementsByTagNameNS(namespace, tagName); + } + return node.getElementsByTagName(name); + }, + + /** + * Parses a multi-status response body. + * + * @param {string} xmlBody + * @param {Array} + */ + parseMultiStatus : function(doc) { + + var result = []; + var resolver = function(foo) { + var ii; + for(ii in this.xmlNamespaces) { + if (this.xmlNamespaces[ii] === foo) { + return ii; + } + } + }.bind(this); + + var responses = this._getElementsByTagName(doc, 'd:response', resolver); + var i; + for (i = 0; i < responses.length; i++) { + var responseNode = responses[i]; + var response = { + href : null, + propStat : [] + }; + + var hrefNode = this._getElementsByTagName(responseNode, 'd:href', resolver)[0]; + + response.href = hrefNode.textContent || hrefNode.text; + + var propStatNodes = this._getElementsByTagName(responseNode, 'd:propstat', resolver); + var j = 0; + + for (j = 0; j < propStatNodes.length; j++) { + var propStatNode = propStatNodes[j]; + var statusNode = this._getElementsByTagName(propStatNode, 'd:status', resolver)[0]; + + var propStat = { + status : statusNode.textContent || statusNode.text, + properties : [] + }; + + var propNode = this._getElementsByTagName(propStatNode, 'd:prop', resolver)[0]; + if (!propNode) { + continue; + } + var k = 0; + for (k = 0; k < propNode.childNodes.length; k++) { + var prop = propNode.childNodes[k]; + var value = prop.textContent || prop.text; + if (prop.childNodes && prop.childNodes.length > 0 && prop.childNodes[0].nodeType === 1) { + value = prop.childNodes; + } + propStat.properties['{' + prop.namespaceURI + '}' + (prop.localName || prop.baseName)] = value; + + } + response.propStat.push(propStat); + } + + result.push(response); + } + + return result; + + } + + + }); + +})(dav); + diff --git a/core/js/js.js b/core/js/js.js index 57c9871233b..ce552bb8ea2 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/oc-dialogs.js b/core/js/oc-dialogs.js index 4448b813021..fe93d0ea657 100644 --- a/core/js/oc-dialogs.js +++ b/core/js/oc-dialogs.js @@ -759,7 +759,7 @@ var OCdialogs = { filename: entry.name, date: OC.Util.relativeModifiedDate(entry.mtime) }); - if (entry.isPreviewAvailable) { + if (entry.type === 'file') { var urlSpec = { file: dir + '/' + entry.name }; diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js index cd387d76ce8..f09a7054c9f 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 00000000000..3a3181d8426 --- /dev/null +++ b/core/js/tests/specs/files/clientSpec.js @@ -0,0 +1,711 @@ +/** +* 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/owncloud/remote.php/webdav/'; + + client = new Client({ + host: 'testhost', + 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}fileid'); + 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}fileid'); + 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 + }); + }); +}); |