Viewing File: /usr/local/cpanel/base/3rdparty/roundcube/plugins/carddav/tests/Unit/AddressbookTest.php
<?php
/*
* RCMCardDAV - CardDAV plugin for Roundcube webmail
*
* Copyright (C) 2011-2022 Benjamin Schieder <rcmcarddav@wegwerf.anderdonau.de>,
* Michael Stilkerich <ms@mike2k.de>
*
* This file is part of RCMCardDAV.
*
* RCMCardDAV is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* RCMCardDAV 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with RCMCardDAV. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace MStilkerich\Tests\RCMCardDAV\Unit;
use PHPUnit\Framework\TestCase;
use MStilkerich\Tests\RCMCardDAV\TestInfrastructure;
use MStilkerich\RCMCardDAV\{Addressbook,DataConversion,DelayedPhotoLoader};
use MStilkerich\RCMCardDAV\Db\AbstractDatabase;
use MStilkerich\RCMCardDAV\Db\Database;
use MStilkerich\RCMCardDAV\Db\DbAndCondition;
use MStilkerich\RCMCardDAV\Frontend\AddressbookManager;
use MStilkerich\CardDavClient\AddressbookCollection;
use rcube_addressbook;
/**
* @psalm-import-type FullAccountRow from AbstractDatabase
* @psalm-import-type FullAbookRow from AbstractDatabase
* @psalm-import-type SaveData from DataConversion
*
* @psalm-import-type Int1 from AddressbookManager
* @psalm-import-type AccountCfg from AddressbookManager
* @psalm-import-type AbookCfg from AddressbookManager
*
* @psalm-type CfgOverride = array{
* refresh_time?: numeric-string,
* last_updated?: numeric-string,
* readonly?: Int1,
* require_always_email?: Int1,
* }
*/
final class AddressbookTest extends TestCase
{
/** @var JsonDatabase */
private $db;
public static function setUpBeforeClass(): void
{
$_SESSION['user_id'] = 1000;
$_SESSION['username'] = 'user@example.com';
}
public function setUp(): void
{
$this->db = new JsonDatabase();
$cache = $this->createMock(\rcube_cache::class);
TestInfrastructure::init($this->db);
TestInfrastructure::$infra->setCache($cache);
}
public function tearDown(): void
{
Utils::cleanupTempImages();
TestInfrastructure::logger()->reset();
}
/**
* Adds the addressbook flags to an addressbook DB row to create an addressbook config.
* @param FullAbookRow $abookRow
* @return AbookCfg
*/
private function addABookFlags(array $abookRow): array
{
foreach (AbstractDatabase::FLAGS_COLS['addressbooks']['fields'] as $attr => $bitPos) {
$flags = intval($abookRow['flags']);
$abookRow[$attr] = ($flags & (1 << $bitPos)) ? '1' : '0';
}
return $abookRow;
}
/**
* Adds the account flags to an account DB row to create an account config.
* @param FullAccountRow $accountRow
* @return AccountCfg
*/
private function addAccountFlags(array $accountRow): array
{
foreach (AbstractDatabase::FLAGS_COLS['accounts']['fields'] as $attr => $bitPos) {
$flags = intval($accountRow['flags']);
$accountRow[$attr] = ($flags & (1 << $bitPos)) ? '1' : '0';
}
return $accountRow;
}
/**
* @param CfgOverride $cfgOverride
*/
private function createAbook(array $cfgOverride = []): Addressbook
{
$db = $this->db;
$db->importData('tests/Unit/data/syncHandlerTest/initial/db.json');
/** @var FullAbookRow */
$abookCfg = $db->lookup("42", [], 'addressbooks');
$abookCfg = $this->addABookFlags($abookCfg);
// Override config settings
$abookCfg = array_merge($abookCfg, $cfgOverride);
/** @var FullAccountRow */
$accountCfg = $db->lookup($abookCfg['account_id'], [], 'accounts');
$accountCfg = $this->addAccountFlags($accountCfg);
$account = TestInfrastructure::$infra->makeAccount($accountCfg);
$abook = new Addressbook("42", $account, $abookCfg);
$davobj = $this->createStub(AddressbookCollection::class);
$davobj->method('downloadResource')->will($this->returnCallback([Utils::class, 'downloadResource']));
TestInfrastructure::setPrivateProperty($abook, 'davAbook', $davobj);
return $abook;
}
/**
* Tests that a newly constructed addressbook has the expected values in its public properties.
*/
public function testAddressbookHasExpectedPublicPropertyValues(): void
{
$db = $this->db;
$abook = $this->createAbook();
$this->assertSame('id', $abook->primary_key);
$this->assertSame(true, $abook->groups);
$this->assertSame(true, $abook->export_groups);
$this->assertSame(false, $abook->readonly);
$this->assertSame(false, $abook->searchonly);
$this->assertSame(false, $abook->undelete);
$this->assertSame(true, $abook->ready);
$this->assertSame('name', $abook->sort_col);
$this->assertSame('ASC', $abook->sort_order);
$this->assertSame(['birthday', 'anniversary'], $abook->date_cols);
$this->assertNull($abook->group_id);
$this->assertIsArray($abook->coltypes['email']);
$this->assertIsArray($abook->coltypes['email']['subtypes']);
$this->assertContains("SpecialLabel", $abook->coltypes['email']['subtypes']);
$this->assertSame("Test Addressbook", $abook->get_name());
$this->assertSame("42", $abook->getId());
/** @var FullAbookRow */
$abookCfg = $db->lookup("42", [], 'addressbooks');
$abookCfg = $this->addABookFlags($abookCfg);
$abookCfg['readonly'] = '1';
/** @var FullAccountRow */
$accountCfg = $db->lookup($abookCfg['account_id'], [], 'accounts');
$accountCfg = $this->addAccountFlags($accountCfg);
$account = TestInfrastructure::$infra->makeAccount($accountCfg);
$roAbook = new Addressbook("42", $account, $abookCfg);
$this->assertSame(true, $roAbook->readonly);
}
/**
* @return list<array{int,string,?string,int,int,null|0|string,?list<string>,bool,int,list<string>}>
*/
public function listRecordsDataProvider(): array
{
return [
// subset, sort_col, sort_order, page, pagesize, group, cols, reqCols, expCount, expRecords
[ 0, 'name', 'ASC', 1, 10, 0, null, false, 6, ["56", "51", "50", "52", "53", "54"] ],
[ 0, 'name', 'DESC', 1, 10, "0", null, false, 6, ["54", "53", "52", "50", "51", "56"] ],
[ 0, 'firstname', null, 1, 10, null, null, false, 6, ["51", "52", "53", "56", "50", "54"] ],
[ 0, 'name', 'ASC', 1, 4, 0, null, false, 6, ["56", "51", "50", "52"] ],
[ 0, 'name', 'ASC', 2, 4, 0, null, false, 6, ["53", "54"] ],
[ 0, 'name', 'DESC', 3, 2, 0, null, false, 6, ["51", "56"] ],
[ 1, 'name', 'DESC', 2, 2, 0, null, false, 6, ["52"] ],
[ 2, 'name', 'DESC', 2, 2, 0, null, false, 6, ["52", "50"] ],
[ 3, 'name', 'DESC', 2, 2, 0, null, false, 6, ["52", "50"] ],
[ -1, 'name', 'DESC', 2, 2, 0, null, false, 6, ["50"] ],
[ -2, 'name', 'DESC', 2, 2, 0, null, false, 6, ["52", "50"] ],
[ -3, 'name', 'DESC', 2, 2, 0, null, false, 6, ["52", "50"] ],
[ 0, 'name', 'ASC', 1, 10, "500", null, false, 2, ["56", "50"] ],
[ 0, 'name', 'ASC', 1, 10, 0, ['name','email'], false, 6, ["56", "51", "50", "52", "53", "54"] ],
[ 0, 'name', 'ASC', 1, 10, 0, ['organization', 'firstname'],
false, 6, ["56", "51", "50", "52", "53", "54"] ],
[ 0, 'name', 'ASC', 1, 10, 0, null, true, 5, ["51", "50", "52", "53", "54"] ],
[ 0, 'name', 'ASC', 2, 2, 0, null, true, 5, ["52", "53"] ],
];
}
/**
* Tests list_records()
*
* @dataProvider listRecordsDataProvider
* @param list<string> $expRecords
* @param null|0|string $gid
* @param ?list<string> $cols
*/
public function testListRecordsReturnsExpectedRecords(
int $subset,
string $sortCol,
?string $sortOrder,
int $page,
int $pagesize,
$gid,
?array $cols,
bool $reqEmail,
int $expCount,
array $expRecords
): void {
$abook = $this->createAbookForSearchTest($sortCol, $sortOrder, $page, $pagesize, $gid, $reqEmail);
$rset = $abook->list_records($cols, $subset);
$this->assertNull($abook->get_error());
$this->assertSame($rset, $abook->get_result(), "Get result does not return last result set");
$this->assertSame(($page - 1) * $pagesize, $rset->first);
$this->assertFalse($rset->searchonly);
$this->assertSame($expCount, $rset->count);
$this->assertCount(count($expRecords), $rset->records);
$lrOrder = array_column($rset->records, 'ID');
$this->assertSame($expRecords, $lrOrder, "Card order mismatch");
for ($i = 0; $i < count($expRecords); ++$i) {
$id = $expRecords[$i];
$fn = "tests/Unit/data/addressbookTest/c{$id}.json";
$saveDataExp = Utils::readSaveDataFromJson($fn);
if (isset($cols)) {
$saveDataExp = $this->stripSaveDataToDbColumns($saveDataExp, $cols);
}
/** @var array $saveDataRc */
$saveDataRc = $rset->records[$i];
Utils::compareSaveData($saveDataExp, $saveDataRc, "Unexpected record data $id");
if (isset($saveDataExp['photo']) && empty($saveDataExp['photo'])) {
$this->assertPhotoDownloadWarning();
}
}
}
/**
* Initializes an addressbook for the tests of list_records(), count() and search().
*
* @param null|0|string $gid
*/
private function createAbookForSearchTest(
string $sortCol,
?string $sortOrder,
int $page,
int $pagesize,
$gid,
bool $reqEmail
): Addressbook {
$abook = $this->createAbook(['require_always_email' => ($reqEmail ? '1' : '0')]);
$abook->set_page($page);
$this->assertSame($page, $abook->list_page);
$abook->set_pagesize($pagesize);
$this->assertSame($pagesize, $abook->page_size);
$abook->set_sort_order($sortCol, $sortOrder);
$this->assertSame($sortCol, $abook->sort_col);
$this->assertSame($sortOrder ?? 'ASC', $abook->sort_order);
$abook->set_group($gid);
if ($gid) {
$this->assertSame($gid, $abook->group_id);
} else {
$this->assertNull($abook->group_id);
}
return $abook;
}
/**
* Tests count()
*
* @dataProvider listRecordsDataProvider
* @param list<string> $expRecords
* @param null|0|string $gid
* @param ?list<string> $cols
*/
public function testCountProvidesExpectedNumberOfRecords(
int $subset,
string $sortCol,
?string $sortOrder,
int $page,
int $pagesize,
$gid,
?array $cols,
bool $reqEmail,
int $expCount,
array $expRecords
): void {
$abook = $this->createAbookForSearchTest($sortCol, $sortOrder, $page, $pagesize, $gid, $reqEmail);
$rset = $abook->count();
$this->assertNull($abook->get_error());
$this->assertSame(($page - 1) * $pagesize, $rset->first);
$this->assertFalse($rset->searchonly);
$this->assertSame($expCount, $rset->count);
$this->assertCount(0, $rset->records);
}
/**
* @return array<string, array{
* string|string[], string|string[], int, bool, bool,
* string,?string,int,int,
* null|0|string,
* bool,
* string|string[],
* int,list<string>
* }>
*/
public function searchDataProvider(): array
{
return [
'Direct ID search single id' => [
'ID', "50", 0, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["50"]
],
'Direct ID search with key property from addressbook' => [
'id', "50", 0, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["50"]
],
'Direct ID search multiple id as string' => [
'ID', "50,56,52,70", 0, true, false, // 70 belongs to a different addressbook and must not be returned
'name', 'ASC', 1, 10,
0,
false, [],
3, ["56", "50", "52"]
],
'Direct ID search multiple id as array' => [
'ID', ["50", "56", "52"], 0, true, false,
'name', 'DESC', 1, 10,
0,
false, [],
3, ["52", "50", "56"]
],
'Single DB field search with prefix search' => [
['name'], ["ROB"], rcube_addressbook::SEARCH_PREFIX, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["53"]
],
'Single Multivalue DB field search with contains search' => [
['email'], ["north@7kingdoms.com"], rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
2, ["60", "50"]
],
'Single Multivalue DB structured-content field search with contains search' => [
['address'], ["Kanto"], rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["60"]
],
'Single Multivalue DB field search with strict search' => [
['email'], ["north@7kingdoms.com"], rcube_addressbook::SEARCH_STRICT, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["60"]
],
'Multi DB field search with contains search' => [
['name', 'email'], ["Lannister", "@example"], rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["51"]
],
'Multi DB/vcard field search with contains search' => [
['name', 'jobtitle'], ["Stark", "Warden"], rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["50"]
],
'Vcard field search with post-search drop' => [ // vcard as a whole matches, but not the asked for field
['assistant'], ["Wesker"], rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
0, []
],
'Vcard field search with exact match' => [
['assistant'], ["Dr. Alexander Isaacs"], rcube_addressbook::SEARCH_STRICT, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["60"]
],
'Vcard field search with exact match (no match)' => [
['assistant'], ["Dr. Alexander Isaac"], rcube_addressbook::SEARCH_STRICT, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
0, []
],
'Multi Vcard field search with exact match' => [
['assistant', 'manager'], ["dr. alex", "no "],
rcube_addressbook::SEARCH_PREFIX, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["60"]
],
'All fields search' => [
'*', 'Birkin',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
1, ["60"]
],
'All fields search (no matches)' => [
'*', 'Birkin22',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
0, []
],
'Two fields OR search' => [
['organization', 'name'], 'the',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
3, ["50", "53", "54"]
],
'Mixed DB/vcard fields OR search' => [
['notes', 'organization', 'name'], 'the',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, [],
4, ["60", "50", "53", "54"]
],
'Results for 2nd page only' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 2, 2,
0,
false, [],
6, ["50", "52"]
],
'Results for 2nd page only (select only)' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, true, true,
'name', 'ASC', 2, 2,
0,
false, [],
2, ["50", "52"]
],
'Results for 2nd page only (count only)' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, false, false,
'name', 'ASC', 2, 2,
0,
false, [],
6, []
],
'With required DB field' => [
'*', 'stark',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, ['firstname'],
2, ["56", "50"]
],
'With required DB field (plus required abook field)' => [
'*', 'stark',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
true, ['firstname'],
1, ["50"]
],
'With required VCard field' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, ['jobtitle'],
2, ["60", "50"]
],
'With required VCard field (as string)' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
0,
false, 'jobtitle',
2, ["60", "50"]
],
'With required VCard field and group filter' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
"500",
false, 'jobtitle',
1, ["50"]
],
'With required VCard field and group filter for empty group' => [
'*', 'example',
rcube_addressbook::SEARCH_ALL, true, false,
'name', 'ASC', 1, 10,
"504",
false, 'jobtitle',
0, []
],
];
}
/**
* Tests the search() function.
*
* @dataProvider searchDataProvider
* @param string|string[] $fields
* @param string|string[] $value
* @param null|0|string $gid
* @param bool $reqEmail If true, set require_always_email parameter for the addressbook
* @param string|string[] $reqColsCl Required columns search() parameter
* @param list<string> $expRecords
*/
public function testSearchReturnsExpectedRecords(
$fields,
$value,
int $mode,
bool $select,
bool $nocount,
string $sortCol,
?string $sortOrder,
int $page,
int $pagesize,
$gid,
bool $reqEmail,
$reqColsCl,
int $expCount,
array $expRecords
): void {
$abook = $this->createAbookForSearchTest($sortCol, $sortOrder, $page, $pagesize, $gid, $reqEmail);
$db = $this->db;
$db->importData('tests/Unit/data/addressbookTest/db2.json');
// Try with search() and a second time with set_search_set() + list_records()
for ($run = 0; $run < 2; ++$run) {
if ($run == 0) {
$rset = $abook->search($fields, $value, $mode, $select, $nocount, $reqColsCl);
} else {
// After the search, the search filter should be installed
$filter = $abook->get_search_set();
$this->assertNotEmpty($filter);
$abook->reset();
$this->assertEmpty($abook->get_search_set());
$this->assertNull($abook->get_result());
$abook->set_search_set($filter);
$rset = $abook->list_records(null, 0, $nocount);
}
$this->assertNull($abook->get_error());
$this->assertSame($rset, $abook->get_result(), "Search does not return last result set");
$this->assertSame(($page - 1) * $pagesize, $rset->first);
$this->assertFalse($rset->searchonly);
if ($nocount) {
$this->assertSame(count($rset->records), $rset->count);
} else {
$this->assertSame($expCount, $rset->count);
}
if ($run == 0 || $select) { // select=false can only be tested with search() (run 0)
$this->assertCount(count($expRecords), $rset->records);
$lrOrder = array_column($rset->records, 'ID');
$this->assertSame($expRecords, $lrOrder, "Card order mismatch (run $run)");
for ($i = 0; $i < count($expRecords); ++$i) {
$id = $expRecords[$i];
$fn = "tests/Unit/data/addressbookTest/c{$id}.json";
$saveDataExp = Utils::readSaveDataFromJson($fn);
/** @var array $saveDataRc */
$saveDataRc = $rset->records[$i];
Utils::compareSaveData($saveDataExp, $saveDataRc, "Unexpected record data $id");
if (isset($saveDataExp['photo']) && empty($saveDataExp['photo'])) {
$this->assertPhotoDownloadWarning();
}
}
}
}
}
/**
* @return array<string, array{mixed}>
*/
public function invalidFilterProvider(): array
{
return [
'SQL string' => [ 'WHERE name="foo"' ],
'Mixed DbAndCondition array' => [ [ new DbAndCondition(), 'WHERE name="foo"' ] ],
];
}
/**
* Tests that set_search_set() throws an error when given an invalid filter type.
*
* @dataProvider invalidFilterProvider
* @param mixed $filter
*/
public function testSetSearchSetThrowsErrorOnInvalidFilter($filter): void
{
$abook = $this->createAbook();
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('requires a DbAndCondition[] type filter');
$abook->set_search_set($filter);
}
/**
* Tests that set_group() ignores an invalid group id and logs an error.
*/
public function testSetInvalidGroupIgnored(): void
{
$abook = $this->createAbook();
$abook->set_group("12345");
$this->assertNull($abook->group_id, "Group ID changed after set_group with invalid ID");
$logger = TestInfrastructure::logger();
$logger->expectMessage('error', 'set_group(12345)');
}
/** @return array<string, array{string,bool,bool}> */
public function getRecordProvider(): array
{
return [
'Valid ID' => [ '50', true, false ],
'Valid ID (rset)' => [ '50', false, false ],
'Valid ID (different addressbook)' => [ '70', true, true ],
'Invalid ID' => [ '500', true, true ],
'Invalid ID (rset)' => [ '500', false, true ],
];
}
/**
* Tests that get_record() returns expected record.
*
* @dataProvider getRecordProvider
*/
public function testGetRecordProvidesExpectedRecord(string $id, bool $assoc, bool $expError): void
{
$abook = $this->createAbook();
$db = $this->db;
// import contact belonging to different addressbook
$db->importData('tests/Unit/data/addressbookTest/db2.json');
$saveDataRc = $abook->get_record($id, $assoc);
if ($expError) {
if (!$assoc) {
$this->assertInstanceOf(\rcube_result_set::class, $saveDataRc);
$this->assertSame($saveDataRc->count, 0);
$this->assertSame($saveDataRc->first, 0);
$this->assertCount(0, $saveDataRc->records);
$this->assertNull($abook->get_result());
}
$logger = TestInfrastructure::logger();
$logger->expectMessage('error', "Could not get contact $id");
$abookErr = $abook->get_error();
$this->assertIsArray($abookErr);
$this->assertSame(rcube_addressbook::ERROR_SEARCH, $abookErr['type']);
$this->assertStringContainsString("Could not get contact $id", (string) $abookErr['message']);
} else {
if (!$assoc) {
$this->assertInstanceOf(\rcube_result_set::class, $saveDataRc);
$this->assertSame($saveDataRc, $abook->get_result());
$this->assertSame($saveDataRc->count, 1);
$this->assertSame($saveDataRc->first, 0);
$this->assertCount(1, $saveDataRc->records);
$this->assertIsArray($saveDataRc->records[0]);
$saveDataRc = $saveDataRc->records[0];
}
$this->assertIsArray($saveDataRc);
// we must use a record with an URI photo to check it remains wrapped in a photo loader
$this->assertInstanceOf(DelayedPhotoLoader::class, $saveDataRc['photo'], "photo not wrapped");
$fn = "tests/Unit/data/addressbookTest/c{$id}.json";
$saveDataExp = Utils::readSaveDataFromJson($fn);
Utils::compareSaveData($saveDataExp, $saveDataRc, "Unexpected record data $id");
if (isset($saveDataExp['photo']) && empty($saveDataExp['photo'])) {
$this->assertPhotoDownloadWarning();
}
}
}
/**
* @return list<array{?string, int, list<string>}>
*/
public function groupFilterProvider(): array
{
return [
[ null, 0, ["506", "501", "502", "500", "503", "504"] ],
[ "House", rcube_addressbook::SEARCH_PREFIX, ["501", "502", "500"] ],
[ "ar", 0, ["501", "500"] ],
[ "ar", rcube_addressbook::SEARCH_ALL, ["501", "500"] ],
[ "Kings", rcube_addressbook::SEARCH_STRICT, ["503"] ],
[ "House", rcube_addressbook::SEARCH_STRICT, [] ],
];
}
/**
* Tests that groups matching the given filter are listed.
*
* @dataProvider groupFilterProvider
* @param list<string> $expRecords
*/
public function testListGroupsProvidesExpectedGroups(?string $filter, int $searchmode, array $expRecords): void
{
$abook = $this->createAbook();
$groups = $abook->list_groups($filter, $searchmode);
$this->assertNull($abook->get_error());
$this->assertCount(count($expRecords), $groups);
$lrOrder = array_column($groups, 'ID');
$this->assertSame($expRecords, $lrOrder, "Group order mismatch");
for ($i = 0; $i < count($expRecords); ++$i) {
$id = $expRecords[$i];
$fn = "tests/Unit/data/addressbookTest/g{$id}.json";
$saveDataExp = Utils::readSaveDataFromJson($fn);
$saveDataRc = $groups[$i];
Utils::compareSaveData($saveDataExp, $saveDataRc, "Unexpected record data $id");
}
}
/** @return array<string, array{string,bool}> */
public function getGroupProvider(): array
{
return [
'Valid ID' => [ '500', false ],
'Valid ID (different addressbook)' => [ '700', true ],
'Invalid ID' => [ '50', true ],
];
}
/**
* Tests that get_group() returns expected record.
*
* @dataProvider getGroupProvider
*/
public function testGetGroupProvidesExpectedRecord(string $id, bool $expError): void
{
$abook = $this->createAbook();
$db = $this->db;
// import contact belonging to different addressbook
$db->importData('tests/Unit/data/addressbookTest/db2.json');
$saveDataRc = $abook->get_group($id);
if ($expError) {
$this->assertNull($saveDataRc);
$logger = TestInfrastructure::logger();
$logger->expectMessage('error', "Could not get group");
$abookErr = $abook->get_error();
$this->assertIsArray($abookErr);
$this->assertSame(rcube_addressbook::ERROR_SEARCH, $abookErr['type']);
$this->assertStringContainsString("Could not get group $id", (string) $abookErr['message']);
} else {
$this->assertIsArray($saveDataRc);
$fn = "tests/Unit/data/addressbookTest/g{$id}.json";
$saveDataExp = Utils::readSaveDataFromJson($fn);
Utils::compareSaveData($saveDataExp, $saveDataRc, "Unexpected record data $id");
}
}
/**
* @return list<array{int,numeric-string,numeric-string,int}>
*/
public function resyncDueProvider(): array
{
$now = time();
$nowStr = (string) $now;
return [
// now refresh lastup expDue
[ $now, "3600", $nowStr, 3600 ],
[ $now, "0", "0", -$now ],
[ $now, "0", $nowStr, 0 ],
[ $now, "1", $nowStr, 1 ],
[ $now, "1", (string) ($now - 1), 0 ],
[ $now, "1", (string) ($now - 2), -1 ],
];
}
/**
* Tests getRefreshTime() and checkResyncDue().
*
* @psalm-param numeric-string $rt
* @psalm-param numeric-string $lu
* @dataProvider resyncDueProvider
*/
public function testCheckResyncDueProvidesExpDelta(int $now, string $rt, string $lu, int $expDue): void
{
$abook = $this->createAbook(['refresh_time' => $rt, 'last_updated' => $lu]);
$this->assertSame(intval($rt), $abook->getRefreshTime());
// allow a tolerance of 1 second
$nowDelta = time() - $now; // delta to now in data provider
$expDue -= $nowDelta;
$this->assertLessThanOrEqual(1, $expDue - $abook->checkResyncDue());
}
/**
* Asserts that a warning message concerning failure to download the photo has been issued for VCards where an
* invalid Photo URI is used.
*/
private function assertPhotoDownloadWarning(): void
{
$logger = TestInfrastructure::logger();
$logger->expectMessage(
'warning',
'downloadPhoto: Attempt to download photo from http://localhost/doesNotExist.jpg failed'
);
}
/**
* Given a full save_data array, it constrains/converts the data such that it only contains fields
* that are present in the given columns.
*
* @param SaveData $saveData
* @param list<string> $cols
* @return SaveData
*/
private function stripSaveDataToDbColumns(array $saveData, array $cols): array
{
$cols[] = 'ID'; // always keep ID in the result
foreach ($saveData as $k => $v) {
// strip subtype from multi-value objects
$kgen = preg_replace('/:.*/', '', $k);
if (in_array($kgen, $cols)) {
if ($kgen != $k) {
/** @var list<string> $oldv */
$oldv = $saveData[$kgen] ?? [];
/** @var list<string> $v */
$saveData[$kgen] = array_merge($oldv, $v);
unset($saveData[$k]);
}
} else {
unset($saveData[$k]);
}
}
/** @var SaveData $saveData */
return $saveData;
}
}
// vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
Back to Directory
File Manager