* * @author Matthias Heinisch * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * 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 * along with this program. If not, see . * */ namespace OCA\Contacts\Service; use OCA\Contacts\Service\Social\CompositeSocialProvider; use OCA\Contacts\Service\Social\ISocialProvider; use OCP\AppFramework\Http; use OCP\Http\Client\IClient; use OCP\Http\Client\IResponse; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\Contacts\IManager; use OCP\IAddressBook; use OCA\DAV\CardDAV\CardDavBackend; use OCP\IURLGenerator; use OCP\IL10N; use OCP\Util; use OCP\AppFramework\Utility\ITimeFactory; use PHPUnit\Framework\MockObject\MockObject; use ChristophWurst\Nextcloud\Testing\TestCase; class SocialApiServiceTest extends TestCase { private $service; /** @var CompositeSocialProvider|MockObject */ private $socialProvider; /** @var IManager|MockObject */ private $manager; /** @var IConfig|MockObject */ private $config; /** @var IClientService|MockObject */ private $clientService; /** @var IL10N|MockObject */ private $l10n; /** @var IURLGenerator|MockObject */ private $urlGen; /** @var CardDavBackend|MockObject */ private $davBackend; /** @var ITimeFactory|MockObject */ private $timeFactory; public function allSocialProfileProviders() { $body = "the body"; $imageType = "jpg"; $contact = [ 'URI' => '3225c0d5-1bd2-43e5-a08c-4e65eaa406b0', 'VERSION' => '4.0' ]; $connector = $this->createMock(ISocialProvider::class); $connector->method('supportsContact')->willReturn(true); $connector->method('getImageUrls')->willReturn(["url1"]); $connectorNoSupport = $this->createMock(ISocialProvider::class); $connectorNoSupport->method('supportsContact')->willReturn(false); $connectorNoUrl = $this->createMock(ISocialProvider::class); $connectorNoUrl->method('supportsContact')->willReturn(true); $connectorNoUrl->method('getImageUrls')->willReturn([]); $addressbookEmpty = $this->createMock(IAddressBook::class); $addressbookEmpty ->method('getUri') ->willReturn('contacts'); $addressbookEmpty ->method('search') ->willReturn(null); $addressbook = $this->createMock(IAddressBook::class); $addressbook ->method('getUri') ->willReturn('contacts'); $addressbook ->method('search') ->willReturn([$contact]); return [ 'no address book found' => [null, [], "", "", Http::STATUS_BAD_REQUEST], 'no contact found' => [[$addressbookEmpty], [], "", "", Http::STATUS_PRECONDITION_FAILED], 'no supporting contacts found' => [[$addressbook], [$connectorNoSupport], "", "", Http::STATUS_PRECONDITION_FAILED], 'no url found' => [[$addressbook], [$connectorNoUrl], "", "", Http::STATUS_BAD_REQUEST], 'no image found' => [[$addressbook], [$connector], "", "", Http::STATUS_NOT_FOUND], 'image found' => [[$addressbook], [$connector], $body, $imageType, Http::STATUS_OK] ]; } public function updateAddressbookProvider() { return [ 'not user enabled' => ['yes', 'no', Http::STATUS_FORBIDDEN], 'not admin allowed' => ['no', 'yes', Http::STATUS_FORBIDDEN], 'not allowed, not enabled' => ['no', 'no', Http::STATUS_FORBIDDEN], 'allowed and enabled' => ['yes', 'yes', Http::STATUS_OK], ]; } protected function setUp(): void { parent::setUp(); $this->manager = $this->createMock(IManager::class); $this->config = $this->createMock(IConfig::class); $this->socialProvider = $this->createMock(CompositeSocialProvider::class); $this->clientService = $this->createMock(IClientService::class); $this->l10n = $this->createMock(IL10N::class); $this->urlGen = $this->createMock(IURLGenerator::class); $this->davBackend = $this->createMock(CardDavBackend::class); $this->timeFactory = $this->createMock(ITimeFactory::class); $this->service = new SocialApiService( $this->socialProvider, $this->manager, $this->config, $this->clientService, $this->l10n, $this->urlGen, $this->davBackend, $this->timeFactory ); } public function testDeactivatedSocial() { $this->config ->method('getAppValue') ->willReturn('no'); $result = $this->service->getSupportedNetworks(); $this->assertEmpty($result); } /** * @dataProvider allSocialProfileProviders */ public function testUpdateContactWithoutNetwork($addressbooks, $providers, $body, $imageType, $status) { $this->manager ->method('getUserAddressBooks') ->willReturn($addressbooks); $this->socialProvider ->method('getSocialConnectors') ->willReturn($providers); $response = $this->createMock(IResponse::class); $response ->method('getBody') ->willReturn($body); $response ->method('getHeader') ->willReturn($imageType); $client = $this->createMock(IClient::class); $client ->method('get') ->willReturn($response); $this->clientService ->method('NewClient') ->willReturn($client); $result = $this->service ->updateContact( 'contacts', '3225c0d5-1bd2-43e5-a08c-4e65eaa406b0', null); $this->assertEquals($status, $result->getStatus()); } public function testUpdateContactWithNetworkVersion3() { $network = "mastodon"; $body = "the body"; $imageType = "jpg"; $addressBookId = "contacts"; $contactId = "3225c0d5-1bd2-43e5-a08c-4e65eaa406b0"; $contact = [ 'URI' => $contactId, 'VERSION' => '3.0' ]; $provider = $this->createMock(ISocialProvider::class); $provider->method('supportsContact')->willReturn(true); $provider->method('getImageUrls')->willReturn(["url1"]); $addressbook = $this->createMock(IAddressBook::class); $addressbook ->method('getUri') ->willReturn('contacts'); $addressbook ->method('search') ->willReturn([$contact]); $this->manager ->method('getUserAddressBooks') ->willReturn([$addressbook]); $this->socialProvider ->method('getSocialConnectors') ->willReturn([$provider]); $this->socialProvider ->method('getSocialConnector') ->willReturn($provider); $response = $this->createMock(IResponse::class); $response ->method('getBody') ->willReturn($body); $response ->method('getHeader') ->willReturn($imageType); $client = $this->createMock(IClient::class); $client ->method('get') ->willReturn($response); $this->clientService ->method('NewClient') ->willReturn($client); $changes = [ 'URI' => $contact['URI'], 'VERSION' => $contact['VERSION'], 'PHOTO;ENCODING=b;TYPE=' . $imageType . ';VALUE=BINARY' => base64_encode($body) ]; $this->socialProvider ->expects($this->once())->method("getSocialConnector")->with($network); $provider->expects($this->once())->method("supportsContact")->with($contact); $addressbook->expects($this->once())->method("createOrUpdate")->with($changes, $addressBookId); $result = $this->service ->updateContact( $addressBookId, $contactId, $network); $this->assertEquals(Http::STATUS_OK, $result->getStatus()); } public function testUpdateContactWithNetworkVersion4() { $network = "mastodon"; $body = "the body"; $imageType = "jpg"; $addressBookId = "contacts"; $contactId = "3225c0d5-1bd2-43e5-a08c-4e65eaa406b0"; $contact = [ 'URI' => $contactId, 'VERSION' => '4.0' ]; $provider = $this->createMock(ISocialProvider::class); $provider->method('supportsContact')->willReturn(true); $provider->method('getImageUrls')->willReturn(["url1"]); $addressbook = $this->createMock(IAddressBook::class); $addressbook ->method('getUri') ->willReturn('contacts'); $addressbook ->method('search') ->willReturn([$contact]); $this->manager ->method('getUserAddressBooks') ->willReturn([$addressbook]); $this->socialProvider ->method('getSocialConnectors') ->willReturn([$provider]); $this->socialProvider ->method('getSocialConnector') ->willReturn($provider); $response = $this->createMock(IResponse::class); $response ->method('getBody') ->willReturn($body); $response ->method('getHeader') ->willReturn($imageType); $client = $this->createMock(IClient::class); $client ->method('get') ->willReturn($response); $this->clientService ->method('NewClient') ->willReturn($client); $changes = [ 'URI' => $contact['URI'], 'VERSION' => $contact['VERSION'], 'PHOTO' => "data:".$imageType.";base64," . base64_encode($body) ]; $this->socialProvider ->expects($this->once())->method("getSocialConnector")->with($network); $provider->expects($this->once())->method("supportsContact")->with($contact); $addressbook->expects($this->once())->method("createOrUpdate")->with($changes, $addressBookId); $result = $this->service ->updateContact( $addressBookId, $contactId, $network); $this->assertEquals(Http::STATUS_OK, $result->getStatus()); } protected function setupAddressbooks() { $validContact1 = [ 'UID' => '11111111-1111-1111-1111-111111111111', 'FN' => 'Valid Contact One', 'VERSION' => '4.0', 'X-SOCIALPROFILE' => [['type' => 'someNetwork', 'value' => 'someId1']], ]; $validContact2 = [ 'UID' => '22222222-2222-2222-2222-222222222222', 'FN' => 'Valid Contact Two', 'VERSION' => '4.0', 'PHOTO' => "data:someHeader;base64," . base64_encode('someBody'), 'X-SOCIALPROFILE' => [['type' => 'someNetwork', 'value' => 'someId2']], ]; $emptyContact = [ 'UID' => '00000000-0000-0000-0000-000000000000', 'FN' => 'Empty Contact', 'VERSION' => '4.0', ]; $invalidContact = [ 'UID' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', 'FN' => 'Invalid Contact', 'VERSION' => '4.0', 'X-SOCIALPROFILE' => [['type' => 'someNetwork', 'value' => 'invalidId']], ]; $addressbook1 = $this->createMock(IAddressBook::class); $addressbook1->method('getUri')->willReturn('contacts1'); $addressbook2 = $this->createMock(IAddressBook::class); $addressbook2->method('getUri')->willReturn('contacts2'); if (Util::getVersion()[0] >= 20) { // TODO: check can be removed as soon as min-dependency of contacts is NCv20 $addressbook1->method('isShared')->willReturn(false); $addressbook1->method('isSystemAddressBook')->willReturn(false); $addressbook2->method('isShared')->willReturn(false); $addressbook2->method('isSystemAddressBook')->willReturn(false); } $searchMap1 = [ ['', ['UID'], ['types' => true], [$validContact1, $invalidContact]], ['', ['X-SOCIALPROFILE'], ['types' => true], [$validContact1, $invalidContact]], [$validContact1['UID'], ['UID'], ['types' => true], [$validContact1]], [$invalidContact['UID'], ['UID'], ['types' => true], [$invalidContact]], ]; $searchMap2 = [ ['', ['UID'], ['types' => true], [$validContact2, $emptyContact]], ['', ['X-SOCIALPROFILE'], ['types' => true], [$validContact2]], [$validContact2['UID'], ['UID'], ['types' => true], [$validContact2]], [$emptyContact['UID'], ['UID'], ['types' => true], [$emptyContact]], ]; $addressbook1 ->method('search') ->will($this->returnValueMap($searchMap1)); $addressbook2 ->method('search') ->will($this->returnValueMap($searchMap2)); $this->manager ->method('getUserAddressBooks') ->willReturn([$addressbook1, $addressbook2]); $providerSupportsMap = [ [$validContact1, true], [$emptyContact, false], [$invalidContact, false], [$validContact2, true] ]; $providerUrlMap = [ [$validContact1, ["url1"]], [$emptyContact, []], [$invalidContact, []], [$validContact2, ["url1"]] ]; $provider = $this->createMock(ISocialProvider::class); $provider->method('getImageUrls') ->will($this->returnValueMap($providerUrlMap)); $provider->method('supportsContact') ->will($this->returnValueMap($providerSupportsMap)); $this->socialProvider ->method('getSocialConnectors') ->willReturn([$provider]); $this->socialProvider ->method('getSocialConnector') ->willReturn($provider); $validResponse = $this->createMock(IResponse::class); $validResponse ->method('getBody') ->willReturn('someBody'); $validResponse ->method('getHeader') ->willReturn('someHeader'); $client = $this->createMock(IClient::class); $client ->method('get') ->willReturn($validResponse); $this->clientService ->method('NewClient') ->willReturn($client); } /** * @dataProvider updateAddressbookProvider */ public function testUpdateAddressbooks($syncAllowedByAdmin, $bgSyncEnabledByUser, $expected) { $this->config ->method('getAppValue') ->willReturn($syncAllowedByAdmin); $this->config ->method('getUserValue') ->willReturn($bgSyncEnabledByUser); $this->timeFactory ->method('getTime') ->willReturn(10); $this->setupAddressbooks(); if ($syncAllowedByAdmin === 'yes' && $bgSyncEnabledByUser === 'yes') { $this->socialProvider ->expects($this->atLeastOnce()) ->method('getSocialConnectors'); } $this->socialProvider ->expects($this->never()) ->method('getSocialConnector'); $result = $this->service->updateAddressbooks('mrstest'); $this->assertEquals($expected, $result->getStatus()); if (($syncAllowedByAdmin === 'yes') && ($bgSyncEnabledByUser === 'yes')) { $report = $result->getData(); $this->assertArrayHasKey('0', $report); $this->assertArrayHasKey('updated', $report[0]); $this->assertContains('Valid Contact One', $report[0]['updated']); $this->assertArrayHasKey('checked', $report[0]); $this->assertContains('Valid Contact Two', $report[0]['checked']); $this->assertArrayHasKey('failed', $report[0]); $this->assertArrayHasKey('412', $report[0]['failed']); $this->assertContains('Invalid Contact', $report[0]['failed']['412']); $this->assertContains('Empty Contact', $report[0]['failed']['412']); } } public function testUpdateAddressbooksTimeout() { $this->config ->method('getAppValue') ->willReturn('yes'); $this->config ->method('getUserValue') ->willReturn('yes'); $this->timeFactory ->method('getTime') ->willReturnOnConsecutiveCalls(10, 11, 999); $this->setupAddressbooks(); $result = $this->service->updateAddressbooks('msstest'); $this->assertEquals(Http::STATUS_PARTIAL_CONTENT, $result->getStatus()); $report = $result->getData(); $this->assertArrayHasKey('0', $report); $this->assertArrayHasKey('stoppedAt', $report[0]); $this->assertArrayHasKey('addressBook', $report[0]['stoppedAt']); $this->assertArrayHasKey('contact', $report[0]['stoppedAt']); } /** * @dataProvider updateAddressbookProvider */ public function testUpdateAddressbooksContinued($syncAllowedByAdmin, $bgSyncEnabledByUser, $expected) { $this->config ->method('getAppValue') ->willReturn($syncAllowedByAdmin); $this->config ->method('getUserValue') ->willReturn($bgSyncEnabledByUser); $this->timeFactory ->method('getTime') ->willReturn(10); $this->setupAddressbooks(); $result = $this->service->updateAddressbooks('mrstest', 'contacts2', '22222222-2222-2222-2222-222222222222'); $this->assertEquals($expected, $result->getStatus()); if (($syncAllowedByAdmin === 'yes') && ($bgSyncEnabledByUser === 'yes')) { $report = $result->getData(); $this->assertArrayHasKey('0', $report); $this->assertArrayHasKey('updated', $report[0]); $this->assertNotContains('Valid Contact One', $report[0]['updated']); $this->assertArrayHasKey('checked', $report[0]); $this->assertContains('Valid Contact Two', $report[0]['checked']); } } public function testExistsContact() { $this->setupAddressbooks(); // all good: $result = $this->service->existsContact('11111111-1111-1111-1111-111111111111', 'contacts1', 'admin'); $this->assertEquals(true, $result); // wrong address book: $result = $this->service->existsContact('22222222-2222-2222-2222-222222222222', 'contacts1', 'admin'); $this->assertEquals(false, $result); // invalid contactId: $result = $this->service->existsContact('not-existing', 'contacts1', 'admin'); $this->assertEquals(false, $result); // invalid addressbookId: $result = $this->service->existsContact('11111111-1111-1111-1111-111111111111', 'not-existing', 'admin'); $this->assertEquals(false, $result); } }