* * @copyright Copyright (c) 2016, ownCloud, Inc. * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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, version 3, * along with this program. If not, see * */ namespace Test\Repair; use OC\Repair\RepairUnmergedShares; use OC\Share\Constants; use OCP\IUser; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; use Test\TestCase; use OC\Share20\DefaultShareProvider; use OCP\IUserManager; use OCP\IGroupManager; /** * Tests for repairing invalid shares * * @group DB * * @see \OC\Repair\RepairUnmergedShares */ class RepairUnmergedSharesTest extends TestCase { /** @var IRepairStep */ private $repair; /** @var \OCP\IDBConnection */ private $connection; /** @var int */ private $lastShareTime; /** @var IUserManager */ private $userManager; /** @var IGroupManager */ private $groupManager; protected function setUp() { parent::setUp(); $config = $this->getMockBuilder('OCP\IConfig') ->disableOriginalConstructor() ->getMock(); $config->expects($this->any()) ->method('getSystemValue') ->with('version') ->will($this->returnValue('9.0.3.0')); $this->connection = \OC::$server->getDatabaseConnection(); $this->deleteAllShares(); $this->userManager = $this->createMock(IUserManager::class); $this->groupManager = $this->createMock(IGroupManager::class); // used to generate incremental stimes $this->lastShareTime = time(); /** @var \OCP\IConfig $config */ $this->repair = new RepairUnmergedShares($config, $this->connection, $this->userManager, $this->groupManager); } protected function tearDown() { $this->deleteAllShares(); parent::tearDown(); } protected function deleteAllShares() { $qb = $this->connection->getQueryBuilder(); $qb->delete('share')->execute(); } private function createShare($type, $sourceId, $recipient, $targetName, $permissions, $parentId = null) { $this->lastShareTime += 100; $qb = $this->connection->getQueryBuilder(); $values = [ 'share_type' => $qb->expr()->literal($type), 'share_with' => $qb->expr()->literal($recipient), 'uid_owner' => $qb->expr()->literal('user1'), 'item_type' => $qb->expr()->literal('folder'), 'item_source' => $qb->expr()->literal($sourceId), 'item_target' => $qb->expr()->literal('/' . $sourceId), 'file_source' => $qb->expr()->literal($sourceId), 'file_target' => $qb->expr()->literal($targetName), 'permissions' => $qb->expr()->literal($permissions), 'stime' => $qb->expr()->literal($this->lastShareTime), ]; if ($parentId !== null) { $values['parent'] = $qb->expr()->literal($parentId); } $qb->insert('share') ->values($values) ->execute(); return $this->connection->lastInsertId('*PREFIX*share'); } private function getShareById($id) { $query = $this->connection->getQueryBuilder(); $results = $query ->select('*') ->from('share') ->where($query->expr()->eq('id', $query->expr()->literal($id))) ->execute() ->fetchAll(); if (!empty($results)) { return $results[0]; } return null; } public function sharesDataProvider() { /** * For all these test cases we have the following situation: * * - "user1" is the share owner * - "user2" is the recipient, and member of "recipientgroup1" and "recipientgroup2" * - "user1" is member of "samegroup1", "samegroup2" for same group tests */ return [ [ // #0 legitimate share: // - outsider shares with group1, group2 // - recipient renamed, resulting in subshares // - one subshare for each group share // - targets of subshare all match [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test renamed', 31, 0], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test renamed', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 31], // leave them alone ['/test renamed', 31], ['/test renamed', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #1 broken share: // - outsider shares with group1, group2 // - only one subshare for two group shares [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // child of the previous one [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 31], ['/test', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #2 bogus share // - outsider shares with group1, group2 // - one subshare for each group share, both with parenthesis // - but the targets do not match when grouped [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 31, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (3)', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 31], // reset to original name as the sub-names have parenthesis ['/test', 31], ['/test', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #3 bogus share // - outsider shares with group1, group2 // - one subshare for each group share, both renamed manually // - but the targets do not match when grouped [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed (1 legit paren)', 31, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed (2 legit paren)', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 31], // reset to less recent subshare name ['/test_renamed (2 legit paren)', 31], ['/test_renamed (2 legit paren)', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #4 bogus share // - outsider shares with group1, group2 // - one subshare for each group share, one with parenthesis // - but the targets do not match when grouped [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 31, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 31], // reset to less recent subshare name but without parenthesis ['/test_renamed', 31], ['/test_renamed', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #5 bogus share // - outsider shares with group1, group2 // - one subshare for each group share // - first subshare not renamed (as in real world scenario) // - but the targets do not match when grouped [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test', 31, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 31], // reset to original name ['/test', 31], ['/test', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #6 bogus share: // - outsider shares with group1, group2 // - one subshare for each group share // - non-matching targets // - recipient deletes one duplicate (unshare from self, permissions 0) [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 15], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 0, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (3)', 15, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 15], // subshares repaired and permissions restored to the max allowed ['/test', 31], ['/test', 15], // leave unrelated alone ['/test (4)', 31], ] ], [ // #7 bogus share: // - outsider shares with group1, group2 // - one subshare for each group share // - non-matching targets // - recipient deletes ALL duplicates (unshare from self, permissions 0) [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 15], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 0, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (3)', 0, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ ['/test', 31], ['/test', 15], // subshares target repaired but left "deleted" as it was the user's choice ['/test', 0], ['/test', 0], // leave unrelated alone ['/test (4)', 31], ] ], [ // #8 bogus share: // - outsider shares with group1, group2 and also user2 // - one subshare for each group share // - one extra share entry for direct share to user2 // - non-matching targets // - user share has more permissions [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 1], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 15], // child of the previous ones [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 1, 0], [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (3)', 15, 1], [Constants::SHARE_TYPE_USER, 123, 'user2', '/test (4)', 31], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (5)', 31], ], [ ['/test', 1], ['/test', 15], // subshares repaired ['/test', 1], ['/test', 15], ['/test', 31], // leave unrelated alone ['/test (5)', 31], ] ], [ // #9 bogus share: // - outsider shares with group1 and also user2 // - no subshare at all // - one extra share entry for direct share to user2 // - non-matching targets // - user share has more permissions [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 1], [Constants::SHARE_TYPE_USER, 123, 'user2', '/test (2)', 31], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (5)', 31], ], [ ['/test', 1], // user share repaired ['/test', 31], // leave unrelated alone ['/test (5)', 31], ] ], [ // #10 legitimate share with own group: // - insider shares with both groups the user is already in // - no subshares in this case [ [Constants::SHARE_TYPE_GROUP, 123, 'samegroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'samegroup2', '/test', 31], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ // leave all alone ['/test', 31], ['/test', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #11 legitimate shares: // - group share with same group // - group share with other group // - user share where recipient renamed // - user share where recipient did not rename [ [Constants::SHARE_TYPE_GROUP, 123, 'samegroup1', '/test', 31], [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], [Constants::SHARE_TYPE_USER, 123, 'user3', '/test legit rename', 31], [Constants::SHARE_TYPE_USER, 123, 'user4', '/test', 31], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ // leave all alone ['/test', 31], ['/test', 31], ['/test legit rename', 31], ['/test', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #12 legitimate share: // - outsider shares with group and user directly with different permissions // - no subshares // - same targets [ [Constants::SHARE_TYPE_GROUP, 123, 'samegroup1', '/test', 1], [Constants::SHARE_TYPE_USER, 123, 'user3', '/test', 31], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], ], [ // leave all alone ['/test', 1], ['/test', 31], // leave unrelated alone ['/test (4)', 31], ] ], [ // #13 bogus share: // - outsider shares with group1, user2 and then group2 // - user renamed share as soon as it arrived before the next share (order) // - one subshare for each group share // - one extra share entry for direct share to user2 // - non-matching targets [ // first share with group [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], // recipient renames [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/first', 31, 0], // then direct share, user renames too [Constants::SHARE_TYPE_USER, 123, 'user2', '/second', 31], // another share with the second group [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], // use renames it [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/third', 31, 1], // different unrelated share [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (5)', 31], ], [ // group share with group1 left alone ['/test', 31], // first subshare repaired ['/third', 31], // direct user share repaired ['/third', 31], // group share with group2 left alone ['/test', 31], // second subshare repaired ['/third', 31], // leave unrelated alone ['/test (5)', 31], ] ], ]; } /** * Test merge shares from group shares * * @dataProvider sharesDataProvider */ public function testMergeGroupShares($shares, $expectedShares) { $user1 = $this->createMock(IUser::class); $user1->expects($this->any()) ->method('getUID') ->will($this->returnValue('user1')); $user2 = $this->createMock(IUser::class); $user2->expects($this->any()) ->method('getUID') ->will($this->returnValue('user2')); $users = [$user1, $user2]; $this->groupManager->expects($this->any()) ->method('getUserGroupIds') ->will($this->returnValueMap([ // owner [$user1, ['samegroup1', 'samegroup2']], // recipient [$user2, ['recipientgroup1', 'recipientgroup2']], ])); $this->userManager->expects($this->once()) ->method('countUsers') ->will($this->returnValue([2])); $this->userManager->expects($this->once()) ->method('callForAllUsers') ->will($this->returnCallback(function(\Closure $closure) use ($users) { foreach ($users as $user) { $closure($user); } })); $shareIds = []; foreach ($shares as $share) { // if parent if (isset($share[5])) { // adjust to real id $share[5] = $shareIds[$share[5]]; } else { $share[5] = null; } $shareIds[] = $this->createShare($share[0], $share[1], $share[2], $share[3], $share[4], $share[5]); } /** @var IOutput | \PHPUnit_Framework_MockObject_MockObject $outputMock */ $outputMock = $this->getMockBuilder('\OCP\Migration\IOutput') ->disableOriginalConstructor() ->getMock(); $this->repair->run($outputMock); foreach ($expectedShares as $index => $expectedShare) { $share = $this->getShareById($shareIds[$index]); $this->assertEquals($expectedShare[0], $share['file_target']); $this->assertEquals($expectedShare[1], $share['permissions']); } } public function duplicateNamesProvider() { return [ // matching ['filename (1).txt', true], ['folder (2)', true], ['filename (1)(2).txt', true], // non-matching ['filename ().txt', false], ['folder ()', false], ['folder (1x)', false], ['folder (x1)', false], ['filename (a)', false], ['filename (1).', false], ['filename (1).txt.txt', false], ['filename (1)..txt', false], ]; } /** * @dataProvider duplicateNamesProvider */ public function testIsPotentialDuplicateName($name, $expectedResult) { $this->assertEquals($expectedResult, $this->invokePrivate($this->repair, 'isPotentialDuplicateName', [$name])); } }