* @author Bjoern Schiessle * @author Björn Schießle * @author Georg Ehrke * @author Joas Schilling * @author Thomas Müller * * @license AGPL-3.0 * * This code is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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, version 3, * along with this program. If not, see * */ namespace OCA\DAV\Tests\unit\CardDAV; use InvalidArgumentException; use OCA\DAV\CardDAV\AddressBook; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\Connector\Sabre\Principal; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IL10N; use OCP\IUserManager; use Sabre\DAV\PropPatch; use Sabre\VObject\Component\VCard; use Sabre\VObject\Property\Text; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; use Test\TestCase; /** * Class CardDavBackendTest * * @group DB * * @package OCA\DAV\Tests\unit\CardDAV */ class CardDavBackendTest extends TestCase { /** @var CardDavBackend */ private $backend; /** @var Principal | \PHPUnit_Framework_MockObject_MockObject */ private $principal; /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ private $userManager; /** @var EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject */ private $dispatcher; /** @var IDBConnection */ private $db; /** @var string */ private $dbCardsTable = 'cards'; /** @var string */ private $dbCardsPropertiesTable = 'cards_properties'; const UNIT_TEST_USER = 'principals/users/carddav-unit-test'; const UNIT_TEST_USER1 = 'principals/users/carddav-unit-test1'; const UNIT_TEST_GROUP = 'principals/groups/carddav-unit-test-group'; public function setUp() { parent::setUp(); $this->userManager = $this->createMock(IUserManager::class); $this->principal = $this->getMockBuilder('OCA\DAV\Connector\Sabre\Principal') ->disableOriginalConstructor() ->setMethods(['getPrincipalByPath', 'getGroupMembership']) ->getMock(); $this->principal->method('getPrincipalByPath') ->willReturn([ 'uri' => 'principals/best-friend', '{DAV:}displayname' => 'User\'s displayname', ]); $this->principal->method('getGroupMembership') ->withAnyParameters() ->willReturn([self::UNIT_TEST_GROUP]); $this->dispatcher = $this->createMock(EventDispatcherInterface::class); $this->db = \OC::$server->getDatabaseConnection(); $this->backend = new CardDavBackend($this->db, $this->principal, $this->userManager, $this->dispatcher); // start every test with a empty cards_properties and cards table $query = $this->db->getQueryBuilder(); $query->delete('cards_properties')->execute(); $query = $this->db->getQueryBuilder(); $query->delete('cards')->execute(); $this->tearDown(); } public function tearDown() { parent::tearDown(); if (is_null($this->backend)) { return; } $this->principal->method('getGroupMembership') ->withAnyParameters() ->willReturn([self::UNIT_TEST_GROUP]); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); foreach ($books as $book) { $this->backend->deleteAddressBook($book['id']); } } public function testAddressBookOperations() { // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $this->assertEquals(1, $this->backend->getAddressBooksForUserCount(self::UNIT_TEST_USER)); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $this->assertEquals('Example', $books[0]['{DAV:}displayname']); $this->assertEquals('User\'s displayname', $books[0]['{http://nextcloud.com/ns}owner-displayname']); // update it's display name $patch = new PropPatch([ '{DAV:}displayname' => 'Unit test', '{urn:ietf:params:xml:ns:carddav}addressbook-description' => 'Addressbook used for unit testing' ]); $this->backend->updateAddressBook($books[0]['id'], $patch); $patch->commit(); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $this->assertEquals('Unit test', $books[0]['{DAV:}displayname']); $this->assertEquals('Addressbook used for unit testing', $books[0]['{urn:ietf:params:xml:ns:carddav}addressbook-description']); // delete the address book $this->backend->deleteAddressBook($books[0]['id']); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(0, count($books)); } public function testAddressBookSharing() { $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $l = $this->createMock(IL10N::class); $addressBook = new AddressBook($this->backend, $books[0], $l); $this->backend->updateShares($addressBook, [ [ 'href' => 'principal:' . self::UNIT_TEST_USER1, ], [ 'href' => 'principal:' . self::UNIT_TEST_GROUP, ] ], []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER1); $this->assertEquals(1, count($books)); // delete the address book $this->backend->deleteAddressBook($books[0]['id']); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(0, count($books)); } public function testCardOperations() { /** @var CardDavBackend | \PHPUnit_Framework_MockObject_MockObject $backend */ $backend = $this->getMockBuilder(CardDavBackend::class) ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher]) ->setMethods(['updateProperties', 'purgeProperties'])->getMock(); // create a new address book $backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $bookId = $books[0]['id']; $uri = $this->getUniqueID('card'); // updateProperties is expected twice, once for createCard and once for updateCard $backend->expects($this->at(0))->method('updateProperties')->with($bookId, $uri, ''); $backend->expects($this->at(1))->method('updateProperties')->with($bookId, $uri, '***'); // Expect event $this->dispatcher->expects($this->at(0)) ->method('dispatch') ->with('\OCA\DAV\CardDAV\CardDavBackend::createCard', $this->callback(function(GenericEvent $e) use ($bookId, $uri) { return $e->getArgument('addressBookId') === $bookId && $e->getArgument('cardUri') === $uri && $e->getArgument('cardData') === ''; })); // create a card $backend->createCard($bookId, $uri, ''); // get all the cards $cards = $backend->getCards($bookId); $this->assertEquals(1, count($cards)); $this->assertEquals('', $cards[0]['carddata']); // get the cards $card = $backend->getCard($bookId, $uri); $this->assertNotNull($card); $this->assertArrayHasKey('id', $card); $this->assertArrayHasKey('uri', $card); $this->assertArrayHasKey('lastmodified', $card); $this->assertArrayHasKey('etag', $card); $this->assertArrayHasKey('size', $card); $this->assertEquals('', $card['carddata']); // Expect event $this->dispatcher->expects($this->at(0)) ->method('dispatch') ->with('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $this->callback(function(GenericEvent $e) use ($bookId, $uri) { return $e->getArgument('addressBookId') === $bookId && $e->getArgument('cardUri') === $uri && $e->getArgument('cardData') === '***'; })); // update the card $backend->updateCard($bookId, $uri, '***'); $card = $backend->getCard($bookId, $uri); $this->assertEquals('***', $card['carddata']); // Expect event $this->dispatcher->expects($this->at(0)) ->method('dispatch') ->with('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', $this->callback(function(GenericEvent $e) use ($bookId, $uri) { return $e->getArgument('addressBookId') === $bookId && $e->getArgument('cardUri') === $uri; })); // delete the card $backend->expects($this->once())->method('purgeProperties')->with($bookId, $card['id']); $backend->deleteCard($bookId, $uri); $cards = $backend->getCards($bookId); $this->assertEquals(0, count($cards)); } public function testMultiCard() { $this->backend = $this->getMockBuilder(CardDavBackend::class) ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher]) ->setMethods(['updateProperties'])->getMock(); // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $bookId = $books[0]['id']; // create a card $uri0 = $this->getUniqueID('card'); $this->backend->createCard($bookId, $uri0, ''); $uri1 = $this->getUniqueID('card'); $this->backend->createCard($bookId, $uri1, ''); $uri2 = $this->getUniqueID('card'); $this->backend->createCard($bookId, $uri2, ''); // get all the cards $cards = $this->backend->getCards($bookId); $this->assertEquals(3, count($cards)); $this->assertEquals('', $cards[0]['carddata']); $this->assertEquals('', $cards[1]['carddata']); $this->assertEquals('', $cards[2]['carddata']); // get the cards $cards = $this->backend->getMultipleCards($bookId, [$uri1, $uri2]); $this->assertEquals(2, count($cards)); foreach($cards as $card) { $this->assertArrayHasKey('id', $card); $this->assertArrayHasKey('uri', $card); $this->assertArrayHasKey('lastmodified', $card); $this->assertArrayHasKey('etag', $card); $this->assertArrayHasKey('size', $card); $this->assertEquals('', $card['carddata']); } // delete the card $this->backend->deleteCard($bookId, $uri0); $this->backend->deleteCard($bookId, $uri1); $this->backend->deleteCard($bookId, $uri2); $cards = $this->backend->getCards($bookId); $this->assertEquals(0, count($cards)); } public function testDeleteWithoutCard() { $this->backend = $this->getMockBuilder(CardDavBackend::class) ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher]) ->setMethods([ 'getCardId', 'addChange', 'purgeProperties', 'updateProperties', ]) ->getMock(); // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getUsersOwnAddressBooks(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $bookId = $books[0]['id']; $uri = $this->getUniqueID('card'); // create a new address book $this->backend->expects($this->once()) ->method('getCardId') ->with($bookId, $uri) ->willThrowException(new \InvalidArgumentException()); $this->backend->expects($this->exactly(2)) ->method('addChange') ->withConsecutive( [$bookId, $uri, 1], [$bookId, $uri, 3] ); $this->backend->expects($this->never()) ->method('purgeProperties'); // create a card $this->backend->createCard($bookId, $uri, ''); // delete the card $this->assertTrue($this->backend->deleteCard($bookId, $uri)); } public function testSyncSupport() { $this->backend = $this->getMockBuilder(CardDavBackend::class) ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher]) ->setMethods(['updateProperties'])->getMock(); // create a new address book $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $bookId = $books[0]['id']; // fist call without synctoken $changes = $this->backend->getChangesForAddressBook($bookId, '', 1); $syncToken = $changes['syncToken']; // add a change $uri0 = $this->getUniqueID('card'); $this->backend->createCard($bookId, $uri0, ''); // look for changes $changes = $this->backend->getChangesForAddressBook($bookId, $syncToken, 1); $this->assertEquals($uri0, $changes['added'][0]); } public function testSharing() { $this->backend->createAddressBook(self::UNIT_TEST_USER, 'Example', []); $books = $this->backend->getAddressBooksForUser(self::UNIT_TEST_USER); $this->assertEquals(1, count($books)); $l = $this->createMock(IL10N::class); $exampleBook = new AddressBook($this->backend, $books[0], $l); $this->backend->updateShares($exampleBook, [['href' => 'principal:principals/best-friend']], []); $shares = $this->backend->getShares($exampleBook->getResourceId()); $this->assertEquals(1, count($shares)); // adding the same sharee again has no effect $this->backend->updateShares($exampleBook, [['href' => 'principal:principals/best-friend']], []); $shares = $this->backend->getShares($exampleBook->getResourceId()); $this->assertEquals(1, count($shares)); $books = $this->backend->getAddressBooksForUser('principals/best-friend'); $this->assertEquals(1, count($books)); $this->backend->updateShares($exampleBook, [], ['principal:principals/best-friend']); $shares = $this->backend->getShares($exampleBook->getResourceId()); $this->assertEquals(0, count($shares)); $books = $this->backend->getAddressBooksForUser('principals/best-friend'); $this->assertEquals(0, count($books)); } public function testUpdateProperties() { $bookId = 42; $cardUri = 'card-uri'; $cardId = 2; $backend = $this->getMockBuilder(CardDavBackend::class) ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher]) ->setMethods(['getCardId'])->getMock(); $backend->expects($this->any())->method('getCardId')->willReturn($cardId); // add properties for new vCard $vCard = new VCard(); $vCard->UID = $cardUri; $vCard->FN = 'John Doe'; $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); $query = $this->db->getQueryBuilder(); $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); $this->assertSame(2, count($result)); $this->assertSame('UID', $result[0]['name']); $this->assertSame($cardUri, $result[0]['value']); $this->assertSame($bookId, (int)$result[0]['addressbookid']); $this->assertSame($cardId, (int)$result[0]['cardid']); $this->assertSame('FN', $result[1]['name']); $this->assertSame('John Doe', $result[1]['value']); $this->assertSame($bookId, (int)$result[1]['addressbookid']); $this->assertSame($cardId, (int)$result[1]['cardid']); // update properties for existing vCard $vCard = new VCard(); $vCard->UID = $cardUri; $this->invokePrivate($backend, 'updateProperties', [$bookId, $cardUri, $vCard->serialize()]); $query = $this->db->getQueryBuilder(); $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); $this->assertSame(1, count($result)); $this->assertSame('UID', $result[0]['name']); $this->assertSame($cardUri, $result[0]['value']); $this->assertSame($bookId, (int)$result[0]['addressbookid']); $this->assertSame($cardId, (int)$result[0]['cardid']); } public function testPurgeProperties() { $query = $this->db->getQueryBuilder(); $query->insert('cards_properties') ->values( [ 'addressbookid' => $query->createNamedParameter(1), 'cardid' => $query->createNamedParameter(1), 'name' => $query->createNamedParameter('name1'), 'value' => $query->createNamedParameter('value1'), 'preferred' => $query->createNamedParameter(0) ] ); $query->execute(); $query = $this->db->getQueryBuilder(); $query->insert('cards_properties') ->values( [ 'addressbookid' => $query->createNamedParameter(1), 'cardid' => $query->createNamedParameter(2), 'name' => $query->createNamedParameter('name2'), 'value' => $query->createNamedParameter('value2'), 'preferred' => $query->createNamedParameter(0) ] ); $query->execute(); $this->invokePrivate($this->backend, 'purgeProperties', [1, 1]); $query = $this->db->getQueryBuilder(); $result = $query->select('*')->from('cards_properties')->execute()->fetchAll(); $this->assertSame(1, count($result)); $this->assertSame(1 ,(int)$result[0]['addressbookid']); $this->assertSame(2 ,(int)$result[0]['cardid']); } public function testGetCardId() { $query = $this->db->getQueryBuilder(); $query->insert('cards') ->values( [ 'addressbookid' => $query->createNamedParameter(1), 'carddata' => $query->createNamedParameter(''), 'uri' => $query->createNamedParameter('uri'), 'lastmodified' => $query->createNamedParameter(4738743), 'etag' => $query->createNamedParameter('etag'), 'size' => $query->createNamedParameter(120) ] ); $query->execute(); $id = $query->getLastInsertId(); $this->assertSame($id, $this->invokePrivate($this->backend, 'getCardId', [1, 'uri'])); } /** * @expectedException InvalidArgumentException */ public function testGetCardIdFailed() { $this->invokePrivate($this->backend, 'getCardId', [1, 'uri']); } /** * @dataProvider dataTestSearch * * @param string $pattern * @param array $properties * @param array $expected */ public function testSearch($pattern, $properties, $expected) { /** @var VCard $vCards */ $vCards = []; $vCards[0] = new VCard(); $vCards[0]->add(new Text($vCards[0], 'UID', 'uid')); $vCards[0]->add(new Text($vCards[0], 'FN', 'John Doe')); $vCards[0]->add(new Text($vCards[0], 'CLOUD', 'john@owncloud.org')); $vCards[1] = new VCard(); $vCards[1]->add(new Text($vCards[1], 'UID', 'uid')); $vCards[1]->add(new Text($vCards[1], 'FN', 'John M. Doe')); $vCardIds = []; $query = $this->db->getQueryBuilder(); for($i=0; $i<2; $i++) { $query->insert($this->dbCardsTable) ->values( [ 'addressbookid' => $query->createNamedParameter(0), 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB), 'uri' => $query->createNamedParameter('uri' . $i), 'lastmodified' => $query->createNamedParameter(time()), 'etag' => $query->createNamedParameter('etag' . $i), 'size' => $query->createNamedParameter(120), ] ); $query->execute(); $vCardIds[] = $query->getLastInsertId(); } $query->insert($this->dbCardsPropertiesTable) ->values( [ 'addressbookid' => $query->createNamedParameter(0), 'cardid' => $query->createNamedParameter($vCardIds[0]), 'name' => $query->createNamedParameter('FN'), 'value' => $query->createNamedParameter('John Doe'), 'preferred' => $query->createNamedParameter(0) ] ); $query->execute(); $query->insert($this->dbCardsPropertiesTable) ->values( [ 'addressbookid' => $query->createNamedParameter(0), 'cardid' => $query->createNamedParameter($vCardIds[0]), 'name' => $query->createNamedParameter('CLOUD'), 'value' => $query->createNamedParameter('John@owncloud.org'), 'preferred' => $query->createNamedParameter(0) ] ); $query->execute(); $query->insert($this->dbCardsPropertiesTable) ->values( [ 'addressbookid' => $query->createNamedParameter(0), 'cardid' => $query->createNamedParameter($vCardIds[1]), 'name' => $query->createNamedParameter('FN'), 'value' => $query->createNamedParameter('John M. Doe'), 'preferred' => $query->createNamedParameter(0) ] ); $query->execute(); $result = $this->backend->search(0, $pattern, $properties); // check result $this->assertSame(count($expected), count($result)); $found = []; foreach ($result as $r) { foreach ($expected as $exp) { if ($r['uri'] === $exp[0] && strpos($r['carddata'], $exp[1]) > 0) { $found[$exp[1]] = true; break; } } } $this->assertSame(count($expected), count($found)); } public function dataTestSearch() { return [ ['John', ['FN'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], ['M. Doe', ['FN'], [['uri1', 'John M. Doe']]], ['Do', ['FN'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], 'check if duplicates are handled correctly' => ['John', ['FN', 'CLOUD'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]], 'case insensitive' => ['john', ['FN'], [['uri0', 'John Doe'], ['uri1', 'John M. Doe']]] ]; } public function testGetCardUri() { $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsTable) ->values( [ 'addressbookid' => $query->createNamedParameter(1), 'carddata' => $query->createNamedParameter('carddata', IQueryBuilder::PARAM_LOB), 'uri' => $query->createNamedParameter('uri'), 'lastmodified' => $query->createNamedParameter(5489543), 'etag' => $query->createNamedParameter('etag'), 'size' => $query->createNamedParameter(120), ] ); $query->execute(); $id = $query->getLastInsertId(); $this->assertSame('uri', $this->backend->getCardUri($id)); } /** * @expectedException InvalidArgumentException */ public function testGetCardUriFailed() { $this->backend->getCardUri(1); } public function testGetContact() { $query = $this->db->getQueryBuilder(); for($i=0; $i<2; $i++) { $query->insert($this->dbCardsTable) ->values( [ 'addressbookid' => $query->createNamedParameter($i), 'carddata' => $query->createNamedParameter('carddata' . $i, IQueryBuilder::PARAM_LOB), 'uri' => $query->createNamedParameter('uri' . $i), 'lastmodified' => $query->createNamedParameter(5489543), 'etag' => $query->createNamedParameter('etag' . $i), 'size' => $query->createNamedParameter(120), ] ); $query->execute(); } $result = $this->backend->getContact(0, 'uri0'); $this->assertSame(7, count($result)); $this->assertSame(0, (int)$result['addressbookid']); $this->assertSame('uri0', $result['uri']); $this->assertSame(5489543, (int)$result['lastmodified']); $this->assertSame('etag0', $result['etag']); $this->assertSame(120, (int)$result['size']); // this shouldn't return any result because 'uri1' is in address book 1 // see https://github.com/nextcloud/server/issues/229 $result = $this->backend->getContact(0, 'uri1'); $this->assertEmpty($result); } public function testGetContactFail() { $this->assertEmpty($this->backend->getContact(0, 'uri')); } public function testCollectCardProperties() { $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) ->values( [ 'addressbookid' => $query->createNamedParameter(666), 'cardid' => $query->createNamedParameter(777), 'name' => $query->createNamedParameter('FN'), 'value' => $query->createNamedParameter('John Doe'), 'preferred' => $query->createNamedParameter(0) ] ) ->execute(); $result = $this->backend->collectCardProperties(666, 'FN'); $this->assertEquals(['John Doe'], $result); } }