CustomPropertiesBackendTest.php 14 KB

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