CustomPropertiesBackendTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2017, Georg Ehrke <oc.list@georgehrke.com>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Georg Ehrke <oc.list@georgehrke.com>
  7. * @author Joas Schilling <coding@schilljs.com>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Richard Steinmetz <richard@steinmetz.cloud>
  12. *
  13. * @license GNU AGPL version 3 or any later version
  14. *
  15. * This program is free software: you can redistribute it and/or modify
  16. * it under the terms of the GNU Affero General Public License as
  17. * published by the Free Software Foundation, either version 3 of the
  18. * License, or (at your option) any later version.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU Affero General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU Affero General Public License
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  27. *
  28. */
  29. namespace OCA\DAV\Tests\DAV;
  30. use OCA\DAV\DAV\CustomPropertiesBackend;
  31. use OCP\DB\QueryBuilder\IQueryBuilder;
  32. use OCP\IDBConnection;
  33. use OCP\IUser;
  34. use Sabre\CalDAV\ICalendar;
  35. use Sabre\DAV\Exception\NotFound;
  36. use Sabre\DAV\PropFind;
  37. use Sabre\DAV\PropPatch;
  38. use Sabre\DAV\Server;
  39. use Sabre\DAV\Tree;
  40. use Sabre\DAV\Xml\Property\Href;
  41. use Sabre\DAVACL\IACL;
  42. use Sabre\DAVACL\IPrincipal;
  43. use Test\TestCase;
  44. /**
  45. * @group DB
  46. */
  47. class CustomPropertiesBackendTest extends TestCase {
  48. private const BASE_URI = '/remote.php/dav/';
  49. /** @var Server | \PHPUnit\Framework\MockObject\MockObject */
  50. private $server;
  51. /** @var Tree | \PHPUnit\Framework\MockObject\MockObject */
  52. private $tree;
  53. /** @var IDBConnection */
  54. private $dbConnection;
  55. /** @var IUser | \PHPUnit\Framework\MockObject\MockObject */
  56. private $user;
  57. /** @var CustomPropertiesBackend | \PHPUnit\Framework\MockObject\MockObject */
  58. private $backend;
  59. protected function setUp(): void {
  60. parent::setUp();
  61. $this->server = $this->createMock(Server::class);
  62. $this->server->method('getBaseUri')
  63. ->willReturn(self::BASE_URI);
  64. $this->tree = $this->createMock(Tree::class);
  65. $this->user = $this->createMock(IUser::class);
  66. $this->user->method('getUID')
  67. ->with()
  68. ->willReturn('dummy_user_42');
  69. $this->dbConnection = \OC::$server->getDatabaseConnection();
  70. $this->backend = new CustomPropertiesBackend(
  71. $this->server,
  72. $this->tree,
  73. $this->dbConnection,
  74. $this->user,
  75. );
  76. }
  77. protected function tearDown(): void {
  78. $query = $this->dbConnection->getQueryBuilder();
  79. $query->delete('properties');
  80. $query->execute();
  81. parent::tearDown();
  82. }
  83. private function formatPath(string $path): string {
  84. if (strlen($path) > 250) {
  85. return sha1($path);
  86. } else {
  87. return $path;
  88. }
  89. }
  90. protected function insertProps(string $user, string $path, array $props) {
  91. foreach ($props as $name => $value) {
  92. $this->insertProp($user, $path, $name, $value);
  93. }
  94. }
  95. protected function insertProp(string $user, string $path, string $name, mixed $value) {
  96. $type = CustomPropertiesBackend::PROPERTY_TYPE_STRING;
  97. if ($value instanceof Href) {
  98. $value = $value->getHref();
  99. $type = CustomPropertiesBackend::PROPERTY_TYPE_HREF;
  100. }
  101. $query = $this->dbConnection->getQueryBuilder();
  102. $query->insert('properties')
  103. ->values([
  104. 'userid' => $query->createNamedParameter($user),
  105. 'propertypath' => $query->createNamedParameter($this->formatPath($path)),
  106. 'propertyname' => $query->createNamedParameter($name),
  107. 'propertyvalue' => $query->createNamedParameter($value),
  108. 'valuetype' => $query->createNamedParameter($type, IQueryBuilder::PARAM_INT)
  109. ]);
  110. $query->execute();
  111. }
  112. protected function getProps(string $user, string $path) {
  113. $query = $this->dbConnection->getQueryBuilder();
  114. $query->select('propertyname', 'propertyvalue', 'valuetype')
  115. ->from('properties')
  116. ->where($query->expr()->eq('userid', $query->createNamedParameter($user)))
  117. ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($this->formatPath($path))));
  118. $result = $query->execute();
  119. $data = [];
  120. while ($row = $result->fetch()) {
  121. $value = $row['propertyvalue'];
  122. if ((int)$row['valuetype'] === CustomPropertiesBackend::PROPERTY_TYPE_HREF) {
  123. $value = new Href($value);
  124. }
  125. $data[$row['propertyname']] = $value;
  126. }
  127. $result->closeCursor();
  128. return $data;
  129. }
  130. public function testPropFindNoDbCalls(): void {
  131. $db = $this->createMock(IDBConnection::class);
  132. $backend = new CustomPropertiesBackend(
  133. $this->server,
  134. $this->tree,
  135. $db,
  136. $this->user,
  137. );
  138. $propFind = $this->createMock(PropFind::class);
  139. $propFind->expects($this->once())
  140. ->method('get404Properties')
  141. ->with()
  142. ->willReturn([
  143. '{http://owncloud.org/ns}permissions',
  144. '{http://owncloud.org/ns}downloadURL',
  145. '{http://owncloud.org/ns}dDC',
  146. '{http://owncloud.org/ns}size',
  147. ]);
  148. $db->expects($this->never())
  149. ->method($this->anything());
  150. $backend->propFind('foo_bar_path_1337_0', $propFind);
  151. }
  152. public function testPropFindCalendarCall(): void {
  153. $propFind = $this->createMock(PropFind::class);
  154. $propFind->method('get404Properties')
  155. ->with()
  156. ->willReturn([
  157. '{DAV:}getcontentlength',
  158. '{DAV:}getcontenttype',
  159. '{DAV:}getetag',
  160. '{abc}def',
  161. ]);
  162. $propFind->method('getRequestedProperties')
  163. ->with()
  164. ->willReturn([
  165. '{DAV:}getcontentlength',
  166. '{DAV:}getcontenttype',
  167. '{DAV:}getetag',
  168. '{DAV:}displayname',
  169. '{urn:ietf:params:xml:ns:caldav}calendar-description',
  170. '{urn:ietf:params:xml:ns:caldav}calendar-timezone',
  171. '{abc}def',
  172. ]);
  173. $props = [
  174. '{abc}def' => 'a',
  175. '{DAV:}displayname' => 'b',
  176. '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'c',
  177. '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'd',
  178. ];
  179. $this->insertProps('dummy_user_42', 'calendars/foo/bar_path_1337_0', $props);
  180. $setProps = [];
  181. $propFind->method('set')
  182. ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
  183. $setProps[$name] = $value;
  184. });
  185. $this->backend->propFind('calendars/foo/bar_path_1337_0', $propFind);
  186. $this->assertEquals($props, $setProps);
  187. }
  188. public function testPropFindPrincipalCall(): void {
  189. $this->tree->method('getNodeForPath')
  190. ->willReturnCallback(function ($uri) {
  191. $node = $this->createMock(ICalendar::class);
  192. $node->method('getOwner')
  193. ->willReturn('principals/users/dummy_user_42');
  194. return $node;
  195. });
  196. $propFind = $this->createMock(PropFind::class);
  197. $propFind->method('get404Properties')
  198. ->with()
  199. ->willReturn([
  200. '{DAV:}getcontentlength',
  201. '{DAV:}getcontenttype',
  202. '{DAV:}getetag',
  203. '{abc}def',
  204. ]);
  205. $propFind->method('getRequestedProperties')
  206. ->with()
  207. ->willReturn([
  208. '{DAV:}getcontentlength',
  209. '{DAV:}getcontenttype',
  210. '{DAV:}getetag',
  211. '{abc}def',
  212. '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
  213. ]);
  214. $props = [
  215. '{abc}def' => 'a',
  216. '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/admin/personal'),
  217. ];
  218. $this->insertProps('dummy_user_42', 'principals/users/dummy_user_42', $props);
  219. $setProps = [];
  220. $propFind->method('set')
  221. ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
  222. $setProps[$name] = $value;
  223. });
  224. $this->backend->propFind('principals/users/dummy_user_42', $propFind);
  225. $this->assertEquals($props, $setProps);
  226. }
  227. public function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): array {
  228. // [ user, nodes, existingProps, requestedProps, returnedProps ]
  229. return [
  230. [ // Exists
  231. 'dummy_user_42',
  232. ['calendars/dummy_user_42/foo/' => ICalendar::class],
  233. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
  234. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
  235. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')],
  236. ],
  237. [ // Doesn't exist
  238. 'dummy_user_42',
  239. ['calendars/dummy_user_42/foo/' => ICalendar::class],
  240. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')],
  241. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
  242. [],
  243. ],
  244. [ // No privilege
  245. 'dummy_user_42',
  246. ['calendars/user2/baz/' => ICalendar::class],
  247. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')],
  248. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
  249. [],
  250. ],
  251. [ // Not a calendar
  252. 'dummy_user_42',
  253. ['foo/dummy_user_42/bar/' => IACL::class],
  254. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/dummy_user_42/bar/')],
  255. ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'],
  256. [],
  257. ],
  258. ];
  259. }
  260. /**
  261. * @dataProvider propFindPrincipalScheduleDefaultCalendarProviderUrlProvider
  262. */
  263. public function testPropFindPrincipalScheduleDefaultCalendarUrl(
  264. string $user,
  265. array $nodes,
  266. array $existingProps,
  267. array $requestedProps,
  268. array $returnedProps,
  269. ): void {
  270. $propFind = $this->createMock(PropFind::class);
  271. $propFind->method('get404Properties')
  272. ->with()
  273. ->willReturn([
  274. '{DAV:}getcontentlength',
  275. '{DAV:}getcontenttype',
  276. '{DAV:}getetag',
  277. ]);
  278. $propFind->method('getRequestedProperties')
  279. ->with()
  280. ->willReturn(array_merge([
  281. '{DAV:}getcontentlength',
  282. '{DAV:}getcontenttype',
  283. '{DAV:}getetag',
  284. '{abc}def',
  285. ],
  286. $requestedProps,
  287. ));
  288. $this->server->method('calculateUri')
  289. ->willReturnCallback(function ($uri) {
  290. if (!str_starts_with($uri, self::BASE_URI)) {
  291. return trim(substr($uri, strlen(self::BASE_URI)), '/');
  292. }
  293. return null;
  294. });
  295. $this->tree->method('getNodeForPath')
  296. ->willReturnCallback(function ($uri) use ($nodes) {
  297. if (str_starts_with($uri, 'principals/')) {
  298. return $this->createMock(IPrincipal::class);
  299. }
  300. if (array_key_exists($uri, $nodes)) {
  301. $owner = explode('/', $uri)[1];
  302. $node = $this->createMock($nodes[$uri]);
  303. $node->method('getOwner')
  304. ->willReturn("principals/users/$owner");
  305. return $node;
  306. }
  307. throw new NotFound('Node not found');
  308. });
  309. $this->insertProps($user, "principals/users/$user", $existingProps);
  310. $setProps = [];
  311. $propFind->method('set')
  312. ->willReturnCallback(function ($name, $value, $status) use (&$setProps): void {
  313. $setProps[$name] = $value;
  314. });
  315. $this->backend->propFind("principals/users/$user", $propFind);
  316. $this->assertEquals($returnedProps, $setProps);
  317. }
  318. /**
  319. * @dataProvider propPatchProvider
  320. */
  321. public function testPropPatch(string $path, array $existing, array $props, array $result): void {
  322. $this->server->method('calculateUri')
  323. ->willReturnCallback(function ($uri) {
  324. if (str_starts_with($uri, self::BASE_URI)) {
  325. return trim(substr($uri, strlen(self::BASE_URI)), '/');
  326. }
  327. return null;
  328. });
  329. $this->tree->method('getNodeForPath')
  330. ->willReturnCallback(function ($uri) {
  331. $node = $this->createMock(ICalendar::class);
  332. $node->method('getOwner')
  333. ->willReturn('principals/users/' . $this->user->getUID());
  334. return $node;
  335. });
  336. $this->insertProps($this->user->getUID(), $path, $existing);
  337. $propPatch = new PropPatch($props);
  338. $this->backend->propPatch($path, $propPatch);
  339. $propPatch->commit();
  340. $storedProps = $this->getProps($this->user->getUID(), $path);
  341. $this->assertEquals($result, $storedProps);
  342. }
  343. public function propPatchProvider() {
  344. $longPath = str_repeat('long_path', 100);
  345. return [
  346. ['foo_bar_path_1337', [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
  347. ['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
  348. ['foo_bar_path_1337', ['{DAV:}displayname' => 'foo'], ['{DAV:}displayname' => null], []],
  349. [$longPath, [], ['{DAV:}displayname' => 'anything'], ['{DAV:}displayname' => 'anything']],
  350. ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
  351. ['principals/users/dummy_user_42', [], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href(self::BASE_URI . 'foo/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/')]],
  352. ];
  353. }
  354. /**
  355. * @dataProvider deleteProvider
  356. */
  357. public function testDelete(string $path): void {
  358. $this->insertProps('dummy_user_42', $path, ['foo' => 'bar']);
  359. $this->backend->delete($path);
  360. $this->assertEquals([], $this->getProps('dummy_user_42', $path));
  361. }
  362. public function deleteProvider() {
  363. return [
  364. ['foo_bar_path_1337'],
  365. [str_repeat('long_path', 100)]
  366. ];
  367. }
  368. /**
  369. * @dataProvider moveProvider
  370. */
  371. public function testMove(string $source, string $target): void {
  372. $this->insertProps('dummy_user_42', $source, ['foo' => 'bar']);
  373. $this->backend->move($source, $target);
  374. $this->assertEquals([], $this->getProps('dummy_user_42', $source));
  375. $this->assertEquals(['foo' => 'bar'], $this->getProps('dummy_user_42', $target));
  376. }
  377. public function moveProvider() {
  378. return [
  379. ['foo_bar_path_1337', 'foo_bar_path_7333'],
  380. [str_repeat('long_path1', 100), str_repeat('long_path2', 100)]
  381. ];
  382. }
  383. }