SyncServiceTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\DAV\Tests\unit\CardDAV;
  8. use GuzzleHttp\Exception\ClientException;
  9. use GuzzleHttp\Psr7\Request as PsrRequest;
  10. use GuzzleHttp\Psr7\Response as PsrResponse;
  11. use OC\Http\Client\Response;
  12. use OCA\DAV\CardDAV\CardDavBackend;
  13. use OCA\DAV\CardDAV\Converter;
  14. use OCA\DAV\CardDAV\SyncService;
  15. use OCP\Http\Client\IClient;
  16. use OCP\Http\Client\IClientService;
  17. use OCP\IConfig;
  18. use OCP\IDBConnection;
  19. use OCP\IUser;
  20. use OCP\IUserManager;
  21. use Psr\Http\Client\ClientExceptionInterface;
  22. use Psr\Log\LoggerInterface;
  23. use Psr\Log\NullLogger;
  24. use Sabre\VObject\Component\VCard;
  25. use Test\TestCase;
  26. class SyncServiceTest extends TestCase {
  27. protected CardDavBackend $backend;
  28. protected IUserManager $userManager;
  29. protected IDBConnection $dbConnection;
  30. protected LoggerInterface $logger;
  31. protected Converter $converter;
  32. protected IClient $client;
  33. protected IConfig $config;
  34. protected SyncService $service;
  35. public function setUp(): void {
  36. $addressBook = [
  37. 'id' => 1,
  38. 'uri' => 'system',
  39. 'principaluri' => 'principals/system/system',
  40. '{DAV:}displayname' => 'system',
  41. // watch out, incomplete address book mock.
  42. ];
  43. $this->backend = $this->createMock(CardDavBackend::class);
  44. $this->backend->method('getAddressBooksByUri')
  45. ->with('principals/system/system', 1)
  46. ->willReturn($addressBook);
  47. $this->userManager = $this->createMock(IUserManager::class);
  48. $this->dbConnection = $this->createMock(IDBConnection::class);
  49. $this->logger = new NullLogger();
  50. $this->converter = $this->createMock(Converter::class);
  51. $this->client = $this->createMock(IClient::class);
  52. $this->config = $this->createMock(IConfig::class);
  53. $clientService = $this->createMock(IClientService::class);
  54. $clientService->method('newClient')
  55. ->willReturn($this->client);
  56. $this->service = new SyncService(
  57. $this->backend,
  58. $this->userManager,
  59. $this->dbConnection,
  60. $this->logger,
  61. $this->converter,
  62. $clientService,
  63. $this->config
  64. );
  65. }
  66. public function testEmptySync(): void {
  67. $this->backend->expects($this->exactly(0))
  68. ->method('createCard');
  69. $this->backend->expects($this->exactly(0))
  70. ->method('updateCard');
  71. $this->backend->expects($this->exactly(0))
  72. ->method('deleteCard');
  73. $body = '<?xml version="1.0"?>
  74. <d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
  75. <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
  76. </d:multistatus>';
  77. $requestResponse = new Response(new PsrResponse(
  78. 207,
  79. ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
  80. $body
  81. ));
  82. $this->client
  83. ->method('request')
  84. ->willReturn($requestResponse);
  85. $token = $this->service->syncRemoteAddressBook(
  86. '',
  87. 'system',
  88. 'system',
  89. '1234567890',
  90. null,
  91. '1',
  92. 'principals/system/system',
  93. []
  94. );
  95. $this->assertEquals('http://sabre.io/ns/sync/1', $token);
  96. }
  97. public function testSyncWithNewElement(): void {
  98. $this->backend->expects($this->exactly(1))
  99. ->method('createCard');
  100. $this->backend->expects($this->exactly(0))
  101. ->method('updateCard');
  102. $this->backend->expects($this->exactly(0))
  103. ->method('deleteCard');
  104. $this->backend->method('getCard')
  105. ->willReturn(false);
  106. $body = '<?xml version="1.0"?>
  107. <d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
  108. <d:response>
  109. <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
  110. <d:propstat>
  111. <d:prop>
  112. <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype>
  113. <d:getetag>&quot;2df155fa5c2a24cd7f750353fc63f037&quot;</d:getetag>
  114. </d:prop>
  115. <d:status>HTTP/1.1 200 OK</d:status>
  116. </d:propstat>
  117. </d:response>
  118. <d:sync-token>http://sabre.io/ns/sync/2</d:sync-token>
  119. </d:multistatus>';
  120. $reportResponse = new Response(new PsrResponse(
  121. 207,
  122. ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
  123. $body
  124. ));
  125. $this->client
  126. ->method('request')
  127. ->willReturn($reportResponse);
  128. $vCard = 'BEGIN:VCARD
  129. VERSION:3.0
  130. PRODID:-//Sabre//Sabre VObject 4.5.4//EN
  131. UID:alice
  132. FN;X-NC-SCOPE=v2-federated:alice
  133. N;X-NC-SCOPE=v2-federated:alice;;;;
  134. X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice
  135. CLOUD:alice@server2.internal
  136. END:VCARD';
  137. $getResponse = new Response(new PsrResponse(
  138. 200,
  139. ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)],
  140. $vCard,
  141. ));
  142. $this->client
  143. ->method('get')
  144. ->willReturn($getResponse);
  145. $token = $this->service->syncRemoteAddressBook(
  146. '',
  147. 'system',
  148. 'system',
  149. '1234567890',
  150. null,
  151. '1',
  152. 'principals/system/system',
  153. []
  154. );
  155. $this->assertEquals('http://sabre.io/ns/sync/2', $token);
  156. }
  157. public function testSyncWithUpdatedElement(): void {
  158. $this->backend->expects($this->exactly(0))
  159. ->method('createCard');
  160. $this->backend->expects($this->exactly(1))
  161. ->method('updateCard');
  162. $this->backend->expects($this->exactly(0))
  163. ->method('deleteCard');
  164. $this->backend->method('getCard')
  165. ->willReturn(true);
  166. $body = '<?xml version="1.0"?>
  167. <d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
  168. <d:response>
  169. <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
  170. <d:propstat>
  171. <d:prop>
  172. <d:getcontenttype>text/vcard; charset=utf-8</d:getcontenttype>
  173. <d:getetag>&quot;2df155fa5c2a24cd7f750353fc63f037&quot;</d:getetag>
  174. </d:prop>
  175. <d:status>HTTP/1.1 200 OK</d:status>
  176. </d:propstat>
  177. </d:response>
  178. <d:sync-token>http://sabre.io/ns/sync/3</d:sync-token>
  179. </d:multistatus>';
  180. $reportResponse = new Response(new PsrResponse(
  181. 207,
  182. ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
  183. $body
  184. ));
  185. $this->client
  186. ->method('request')
  187. ->willReturn($reportResponse);
  188. $vCard = 'BEGIN:VCARD
  189. VERSION:3.0
  190. PRODID:-//Sabre//Sabre VObject 4.5.4//EN
  191. UID:alice
  192. FN;X-NC-SCOPE=v2-federated:alice
  193. N;X-NC-SCOPE=v2-federated:alice;;;;
  194. X-SOCIALPROFILE;TYPE=NEXTCLOUD;X-NC-SCOPE=v2-published:https://server2.internal/index.php/u/alice
  195. CLOUD:alice@server2.internal
  196. END:VCARD';
  197. $getResponse = new Response(new PsrResponse(
  198. 200,
  199. ['Content-Type' => 'text/vcard; charset=utf-8', 'Content-Length' => strlen($vCard)],
  200. $vCard,
  201. ));
  202. $this->client
  203. ->method('get')
  204. ->willReturn($getResponse);
  205. $token = $this->service->syncRemoteAddressBook(
  206. '',
  207. 'system',
  208. 'system',
  209. '1234567890',
  210. null,
  211. '1',
  212. 'principals/system/system',
  213. []
  214. );
  215. $this->assertEquals('http://sabre.io/ns/sync/3', $token);
  216. }
  217. public function testSyncWithDeletedElement(): void {
  218. $this->backend->expects($this->exactly(0))
  219. ->method('createCard');
  220. $this->backend->expects($this->exactly(0))
  221. ->method('updateCard');
  222. $this->backend->expects($this->exactly(1))
  223. ->method('deleteCard');
  224. $body = '<?xml version="1.0"?>
  225. <d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
  226. <d:response>
  227. <d:href>/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf</d:href>
  228. <d:status>HTTP/1.1 404 Not Found</d:status>
  229. </d:response>
  230. <d:sync-token>http://sabre.io/ns/sync/4</d:sync-token>
  231. </d:multistatus>';
  232. $reportResponse = new Response(new PsrResponse(
  233. 207,
  234. ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
  235. $body
  236. ));
  237. $this->client
  238. ->method('request')
  239. ->willReturn($reportResponse);
  240. $token = $this->service->syncRemoteAddressBook(
  241. '',
  242. 'system',
  243. 'system',
  244. '1234567890',
  245. null,
  246. '1',
  247. 'principals/system/system',
  248. []
  249. );
  250. $this->assertEquals('http://sabre.io/ns/sync/4', $token);
  251. }
  252. public function testEnsureSystemAddressBookExists(): void {
  253. /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */
  254. $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
  255. $backend->expects($this->exactly(1))->method('createAddressBook');
  256. $backend->expects($this->exactly(2))
  257. ->method('getAddressBooksByUri')
  258. ->willReturnOnConsecutiveCalls(
  259. null,
  260. [],
  261. );
  262. /** @var IUserManager $userManager */
  263. $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock();
  264. $dbConnection = $this->createMock(IDBConnection::class);
  265. $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
  266. $converter = $this->createMock(Converter::class);
  267. $clientService = $this->createMock(IClientService::class);
  268. $config = $this->createMock(IConfig::class);
  269. $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
  270. $ss->ensureSystemAddressBookExists('principals/users/adam', 'contacts', []);
  271. }
  272. public function dataActivatedUsers() {
  273. return [
  274. [true, 1, 1, 1],
  275. [false, 0, 0, 3],
  276. ];
  277. }
  278. /**
  279. * @dataProvider dataActivatedUsers
  280. *
  281. * @param boolean $activated
  282. * @param integer $createCalls
  283. * @param integer $updateCalls
  284. * @param integer $deleteCalls
  285. * @return void
  286. */
  287. public function testUpdateAndDeleteUser($activated, $createCalls, $updateCalls, $deleteCalls): void {
  288. /** @var CardDavBackend | \PHPUnit\Framework\MockObject\MockObject $backend */
  289. $backend = $this->getMockBuilder(CardDavBackend::class)->disableOriginalConstructor()->getMock();
  290. $logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
  291. $backend->expects($this->exactly($createCalls))->method('createCard');
  292. $backend->expects($this->exactly($updateCalls))->method('updateCard');
  293. $backend->expects($this->exactly($deleteCalls))->method('deleteCard');
  294. $backend->method('getCard')->willReturnOnConsecutiveCalls(false, [
  295. 'carddata' => "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 3.4.8//EN\r\nUID:test-user\r\nFN:test-user\r\nN:test-user;;;;\r\nEND:VCARD\r\n\r\n"
  296. ]);
  297. $backend->method('getAddressBooksByUri')
  298. ->with('principals/system/system', 'system')
  299. ->willReturn(['id' => -1]);
  300. /** @var IUserManager | \PHPUnit\Framework\MockObject\MockObject $userManager */
  301. $userManager = $this->getMockBuilder(IUserManager::class)->disableOriginalConstructor()->getMock();
  302. $dbConnection = $this->createMock(IDBConnection::class);
  303. /** @var IUser | \PHPUnit\Framework\MockObject\MockObject $user */
  304. $user = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
  305. $user->method('getBackendClassName')->willReturn('unittest');
  306. $user->method('getUID')->willReturn('test-user');
  307. $user->method('getCloudId')->willReturn('cloudId');
  308. $user->method('getDisplayName')->willReturn('test-user');
  309. $user->method('isEnabled')->willReturn($activated);
  310. $converter = $this->createMock(Converter::class);
  311. $converter->expects($this->any())
  312. ->method('createCardFromUser')
  313. ->willReturn($this->createMock(VCard::class));
  314. $clientService = $this->createMock(IClientService::class);
  315. $config = $this->createMock(IConfig::class);
  316. $ss = new SyncService($backend, $userManager, $dbConnection, $logger, $converter, $clientService, $config);
  317. $ss->updateUser($user);
  318. $ss->updateUser($user);
  319. $ss->deleteUser($user);
  320. }
  321. public function testDeleteAddressbookWhenAccessRevoked(): void {
  322. $this->expectException(ClientExceptionInterface::class);
  323. $this->backend->expects($this->exactly(0))
  324. ->method('createCard');
  325. $this->backend->expects($this->exactly(0))
  326. ->method('updateCard');
  327. $this->backend->expects($this->exactly(0))
  328. ->method('deleteCard');
  329. $this->backend->expects($this->exactly(1))
  330. ->method('deleteAddressBook');
  331. $request = new PsrRequest(
  332. 'REPORT',
  333. 'https://server2.internal/remote.php/dav/addressbooks/system/system/system',
  334. ['Content-Type' => 'application/xml'],
  335. );
  336. $body = '<?xml version="1.0" encoding="utf-8"?>
  337. <d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
  338. <s:exception>Sabre\DAV\Exception\NotAuthenticated</s:exception>
  339. <s:message>No public access to this resource., Username or password was incorrect, No \'Authorization: Bearer\' header found. Either the client didn\'t send one, or the server is mis-configured, Username or password was incorrect</s:message>
  340. </d:error>';
  341. $response = new PsrResponse(
  342. 401,
  343. ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
  344. $body
  345. );
  346. $message = 'Client error: `REPORT https://server2.internal/cloud/remote.php/dav/addressbooks/system/system/system` resulted in a `401 Unauthorized` response:
  347. <?xml version="1.0" encoding="utf-8"?>
  348. <d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
  349. <s:exception>Sabre\DA (truncated...)
  350. ';
  351. $reportException = new ClientException(
  352. $message,
  353. $request,
  354. $response
  355. );
  356. $this->client
  357. ->method('request')
  358. ->willThrowException($reportException);
  359. $this->service->syncRemoteAddressBook(
  360. '',
  361. 'system',
  362. 'system',
  363. '1234567890',
  364. null,
  365. '1',
  366. 'principals/system/system',
  367. []
  368. );
  369. }
  370. /**
  371. * @dataProvider providerUseAbsoluteUriReport
  372. */
  373. public function testUseAbsoluteUriReport(string $host, string $expected): void {
  374. $body = '<?xml version="1.0"?>
  375. <d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:card="urn:ietf:params:xml:ns:carddav" xmlns:oc="http://owncloud.org/ns">
  376. <d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
  377. </d:multistatus>';
  378. $requestResponse = new Response(new PsrResponse(
  379. 207,
  380. ['Content-Type' => 'application/xml; charset=utf-8', 'Content-Length' => strlen($body)],
  381. $body
  382. ));
  383. $this->client
  384. ->method('request')
  385. ->with(
  386. 'REPORT',
  387. $this->callback(function ($uri) use ($expected) {
  388. $this->assertEquals($expected, $uri);
  389. return true;
  390. }),
  391. $this->callback(function ($options) {
  392. $this->assertIsArray($options);
  393. return true;
  394. }),
  395. )
  396. ->willReturn($requestResponse);
  397. $this->service->syncRemoteAddressBook(
  398. $host,
  399. 'system',
  400. 'remote.php/dav/addressbooks/system/system/system',
  401. '1234567890',
  402. null,
  403. '1',
  404. 'principals/system/system',
  405. []
  406. );
  407. }
  408. public function providerUseAbsoluteUriReport(): array {
  409. return [
  410. ['https://server.internal', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'],
  411. ['https://server.internal/', 'https://server.internal/remote.php/dav/addressbooks/system/system/system'],
  412. ['https://server.internal/nextcloud', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'],
  413. ['https://server.internal/nextcloud/', 'https://server.internal/nextcloud/remote.php/dav/addressbooks/system/system/system'],
  414. ['https://server.internal:8080', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'],
  415. ['https://server.internal:8080/', 'https://server.internal:8080/remote.php/dav/addressbooks/system/system/system'],
  416. ['https://server.internal:8080/nextcloud', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'],
  417. ['https://server.internal:8080/nextcloud/', 'https://server.internal:8080/nextcloud/remote.php/dav/addressbooks/system/system/system'],
  418. ];
  419. }
  420. }