diff --git a/.drone.yml b/.drone.yml index 9b6a01bd4f0..4f2e14820de 100644 --- a/.drone.yml +++ b/.drone.yml @@ -485,6 +485,13 @@ pipeline: when: matrix: TESTS-ACCEPTANCE: access-levels + acceptance-app-files: + image: nextcloudci/php7.0:php7.0-7 + commands: + - tests/acceptance/run-local.sh allow-git-repository-modifications features/app-files.feature + when: + matrix: + TESTS-ACCEPTANCE: app-files acceptance-login: image: nextcloudci/php7.0:php7.0-7 commands: @@ -567,6 +574,8 @@ matrix: - TESTS: integration-trashbin - TESTS: acceptance TESTS-ACCEPTANCE: access-levels + - TESTS: acceptance + TESTS-ACCEPTANCE: app-files - TESTS: acceptance TESTS-ACCEPTANCE: login - TESTS: jsunit diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js index 4904ed493cc..9368982d916 100644 --- a/core/js/sharedialoglinkshareview.js +++ b/core/js/sharedialoglinkshareview.js @@ -307,10 +307,12 @@ this.model.saveLinkShare({ password: password }, { + complete: function(model) { + $loading.removeClass('inlineblock').addClass('hidden'); + }, error: function(model, msg) { // destroy old tooltips $input.tooltip('destroy'); - $loading.removeClass('inlineblock').addClass('hidden'); $input.addClass('error'); $input.attr('title', msg); $input.tooltip({placement: 'bottom', trigger: 'manual'}); diff --git a/core/js/shareitemmodel.js b/core/js/shareitemmodel.js index bc3ea88aa56..41f9eb5e0aa 100644 --- a/core/js/shareitemmodel.js +++ b/core/js/shareitemmodel.js @@ -104,7 +104,14 @@ /** * Saves the current link share information. * - * This will trigger an ajax call and refetch the model afterwards. + * This will trigger an ajax call and, if successful, refetch the model + * afterwards. Callbacks "success", "error" and "complete" can be given + * in the options object; "success" is called after a successful save + * once the model is refetch, "error" is called after a failed save, and + * "complete" is called both after a successful save and after a failed + * save. Note that "complete" is called before "success" and "error" are + * called (unlike in jQuery, in which it is called after them); this + * ensures that "complete" is called even if refetching the model fails. * * TODO: this should be a separate model */ @@ -149,7 +156,6 @@ addShare: function(attributes, options) { var shareType = attributes.shareType; - options = options || {}; attributes = _.extend({}, attributes); // Default permissions are Edit (CRUD) and Share @@ -173,53 +179,43 @@ attributes.path = this.fileInfoModel.getFullPath(); } - var self = this; - return $.ajax({ + return this._addOrUpdateShare({ type: 'POST', url: this._getUrl('shares'), data: attributes, dataType: 'json' - }).done(function() { - self.fetch().done(function() { - if (_.isFunction(options.success)) { - options.success(self); - } - }); - }).fail(function(xhr) { - var msg = t('core', 'Error'); - var result = xhr.responseJSON; - if (result && result.ocs && result.ocs.meta) { - msg = result.ocs.meta.message; - } - - if (_.isFunction(options.error)) { - options.error(self, msg); - } else { - OC.dialogs.alert(msg, t('core', 'Error while sharing')); - } - }); + }, options); }, updateShare: function(shareId, attrs, options) { - var self = this; - options = options || {}; - return $.ajax({ + return this._addOrUpdateShare({ type: 'PUT', url: this._getUrl('shares/' + encodeURIComponent(shareId)), data: attrs, dataType: 'json' + }, options); + }, + + _addOrUpdateShare: function(ajaxSettings, options) { + var self = this; + options = options || {}; + + return $.ajax( + ajaxSettings + ).always(function() { + if (_.isFunction(options.complete)) { + options.complete(self); + } }).done(function() { - self.fetch({ - success: function() { - if (_.isFunction(options.success)) { - options.success(self); - } + self.fetch().done(function() { + if (_.isFunction(options.success)) { + options.success(self); } }); }).fail(function(xhr) { var msg = t('core', 'Error'); var result = xhr.responseJSON; - if (result.ocs && result.ocs.meta) { + if (result && result.ocs && result.ocs.meta) { msg = result.ocs.meta.message; } diff --git a/core/js/tests/specs/sharedialoglinkshareview.js b/core/js/tests/specs/sharedialoglinkshareview.js new file mode 100644 index 00000000000..811919b5603 --- /dev/null +++ b/core/js/tests/specs/sharedialoglinkshareview.js @@ -0,0 +1,143 @@ +/** + * + * @copyright Copyright (c) 2015, Tom Needham (tom@owncloud.com) + * @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com) + * + * @license GNU AGPL version 3 or any later version + * + * This program 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 (at your option) 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 . + * + */ + +describe('OC.Share.ShareDialogLinkShareView', function () { + + var configModel; + var shareModel; + var view; + + beforeEach(function () { + + var fileInfoModel = new OCA.Files.FileInfoModel({ + id: 123, + name: 'shared_file_name.txt', + path: '/subdir', + size: 100, + mimetype: 'text/plain', + permissions: OC.PERMISSION_ALL, + sharePermissions: OC.PERMISSION_ALL + }); + + var attributes = { + itemType: fileInfoModel.isDirectory() ? 'folder' : 'file', + itemSource: fileInfoModel.get('id'), + possiblePermissions: OC.PERMISSION_ALL, + permissions: OC.PERMISSION_ALL + }; + + configModel = new OC.Share.ShareConfigModel({ + enforcePasswordForPublicLink: false, + isResharingAllowed: true, + enforcePasswordForPublicLink: false, + isDefaultExpireDateEnabled: false, + isDefaultExpireDateEnforced: false, + defaultExpireDate: 7 + }); + + sinon.stub(configModel, 'isShareWithLinkAllowed'); + + shareModel = new OC.Share.ShareItemModel(attributes, { + configModel: configModel, + fileInfoModel: fileInfoModel + }); + + view = new OC.Share.ShareDialogLinkShareView({ + configModel: configModel, + model: shareModel + }); + + }); + + afterEach(function () { + view.remove(); + configModel.isShareWithLinkAllowed.restore(); + }); + + describe('onPasswordEntered', function () { + + var $passwordText; + var $workingIcon; + + beforeEach(function () { + + // Needed to render the view + configModel.isShareWithLinkAllowed.returns(true); + + // Setting the share also triggers the rendering + shareModel.set({ + linkShare: { + isLinkShare: true, + password: 'password' + } + }); + + var $passwordDiv = view.$el.find('#linkPass'); + $passwordText = view.$el.find('.linkPassText'); + $workingIcon = view.$el.find('.linkPass .icon-loading-small'); + + sinon.stub(shareModel, 'saveLinkShare'); + + expect($passwordDiv.hasClass('hidden')).toBeFalsy(); + expect($passwordText.hasClass('hidden')).toBeFalsy(); + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + + $passwordText.val('myPassword'); + }); + + afterEach(function () { + shareModel.saveLinkShare.restore(); + }); + + it('shows the working icon when called', function () { + view.onPasswordEntered(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ password: 'myPassword' }).calledOnce).toBeTruthy(); + }); + + it('hides the working icon when saving the password succeeds', function () { + view.onPasswordEntered(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ password: 'myPassword' }).calledOnce).toBeTruthy(); + + shareModel.saveLinkShare.yieldTo("complete", [shareModel]); + + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + }); + + it('hides the working icon when saving the password fails', function () { + view.onPasswordEntered(); + + expect($workingIcon.hasClass('hidden')).toBeFalsy(); + expect(shareModel.saveLinkShare.withArgs({ password: 'myPassword' }).calledOnce).toBeTruthy(); + + shareModel.saveLinkShare.yieldTo("complete", [shareModel]); + shareModel.saveLinkShare.yieldTo("error", [shareModel, "The error message"]); + + expect($workingIcon.hasClass('hidden')).toBeTruthy(); + }); + + }); + +}); diff --git a/core/js/tests/specs/shareitemmodelSpec.js b/core/js/tests/specs/shareitemmodelSpec.js index 3d3baf75d15..771a9263709 100644 --- a/core/js/tests/specs/shareitemmodelSpec.js +++ b/core/js/tests/specs/shareitemmodelSpec.js @@ -670,6 +670,83 @@ describe('OC.Share.ShareItemModel', function() { shareWith: 'group1' }); }); + it('calls complete handler before refreshing the model', function() { + var completeStub = sinon.stub(); + model.addShare({ + shareType: OC.Share.SHARE_TYPE_GROUP, + shareWith: 'group1' + }, { + complete: completeStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(completeStub.calledOnce).toEqual(true); + }); + it('calls success handler after refreshing the model', function() { + var successStub = sinon.stub(); + model.addShare({ + shareType: OC.Share.SHARE_TYPE_GROUP, + shareWith: 'group1' + }, { + success: successStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(successStub.called).toEqual(false); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(successStub.calledOnce).toEqual(true); + expect(successStub.lastCall.args[0]).toEqual(model); + }); + it('calls complete handler before error handler', function() { + var completeStub = sinon.stub(); + var errorStub = sinon.stub(); + model.addShare({ + shareType: OC.Share.SHARE_TYPE_GROUP, + shareWith: 'group1' + }, { + complete: completeStub, + error: errorStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 400, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + ocs: { + meta: { + message: 'Some error message' + } + } + }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + expect(errorStub.calledOnce).toEqual(true); + expect(completeStub.calledBefore(errorStub)).toEqual(true); + }); it('calls error handler with error message', function() { var errorStub = sinon.stub(); model.addShare({ @@ -693,6 +770,7 @@ describe('OC.Share.ShareItemModel', function() { ); expect(errorStub.calledOnce).toEqual(true); + expect(errorStub.lastCall.args[0]).toEqual(model); expect(errorStub.lastCall.args[1]).toEqual('Some error message'); }); }); @@ -712,6 +790,80 @@ describe('OC.Share.ShareItemModel', function() { permissions: '' + (OC.PERMISSION_READ | OC.PERMISSION_SHARE) }); }); + it('calls complete handler before refreshing the model', function() { + var completeStub = sinon.stub(); + model.updateShare(123, { + permissions: OC.PERMISSION_READ | OC.PERMISSION_SHARE + }, { + complete: completeStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(completeStub.calledOnce).toEqual(true); + }); + it('calls success handler after refreshing the model', function() { + var successStub = sinon.stub(); + model.updateShare(123, { + permissions: OC.PERMISSION_READ | OC.PERMISSION_SHARE + }, { + success: successStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ }) + ); + + expect(successStub.called).toEqual(false); + + fetchReshareDeferred.resolve(makeOcsResponse([])); + fetchSharesDeferred.resolve(makeOcsResponse([])); + + expect(successStub.calledOnce).toEqual(true); + expect(successStub.lastCall.args[0]).toEqual(model); + }); + it('calls complete handler before error handler', function() { + var completeStub = sinon.stub(); + var errorStub = sinon.stub(); + model.updateShare(123, { + permissions: OC.PERMISSION_READ | OC.PERMISSION_SHARE + }, { + complete: completeStub, + error: errorStub + }); + + expect(fakeServer.requests.length).toEqual(1); + fakeServer.requests[0].respond( + 400, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + ocs: { + meta: { + message: 'Some error message' + } + } + }) + ); + + expect(completeStub.calledOnce).toEqual(true); + expect(completeStub.lastCall.args[0]).toEqual(model); + expect(errorStub.calledOnce).toEqual(true); + expect(completeStub.calledBefore(errorStub)).toEqual(true); + }); it('calls error handler with error message', function() { var errorStub = sinon.stub(); model.updateShare(123, { @@ -734,6 +886,7 @@ describe('OC.Share.ShareItemModel', function() { ); expect(errorStub.calledOnce).toEqual(true); + expect(errorStub.lastCall.args[0]).toEqual(model); expect(errorStub.lastCall.args[1]).toEqual('Some error message'); }); }); diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 41512d66159..06d03f2ec7c 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -713,36 +713,17 @@ class Manager implements IManager { } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK) { $this->linkCreateChecks($share); - // Password updated. - if ($share->getPassword() !== $originalShare->getPassword()) { - //Verify the password - $this->verifyPassword($share->getPassword()); - - // If a password is set. Hash it! - if ($share->getPassword() !== null) { - $share->setPassword($this->hasher->hash($share->getPassword())); - } - } + $this->updateSharePasswordIfNeeded($share, $originalShare); if ($share->getExpirationDate() != $originalShare->getExpirationDate()) { //Verify the expiration date $this->validateExpirationDate($share); $expirationDateUpdated = true; } - } - - $plainTextPassword = null; - if ($share->getShareType() === \OCP\Share::SHARE_TYPE_LINK || $share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { - // Password updated. - if ($share->getPassword() !== $originalShare->getPassword()) { - //Verify the password - $this->verifyPassword($share->getPassword()); - - // If a password is set. Hash it! - if ($share->getPassword() !== null) { - $plainTextPassword = $share->getPassword(); - $share->setPassword($this->hasher->hash($plainTextPassword)); - } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + $plainTextPassword = $share->getPassword(); + if (!$this->updateSharePasswordIfNeeded($share, $originalShare)) { + $plainTextPassword = null; } } @@ -795,6 +776,32 @@ class Manager implements IManager { return $share; } + /** + * Updates the password of the given share if it is not the same as the + * password of the original share. + * + * @param \OCP\Share\IShare $share the share to update its password. + * @param \OCP\Share\IShare $originalShare the original share to compare its + * password with. + * @return boolean whether the password was updated or not. + */ + private function updateSharePasswordIfNeeded(\OCP\Share\IShare $share, \OCP\Share\IShare $originalShare) { + // Password updated. + if ($share->getPassword() !== $originalShare->getPassword()) { + //Verify the password + $this->verifyPassword($share->getPassword()); + + // If a password is set. Hash it! + if ($share->getPassword() !== null) { + $share->setPassword($this->hasher->hash($share->getPassword())); + + return true; + } + } + + return false; + } + /** * Delete all the children of this share * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in diff --git a/tests/acceptance/config/behat.yml b/tests/acceptance/config/behat.yml index 6c3d9e4a7b9..15310e6883f 100644 --- a/tests/acceptance/config/behat.yml +++ b/tests/acceptance/config/behat.yml @@ -11,6 +11,7 @@ default: - FeatureContext - FilesAppContext + - FilesSharingAppContext - LoginPageContext - NotificationContext - SettingsMenuContext diff --git a/tests/acceptance/features/app-files.feature b/tests/acceptance/features/app-files.feature new file mode 100644 index 00000000000..7adc618e02e --- /dev/null +++ b/tests/acceptance/features/app-files.feature @@ -0,0 +1,31 @@ +Feature: app-files + + Scenario: set a password to a shared link + Given I am logged in + And I share the link for "welcome.txt" + When I protect the shared link with the password "abcdef" + Then I see that the working icon for password protect is shown + And I see that the working icon for password protect is eventually not shown + + Scenario: access a shared link protected by password with a valid password + Given I act as John + And I am logged in + And I share the link for "welcome.txt" protected by the password "abcdef" + And I write down the shared link + When I act as Jane + And I visit the shared link I wrote down + And I see that the current page is the Authenticate page for the shared link I wrote down + And I authenticate with password "abcdef" + Then I see that the current page is the shared link I wrote down + And I see that the shared file preview shows the text "Welcome to your Nextcloud account!" + + Scenario: access a shared link protected by password with an invalid password + Given I act as John + And I am logged in + And I share the link for "welcome.txt" protected by the password "abcdef" + And I write down the shared link + When I act as Jane + And I visit the shared link I wrote down + And I authenticate with password "fedcba" + Then I see that the current page is the Authenticate page for the shared link I wrote down + And I see that a wrong password for the shared file message is shown diff --git a/tests/acceptance/features/bootstrap/FilesAppContext.php b/tests/acceptance/features/bootstrap/FilesAppContext.php index 9702e64b552..7e7f592a44e 100644 --- a/tests/acceptance/features/bootstrap/FilesAppContext.php +++ b/tests/acceptance/features/bootstrap/FilesAppContext.php @@ -27,6 +27,105 @@ class FilesAppContext implements Context, ActorAwareInterface { use ActorAware; + /** + * @return Locator + */ + public static function currentSectionMainView() { + return Locator::forThe()->xpath("//*[starts-with(@id, 'app-content-') and not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]")-> + describedAs("Current section main view in Files app"); + } + + /** + * @return Locator + */ + public static function currentSectionDetailsView() { + return Locator::forThe()->xpath("/preceding-sibling::*[position() = 1 and @id = 'app-sidebar']")-> + descendantOf(self::currentSectionMainView())-> + describedAs("Current section details view in Files app"); + } + + /** + * @return Locator + */ + public static function shareLinkCheckbox() { + return Locator::forThe()->content("Share link")->descendantOf(self::currentSectionDetailsView())-> + describedAs("Share link checkbox in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function shareLinkField() { + return Locator::forThe()->css(".linkText")->descendantOf(self::currentSectionDetailsView())-> + describedAs("Share link field in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function passwordProtectCheckbox() { + return Locator::forThe()->content("Password protect")->descendantOf(self::currentSectionDetailsView())-> + describedAs("Password protect checkbox in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function passwordProtectField() { + return Locator::forThe()->css(".linkPassText")->descendantOf(self::currentSectionDetailsView())-> + describedAs("Password protect field in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function passwordProtectWorkingIcon() { + return Locator::forThe()->css(".linkPass .icon-loading-small")->descendantOf(self::currentSectionDetailsView())-> + describedAs("Password protect working icon in the details view in Files app"); + } + + /** + * @return Locator + */ + public static function rowForFile($fileName) { + return Locator::forThe()->xpath("//*[@id = 'fileList']//span[contains(concat(' ', normalize-space(@class), ' '), ' nametext ') and normalize-space() = '$fileName']/ancestor::tr")-> + descendantOf(self::currentSectionMainView())-> + describedAs("Row for file $fileName in Files app"); + } + + /** + * @return Locator + */ + public static function shareActionForFile($fileName) { + return Locator::forThe()->css(".action-share")->descendantOf(self::rowForFile($fileName))-> + describedAs("Share action for file $fileName in Files app"); + } + + /** + * @Given I share the link for :fileName + */ + public function iShareTheLinkFor($fileName) { + $this->actor->find(self::shareActionForFile($fileName), 10)->click(); + + $this->actor->find(self::shareLinkCheckbox(), 5)->click(); + } + + /** + * @Given I write down the shared link + */ + public function iWriteDownTheSharedLink() { + $this->actor->getSharedNotebook()["shared link"] = $this->actor->find(self::shareLinkField(), 10)->getValue(); + } + + /** + * @When I protect the shared link with the password :password + */ + public function iProtectTheSharedLinkWithThePassword($password) { + $this->actor->find(self::passwordProtectCheckbox(), 10)->click(); + + $this->actor->find(self::passwordProtectField(), 2)->setValue($password . "\r"); + } + /** * @Then I see that the current page is the Files app */ @@ -36,4 +135,43 @@ class FilesAppContext implements Context, ActorAwareInterface { $this->actor->getSession()->getCurrentUrl()); } + /** + * @Then I see that the working icon for password protect is shown + */ + public function iSeeThatTheWorkingIconForPasswordProtectIsShown() { + PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::passwordProtectWorkingIcon(), 10)); + } + + /** + * @Then I see that the working icon for password protect is eventually not shown + */ + public function iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown() { + $timeout = 10; + $timeoutStep = 1; + + $actor = $this->actor; + $passwordProtectWorkingIcon = self::passwordProtectWorkingIcon(); + + $workingIconNotFoundCallback = function() use ($actor, $passwordProtectWorkingIcon) { + try { + return !$actor->find($passwordProtectWorkingIcon)->isVisible(); + } catch (NoSuchElementException $exception) { + return true; + } + }; + if (!Utils::waitFor($workingIconNotFoundCallback, $timeout, $timeoutStep)) { + PHPUnit_Framework_Assert::fail("The working icon for password protect is still shown after $timeout seconds"); + } + } + + /** + * @Given I share the link for :fileName protected by the password :password + */ + public function iShareTheLinkForProtectedByThePassword($fileName, $password) { + $this->iShareTheLinkFor($fileName); + $this->iProtectTheSharedLinkWithThePassword($password); + $this->iSeeThatTheWorkingIconForPasswordProtectIsShown(); + $this->iSeeThatTheWorkingIconForPasswordProtectIsEventuallyNotShown(); + } + } diff --git a/tests/acceptance/features/bootstrap/FilesSharingAppContext.php b/tests/acceptance/features/bootstrap/FilesSharingAppContext.php new file mode 100644 index 00000000000..d9d5eca7359 --- /dev/null +++ b/tests/acceptance/features/bootstrap/FilesSharingAppContext.php @@ -0,0 +1,110 @@ +. + * + */ + +use Behat\Behat\Context\Context; + +class FilesSharingAppContext implements Context, ActorAwareInterface { + + use ActorAware; + + /** + * @return Locator + */ + public static function passwordField() { + return Locator::forThe()->field("password")-> + describedAs("Password field in Authenticate page"); + } + + /** + * @return Locator + */ + public static function authenticateButton() { + return Locator::forThe()->id("password-submit")-> + describedAs("Authenticate button in Authenticate page"); + } + + /** + * @return Locator + */ + public static function wrongPasswordMessage() { + return Locator::forThe()->content("The password is wrong. Try again.")-> + describedAs("Wrong password message in Authenticate page"); + } + + /** + * @return Locator + */ + public static function textPreview() { + return Locator::forThe()->css(".text-preview")-> + describedAs("Text preview in Shared file page"); + } + + /** + * @When I visit the shared link I wrote down + */ + public function iVisitTheSharedLinkIWroteDown() { + $this->actor->getSession()->visit($this->actor->getSharedNotebook()["shared link"]); + } + + /** + * @When I authenticate with password :password + */ + public function iAuthenticateWithPassword($password) { + $this->actor->find(self::passwordField(), 10)->setValue($password); + $this->actor->find(self::authenticateButton())->click(); + } + + /** + * @Then I see that the current page is the Authenticate page for the shared link I wrote down + */ + public function iSeeThatTheCurrentPageIsTheAuthenticatePageForTheSharedLinkIWroteDown() { + PHPUnit_Framework_Assert::assertEquals( + $this->actor->getSharedNotebook()["shared link"] . "/authenticate", + $this->actor->getSession()->getCurrentUrl()); + } + + /** + * @Then I see that the current page is the shared link I wrote down + */ + public function iSeeThatTheCurrentPageIsTheSharedLinkIWroteDown() { + PHPUnit_Framework_Assert::assertEquals( + $this->actor->getSharedNotebook()["shared link"], + $this->actor->getSession()->getCurrentUrl()); + } + + /** + * @Then I see that a wrong password for the shared file message is shown + */ + public function iSeeThatAWrongPasswordForTheSharedFileMessageIsShown() { + PHPUnit_Framework_Assert::assertTrue( + $this->actor->find(self::wrongPasswordMessage(), 10)->isVisible()); + } + + /** + * @Then I see that the shared file preview shows the text :text + */ + public function iSeeThatTheSharedFilePreviewShowsTheText($text) { + PHPUnit_Framework_Assert::assertContains($text, $this->actor->find(self::textPreview(), 10)->getText()); + } + +} diff --git a/tests/acceptance/features/core/Actor.php b/tests/acceptance/features/core/Actor.php index a27e8e6a015..0c23b5f7a40 100644 --- a/tests/acceptance/features/core/Actor.php +++ b/tests/acceptance/features/core/Actor.php @@ -48,6 +48,10 @@ * before giving up without modifying the tests themselves. Note that the * multiplier affects the timeout, but not the timeout step; the rate at which * find() will try again to find the element does not change. + * + * All actors share a notebook in which data can be annotated. This makes + * possible to share data between different test steps, no matter which Actor + * performs them. */ class Actor { @@ -66,16 +70,23 @@ class Actor { */ private $findTimeoutMultiplier; + /** + * @var array + */ + private $sharedNotebook; + /** * Creates a new Actor. * * @param \Behat\Mink\Session $session the Mink Session used to control its * web browser. * @param string $baseUrl the base URL used when solving relative URLs. + * @param array $sharedNotebook the notebook shared between all actors. */ - public function __construct(\Behat\Mink\Session $session, $baseUrl) { + public function __construct(\Behat\Mink\Session $session, $baseUrl, &$sharedNotebook) { $this->session = $session; $this->baseUrl = $baseUrl; + $this->sharedNotebook = &$sharedNotebook; $this->findTimeoutMultiplier = 1; } @@ -221,4 +232,13 @@ class Actor { return $ancestorElement; } + /** + * Returns the shared notebook of the Actors. + * + * @return array the shared notebook of the Actors. + */ + public function &getSharedNotebook() { + return $this->sharedNotebook; + } + } diff --git a/tests/acceptance/features/core/ActorContext.php b/tests/acceptance/features/core/ActorContext.php index 9667ef2f01c..86fe3832f66 100644 --- a/tests/acceptance/features/core/ActorContext.php +++ b/tests/acceptance/features/core/ActorContext.php @@ -53,6 +53,11 @@ class ActorContext extends RawMinkContext { */ private $actors; + /** + * @var array + */ + private $sharedNotebook; + /** * @var Actor */ @@ -102,8 +107,9 @@ class ActorContext extends RawMinkContext { */ public function initializeActors() { $this->actors = array(); + $this->sharedNotebook = array(); - $this->actors["default"] = new Actor($this->getSession(), $this->getMinkParameter("base_url")); + $this->actors["default"] = new Actor($this->getSession(), $this->getMinkParameter("base_url"), $this->sharedNotebook); $this->actors["default"]->setFindTimeoutMultiplier($this->actorFindTimeoutMultiplier); $this->currentActor = $this->actors["default"]; @@ -127,7 +133,7 @@ class ActorContext extends RawMinkContext { */ public function iActAs($actorName) { if (!array_key_exists($actorName, $this->actors)) { - $this->actors[$actorName] = new Actor($this->getSession($actorName), $this->getMinkParameter("base_url")); + $this->actors[$actorName] = new Actor($this->getSession($actorName), $this->getMinkParameter("base_url"), $this->sharedNotebook); $this->actors[$actorName]->setFindTimeoutMultiplier($this->actorFindTimeoutMultiplier); } diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index 7de73421d3e..13556285b61 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -2510,6 +2510,7 @@ class ManagerTest extends \Test\TestCase { $share->setProviderId('foo') ->setId('42') ->setShareType(\OCP\Share::SHARE_TYPE_LINK) + ->setToken('token') ->setSharedBy('owner') ->setShareOwner('owner') ->setPassword('password') @@ -2520,6 +2521,12 @@ class ManagerTest extends \Test\TestCase { $manager->expects($this->once())->method('canShare')->willReturn(true); $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); $manager->expects($this->once())->method('validateExpirationDate')->with($share); + $manager->expects($this->once())->method('verifyPassword')->with('password'); + + $this->hasher->expects($this->once()) + ->method('hash') + ->with('password') + ->willReturn('hashed'); $this->defaultProvider->expects($this->once()) ->method('update') @@ -2536,9 +2543,94 @@ class ManagerTest extends \Test\TestCase { ]); $hookListner2 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); - \OCP\Util::connectHook('OCP\Share', 'post_update_permissions', $hookListner2, 'post'); - $hookListner2->expects($this->never())->method('post'); + \OCP\Util::connectHook('OCP\Share', 'post_update_password', $hookListner2, 'post'); + $hookListner2->expects($this->once())->method('post')->with([ + 'itemType' => 'file', + 'itemSource' => 100, + 'uidOwner' => 'owner', + 'token' => 'token', + 'disabled' => false, + ]); + + $hookListner3 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_permissions', $hookListner3, 'post'); + $hookListner3->expects($this->never())->method('post'); + + + $manager->updateShare($share); + } + + public function testUpdateShareMail() { + $manager = $this->createManagerMock() + ->setMethods([ + 'canShare', + 'getShareById', + 'generalCreateChecks', + 'verifyPassword', + 'pathCreateChecks', + 'linkCreateChecks', + 'validateExpirationDate', + ]) + ->getMock(); + + $originalShare = $this->manager->newShare(); + $originalShare->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) + ->setPermissions(\OCP\Constants::PERMISSION_ALL); + + $tomorrow = new \DateTime(); + $tomorrow->setTime(0,0,0); + $tomorrow->add(new \DateInterval('P1D')); + + $file = $this->createMock(File::class); + $file->method('getId')->willReturn(100); + + $share = $this->manager->newShare(); + $share->setProviderId('foo') + ->setId('42') + ->setShareType(\OCP\Share::SHARE_TYPE_EMAIL) + ->setToken('token') + ->setSharedBy('owner') + ->setShareOwner('owner') + ->setPassword('password') + ->setExpirationDate($tomorrow) + ->setNode($file) + ->setPermissions(\OCP\Constants::PERMISSION_ALL); + + $manager->expects($this->once())->method('canShare')->willReturn(true); + $manager->expects($this->once())->method('getShareById')->with('foo:42')->willReturn($originalShare); + $manager->expects($this->once())->method('generalCreateChecks')->with($share); + $manager->expects($this->once())->method('verifyPassword')->with('password'); + $manager->expects($this->once())->method('pathCreateChecks')->with($file); + $manager->expects($this->never())->method('linkCreateChecks'); + $manager->expects($this->never())->method('validateExpirationDate'); + + $this->hasher->expects($this->once()) + ->method('hash') + ->with('password') + ->willReturn('hashed'); + + $this->defaultProvider->expects($this->once()) + ->method('update') + ->with($share, 'password') + ->willReturn($share); + + $hookListner = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_set_expiration_date', $hookListner, 'post'); + $hookListner->expects($this->never())->method('post'); + + $hookListner2 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_password', $hookListner2, 'post'); + $hookListner2->expects($this->once())->method('post')->with([ + 'itemType' => 'file', + 'itemSource' => 100, + 'uidOwner' => 'owner', + 'token' => 'token', + 'disabled' => false, + ]); + $hookListner3 = $this->getMockBuilder('Dummy')->setMethods(['post'])->getMock(); + \OCP\Util::connectHook('OCP\Share', 'post_update_permissions', $hookListner3, 'post'); + $hookListner3->expects($this->never())->method('post'); $manager->updateShare($share); }