From dff991b97f855811936688d92bb288642283606e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 15 Jun 2018 00:07:30 +0200 Subject: [PATCH 1/4] Handle persistent locks in files app UI Handle http 423 on file operations Show lock status to file list in its own sidebar tab --- apps/files/js/filelist.js | 21 ++- apps/files/js/filelockplugin.js | 129 +++++++++++++++++++ apps/files/js/locktabview.js | 89 +++++++++++++ apps/files/lib/Controller/ViewController.php | 2 + core/css/icons.css | 8 ++ core/img/actions/lock-closed.svg | 1 + core/img/actions/lock-open.svg | 1 + 7 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 apps/files/js/filelockplugin.js create mode 100644 apps/files/js/locktabview.js create mode 100644 core/img/actions/lock-closed.svg create mode 100644 core/img/actions/lock-open.svg diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 59400e7df044..f8312fcb569e 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -1979,6 +1979,10 @@ OC.Notification.show(t('files', 'Could not move "{file}", target exists', {file: fileName}, null, {escape: false}), {type: 'error'} ); + } else if (status === 423) { + OC.Notification.show(t('files', 'Could not move "{file}" because either the file or the target are locked.', + {file: fileName, message: result.message}), {type: 'error'} + ); } else if (result != null && typeof result.message !== "undefined") { OC.Notification.show(t('files', 'Could not move "{file}": {message}', {file: fileName, message: result.message}), {type: 'error'} @@ -2126,6 +2130,11 @@ type: 'error' } ); + } else if (status === 423) { + // restore the item to its previous state + OC.Notification.show(t('files', 'The file "{fileName}" is locked and can not be renamed.', + {fileName: oldName}), {type: 'error'} + ); } else { // restore the item to its previous state OC.Notification.show(t('files', 'Could not rename "{fileName}"', @@ -2455,9 +2464,15 @@ removeFromList(file); } else { // only reset the spinner for that one file - OC.Notification.show(t('files', 'Error deleting file "{fileName}".', - {fileName: file}), {type: 'error'} - ); + if (status === 423) { + OC.Notification.show(t('files', 'The file "{fileName}" is locked and cannot be deleted.', + {fileName: file}), {type: 'error'} + ); + } else { + OC.Notification.show(t('files', 'Error deleting file "{fileName}".', + {fileName: file}), {type: 'error'} + ); + } var deleteAction = self.findFileEl(file).find('.action.delete'); deleteAction.removeClass('icon-loading-small').addClass('icon-delete'); self.showFileBusyState(files, false); diff --git a/apps/files/js/filelockplugin.js b/apps/files/js/filelockplugin.js new file mode 100644 index 000000000000..bfd1c9301dd0 --- /dev/null +++ b/apps/files/js/filelockplugin.js @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2018 Thomas Müller + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OCA) { + + var TEMPLATE_LOCK_STATUS_ACTION = + '' + + '' + + ''; + + OCA.Files = OCA.Files || {}; + + /** + * @namespace OCA.Files.LockPlugin + */ + OCA.Files.LockPlugin = { + + /** + * @param fileList + */ + attach: function(fileList) { + this._extendFileActions(fileList); + + var oldCreateRow = fileList._createRow; + fileList._createRow = function(fileData) { + var $tr = oldCreateRow.apply(this, arguments); + if (fileData.activeLocks) { + $tr.attr('data-activeLocks', JSON.stringify(fileData.activeLocks)); + } + return $tr; + }; + var oldElementToFile = fileList.elementToFile; + fileList.elementToFile = function($el) { + var fileInfo = oldElementToFile.apply(this, arguments); + var activeLocks = $el.attr('data-activelocks'); + if (_.isUndefined(activeLocks)) { + activeLocks = '[]'; + } + fileInfo.activeLocks = JSON.parse(activeLocks); + return fileInfo; + }; + + var oldGetWebdavProperties = fileList._getWebdavProperties; + fileList._getWebdavProperties = function() { + var props = oldGetWebdavProperties.apply(this, arguments); + props.push('{DAV:}lockdiscovery'); + return props; + }; + + var lockTab = new OCA.Files.LockTabView('lockTabView', {order: -20}); + fileList.registerTabView(lockTab); + + fileList.filesClient.addFileInfoParser(function(response) { + var data = {}; + var props = response.propStat[0].properties; + var activeLocks = props['{DAV:}lockdiscovery']; + if (!_.isUndefined(activeLocks) && activeLocks !== '') { + data.activeLocks = _.chain(activeLocks).filter(function(xmlvalue) { + return (xmlvalue.namespaceURI === OC.Files.Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'activelock'); + }).map(function(xmlvalue) { + return { + lockscope: xmlvalue.getElementsByTagName('d:lockscope')[0].children[0].localName, + locktype: xmlvalue.getElementsByTagName('d:locktype')[0].children[0].localName, + lockroot: xmlvalue.getElementsByTagName('d:lockroot')[0].children[0].innerHTML, + depth: parseInt(xmlvalue.getElementsByTagName('d:depth')[0].innerHTML, 10), + timeout: xmlvalue.getElementsByTagName('d:timeout')[0].innerHTML, + locktoken: xmlvalue.getElementsByTagName('d:locktoken')[0].children[0].innerHTML, + owner: xmlvalue.getElementsByTagName('d:owner')[0].innerHTML + }; + }).value(); + + } + return data; + }); + + + }, + + /** + * @param fileList + * @private + */ + _extendFileActions: function(fileList) { + var self = this; + fileList.fileActions.registerAction({ + name: 'lock-status', + displayName: t('files', 'Lock status'), + mime: 'all', + permissions: OC.PERMISSION_READ, + type: OCA.Files.FileActions.TYPE_INLINE, + render: function(actionSpec, isDefault, context) { + var $file = context.$file; + var isLocked = $file.data('activelocks'); + if (isLocked) { + var $actionLink = $(self.renderLink()); + context.$file.find('a.name>span.fileactions').append($actionLink); + return $actionLink; + } + return ''; + }, + actionHandler: function(fileName) { + fileList.showDetailsView(fileName, 'lockTabView'); + } + }); + + }, + + renderLink: function () { + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE_LOCK_STATUS_ACTION); + } + return this._template({ + message: t('files', 'This resource is locked. Click to see more details.') + }); + } + +}; + +})(OCA); + +OC.Plugins.register('OCA.Files.FileList', OCA.Files.LockPlugin); + diff --git a/apps/files/js/locktabview.js b/apps/files/js/locktabview.js new file mode 100644 index 000000000000..7a934f5142d0 --- /dev/null +++ b/apps/files/js/locktabview.js @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2018 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function () { + var TEMPLATE = + '' + + '
' + + '{{#each locks}}' + + '
' + + '
{{owner}} has locked this resource via {{lockroot}}
' + + // TODO: no inline css + '' + + '' + + '
' + + '{{else}}' + + '
{{emptyResultLabel}}
' + + '{{/each}}' + + ''; + + /** + * @memberof OCA.Files + */ + var LockTabView = OCA.Files.DetailTabView.extend( + /** @lends OCA.Files.LockTabView.prototype */ { + id: 'lockTabView', + className: 'tab lockTabView', + + events: { + 'click a.unlock': '_onClickUnlock' + }, + + _onClickUnlock: function (event) { + var self = this; + var lockToken = event.target.getAttribute('data-lock-token'); + var lockUrl = event.target.getAttribute('data-lock-root'); + + this.model._filesClient.getClient().request('UNLOCK', + lockUrl, + { + 'Lock-Token': lockToken + }).then(function (result) { + if (result.status === 204) { + var locks = self.model.get('activeLocks'); + locks = locks.filter(function(item) { + return item.locktoken !== lockToken; + }); + self.model.set('activeLocks', locks); + self.render(); + } else { + // TODO: add more information + OC.Notification.show('Unlock failed with status: ' + result.status); + } + }); + }, + + getLabel: function () { + return t('files', 'Locks'); + }, + + template: function (data) { + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + + return this._template(data); + }, + + /** + * Renders this details view + */ + render: function () { + this.$el.html(this.template({ + emptyResultLabel: t('files', 'Resource is not locked'), + locks: this.model.get('activeLocks'), + model: this.model + })); + } + }); + + OCA.Files.LockTabView = LockTabView; +})(); + diff --git a/apps/files/lib/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php index 2b8eca290d3d..6f950c2eb26d 100644 --- a/apps/files/lib/Controller/ViewController.php +++ b/apps/files/lib/Controller/ViewController.php @@ -173,12 +173,14 @@ public function index($dir = '', $view = '', $fileid = null, $details = null) { \OCP\Util::addScript('files', 'favoritesfilelist'); \OCP\Util::addScript('files', 'tagsplugin'); \OCP\Util::addScript('files', 'favoritesplugin'); + \OCP\Util::addScript('files', 'filelockplugin'); \OCP\Util::addScript('files', 'detailfileinfoview'); \OCP\Util::addScript('files', 'detailtabview'); \OCP\Util::addScript('files', 'mainfileinfodetailview'); \OCP\Util::addScript('files', 'detailsview'); \OCP\Util::addStyle('files', 'detailsView'); + \OCP\Util::addScript('files', 'locktabview'); \OC_Util::addVendorScript('core', 'handlebars/handlebars'); diff --git a/core/css/icons.css b/core/css/icons.css index 0e90c5f5b4d6..e0da1fb43367 100644 --- a/core/css/icons.css +++ b/core/css/icons.css @@ -205,6 +205,14 @@ img.icon-loading-small-dark, object.icon-loading-small-dark, video.icon-loading- background-image: url('../img/actions/info-white.svg'); } +.icon-lock-closed { + background-image: url('../img/actions/lock-closed.svg'); +} + +.icon-lock-open { + background-image: url('../img/actions/lock-open.svg'); +} + .icon-logout { background-image: url('../img/actions/logout.svg'); } diff --git a/core/img/actions/lock-closed.svg b/core/img/actions/lock-closed.svg new file mode 100644 index 000000000000..6b105623e436 --- /dev/null +++ b/core/img/actions/lock-closed.svg @@ -0,0 +1 @@ + diff --git a/core/img/actions/lock-open.svg b/core/img/actions/lock-open.svg new file mode 100644 index 000000000000..63e0056acf0c --- /dev/null +++ b/core/img/actions/lock-open.svg @@ -0,0 +1 @@ + From f823ffb14f6c0ba10419fe0cb2416eb66a522e6f Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 24 Oct 2018 15:00:55 +0200 Subject: [PATCH 2/4] Improve locks tab + add JS tests Improve lock tab view code. Add tests for lock messages in file list Added tests for the lock notification messages when performing move, rename or delete. Add JS tests for file locking plugin Fixed XML parsing code to work with PhantomJS and potentially others by using more standard functions. Added filter to remove text nodes in case of paddings in the XML. Add missing translation. Don't show lock icon if lock array is empty Don't display sidebar lock tab when no locks set Properly trigger change event for activeLocks Adjust locked error message for upload --- apps/files/js/file-upload.js | 3 + apps/files/js/filelockplugin.js | 62 +++++-- apps/files/js/locktabview.js | 65 +++++-- apps/files/tests/js/filelistSpec.js | 45 ++++- apps/files/tests/js/filelockpluginSpec.js | 206 ++++++++++++++++++++++ apps/files/tests/js/locktabviewSpec.js | 187 ++++++++++++++++++++ core/js/files/client.js | 13 ++ core/js/tests/specs/files/clientSpec.js | 15 ++ 8 files changed, 567 insertions(+), 29 deletions(-) create mode 100644 apps/files/tests/js/filelockpluginSpec.js create mode 100644 apps/files/tests/js/locktabviewSpec.js diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 2cdaf15ebb4b..2056097b3825 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -1203,6 +1203,9 @@ OC.Uploader.prototype = _.extend({ OC.Notification.show(t('files', 'Target folder does not exist any more'), {type: 'error'}); } self.cancelUploads(); + } else if (status === 423) { + // not enough space + OC.Notification.show(t('files', 'The file {file} is currently locked, please try again later', {file: upload.getFileName()}), {type: 'error'}); } else if (status === 507) { // not enough space OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'}); diff --git a/apps/files/js/filelockplugin.js b/apps/files/js/filelockplugin.js index bfd1c9301dd0..5dcf9647f553 100644 --- a/apps/files/js/filelockplugin.js +++ b/apps/files/js/filelockplugin.js @@ -9,12 +9,56 @@ */ (function(OCA) { + var NS_DAV = OC.Files.Client.NS_DAV; var TEMPLATE_LOCK_STATUS_ACTION = - '' + + '' + '' + ''; + /** + * Parses an XML lock node + * + * @param {Node} xmlvalue node to parse + * @return {Object} parsed values in associative array + */ + function parseLockNode(xmlvalue) { + return { + lockscope: getChildNodeLocalName(xmlvalue.getElementsByTagNameNS(NS_DAV, 'lockscope')[0]), + locktype: getChildNodeLocalName(xmlvalue.getElementsByTagNameNS(NS_DAV, 'locktype')[0]), + lockroot: getHrefNodeContents(xmlvalue.getElementsByTagNameNS(NS_DAV, 'lockroot')[0]), + // string, as it can also be "infinite" + depth: xmlvalue.getElementsByTagNameNS(NS_DAV, 'depth')[0].textContent, + timeout: xmlvalue.getElementsByTagNameNS(NS_DAV, 'timeout')[0].textContent, + locktoken: getHrefNodeContents(xmlvalue.getElementsByTagNameNS(NS_DAV, 'locktoken')[0]), + owner: xmlvalue.getElementsByTagNameNS(NS_DAV, 'owner')[0].textContent + }; + } + + function getHrefNodeContents(node) { + var nodes = node.getElementsByTagNameNS(NS_DAV, 'href'); + if (!nodes.length) { + return null; + } + return nodes[0].textContent; + } + + /** + * Filter out text nodes from a list of XML nodes + * + * @param {Array.} nodes nodes to filter + * @return {Array.} filtered array of nodes + */ + function getChildNodeLocalName(node) { + for (var i = 0; i < node.childNodes.length; i++) { + // skip pure text nodes + if (node.childNodes[i].nodeType === 1) { + return node.childNodes[i].localName; + } + } + return null; + } + OCA.Files = OCA.Files || {}; /** @@ -32,7 +76,7 @@ fileList._createRow = function(fileData) { var $tr = oldCreateRow.apply(this, arguments); if (fileData.activeLocks) { - $tr.attr('data-activeLocks', JSON.stringify(fileData.activeLocks)); + $tr.attr('data-activelocks', JSON.stringify(fileData.activeLocks)); } return $tr; }; @@ -63,17 +107,9 @@ var activeLocks = props['{DAV:}lockdiscovery']; if (!_.isUndefined(activeLocks) && activeLocks !== '') { data.activeLocks = _.chain(activeLocks).filter(function(xmlvalue) { - return (xmlvalue.namespaceURI === OC.Files.Client.NS_DAV && xmlvalue.nodeName.split(':')[1] === 'activelock'); + return (xmlvalue.namespaceURI === NS_DAV && xmlvalue.nodeName.split(':')[1] === 'activelock'); }).map(function(xmlvalue) { - return { - lockscope: xmlvalue.getElementsByTagName('d:lockscope')[0].children[0].localName, - locktype: xmlvalue.getElementsByTagName('d:locktype')[0].children[0].localName, - lockroot: xmlvalue.getElementsByTagName('d:lockroot')[0].children[0].innerHTML, - depth: parseInt(xmlvalue.getElementsByTagName('d:depth')[0].innerHTML, 10), - timeout: xmlvalue.getElementsByTagName('d:timeout')[0].innerHTML, - locktoken: xmlvalue.getElementsByTagName('d:locktoken')[0].children[0].innerHTML, - owner: xmlvalue.getElementsByTagName('d:owner')[0].innerHTML - }; + return parseLockNode(xmlvalue); }).value(); } @@ -98,7 +134,7 @@ render: function(actionSpec, isDefault, context) { var $file = context.$file; var isLocked = $file.data('activelocks'); - if (isLocked) { + if (isLocked && isLocked.length > 0) { var $actionLink = $(self.renderLink()); context.$file.find('a.name>span.fileactions').append($actionLink); return $actionLink; diff --git a/apps/files/js/locktabview.js b/apps/files/js/locktabview.js index 7a934f5142d0..b101c2c28ca9 100644 --- a/apps/files/js/locktabview.js +++ b/apps/files/js/locktabview.js @@ -11,19 +11,36 @@ (function () { var TEMPLATE = '
    ' + - '
    ' + + '
    ' + '{{#each locks}}' + - '
    ' + - '
    {{owner}} has locked this resource via {{lockroot}}
    ' + + '
    ' + + '
    {{displayText}}
    ' + // TODO: no inline css '' + - '' + + '' + '
    ' + '{{else}}' + '
    {{emptyResultLabel}}
    ' + '{{/each}}' + ''; + function formatLocks(locks) { + var client = OC.Files.getClient(); + + return _.map(locks, function(lock, index) { + var path = client.getRelativePath(lock.lockroot) || lock.lockroot; + + // TODO: what if user in root doesn't match ? + + return { + index: index, + displayText: t('files', '{owner} has locked this resource via {path}', {owner: lock.owner, path: path}), + locktoken: lock.locktoken, + lockroot: lock.lockroot + }; + }); + } + /** * @memberof OCA.Files */ @@ -38,24 +55,28 @@ _onClickUnlock: function (event) { var self = this; - var lockToken = event.target.getAttribute('data-lock-token'); - var lockUrl = event.target.getAttribute('data-lock-root'); + var $target = $(event.target).closest('.lock-entry'); + var lockIndex = parseInt($target.attr('data-index'), 10); + + var currentLock = this.model.get('activeLocks')[lockIndex]; + // FIXME: move to FileInfoModel this.model._filesClient.getClient().request('UNLOCK', - lockUrl, + currentLock.lockroot, { - 'Lock-Token': lockToken + 'Lock-Token': currentLock.locktoken }).then(function (result) { if (result.status === 204) { - var locks = self.model.get('activeLocks'); - locks = locks.filter(function(item) { - return item.locktoken !== lockToken; - }); + // implicit clone of array else backbone doesn't fire change event + var locks = _.without(self.model.get('activeLocks') || [], currentLock); self.model.set('activeLocks', locks); self.render(); + } + else if (result.status === 403) { + OC.Notification.show(t('files', 'Could not unlock, please contact the lock owner {owner}', {owner: currentLock.owner})); } else { // TODO: add more information - OC.Notification.show('Unlock failed with status: ' + result.status); + OC.Notification.show(t('files', 'Unlock failed with status {status}', {status: result.status})); } }); }, @@ -76,11 +97,25 @@ * Renders this details view */ render: function () { + if (!this.model) { + return; + } this.$el.html(this.template({ emptyResultLabel: t('files', 'Resource is not locked'), - locks: this.model.get('activeLocks'), - model: this.model + locks: formatLocks(this.model.get('activeLocks')) })); + }, + + /** + * Returns whether the current tab is able to display + * the given file info, for example based on mime type. + * + * @param {OCA.Files.FileInfoModel} fileInfo file info model + * @return {bool} whether to display this tab + */ + canDisplay: function(fileInfo) { + // don't display if no lock is set + return fileInfo && fileInfo.get('activeLocks') && fileInfo.get('activeLocks').length > 0; } }); diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index ea428686cc59..8df835155e4e 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -567,6 +567,8 @@ describe('OCA.Files.FileList tests', function() { expect(fileList.$fileList.find('tr').length).toEqual(4); expect(notificationStub.calledTwice).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('Error deleting file "One.txt".'); + expect(notificationStub.getCall(1).args[0]).toEqual('Error deleting file "Two.jpg".'); }); it('remove file from list if delete call returned 404 not found', function() { fileList.setFiles(testFiles); @@ -574,13 +576,28 @@ describe('OCA.Files.FileList tests', function() { deferredDelete.reject(404); - // files are still in the list + // files are removed from the list expect(fileList.findFileEl('One.txt').length).toEqual(0); expect(fileList.findFileEl('Two.jpg').length).toEqual(0); expect(fileList.$fileList.find('tr').length).toEqual(2); expect(notificationStub.notCalled).toEqual(true); }); + it('shows notification about lock when file is locked', function() { + fileList.setFiles(testFiles); + doDelete(); + + deferredDelete.reject(423, {message: 'locked'}); + + // files are still in the list + expect(fileList.findFileEl('One.txt').length).toEqual(1); + expect(fileList.findFileEl('Two.jpg').length).toEqual(1); + expect(fileList.$fileList.find('tr').length).toEqual(4); + + expect(notificationStub.calledTwice).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('The file "One.txt" is locked and cannot be deleted.'); + expect(notificationStub.getCall(1).args[0]).toEqual('The file "Two.jpg" is locked and cannot be deleted.'); + }); }); describe('Renaming files', function() { var deferredRename; @@ -653,6 +670,20 @@ describe('OCA.Files.FileList tests', function() { expect(fileList.findFileEl('Tu_after_three.txt').length).toEqual(0); expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('Could not rename "One.txt"'); + }); + it('Shows notification in case file is locked', function() { + doRename(); + + deferredRename.reject(423, {message: 'locked'}); + + // element was reverted + expect(fileList.findFileEl('One.txt').length).toEqual(1); + expect(fileList.findFileEl('One.txt').index()).toEqual(1); // after somedir + expect(fileList.findFileEl('Tu_after_three.txt').length).toEqual(0); + + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('The file "One.txt" is locked and can not be renamed.'); }); it('Correctly updates file link after rename', function() { var $tr; @@ -836,6 +867,18 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.calledOnce).toEqual(true); expect(notificationStub.getCall(0).args[0]).toEqual('Could not move "One.txt"'); }); + it('Shows notification about lock if a file could not be moved due to lock', function() { + fileList.move('One.txt', '/somedir'); + + expect(moveStub.calledOnce).toEqual(true); + + deferredMove.reject(423, {message: 'locked'}); + + expect(fileList.findFileEl('One.txt').length).toEqual(1); + + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toEqual('Could not move "One.txt" because either the file or the target are locked.'); + }); it('Restores thumbnail if a file could not be moved', function() { fileList.move('One.txt', '/somedir'); diff --git a/apps/files/tests/js/filelockpluginSpec.js b/apps/files/tests/js/filelockpluginSpec.js new file mode 100644 index 000000000000..b273dab2ca9f --- /dev/null +++ b/apps/files/tests/js/filelockpluginSpec.js @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2018 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +/* global dav */ +describe('OCA.Files.LockPlugin tests', function() { + var fileList; + var testFiles; + var testLockData; + var currentUserStub; + + beforeEach(function() { + var $content = $('
    '); + $('#testArea').append($content); + // dummy file list + var $div = $( + '
    ' + + '' + + '' + + '' + + '
    ' + + '
    '); + $('#content').append($div); + + currentUserStub = sinon.stub(OC, 'getCurrentUser').returns({uid: 'currentuser'}); + + fileList = new OCA.Files.FileList($div); + OCA.Files.LockPlugin.attach(fileList); + + testLockData = { + lockscope: 'exclusive', + locktype: 'write', + lockroot: '/owncloud/remote.php/dav/files/currentuser/basepath', + depth: 'infinity', + timeout: 'Second-12345', + locktoken: 'tehtoken', + owner: 'lock owner' + }; + + testFiles = [{ + id: '1', + type: 'file', + name: 'One.txt', + path: '/subdir', + mimetype: 'text/plain', + size: 12, + permissions: OC.PERMISSION_ALL, + etag: 'abc', + activeLocks: [testLockData] + }]; + }); + afterEach(function() { + fileList.destroy(); + fileList = null; + currentUserStub.restore(); + }); + + describe('FileList extensions', function() { + it('sets active locks attribute', function() { + var $tr; + fileList.setFiles(testFiles); + $tr = fileList.$el.find('tbody tr:first'); + expect(JSON.parse($tr.attr('data-activeLocks'))).toEqual([testLockData]); + }); + }); + describe('elementToFile', function() { + it('returns lock data', function() { + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + var data = fileList.elementToFile($tr); + expect(data.activeLocks).toEqual([testLockData]); + }); + it('returns empty array when no lock data is present', function() { + delete testFiles[0].activeLocks; + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + var data = fileList.elementToFile($tr); + expect(data.activeLocks).toEqual([]); + }); + }); + describe('lock status file action', function() { + it('renders file action if lock exists', function() { + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + expect($tr.find('.action.action-lock-status').length).toEqual(1); + }); + it('does not render file action if no lock exists', function() { + delete testFiles[0].activeLocks; + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + expect($tr.find('.action.action-lock-status').length).toEqual(0); + }); + it('does not render file action if no lock exists (empty array)', function() { + testFiles[0].activeLocks = []; + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + expect($tr.find('.action.action-lock-status').length).toEqual(0); + }); + it('opens locks sidebar tab on click', function() { + var showDetailsViewStub = sinon.stub(OCA.Files.FileList.prototype, 'showDetailsView'); + fileList.setFiles(testFiles); + var $tr = fileList.findFileEl('One.txt'); + $tr.find('.action.action-lock-status').click(); + expect(showDetailsViewStub.calledOnce).toEqual(true); + expect(showDetailsViewStub.getCall(0).args[0]).toEqual('One.txt'); + expect(showDetailsViewStub.getCall(0).args[1]).toEqual('lockTabView'); + showDetailsViewStub.restore(); + }); + }); + describe('tab view', function() { + it('registers lock tab view', function() { + var registerTabViewStub = sinon.stub(OCA.Files.FileList.prototype, 'registerTabView'); + + fileList.destroy(); + fileList = new OCA.Files.FileList($('#content')); + OCA.Files.LockPlugin.attach(fileList); + + expect(registerTabViewStub.calledOnce).toEqual(true); + expect(registerTabViewStub.getCall(0).args[0].id).toEqual('lockTabView'); + + registerTabViewStub.restore(); + }); + }); + describe('Webdav requests', function() { + var requestDeferred; + var requestStub; + beforeEach(function() { + requestDeferred = new $.Deferred(); + requestStub = sinon.stub(dav.Client.prototype, 'propFind').returns(requestDeferred.promise()); + }); + afterEach(function() { + requestStub.restore(); + }); + + it('parses lock information from response XML to JSON', function(done) { + var xml = + '' + + ' ' + + ' /owncloud/remote.php/dav/files/currentuser/basepath' + + ' ' + + ' ' + + ' RDNVCK' + + ' ' + + ' HTTP/1.1 200 OK' + + ' ' + + ' ' + + ' ' + + ' /owncloud/remote.php/dav/files/currentuser/basepath/One.txt' + + ' ' + + ' ' + + ' RDNVCK' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' /owncloud/remote.php/dav/files/currentuser/basepath' + + ' ' + + ' infinity' + + ' Second-12345' + + ' ' + + ' tehtoken' + + ' ' + + ' lock owner' + + ' ' + + ' ' + + ' ' + + ' HTTP/1.1 200 OK' + + ' ' + + ' ' + + ''; + + xml = dav.Client.prototype.parseMultiStatus(xml); + var promise = fileList.reload(); + requestDeferred.resolve({ + status: 207, + body: xml + }); + + promise.then(function(status, response) { + var $tr = fileList.findFileEl('One.txt'); + var data = fileList.elementToFile($tr); + expect(data.activeLocks.length).toEqual(1); + expect(data.activeLocks[0]).toEqual(testLockData); + done(); + }); + }); + + it('sends lockdiscovery in PROPFIND request', function() { + fileList.reload(); + + expect(requestStub.calledOnce).toEqual(true); + expect(requestStub.getCall(0).args[1]).toContain('{DAV:}lockdiscovery'); + }); + }); +}); diff --git a/apps/files/tests/js/locktabviewSpec.js b/apps/files/tests/js/locktabviewSpec.js new file mode 100644 index 000000000000..dcafbc58e1e9 --- /dev/null +++ b/apps/files/tests/js/locktabviewSpec.js @@ -0,0 +1,187 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright Copyright (c) 2018 Vincent Petry +* +* 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 +* comment 3 of the License, or any later comment. +* +* 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 . +* +*/ + +/* global dav */ +describe('OCA.Files.LockTabView tests', function() { + var currentUserStub; + var notificationStub; + var view, fileInfoModel; + var lockData1; + var lockData2; + + beforeEach(function() { + currentUserStub = sinon.stub(OC, 'getCurrentUser').returns({uid: 'currentuser'}); + notificationStub = sinon.stub(OC.Notification, 'show'); + + view = new OCA.Files.LockTabView(); + lockData1 = { + lockscope: 'shared', + locktype: 'read', + lockroot: '/owncloud/remote.php/dav/files/currentuser/basepath', + depth: 'infinite', + timeout: 'Second-12345', + locktoken: 'tehtoken', + owner: 'some girl' + }; + lockData2 = { + lockscope: 'shared', + locktype: 'read', + lockroot: '/owncloud/remote.php/dav/files/currentuser/basepath/One.txt', + depth: '0', + timeout: 'Second-12345', + locktoken: 'anothertoken', + owner: 'some guy' + }; + fileInfoModel = new OCA.Files.FileInfoModel({ + id: '5', + name: 'One.txt', + mimetype: 'text/plain', + permissions: 31, + path: '/subdir', + size: 123456789, + etag: 'abcdefg', + mtime: Date.UTC(2016, 1, 0, 0, 0, 0), + activeLocks: [lockData1, lockData2], + }, { + filesClient: OC.Files.getClient() + }); + view.render(); + }); + afterEach(function() { + currentUserStub.restore(); + notificationStub.restore(); + view.remove(); + view = undefined; + }); + describe('visibility', function() { + it('displays tab when locks are set', function() { + expect(view.canDisplay(fileInfoModel)).toEqual(true); + }); + it('does not display tab when no locks are set', function() { + fileInfoModel.set('activeLocks', []); + expect(view.canDisplay(fileInfoModel)).toEqual(false); + }); + }); + describe('rendering', function() { + it('renders list of locks', function() { + view.setFileInfo(fileInfoModel); + expect(view.$('.lock-entry').length).toEqual(2); + var $lock1 = view.$('.lock-entry').eq(0); + var $lock2 = view.$('.lock-entry').eq(1); + + expect($lock1.first().text()).toEqual('some girl has locked this resource via /basepath'); + expect($lock2.first().text()).toEqual('some guy has locked this resource via /basepath/One.txt'); + }); + }); + describe('unlocking', function() { + var requestDeferred; + var requestStub; + + beforeEach(function() { + requestDeferred = new $.Deferred(); + requestStub = sinon.stub(dav.Client.prototype, 'request').returns(requestDeferred.promise()); + }); + afterEach(function() { + requestStub.restore(); + }); + + it('sends unlock request then updates model', function() { + var changeEvent = sinon.stub(); + + view.setFileInfo(fileInfoModel); + expect(view.$('.lock-entry').length).toEqual(2); + view.$('.lock-entry').eq(1).find('.unlock').click(); + + expect(requestStub.calledOnce).toEqual(true); + expect(requestStub.getCall(0).args[0]).toEqual('UNLOCK'); + expect(requestStub.getCall(0).args[1]).toEqual('/owncloud/remote.php/dav/files/currentuser/basepath/One.txt'); + expect(requestStub.getCall(0).args[2]).toEqual({'Lock-Token': 'anothertoken'}); + + fileInfoModel.on('change:activeLocks', changeEvent); + + requestDeferred.resolve({ + status: 204, + body: '' + }); + + // only one lock left + expect(view.$('.lock-entry').length).toEqual(1); + var $lock1 = view.$('.lock-entry').eq(0); + + expect($lock1.first().text()).toEqual('some girl has locked this resource via /basepath'); + + expect(fileInfoModel.get('activeLocks')).toEqual([lockData1]); + expect(changeEvent.calledOnce).toEqual(true); + }); + it('adjusts message when removing last lock', function() { + fileInfoModel.set('activeLocks', [lockData1]); + view.setFileInfo(fileInfoModel); + expect(view.$('.lock-entry').length).toEqual(1); + view.$('.lock-entry').eq(0).find('.unlock').click(); + + requestDeferred.resolve({ + status: 204, + body: '' + }); + + // only one lock left + expect(view.$('.lock-entry').length).toEqual(0); + + expect(view.$('.empty').text()).toEqual('Resource is not locked'); + + expect(fileInfoModel.get('activeLocks')).toEqual([]); + }); + it('displays message when unlock is forbidden', function() { + view.setFileInfo(fileInfoModel); + expect(view.$('.lock-entry').length).toEqual(2); + view.$('.lock-entry').eq(1).find('.unlock').click(); + + requestDeferred.resolve({ + status: 403, + body: '' + }); + + // locks left as is + expect(view.$('.lock-entry').length).toEqual(2); + expect(fileInfoModel.get('activeLocks')).toEqual([lockData1, lockData2]); + + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toContain('Could not unlock, please contact the lock owner some guy'); + }); + it('displays error message when unlock failed for unknown reason', function() { + view.setFileInfo(fileInfoModel); + expect(view.$('.lock-entry').length).toEqual(2); + view.$('.lock-entry').eq(1).find('.unlock').click(); + + requestDeferred.resolve({ + status: 500, + body: '' + }); + + // locks left as is + expect(view.$('.lock-entry').length).toEqual(2); + expect(fileInfoModel.get('activeLocks')).toEqual([lockData1, lockData2]); + + expect(notificationStub.calledOnce).toEqual(true); + expect(notificationStub.getCall(0).args[0]).toContain('Unlock failed with status 500'); + }); + }); +}); diff --git a/core/js/files/client.js b/core/js/files/client.js index ae1c76dc5987..e664d01569f5 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -243,6 +243,19 @@ return etag; }, + /** + * Returns the relative path from the given absolute path based + * on this client's base URL. + * + * @param {String} path href path + * @return {String} sub-path section or null if base path mismatches + * + * @since 10.1.0 + */ + getRelativePath: function(path) { + return this._extractPath(path); + }, + /** * Parse sub-path from href * diff --git a/core/js/tests/specs/files/clientSpec.js b/core/js/tests/specs/files/clientSpec.js index f33eb0186cbd..09010135933a 100644 --- a/core/js/tests/specs/files/clientSpec.js +++ b/core/js/tests/specs/files/clientSpec.js @@ -870,6 +870,21 @@ describe('OC.Files.Client tests', function() { }); }); + describe('getRelativePath', function() { + it('returns relative path when given path applies', function() { + expect(client.getRelativePath('/owncloud/remote.php/webdav/abc/def')) + .toEqual('/abc/def'); + }); + it('returns empty string when given path is equal to the root', function() { + expect(client.getRelativePath('/owncloud/remote.php/webdav/')) + .toEqual(''); + }); + it('returns null if given path is not relative to root', function() { + expect(client.getRelativePath('/other/remote.php/webdav/abc/def')) + .toEqual(null); + }); + }); + describe('default client', function() { var getCurrentUserStub; var propFindStub; From 3205e388ecc8ed724e177d5db9c7556d001c0854 Mon Sep 17 00:00:00 2001 From: Artur Neumann Date: Tue, 13 Nov 2018 16:53:52 +0545 Subject: [PATCH 3/4] Acceptance tests for webdav locking frontend --- .drone.yml | 9 + tests/acceptance/config/behat.yml | 12 + .../bootstrap/WebDavLockingContext.php | 147 ++++ .../bootstrap/WebUIWebDavLockingContext.php | 177 +++++ .../features/lib/FilesPageElement/FileRow.php | 63 ++ .../lib/FilesPageElement/LockDialog.php | 111 +++ .../LockDialogElement/LockEntry.php | 119 +++ .../features/webUIWebdavLocks/locks.feature | 721 ++++++++++++++++++ 8 files changed, 1359 insertions(+) create mode 100644 tests/acceptance/features/bootstrap/WebDavLockingContext.php create mode 100644 tests/acceptance/features/bootstrap/WebUIWebDavLockingContext.php create mode 100644 tests/acceptance/features/lib/FilesPageElement/LockDialog.php create mode 100644 tests/acceptance/features/lib/FilesPageElement/LockDialogElement/LockEntry.php create mode 100644 tests/acceptance/features/webUIWebdavLocks/locks.feature diff --git a/.drone.yml b/.drone.yml index 902693f5d251..d83d1e0a8d59 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1231,6 +1231,15 @@ matrix: OWNCLOUD_LOG: true INSTALL_TESTING-APP: true + - PHP_VERSION: 7.1 + TEST_SUITE: selenium + BEHAT_SUITE: webUIWebdavLocks + DB_TYPE: mariadb + USE_SERVER: true + INSTALL_SERVER: true + CHOWN_SERVER: true + OWNCLOUD_LOG: true + # caldav test - PHP_VERSION: 7.1 TEST_SUITE: caldav diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 6a83f84ab1ce..d26f84bf554c 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -379,6 +379,18 @@ default: - WebUISharingContext: - WebUIUsersContext: + webUIWebdavLocks: + paths: + - %paths.base%/../features/webUIWebdavLocks + contexts: + - FeatureContext: *common_feature_context_params + - WebDavLockingContext: + - WebUIFilesContext: + - WebUIGeneralContext: + - WebUILoginContext: + - WebUIWebDavLockingContext: + - WebUISharingContext: + extensions: jarnaiz\JUnitFormatter\JUnitFormatterExtension: filename: report.xml diff --git a/tests/acceptance/features/bootstrap/WebDavLockingContext.php b/tests/acceptance/features/bootstrap/WebDavLockingContext.php new file mode 100644 index 000000000000..8ddc490c2083 --- /dev/null +++ b/tests/acceptance/features/bootstrap/WebDavLockingContext.php @@ -0,0 +1,147 @@ + + * @copyright Copyright (c) 2018 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * This program 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 program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\TableNode; +use TestHelpers\WebDavHelper; + +require_once 'bootstrap.php'; + +/** + * context containing API steps needed for the locking mechanism of webdav + */ +class WebDavLockingContext implements Context { + + /** + * + * @var FeatureContext + */ + private $featureContext; + /** + * + * @var string[][] + */ + private $tokenOfLastLock = []; + + /** + * @When the user :user locks file/folder :file using the WebDAV API setting following properties + * @Given the user :user has locked file/folder :file setting following properties + * + * @param string $user + * @param string $file + * @param TableNode $properties table with no heading with | property | value | + * + * @return void + */ + public function lockFileUsingWebDavAPI($user, $file, TableNode $properties) { + $baseUrl = $this->featureContext->getBaseUrl(); + $password = $this->featureContext->getPasswordForUser($user); + $body + = "" . + " "; + $headers = []; + $propertiesRows = $properties->getRows(); + foreach ($propertiesRows as $property) { + if ($property[0] === "depth") { //depth is set in the header not in the xml + $headers["Depth"] = $property[1]; + } else { + $body .= ""; + } + } + + $body .= ""; + $response = WebDavHelper::makeDavRequest( + $baseUrl, $user, $password, "LOCK", $file, $headers, $body, null, + $this->featureContext->getDavPathVersion() + ); + $responseXml = $this->featureContext->getResponseXml($response); + $responseXml->registerXPathNamespace('d', 'DAV:'); + $xmlPart = $responseXml->xpath("//d:locktoken/d:href"); + $this->tokenOfLastLock[$user][$file] = (string)$xmlPart[0]; + } + + /** + * @When the user :user unlocks the last created lock of file/folder :file using the WebDAV API + * + * @param string $user + * @param string $file + * + * @return void + */ + public function unlockLastLockUsingWebDavAPI($user, $file) { + $baseUrl = $this->featureContext->getBaseUrl(); + $password = $this->featureContext->getPasswordForUser($user); + $headers = ["Lock-Token" => $this->tokenOfLastLock[$user][$file]]; + WebDavHelper::makeDavRequest( + $baseUrl, $user, $password, "UNLOCK", $file, $headers, null, null, + $this->featureContext->getDavPathVersion() + ); + } + + /** + * @Then :count locks should be reported for file/folder :file of user :user by the WebDAV API + * + * @param int $count + * @param string $file + * @param string $user + * + * @return void + */ + public function numberOfLockShouldBeReported($count, $file, $user) { + $baseUrl = $this->featureContext->getBaseUrl(); + $password = $this->featureContext->getPasswordForUser($user); + $body + = "" . + " " . + "" . + ""; + $response = WebDavHelper::makeDavRequest( + $baseUrl, $user, $password, "PROPFIND", $file, null, $body, null, + $this->featureContext->getDavPathVersion() + ); + $responseXml = $this->featureContext->getResponseXml($response); + $responseXml->registerXPathNamespace('d', 'DAV:'); + $xmlPart = $responseXml->xpath("//d:response//d:lockdiscovery/d:activelock"); + PHPUnit_Framework_Assert::assertCount( + (int)$count, $xmlPart, + "expected $count lock(s) for '$file' but found " . \count($xmlPart) + ); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope) { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->featureContext = $environment->getContext('FeatureContext'); + } +} diff --git a/tests/acceptance/features/bootstrap/WebUIWebDavLockingContext.php b/tests/acceptance/features/bootstrap/WebUIWebDavLockingContext.php new file mode 100644 index 000000000000..77d7e5dda81c --- /dev/null +++ b/tests/acceptance/features/bootstrap/WebUIWebDavLockingContext.php @@ -0,0 +1,177 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * This program 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 program. If not, see + * + */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\MinkExtension\Context\RawMinkContext; +use Page\FilesPage; +use Page\SharedWithYouPage; + +require_once 'bootstrap.php'; + +/** + * context containing webUI steps needed for the locking mechanism of webdav + */ +class WebUIWebDavLockingContext extends RawMinkContext implements Context { + + /** + * + * @var FilesPage + */ + private $filesPage; + + /** + * + * @var SharedWithYouPage + */ + private $sharedWithYouPage; + + /** + * + * @var WebUIGeneralContext + */ + private $webUIGeneralContext; + + private $uploadConflictDialogTitle = "file conflict"; + + /** + * WebUIFilesContext constructor. + * + * @param FilesPage $filesPage + * @param SharedWithYouPage $sharedWithYouPage + * + * @return void + */ + public function __construct( + FilesPage $filesPage, + SharedWithYouPage $sharedWithYouPage + ) { + $this->filesPage = $filesPage; + $this->sharedWithYouPage = $sharedWithYouPage; + } + + /** + * @When the user unlocks the lock no :lockNumber of file/folder :file on the webUI + * + * @param int $lockNumber + * @param string $file + * + * @return void + */ + public function unlockFileOnTheWebui($lockNumber, $file) { + $this->closeDetailsDialog(); + $pageObject = $this->webUIGeneralContext->getCurrentPageObject(); + $fileRow = $pageObject->findFileRowByName($file, $this->getSession()); + $fileRow->deleteLockByNumber($lockNumber, $this->getSession()); + } + + /** + * @Then file/folder :file should be marked as locked on the webUI + * + * @param string $file + * + * @return void + */ + public function theFileShouldBeMarkedAsLockedOnTheWebui($file) { + $this->closeDetailsDialog(); + $pageObject = $this->webUIGeneralContext->getCurrentPageObject(); + $fileRow = $pageObject->findFileRowByName($file, $this->getSession()); + PHPUnit_Framework_Assert::assertTrue( + $fileRow->getLockState(), + "'$file' should be marked as locked, but its not" + ); + } + + /** + * @Then file/folder :file should not be marked as locked on the webUI + * + * @param string $file + * + * @return void + */ + public function theFileShouldNotBeMarkedAsLockedOnTheWebui($file) { + $this->closeDetailsDialog(); + $pageObject = $this->webUIGeneralContext->getCurrentPageObject(); + $fileRow = $pageObject->findFileRowByName($file, $this->getSession()); + PHPUnit_Framework_Assert::assertFalse( + $fileRow->getLockState(), + "'$file' should not be marked as locked, but it is" + ); + } + + /** + * @Then file/folder :file should be marked as locked by user :lockedBy in the locks tab of the details panel on the webUI + * + * @param string $file + * @param string $lockedBy + * + * @return boolean + */ + public function theFileShouldBeMarkedAsLockedByUserInLocksTab( + $file, $lockedBy + ) { + $this->closeDetailsDialog(); + $pageObject = $this->webUIGeneralContext->getCurrentPageObject(); + $fileRow = $pageObject->findFileRowByName($file, $this->getSession()); + $lockDialog = $fileRow->openLockDialog(); + $locks = $lockDialog->getAllLocks(); + foreach ($locks as $lock) { + $locker = $lock->getLockingUser(); + if ($lockedBy === $locker) { + return true; + } + } + PHPUnit_Framework_Assert::fail("cannot find a lock set by $lockedBy"); + } + + /** + * This will run before EVERY scenario. + * It will set the properties for this object. + * + * @BeforeScenario @webUI + * + * @param BeforeScenarioScope $scope + * + * @return void + */ + public function before(BeforeScenarioScope $scope) { + // Get the environment + $environment = $scope->getEnvironment(); + // Get all the contexts you need in this context + $this->webUIGeneralContext = $environment->getContext('WebUIGeneralContext'); + } + + /** + * closes any open details dialog but ignores any error (e.g. no dialog open) + * + * @return void + */ + private function closeDetailsDialog() { + $pageObject = $this->webUIGeneralContext->getCurrentPageObject(); + try { + $pageObject->closeDetailsDialog(); + } catch (Exception $e) { + //ignoge if dialog cannot be closed + //most likely there is no dialog open + } + } +} diff --git a/tests/acceptance/features/lib/FilesPageElement/FileRow.php b/tests/acceptance/features/lib/FilesPageElement/FileRow.php index 7d3f2a1bb973..b3ad88b53ac5 100644 --- a/tests/acceptance/features/lib/FilesPageElement/FileRow.php +++ b/tests/acceptance/features/lib/FilesPageElement/FileRow.php @@ -56,10 +56,12 @@ class FileRow extends OwncloudPage { protected $notMarkedFavoriteXpath = "//span[contains(@class,'icon-star')]"; protected $markedFavoriteXpath = "//span[contains(@class,'icon-starred')]"; protected $shareStateXpath = "//span[@class='state']"; + protected $lockStateXpath = "//span[contains(@class,'icon-lock')]"; protected $sharerXpath = "//a[@data-action='Share']"; protected $acceptShareBtnXpath = "//span[@class='fileactions']//a[contains(@class,'accept')]"; protected $declinePendingShareBtnXpath = "//a[@data-action='Reject']"; protected $sharingDialogXpath = ".//div[@class='dialogContainer']"; + protected $lockDialogId = "lockTabView"; protected $highlightsXpath = "//div[@class='highlights']"; /** @@ -211,6 +213,52 @@ public function openSharingDialog(Session $session) { return $sharingDialogPage; } + /** + * opens the lock dialog that list all locks of the given file row + * + * @throws ElementNotFoundException + * @return LockDialog + */ + public function openLockDialog() { + $element = $this->rowElement->find("xpath", $this->lockStateXpath); + $this->assertElementNotNull( + $element, + __METHOD__ . + " xpath $this->lockStateXpath could not find lock button in row" + ); + $element->click(); + $lockDailogElement = $this->findById($this->lockDialogId); + $this->assertElementNotNull( + $lockDailogElement, + __METHOD__ . + " id $this->lockDialogId could not find lock dialog" + ); + $this->waitFor( + STANDARD_UI_WAIT_TIMEOUT_MILLISEC / 1000, [$lockDailogElement, 'isVisible'] + ); + + /** + * + * @var LockDialog $lockDialog + */ + $lockDialog = $this->getPage("FilesPageElement\\LockDialog"); + $lockDialog->setElement($lockDailogElement); + return $lockDialog; + } + + /** + * deletes a lock by its order in the list + * + * @param int $number + * @param Session $session + * + * @return void + */ + public function deleteLockByNumber($number, Session $session) { + $lockDialog = $this->openLockDialog(); + $lockDialog->deleteLockByNumber($number, $session); + } + /** * finds the input field to rename the file/folder * @@ -419,6 +467,21 @@ public function unmarkFavorite() { $element->click(); } + /** + * returns the lock state + * + * @throws ElementNotFoundException + * + * @return bool + */ + public function getLockState() { + $element = $this->rowElement->find("xpath", $this->lockStateXpath); + if ($element === null) { + return false; + } + return $element->isVisible(); + } + /** * returns the share state (only works on the "Shared with you" page) * diff --git a/tests/acceptance/features/lib/FilesPageElement/LockDialog.php b/tests/acceptance/features/lib/FilesPageElement/LockDialog.php new file mode 100644 index 000000000000..e89718935798 --- /dev/null +++ b/tests/acceptance/features/lib/FilesPageElement/LockDialog.php @@ -0,0 +1,111 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * This program 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 program. If not, see + * + */ + +namespace Page\FilesPageElement; + +use Behat\Mink\Session; +use Behat\Mink\Element\NodeElement; +use Page\OwncloudPage; +use Page\FilesPageElement\LockDialogElement\LockEntry; + +/** + * The Lock Dialog, lists all the locks and gives a posibility to delete them + * + */ +class LockDialog extends OwncloudPage { + + /** + * + * @var string $path + */ + protected $path = '/index.php/apps/files/'; + /** + * @var NodeElement of this dialog + */ + protected $dialogElement; + protected $lockEntriesXpath = "//div[@class='lock-entry']"; + + /** + * sets the NodeElement for the current dialog + * a little bit like __construct() but as we access this "sub-page-object" + * from an other Page Object by + * $this->getPage("FilesPageElement\\LockDialog") + * there is no real __construct() that can take arguments + * + * @param NodeElement $dialogElement + * + * @return void + */ + public function setElement(NodeElement $dialogElement) { + $this->dialogElement = $dialogElement; + } + + /** + * + * @return LockEntry[] + */ + public function getAllLocks() { + $lockEntryElements = $this->dialogElement->findAll("xpath", $this->lockEntriesXpath); + $lockEntries = []; + foreach ($lockEntryElements as $lockEntryElement) { + /** + * + * @var LockEntry $lockEntry + */ + $lockEntry = $this->getPage("FilesPageElement\\LockDialogElement\\LockEntry"); + $lockEntry->setElement($lockEntryElement); + $lockEntries[] = $lockEntry; + } + return $lockEntries; + } + + /** + * find the lock by it order in the list + * + * @param int $number + * @param Session $session + * + * @return void + */ + public function deleteLockByNumber($number, Session $session) { + $lockEntryElements = $this->dialogElement->findAll("xpath", $this->lockEntriesXpath); + /** + * + * @var LockEntry $lockEntry + */ + $lockEntry = $this->getPage("FilesPageElement\\LockDialogElement\\LockEntry"); + $lockEntry->setElement($lockEntryElements[$number - 1]); + $lockEntry->delete($session); + } + + /** + * + * @param string $user + * @param string $resource + * + * @return void + */ + public function deleteLockByUserAndLockingResource($user, $resource) { + throw new \Exception("not yet implemented"); + } +} diff --git a/tests/acceptance/features/lib/FilesPageElement/LockDialogElement/LockEntry.php b/tests/acceptance/features/lib/FilesPageElement/LockDialogElement/LockEntry.php new file mode 100644 index 000000000000..c01aa8bc12ae --- /dev/null +++ b/tests/acceptance/features/lib/FilesPageElement/LockDialogElement/LockEntry.php @@ -0,0 +1,119 @@ + + * @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, + * as published by the Free Software Foundation; + * either version 3 of the License, or any later version. + * + * This program 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 program. If not, see + * + */ + +namespace Page\FilesPageElement\LockDialogElement; + +use Behat\Mink\Session; +use Behat\Mink\Element\NodeElement; +use Page\OwncloudPage; + +/** + * A single Lock Entry + * + */ +class LockEntry extends OwncloudPage { + + /** + * + * @var string $path + */ + protected $path = '/index.php/apps/files/'; + /** + * @var NodeElement of this dialog + */ + protected $lockElement; + protected $lockDescriptionXpath = "/div"; + protected $lockingUserExtractPattern = "/^(.*)\shas locked/"; + protected $unlockButtonXpath = "/a[@class='unlock']"; + + /** + * sets the NodeElement for the current entry + * a little bit like __construct() but as we access this "sub-page-object" + * from an other Page Object by + * $this->getPage("FilesPageElement\\LockDialog") + * there is no real __construct() that can take arguments + * + * @param NodeElement $lockElement + * + * @return void + */ + public function setElement(NodeElement $lockElement) { + $this->lockElement = $lockElement; + } + + /** + * delete the lock + * + * @param Session $session + * + * @return void + */ + public function delete(Session $session) { + $unlockButton = $this->lockElement->find("xpath", $this->unlockButtonXpath); + $this->assertElementNotNull( + $unlockButton, + __METHOD__ . " xpath $this->unlockButtonXpath " . + " cannot find unlock button" + ); + $unlockButton->click(); + $this->waitForAjaxCallsToStartAndFinish($session); + } + + /** + * gets the user that has locked the ressource + * + * @throws \Exception + * + * @return string + */ + public function getLockingUser() { + $lockDescriptionElement = $this->lockElement->find( + "xpath", $this->lockDescriptionXpath + ); + $this->assertElementNotNull( + $lockDescriptionElement, + __METHOD__ . " xpath $this->lockDescriptionXpath " . + " cannot find lock description element" + ); + $matches = []; + if (\preg_match( + $this->lockingUserExtractPattern, + $lockDescriptionElement->getText(), + $matches + ) + ) { + return $matches [1]; + } + throw new \Exception( + "could not extract locking user name from lock description '" . + $lockDescriptionElement->getText() . "'" + ); + } + + /** + * @return void + */ + public function getLockingResource() { + throw new \Exception("not yet implemented"); + } +} diff --git a/tests/acceptance/features/webUIWebdavLocks/locks.feature b/tests/acceptance/features/webUIWebdavLocks/locks.feature new file mode 100644 index 000000000000..fadf39283df9 --- /dev/null +++ b/tests/acceptance/features/webUIWebdavLocks/locks.feature @@ -0,0 +1,721 @@ +@webUI @insulated @disablePreviews +Feature: Locks + As a user + I would like to be able to delete locks of files and folders + So that I can access files with locks that have not been cleared + + Background: + #do not set email, see bugs in https://github.com/owncloud/core/pull/32250#issuecomment-434615887 + Given these users have been created: + |username | + |brand-new-user| + And user "brand-new-user" has logged in using the webUI + + Scenario: setting a lock shows the lock symbols at the correct files/folders + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "data.zip" setting following properties + | lockscope | exclusive | + When the user browses to the files page + Then folder "simple-folder" should be marked as locked on the webUI + And folder "simple-folder" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But folder "simple-empty-folder" should not be marked as locked on the webUI + And file "data.zip" should be marked as locked on the webUI + And file "data.zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But file "data.tar.gz" should not be marked as locked on the webUI + + Scenario: setting a lock shows the display name of a user in the locking details + Given these users have been created: + |username |displayname | + |user-with-display-name|My fancy name| + Given the user "user-with-display-name" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "user-with-display-name" has locked file "data.zip" setting following properties + | lockscope | exclusive | + When the user re-logs in with username "user-with-display-name" and password "%regular%" using the webUI + And folder "simple-folder" should be marked as locked by user "My fancy name" in the locks tab of the details panel on the webUI + And file "data.zip" should be marked as locked by user "My fancy name" in the locks tab of the details panel on the webUI + + Scenario: setting a lock shows the display name of a user in the locking details (user has set email address) + Given these users have been created: + |username |displayname |email | + |user-with-display-name|My fancy name|mail@oc.org| + Given the user "user-with-display-name" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "user-with-display-name" has locked file "data.zip" setting following properties + | lockscope | exclusive | + When the user re-logs in with username "user-with-display-name" and password "%regular%" using the webUI + And folder "simple-folder" should be marked as locked by user "My fancy name (mail@oc.org)" in the locks tab of the details panel on the webUI + And file "data.zip" should be marked as locked by user "My fancy name (mail@oc.org)" in the locks tab of the details panel on the webUI + + Scenario: setting a lock shows the user name of a user in the locking details (user has set email address) + Given these users have been created: + |username |email | + |user-with-email |mail@oc.org| + Given the user "user-with-email" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "user-with-email" has locked file "data.zip" setting following properties + | lockscope | exclusive | + When the user re-logs in with username "user-with-email" and password "%regular%" using the webUI + And folder "simple-folder" should be marked as locked by user "user-with-email (mail@oc.org)" in the locks tab of the details panel on the webUI + And file "data.zip" should be marked as locked by user "user-with-email (mail@oc.org)" in the locks tab of the details panel on the webUI + + Scenario: setting a lock shows the lock symbols at the correct files/folders on the favorites page + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "data.zip" setting following properties + | lockscope | exclusive | + When the user marks folder "simple-folder" as favorite using the webUI + And the user marks folder "simple-empty-folder" as favorite using the webUI + And the user marks file "data.zip" as favorite using the webUI + And the user marks file "data.tar.gz" as favorite using the webUI + And the user browses to the favorites page + Then folder "simple-folder" should be marked as locked on the webUI + And folder "simple-folder" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But folder "simple-empty-folder" should not be marked as locked on the webUI + And file "data.zip" should be marked as locked on the webUI + And file "data.zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But file "data.tar.gz" should not be marked as locked on the webUI + + @skip @issue-33867 + Scenario: setting a lock shows the lock symbols at the correct files/folders on the shared-with-others page + Given these users have been created: + |username | + |receiver | + And the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "data.zip" setting following properties + | lockscope | exclusive | + And user "brand-new-user" has shared file "data.zip" with user "receiver" + And user "brand-new-user" has shared file "data.tar.gz" with user "receiver" + And user "brand-new-user" has shared folder "simple-folder" with user "receiver" + And user "brand-new-user" has shared folder "simple-empty-folder" with user "receiver" + When the user browses to the shared-with-others page + Then folder "simple-folder" should be marked as locked on the webUI + And folder "simple-folder" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But folder "simple-empty-folder" should not be marked as locked on the webUI + And file "data.zip" should be marked as locked on the webUI + And file "data.zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But file "data.tar.gz" should not be marked as locked on the webUI + + @skip @issue-33867 + Scenario: setting a lock shows the lock symbols at the correct files/folders on the shared-by-link page + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "data.zip" setting following properties + | lockscope | exclusive | + And user "brand-new-user" has created a public link share with settings + | path | data.zip | + And user "brand-new-user" has created a public link share with settings + | path | data.tar.gz | + And user "brand-new-user" has created a public link share with settings + | path | simple-folder | + And user "brand-new-user" has created a public link share with settings + | path | simple-empty-folder | + When the user browses to the shared-by-link page + Then folder "simple-folder" should be marked as locked on the webUI + And folder "simple-folder" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But folder "simple-empty-folder" should not be marked as locked on the webUI + And file "data.zip" should be marked as locked on the webUI + And file "data.zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But file "data.tar.gz" should not be marked as locked on the webUI + + @skip @issue-33867 + Scenario: setting a lock shows the lock symbols at the correct files/folders on the shared-with-you page + Given these users have been created: + |username | + |sharer | + And the user "sharer" has locked folder "simple-folder" setting following properties + | lockscope | shared | + And the user "sharer" has locked file "data.zip" setting following properties + | lockscope | exclusive | + And user "sharer" has shared file "data.zip" with user "brand-new-user" + And user "sharer" has shared file "data.tar.gz" with user "brand-new-user" + And user "sharer" has shared folder "simple-folder" with user "brand-new-user" + And user "sharer" has shared folder "simple-empty-folder" with user "brand-new-user" + When the user browses to the shared-with-you page + Then folder "simple-folder (2)" should be marked as locked on the webUI + And folder "simple-folder (2)" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But folder "simple-empty-folder (2)" should not be marked as locked on the webUI + And file "data (2).zip" should be marked as locked on the webUI + And file "data (2).zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But file "data (2).tar.gz" should not be marked as locked on the webUI + + Scenario: clicking other tabs does not change the lock symbol + When the user opens the share dialog for folder "simple-folder" + Then folder "simple-folder" should not be marked as locked on the webUI + + Scenario: lock set on a shared file shows the lock information for all involved users + Given these users have been created: + |username | + |sharer | + |receiver | + |receiver2 | + And group "receiver-group" has been created + And user "receiver2" has been added to group "receiver-group" + And user "sharer" has shared file "data.zip" with user "receiver" + And user "sharer" has shared file "data.tar.gz" with group "receiver-group" + And user "receiver" has shared file "data (2).zip" with user "brand-new-user" + And the user "sharer" has locked file "data.zip" setting following properties + | lockscope | shared | + And the user "receiver" has locked file "data (2).zip" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "data (2).zip" setting following properties + | lockscope | shared | + And the user "receiver2" has locked file "data.tar (2).gz" setting following properties + | lockscope | shared | + When the user browses to the files page + Then file "data (2).zip" should be marked as locked on the webUI + And file "data (2).zip" should be marked as locked by user "sharer" in the locks tab of the details panel on the webUI + And file "data (2).zip" should be marked as locked by user "receiver" in the locks tab of the details panel on the webUI + And file "data (2).zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + But file "data.zip" should not be marked as locked on the webUI + When the user re-logs in as "sharer" using the webUI + Then file "data.zip" should be marked as locked on the webUI + And file "data.zip" should be marked as locked by user "sharer" in the locks tab of the details panel on the webUI + And file "data.zip" should be marked as locked by user "receiver" in the locks tab of the details panel on the webUI + And file "data.zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And file "data.tar.gz" should be marked as locked on the webUI + And file "data.tar.gz" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + When the user re-logs in as "receiver2" using the webUI + Then file "data.tar (2).gz" should be marked as locked on the webUI + And file "data.tar (2).gz" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + + Scenario: setting a lock on a folder shows the symbols at the sub-elements + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | shared | + When the user opens folder "simple-folder" using the webUI + Then folder "simple-empty-folder" should be marked as locked on the webUI + And folder "simple-empty-folder" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And file "data.zip" should be marked as locked on the webUI + And file "data.zip" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + + Scenario: setting a depth:0 lock on a folder does not shows the symbols at the sub-elements + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | depth | 0 | + When the user browses to the files page + Then folder "simple-folder" should be marked as locked on the webUI + When the user opens folder "simple-folder" using the webUI + Then folder "simple-empty-folder" should not be marked as locked on the webUI + And file "data.zip" should not be marked as locked on the webUI + + Scenario: unlocking by webDAV deletes the lock symbols at the correct files/folders + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | shared | + When the user "brand-new-user" unlocks the last created lock of folder "simple-folder" using the WebDAV API + And the user browses to the files page + Then folder "simple-folder" should not be marked as locked on the webUI + + Scenario Outline: deleting the only remaining lock of a file/folder + Given the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | | + And the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user unlocks the lock no 1 of file "lorem.txt" on the webUI + And the user unlocks the lock no 1 of folder "simple-folder" on the webUI + Then file "lorem.txt" should not be marked as locked on the webUI + And folder "simple-folder" should not be marked as locked on the webUI + And 0 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for folder "simple-folder" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: deleting the only remaining lock of a file/folder and reloading the page + Given the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | exclusive | + And the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | exclusive | + And the user has browsed to the files page + When the user unlocks the lock no 1 of file "lorem.txt" on the webUI + And the user unlocks the lock no 1 of folder "simple-folder" on the webUI + And the user reloads the current page of the webUI + Then file "lorem.txt" should not be marked as locked on the webUI + And folder "simple-folder" should not be marked as locked on the webUI + And 0 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for folder "simple-folder" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: deleting the only remaining lock of a folder by deleting it from a file in folder + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + And the user has opened folder "simple-folder" using the webUI + When the user unlocks the lock no 1 of file "lorem.txt" on the webUI + And the user reloads the current page of the webUI + Then file "lorem.txt" should not be marked as locked on the webUI + And folder "simple-empty-folder" should not be marked as locked on the webUI + When the user browses to the files page + Then folder "simple-folder" should not be marked as locked on the webUI + And 0 locks should be reported for folder "simple-folder" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for folder "simple-folder/simple-empty-folder" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: deleting the only remaining lock of a folder by deleting it from a file in folder and reloading the page + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + And the user has opened folder "simple-folder" using the webUI + When the user unlocks the lock no 1 of file "lorem.txt" on the webUI + And the user reloads the current page of the webUI + Then file "lorem.txt" should not be marked as locked on the webUI + And folder "simple-empty-folder" should not be marked as locked on the webUI + When the user browses to the files page + Then folder "simple-folder" should not be marked as locked on the webUI + And 0 locks should be reported for folder "simple-folder" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for folder "simple-folder/simple-empty-folder" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario: deleting the first one of multiple shared locks on the webUI + Given these users have been created: + |username | + |receiver1 | + |receiver2 | + And the user has created folder "/FOLDER_TO_SHARE" + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver1" + And user "brand-new-user" has shared folder "/FOLDER_TO_SHARE" with user "receiver1" + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver2" + And user "brand-new-user" has shared folder "/FOLDER_TO_SHARE" with user "receiver2" + And the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user "receiver1" has locked file "lorem (2).txt" setting following properties + | lockscope | shared | + And the user "receiver1" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user "receiver2" has locked file "lorem (2).txt" setting following properties + | lockscope | shared | + And the user "receiver2" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user has browsed to the files page + When the user unlocks the lock no 1 of file "lorem.txt" on the webUI + Then file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And file "lorem.txt" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + And 2 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + And 2 locks should be reported for file "lorem (2).txt" of user "receiver1" by the WebDAV API + And 2 locks should be reported for file "lorem (2).txt" of user "receiver2" by the WebDAV API + When the user unlocks the lock no 1 of folder "FOLDER_TO_SHARE" on the webUI + Then folder "FOLDER_TO_SHARE" should be marked as locked on the webUI + And folder "FOLDER_TO_SHARE" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And folder "FOLDER_TO_SHARE" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "brand-new-user" by the WebDAV API + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "receiver1" by the WebDAV API + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "receiver2" by the WebDAV API + + Scenario: deleting the second one of multiple shared locks on the webUI + Given these users have been created: + |username | + |receiver1 | + |receiver2 | + And the user has created folder "/FOLDER_TO_SHARE" + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver1" + And user "brand-new-user" has shared folder "/FOLDER_TO_SHARE" with user "receiver1" + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver2" + And user "brand-new-user" has shared folder "/FOLDER_TO_SHARE" with user "receiver2" + And the user "receiver1" has locked file "lorem (2).txt" setting following properties + | lockscope | shared | + And the user "receiver1" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user "receiver2" has locked file "lorem (2).txt" setting following properties + | lockscope | shared | + And the user "receiver2" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user has browsed to the files page + When the user unlocks the lock no 2 of file "lorem.txt" on the webUI + Then file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And file "lorem.txt" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + And 2 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + And 2 locks should be reported for file "lorem (2).txt" of user "receiver1" by the WebDAV API + And 2 locks should be reported for file "lorem (2).txt" of user "receiver2" by the WebDAV API + When the user unlocks the lock no 2 of folder "FOLDER_TO_SHARE" on the webUI + Then folder "FOLDER_TO_SHARE" should be marked as locked on the webUI + And folder "FOLDER_TO_SHARE" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And folder "FOLDER_TO_SHARE" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "brand-new-user" by the WebDAV API + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "receiver1" by the WebDAV API + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "receiver2" by the WebDAV API + + Scenario: deleting the last one of multiple shared locks on the webUI + Given these users have been created: + |username | + |receiver1 | + |receiver2 | + And the user has created folder "/FOLDER_TO_SHARE" + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver1" + And user "brand-new-user" has shared folder "/FOLDER_TO_SHARE" with user "receiver1" + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver2" + And user "brand-new-user" has shared folder "/FOLDER_TO_SHARE" with user "receiver2" + And the user "receiver1" has locked file "lorem (2).txt" setting following properties + | lockscope | shared | + And the user "receiver1" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user "receiver2" has locked file "lorem (2).txt" setting following properties + | lockscope | shared | + And the user "receiver2" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | shared | + And the user "brand-new-user" has locked folder "FOLDER_TO_SHARE" setting following properties + | lockscope | shared | + And the user has browsed to the files page + When the user unlocks the lock no 3 of file "lorem.txt" on the webUI + Then file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And file "lorem.txt" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + And 2 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + And 2 locks should be reported for file "lorem (2).txt" of user "receiver1" by the WebDAV API + And 2 locks should be reported for file "lorem (2).txt" of user "receiver2" by the WebDAV API + When the user unlocks the lock no 3 of folder "FOLDER_TO_SHARE" on the webUI + Then folder "FOLDER_TO_SHARE" should be marked as locked on the webUI + And folder "FOLDER_TO_SHARE" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And folder "FOLDER_TO_SHARE" should be marked as locked by user "receiver2" in the locks tab of the details panel on the webUI + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "brand-new-user" by the WebDAV API + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "receiver1" by the WebDAV API + And 2 locks should be reported for folder "FOLDER_TO_SHARE" of user "receiver2" by the WebDAV API + + Scenario Outline: deleting a lock that was created by an other user + Given these users have been created: + |username | + |receiver1 | + And user "brand-new-user" has shared file "/lorem.txt" with user "receiver1" + And the user "receiver1" has locked file "lorem (2).txt" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user unlocks the lock no 1 of file "lorem.txt" on the webUI + Then notifications should be displayed on the webUI with the text + | Could not unlock, please contact the lock owner receiver1 | + And file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "receiver1" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + And 1 locks should be reported for file "lorem (2).txt" of user "receiver1" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: deleting a locked file + Given the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user deletes file "lorem.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file "lorem.txt" is locked and cannot be deleted. | + And as "brand-new-user" file "lorem.txt" should exist + And file "lorem.txt" should be listed on the webUI + And file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: moving a locked file + Given the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user moves file "lorem.txt" into folder "simple-empty-folder" using the webUI + Then notifications should be displayed on the webUI with the text + | Could not move "lorem.txt" because either the file or the target are locked. | + And as "brand-new-user" file "lorem.txt" should exist + And file "lorem.txt" should be listed on the webUI + And file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: moving a file trying to overwrite a locked file + Given the user "brand-new-user" has locked file "/simple-folder/lorem.txt" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user moves file "lorem.txt" into folder "simple-folder" using the webUI + Then notifications should be displayed on the webUI with the text + | Could not move "lorem.txt" because either the file or the target are locked. | + And as "brand-new-user" file "lorem.txt" should exist + And file "lorem.txt" should be listed on the webUI + And file "lorem.txt" should not be marked as locked on the webUI + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: moving a file into a locked folder + Given the user "brand-new-user" has locked file "/simple-empty-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user moves file "lorem.txt" into folder "simple-empty-folder" using the webUI + And as "brand-new-user" file "/simple-empty-folder/lorem.txt" should exist + And as "brand-new-user" file "lorem.txt" should not exist + And file "lorem.txt" should not be listed on the webUI + And 1 locks should be reported for file "/simple-empty-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: renaming of a locked file + Given the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user renames file "lorem.txt" to "a-renamed-file.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file "lorem.txt" is locked and can not be renamed. | + And as "brand-new-user" file "lorem.txt" should exist + And file "lorem.txt" should be listed on the webUI + And file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: uploading a file, trying to overwrite a locked file + Given the user "brand-new-user" has locked file "lorem.txt" setting following properties + | lockscope | | + And the user has browsed to the files page + When the user uploads overwriting file "lorem.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file lorem.txt is currently locked, please try again later | + And the content of "lorem.txt" should not have changed + And file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: uploading a file, trying to overwrite a file in a locked folder + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has opened folder "simple-folder" using the webUI + When the user uploads overwriting file "lorem.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file lorem.txt is currently locked, please try again later | + And the content of "lorem.txt" should not have changed + And file "lorem.txt" should be marked as locked on the webUI + And file "lorem.txt" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: uploading a new file into a locked folder + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has opened folder "simple-folder" using the webUI + When the user uploads file "new-lorem.txt" using the webUI + Then file "new-lorem.txt" should be marked as locked on the webUI + And file "new-lorem.txt" should be marked as locked by user "brand-new-user" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "simple-folder/new-lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: deleting a file in a public share of a locked folder + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + And the user has created a new public link for folder "simple-folder" using the webUI with + | permission | read-write | + When the public accesses the last created public link using the webUI + And the user deletes folder "lorem.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file "lorem.txt" is locked and cannot be deleted. | + And as "brand-new-user" file "simple-folder/lorem.txt" should exist + And 1 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: renaming a file in a public share of a locked folder + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + And the user has created a new public link for folder "simple-folder" using the webUI with + | permission | read-write | + When the public accesses the last created public link using the webUI + And the user renames file "lorem.txt" to "a-renamed-file.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file "lorem.txt" is locked and can not be renamed. | + And as "brand-new-user" file "simple-folder/lorem.txt" should exist + And as "brand-new-user" file "simple-folder/a-renamed-file.txt" should not exist + And 1 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: moving a locked file into an other folder in a public share + Given the user "brand-new-user" has locked file "simple-folder/lorem.txt" setting following properties + | lockscope | | + And the user has browsed to the files page + And the user has created a new public link for folder "simple-folder" using the webUI with + | permission | read-write | + When the public accesses the last created public link using the webUI + And the user moves file "lorem.txt" into folder "simple-empty-folder" using the webUI + Then notifications should be displayed on the webUI with the text + | Could not move "lorem.txt" because either the file or the target are locked. | + And as "brand-new-user" file "simple-folder/lorem.txt" should exist + And as "brand-new-user" file "simple-folder/simple-empty-folder/lorem.txt" should not exist + And file "lorem.txt" should be listed on the webUI + And 1 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: uploading a file, trying to overwrite a file in a locked folder in a public share + Given the user "brand-new-user" has locked folder "simple-folder" setting following properties + | lockscope | | + And the user has browsed to the files page + And the user has created a new public link for folder "simple-folder" using the webUI with + | permission | read-write | + When the public accesses the last created public link using the webUI + And the user uploads overwriting file "lorem.txt" using the webUI + Then notifications should be displayed on the webUI with the text + | The file lorem.txt is currently locked, please try again later | + And the content of "simple-folder/lorem.txt" should not have changed + And 1 locks should be reported for file "simple-folder/lorem.txt" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: decline locked folder + Given these users have been created: + |username | + |sharer | + And user "sharer" has created folder "/to-share-folder" + And the user "sharer" has locked folder "to-share-folder" setting following properties + | lockscope | | + And user "sharer" has shared folder "to-share-folder" with user "brand-new-user" + When the user declines share "to-share-folder" offered by user "sharer" using the webUI + And the user has browsed to the files page + Then folder "to-share-folder" should not be listed on the webUI + And 1 locks should be reported for file "to-share-folder" of user "sharer" by the WebDAV API + And 0 locks should be reported for file "to-share-folder" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: accept previously declined locked folder + Given these users have been created: + |username | + |sharer | + And user "sharer" has created folder "/to-share-folder" + And the user "sharer" has locked folder "to-share-folder" setting following properties + | lockscope | | + And user "sharer" has shared folder "to-share-folder" with user "brand-new-user" + When the user declines share "to-share-folder" offered by user "sharer" using the webUI + And the user accepts share "to-share-folder" offered by user "sharer" using the webUI + And the user has browsed to the files page + Then folder "to-share-folder" should be marked as locked on the webUI + And folder "to-share-folder" should be marked as locked by user "sharer" in the locks tab of the details panel on the webUI + And 1 locks should be reported for file "to-share-folder" of user "sharer" by the WebDAV API + And 1 locks should be reported for file "to-share-folder" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + Scenario Outline: accept previously declined locked folder but create a folder with same name in between + Given these users have been created: + |username | + |sharer | + And user "sharer" has created folder "/to-share-folder" + And the user "sharer" has locked folder "to-share-folder" setting following properties + | lockscope | | + And user "sharer" has shared folder "to-share-folder" with user "brand-new-user" + When the user declines share "to-share-folder" offered by user "sharer" using the webUI + And user "brand-new-user" creates folder "to-share-folder" using the WebDAV API + And the user accepts share "to-share-folder" offered by user "sharer" using the webUI + And the user has browsed to the files page + Then folder "to-share-folder (2)" should be marked as locked on the webUI + And folder "to-share-folder (2)" should be marked as locked by user "sharer" in the locks tab of the details panel on the webUI + But folder "to-share-folder" should not be marked as locked on the webUI + And 1 locks should be reported for file "to-share-folder" of user "sharer" by the WebDAV API + And 1 locks should be reported for file "to-share-folder (2)" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + @skip @issue-33885 + Scenario Outline: creating a subfolder structure that is the same as the structure of a declined & locked share + Given these users have been created: + |username | + |sharer | + And user "sharer" has created folder "/parent" + And user "sharer" has created folder "/parent/subfolder" + And the user "sharer" has locked folder "parent" setting following properties + | lockscope | | + And user "sharer" has shared folder "parent" with user "brand-new-user" + When the user declines share "parent" offered by user "sharer" using the webUI + And user "brand-new-user" creates folder "parent" using the WebDAV API + And user "brand-new-user" creates folder "parent/subfolder" using the WebDAV API + And the user has browsed to the files page + Then folder "parent" should not be marked as locked on the webUI + When the user opens folder "parent" using the webUI + Then folder "subfolder" should not be marked as locked on the webUI + And 0 locks should be reported for file "parent" of user "brand-new-user" by the WebDAV API + And 0 locks should be reported for file "parent/subfolder" of user "brand-new-user" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | + + @skip @issue-33847 + Scenario Outline: unsharing a locked file/folder + Given these users have been created: + |username | + |sharer | + And the user "sharer" has locked file "lorem.txt" setting following properties + | lockscope | | + And the user "sharer" has locked folder "simple-folder" setting following properties + | lockscope | | + And user "sharer" has shared file "lorem.txt" with user "brand-new-user" + And user "sharer" has shared folder "simple-folder" with user "brand-new-user" + And the user has browsed to the files page + When the user unshares file "lorem (2).txt" using the webUI + And the user unshares folder "simple-folder (2)" using the webUI + Then as "brand-new-user" file "lorem (2).txt" should not exist + And as "brand-new-user" folder "simple-folder (2)" should not exist + And file "lorem (2).txt" should not be listed on the webUI + And folder "simple-folder (2)" should not be listed on the webUI + And 1 locks should be reported for file "lorem.txt" of user "sharer" by the WebDAV API + And 1 locks should be reported for file "simple-folder/lorem.txt" of user "sharer" by the WebDAV API + Examples: + | lockscope | + | exclusive | + | shared | From cb5bd0ddd25fd224d3c38f6be2e154ed6f5fd33c Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Thu, 20 Dec 2018 22:38:37 +0545 Subject: [PATCH 4/4] Install testing app when running webUIWebdavLocks --- .drone.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.drone.yml b/.drone.yml index d83d1e0a8d59..9a419846f9e2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1239,6 +1239,7 @@ matrix: INSTALL_SERVER: true CHOWN_SERVER: true OWNCLOUD_LOG: true + INSTALL_TESTING-APP: true # caldav test - PHP_VERSION: 7.1