Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions lib/Horde/Core/ActiveSync/Connector.php
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,12 @@ public function getRecipientCache($max = 100)
*/
public function contacts_search($query, array $options = [])
{
// IMP can advertise contacts/favouriteRecipients without a full contacts
// provider (Turba). hasInterface('contacts') is false in that case.
if (!$this->_registry->hasInterface('contacts')) {
return [];
}

if ((!$gal = $this->contacts_getGal()) && empty($options['recipient_cache_search'])) {
return [];
}
Expand Down Expand Up @@ -522,6 +528,10 @@ public function resolveRecipient($query, array $opts = [])
}
}

if (!$this->_registry->hasInterface('contacts')) {
return [$query => []];
}

$gal = $this->contacts_getGal();
$sources = array_keys($this->_registry->contacts->sources(false, true));
if (!in_array($gal, $sources)) {
Expand Down Expand Up @@ -556,8 +566,14 @@ public function resolveRecipient($query, array $opts = [])
*/
public function contacts_getGal()
{
if (empty($this->_gal)) {
$this->_gal = $this->_registry->contacts->getGalUid();
if (!isset($this->_gal)) {
// Avoid $registry->contacts->getGalUid() when no contacts provider
// is registered. listAPIs() may still list "contacts" because IMP
// provides contacts/favouriteRecipients, which makes __get('contacts')
// succeed and then call() throw for getGalUid.
$this->_gal = $this->_registry->hasMethod('contacts/getGalUid')
? $this->_registry->contacts->getGalUid()
: false;
}
return $this->_gal;
}
Expand Down
118 changes: 118 additions & 0 deletions test/Unit/ActiveSync/ConnectorContactsGetGalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @author Torben Dannhauer <torben@dannhauer.de>
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
* @subpackage UnitTests
*/

namespace Horde\Core\Test\Unit\ActiveSync;

use Horde_Core_ActiveSync_Connector;
use Horde_Registry;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;

/**
* Unit tests for Horde_Core_ActiveSync_Connector contacts helpers without Turba.
*/
#[CoversMethod(Horde_Core_ActiveSync_Connector::class, 'contacts_getGal')]
#[CoversMethod(Horde_Core_ActiveSync_Connector::class, 'contacts_search')]
#[CoversMethod(Horde_Core_ActiveSync_Connector::class, 'resolveRecipient')]
class ConnectorContactsGetGalTest extends TestCase
{
public function testContactsGetGalReturnsFalseWhenMethodMissing(): void
{
$registry = $this->createMock(Horde_Registry::class);
$registry->expects($this->once())
->method('hasMethod')
->with('contacts/getGalUid')
->willReturn(false);
$registry->expects($this->never())
->method('__get');

$connector = new Horde_Core_ActiveSync_Connector([
'registry' => $registry,
]);

$this->assertFalse($connector->contacts_getGal());
// Cached: second call must not hit the registry again.
$this->assertFalse($connector->contacts_getGal());
}

public function testContactsGetGalReturnsUidWhenAvailable(): void
{
$contacts = new class () {
public function getGalUid()
{
return 'gal-source';
}
};

$registry = $this->createMock(Horde_Registry::class);
$registry->expects($this->once())
->method('hasMethod')
->with('contacts/getGalUid')
->willReturn(true);
$registry->expects($this->once())
->method('__get')
->with('contacts')
->willReturn($contacts);

$connector = new Horde_Core_ActiveSync_Connector([
'registry' => $registry,
]);

$this->assertSame('gal-source', $connector->contacts_getGal());
$this->assertSame('gal-source', $connector->contacts_getGal());
}

public function testContactsSearchReturnsEmptyWithoutContactsInterface(): void
{
$registry = $this->createMock(Horde_Registry::class);
$registry->expects($this->once())
->method('hasInterface')
->with('contacts')
->willReturn(false);
$registry->expects($this->never())
->method('hasMethod');
$registry->expects($this->never())
->method('__get');

$connector = new Horde_Core_ActiveSync_Connector([
'registry' => $registry,
]);

$this->assertSame([], $connector->contacts_search('alice'));
}

public function testResolveRecipientReturnsEmptyWithoutContactsInterface(): void
{
$registry = $this->createMock(Horde_Registry::class);
$registry->expects($this->once())
->method('hasInterface')
->with('contacts')
->willReturn(false);
$registry->expects($this->never())
->method('__get');

$connector = new Horde_Core_ActiveSync_Connector([
'registry' => $registry,
]);

$this->assertSame(
['alice@example.com' => []],
$connector->resolveRecipient('alice@example.com')
);
}
}
Loading