SyncServiceTest.php 13 KB

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