diff options
author | Jan-Christoph Borchardt <hey@jancborchardt.net> | 2016-12-05 15:11:27 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-05 15:11:27 +0300 |
commit | 7a73527deadb1866f22eb85b58c44d0e958b7202 (patch) | |
tree | 2d719423975acda1bad1c7b4e531b0219ed881be | |
parent | a1b015dcd47fcd8b31ddd181172138b65c31eb7c (diff) | |
parent | 3fc45355d8a857428870da26aca48d5d14f84ca1 (diff) |
Merge pull request #151 from nextcloud/clean-up-collected-addresses
Clean up collected addresses and split into address and display name
-rw-r--r-- | appinfo/database.xml | 6 | ||||
-rw-r--r-- | appinfo/info.xml | 7 | ||||
-rw-r--r-- | js/views/composerview.js | 4 | ||||
-rw-r--r-- | lib/Db/CollectedAddress.php | 3 | ||||
-rw-r--r-- | lib/Db/CollectedAddressMapper.php | 40 | ||||
-rw-r--r-- | lib/Migration/FixCollectedAddresses.php | 84 | ||||
-rw-r--r-- | lib/Service/AutoCompletion/AddressCollector.php | 21 | ||||
-rw-r--r-- | lib/Service/AutoCompletion/AutoCompleteService.php | 7 | ||||
-rw-r--r-- | tests/Db/CollectedAddressMapperTest.php | 78 | ||||
-rw-r--r-- | tests/Migration/FixCollectedAddressesTest.php | 139 | ||||
-rw-r--r-- | tests/Service/Autocompletion/AddressCollectorTest.php | 31 | ||||
-rw-r--r-- | tests/Service/Autocompletion/AutoCompleteServiceTest.php | 16 |
12 files changed, 389 insertions, 47 deletions
diff --git a/appinfo/database.xml b/appinfo/database.xml index 9e79a6aeb..7cceb2078 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -140,6 +140,12 @@ <notnull>true</notnull> <length>255</length> </field> + <field> + <name>display_name</name> + <type>text</type> + <notnull>false</notnull> + <length>255</length> + </field> <index> <name>mail_collected_addr_userid_index</name> <field> diff --git a/appinfo/info.xml b/appinfo/info.xml index f9d93a7d0..0ec576772 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ <name>Mail</name> <summary>IMAP web client</summary> <description>Easy to use email client which connects to your mail server via IMAP and SMTP.</description> - <version>0.6.0</version> + <version>0.6.1</version> <licence>agpl</licence> <author>Christoph Wurst</author> <author>Jan-Christoph Borchardt</author> @@ -25,5 +25,10 @@ <owncloud min-version="9.1" max-version="9.2" /> <nextcloud min-version="9.1" max-version="11" /> </dependencies> + <repair-steps> + <post-migration> + <step>OCA\Mail\Migration\FixCollectedAddresses</step> + </post-migration> + </repair-steps> <ocsid>169914</ocsid> </info> diff --git a/js/views/composerview.js b/js/views/composerview.js index c62bad842..4b5d69f39 100644 --- a/js/views/composerview.js +++ b/js/views/composerview.js @@ -495,14 +495,14 @@ define(function(require) { $row.append($avatar); } else { $placeholder = $('<div/>'); - $placeholder.imageplaceholder(item.value); + $placeholder.imageplaceholder(item.label || item.value); $placeholder.addClass('avatar'); $row.append($placeholder); } prevUID = item.id; - $row.append($('<span>').text(item.value)); + $row.append($('<span>').text(item.label || item.value)); $item.append($row); $item.appendTo($ul); diff --git a/lib/Db/CollectedAddress.php b/lib/Db/CollectedAddress.php index 2c2598417..aed152166 100644 --- a/lib/Db/CollectedAddress.php +++ b/lib/Db/CollectedAddress.php @@ -28,10 +28,13 @@ use OCP\AppFramework\Db\Entity; * @method string getUserId() * @method null setEmail(string $email) * @method string getEmail() + * @method null setDisplayName(string $displayName) + * @method string getDisplayName() */ class CollectedAddress extends Entity { protected $userId; protected $email; + protected $displayName; } diff --git a/lib/Db/CollectedAddressMapper.php b/lib/Db/CollectedAddressMapper.php index 8d387eebf..483045361 100644 --- a/lib/Db/CollectedAddressMapper.php +++ b/lib/Db/CollectedAddressMapper.php @@ -23,6 +23,7 @@ namespace OCA\Mail\Db; use OCP\AppFramework\Db\Mapper; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDb; class CollectedAddressMapper extends Mapper { @@ -42,10 +43,11 @@ class CollectedAddressMapper extends Mapper { * @return CollectedAddress[] */ public function findMatching($userId, $query) { - $sql = 'SELECT * FROM *PREFIX*mail_collected_addresses WHERE `user_id` = ? AND `email` ILIKE ?'; + $sql = 'SELECT * FROM *PREFIX*mail_collected_addresses WHERE `user_id` = ? AND (`email` ILIKE ? OR `display_name` ILIKE ?)'; $params = [ $userId, '%' . $query . '%', + '%' . $query . '%', ]; return $this->findEntities($sql, $params); } @@ -59,4 +61,40 @@ class CollectedAddressMapper extends Mapper { return count($this->findEntities($sql, $params)) > 0; } + public function getTotal() { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select($qb->createFunction('COUNT(*)')) + ->from($this->getTableName()); + $result = $qb->execute(); + + $count = (int) $result->fetchColumn(0); + $result->closeCursor(); + return $count; + } + + /** + * @param int|null $minId + */ + public function getChunk($minId = null) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $query = $qb->select('*') + ->from($this->getTableName()) + ->orderBy('id') + ->setMaxResults(100); + if (!is_null($minId)) { + $query = $query->where($qb->expr()->gte('id', + $qb->createNamedParameter($minId))); + } + + $result = $qb->execute(); + $rows = $result->fetchAll(); + $result->closeCursor(); + + return array_map(function(array $data) { + return CollectedAddress::fromRow($data); + }, $rows); + } + } diff --git a/lib/Migration/FixCollectedAddresses.php b/lib/Migration/FixCollectedAddresses.php new file mode 100644 index 000000000..05be85466 --- /dev/null +++ b/lib/Migration/FixCollectedAddresses.php @@ -0,0 +1,84 @@ +<?php + +/** + * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * + * Mail + * + * 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 <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Migration; + +use Horde_Mail_Exception; +use Horde_Mail_Rfc822_Address; +use OCA\Mail\Db\CollectedAddress; +use OCA\Mail\Db\CollectedAddressMapper; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class FixCollectedAddresses implements IRepairStep { + + /** @var CollectedAddressMapper */ + private $mapper; + + public function __construct(CollectedAddressMapper $mapper) { + $this->mapper = $mapper; + } + + public function getName() { + return 'Purify and migrate collected mail addresses'; + } + + public function run(IOutput $output) { + $nrOfAddresses = $this->mapper->getTotal(); + $output->startProgress($nrOfAddresses); + + $chunk = $this->mapper->getChunk(); + while (count($chunk) > 0) { + $maxId = null; + foreach ($chunk as $address) { + /* @var $address CollectedAddress */ + $maxId = $address->getId(); + $this->fixAddress($address, $output); + } + + $output->advance(count($chunk)); + $chunk = $this->mapper->getChunk($maxId + 1); + } + $output->finishProgress(); + } + + private function fixAddress(CollectedAddress $address, IOutput $output) { + if (!is_null($address->getDisplayName())) { + // Nothing to fix + return; + } + + try { + $hordeAddress = new Horde_Mail_Rfc822_Address($address->getEmail()); + if (!$hordeAddress->valid) { + throw new Horde_Mail_Exception(); + } + $address->setDisplayName($hordeAddress->label); + $address->setEmail($hordeAddress->bare_address); + $this->mapper->update($address); + } catch (Horde_Mail_Exception $ex) { + $output->warning('removed invalid address <' . $address->getEmail() . '>'); + // Invalid address, let's delete it to prevent further errors + $this->mapper->delete($address); + } + } + +} diff --git a/lib/Service/AutoCompletion/AddressCollector.php b/lib/Service/AutoCompletion/AddressCollector.php index 7d4d65ff3..f9e27ae07 100644 --- a/lib/Service/AutoCompletion/AddressCollector.php +++ b/lib/Service/AutoCompletion/AddressCollector.php @@ -21,6 +21,7 @@ namespace OCA\Mail\Service\AutoCompletion; +use Horde_Mail_Exception; use Horde_Mail_Rfc822_Address; use OCA\Mail\Db\CollectedAddress; use OCA\Mail\Db\CollectedAddressMapper; @@ -58,13 +59,23 @@ class AddressCollector { public function addAddresses($addresses) { $this->logger->debug("collecting " . count($addresses) . " email addresses"); foreach ($addresses as $address) { - - if (!$this->mapper->exists($this->userId, $address)) { + try { + $hordeAddress = new Horde_Mail_Rfc822_Address($address); + if (!$hordeAddress->valid) { + throw new Horde_Mail_Exception(); + } + } catch (Horde_Mail_Exception $ex) { + // Ignore it + $this->logger->debug("<$address> is not a valid RFC822 mail address"); + return; + } + if (!$this->mapper->exists($this->userId, $hordeAddress->bare_address)) { $this->logger->debug("saving new address <$address>"); - + $entity = new CollectedAddress(); $entity->setUserId($this->userId); - $entity->setEmail($address); + $entity->setDisplayName($hordeAddress->label); + $entity->setEmail($hordeAddress->bare_address); $this->mapper->insert($entity); } } @@ -73,7 +84,7 @@ class AddressCollector { /** * Find and return all known and matching email addresses * - * @param Horde_Mail_Rfc822_Address[] $term + * @param string $term */ public function searchAddress($term) { $this->logger->debug("searching for collected address <$term>"); diff --git a/lib/Service/AutoCompletion/AutoCompleteService.php b/lib/Service/AutoCompletion/AutoCompleteService.php index 2e430c356..0f8cbe6b6 100644 --- a/lib/Service/AutoCompletion/AutoCompleteService.php +++ b/lib/Service/AutoCompletion/AutoCompleteService.php @@ -21,6 +21,7 @@ namespace OCA\Mail\Service\AutoCompletion; +use OCA\Mail\Db\CollectedAddress; use OCA\Mail\Service\ContactsIntegration; class AutoCompleteService { @@ -41,11 +42,11 @@ class AutoCompleteService { $fromCollector = $this->addressCollector->searchAddress($term); // Convert collected addresses into same format as CI creates - $recipientsFromCollector = array_map(function ($address) { + $recipientsFromCollector = array_map(function (CollectedAddress $address) { return [ 'id' => $address->getId(), - 'label' => $address->getEmail(), - 'value' => $address->getEmail(), + 'label' => $address->getDisplayName(), + 'value' => '"' . $address->getDisplayName() . '" <' . $address->getEmail() . '>', ]; }, $fromCollector); diff --git a/tests/Db/CollectedAddressMapperTest.php b/tests/Db/CollectedAddressMapperTest.php index b4ed92bdf..f3f81eed6 100644 --- a/tests/Db/CollectedAddressMapperTest.php +++ b/tests/Db/CollectedAddressMapperTest.php @@ -22,53 +22,84 @@ namespace OCA\Mail\Tests\Db; -use OC; use OC\AppFramework\Db\Db; -use OCA\Mail\Db\CollectedAddress; +use Test\TestCase; use OCA\Mail\Db\CollectedAddressMapper; -use OCP\IDBConnection; -use PHPUnit_Framework_TestCase; +use OCA\Mail\Db\CollectedAddress; -class CollectedAddressMapperTest extends PHPUnit_Framework_TestCase { +/** + * Class CollectedAddressMapperTest + * + * @group DB + * + * @package OCA\Mail\Tests\Db + */ +class CollectedAddressMapperTest extends TestCase { - /** @var IDBConnection */ + /** @var \OCP\IDBConnection */ private $db; - /** @var string */ private $userId = 'testuser'; + /** @var CollectedAddressMapper */ private $mapper; + /** @var CollectedAddress */ private $address1; + /** @var CollectedAddress */ private $address2; + /** @var CollectedAddress */ + private $address3; + protected function setUp() { parent::setUp(); - $this->db = OC::$server->getDatabaseConnection(); + $this->db = \OC::$server->getDatabaseConnection(); $this->mapper = new CollectedAddressMapper(new Db($this->db)); $this->address1 = new CollectedAddress(); $this->address1->setEmail('user1@example.com'); + $this->address1->setDisplayName('User 1'); $this->address1->setUserId($this->userId); $this->address2 = new CollectedAddress(); $this->address2->setEmail('user2@example.com'); + $this->address2->setDisplayName('User 2'); $this->address2->setUserId($this->userId); - $sql = 'INSERT INTO *PREFIX*mail_collected_addresses (`email`, `user_id`) VALUES (?, ?)'; + $this->address3 = new CollectedAddress(); + $this->address3->setEmail('"User 3" <user3@domain.com>'); + $this->address3->setDisplayName('User 3'); + $this->address3->setUserId($this->userId); + + $qb = $this->db->getQueryBuilder(); + $sql = 'INSERT INTO *PREFIX*mail_collected_addresses (`email`, `display_name`, `user_id`) VALUES (?, ?, ?)'; $stmt = $this->db->prepare($sql); + // Empty DB + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->mapper->getTableName()); + $qb->execute(); + $stmt->execute([ $this->address1->getEmail(), + $this->address1->getDisplayName(), $this->address1->getUserId(), ]); $this->address1->setId($this->db->lastInsertId('PREFIX*mail_collected_addresses')); $stmt->execute([ $this->address2->getEmail(), + $this->address2->getDisplayName(), $this->address2->getUserId(), ]); $this->address2->setId($this->db->lastInsertId('PREFIX*mail_collected_addresses')); + $stmt->execute([ + $this->address3->getEmail(), + $this->address3->getDisplayName(), + $this->address3->getUserId(), + ]); + $this->address3->setId($this->db->lastInsertId('PREFIX*mail_collected_addresses')); } protected function tearDown() { @@ -82,12 +113,15 @@ class CollectedAddressMapperTest extends PHPUnit_Framework_TestCase { if (!empty($this->address2)) { $stmt->execute([$this->address2->getId()]); } + if (!empty($this->address3)) { + $stmt->execute([$this->address3->getId()]); + } } public function matchingData() { return [ - ['user1@example.com', ['user1@example.com']], - ['ser', ['user1@example.com', 'user2@example.com']], + ['user1@example.com', ['user1@example.com']], + ['examp', ['user1@example.com', 'user2@example.com']], ]; } @@ -109,8 +143,8 @@ class CollectedAddressMapperTest extends PHPUnit_Framework_TestCase { public function existsData() { return [ - ['user1@example.com', true], - ['user3@example.com', false], + ['user1@example.com', true], + ['user3@example.com', false], ]; } @@ -123,4 +157,22 @@ class CollectedAddressMapperTest extends PHPUnit_Framework_TestCase { $this->assertSame($expected, $actual); } + public function testGetTotal() { + $total = $this->mapper->getTotal(); + + $this->assertSame(3, $total); + } + + public function testGetChunk() { + $chunk = $this->mapper->getChunk(); + + $this->assertCount(3, $chunk); + } + + public function testGetChunkWithOffset() { + $chunk = $this->mapper->getChunk($this->address2->getId()); + + $this->assertCount(2, $chunk); + } + } diff --git a/tests/Migration/FixCollectedAddressesTest.php b/tests/Migration/FixCollectedAddressesTest.php new file mode 100644 index 000000000..cc7bc4e0a --- /dev/null +++ b/tests/Migration/FixCollectedAddressesTest.php @@ -0,0 +1,139 @@ +<?php + +use OCA\Mail\Db\CollectedAddress; +use OCA\Mail\Db\CollectedAddressMapper; +use OCA\Mail\Migration\FixCollectedAddresses; +use OCP\Migration\IOutput; +use Test\TestCase; + +/** + * @author Christoph Wurst <christoph@winzerhof-wurst.at> + * + * Mail + * + * 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 <http://www.gnu.org/licenses/> + * + */ + +/** + * @group DB + */ +class FixCollectedAddressesTest extends TestCase { + + /** @var CollectedAddressMapper */ + private $mapper; + + /** @var IOutput */ + private $output; + + /** @var FixCollectedAddresses */ + private $repairStep; + + protected function setUp() { + parent::setUp(); + + $this->mapper = $this->getMockBuilder('OCA\Mail\Db\CollectedAddressMapper') + ->disableOriginalConstructor() + ->getMock(); + $this->output = $this->getMockBuilder('OCP\Migration\IOutput') + ->getMock(); + $this->repairStep = new FixCollectedAddresses($this->mapper); + } + + public function testRunNothingToMigrate() { + $address1 = new CollectedAddress(); + $address1->setId(100); + $address1->setEmail('user1@domain1.com'); + $address1->setDisplayName('User 1'); + $address2 = new CollectedAddress(); + $address2->setId(200); + $address2->setEmail('user2@domain2.com'); + $address2->setDisplayName('User 2'); + + $this->mapper->expects($this->exactly(2)) + ->method('getChunk') + ->will($this->returnValueMap([ + [ + null, [ + $address1, + $address2, + ]], + [ + 201, []] + ])); + $this->mapper->expects($this->never()) + ->method('update'); + + $this->repairStep->run($this->output); + } + + public function testRunMigrateOne() { + $address1 = new CollectedAddress(); + $address1->setId(100); + $address1->setEmail('"User 1" <user1@domain1.com>'); + $address1->setDisplayName(null); + $address2 = new CollectedAddress(); + $address2->setId(200); + $address2->setEmail('user2@domain2.com'); + $address2->setDisplayName('User 2'); + + $this->mapper->expects($this->exactly(2)) + ->method('getChunk') + ->will($this->returnValueMap([ + [ + null, [ + $address1, + $address2, + ]], + [ + 201, []] + ])); + $this->mapper->expects($this->once()) + ->method('update') + ->with($address1); + + $this->repairStep->run($this->output); + + $this->assertEquals('user1@domain1.com', $address1->getEmail()); + $this->assertEquals('User 1', $address1->getDisplayName()); + } + + public function testRunDeleteFaulty() { + $address1 = new CollectedAddress(); + $address1->setId(100); + $address1->setEmail('"User 1 <user1@domain1.com>>>'); + $address1->setDisplayName(null); + $address2 = new CollectedAddress(); + $address2->setId(200); + $address2->setEmail('user2@domain2.com'); + $address2->setDisplayName('User 2'); + + $this->mapper->expects($this->exactly(2)) + ->method('getChunk') + ->will($this->returnValueMap([ + [ + null, [ + $address1, + $address2, + ]], + [ + 201, []] + ])); + $this->mapper->expects($this->once()) + ->method('delete') + ->with($address1); + + $this->repairStep->run($this->output); + } + +} diff --git a/tests/Service/Autocompletion/AddressCollectorTest.php b/tests/Service/Autocompletion/AddressCollectorTest.php index e7d1147cd..b065cd9a6 100644 --- a/tests/Service/Autocompletion/AddressCollectorTest.php +++ b/tests/Service/Autocompletion/AddressCollectorTest.php @@ -42,36 +42,37 @@ class AddressCollectorTest extends PHPUnit_Framework_TestCase { ->disableOriginalConstructor() ->getMock(); - $this->collector = new AddressCollector($this->mapper, $this->userId, - $this->logger); + $this->collector = new AddressCollector($this->mapper, $this->userId, $this->logger); } public function testAddAddresses() { $addresses = [ - 'user@example.com', - 'example@user.com', + '"User" <user@example.com>', + 'Example <example@user.com>', ]; - $data = array_map(function($address) { - $ca = new CollectedAddress(); - $ca->setEmail($address); - $ca->setUserId($this->userId); - return $ca; - }, $addresses); + $address1 = new CollectedAddress(); + $address1->setDisplayName('User'); + $address1->setEmail('user@example.com'); + $address1->setUserId($this->userId); + $address2 = new CollectedAddress(); + $address2->setDisplayName('Example'); + $address2->setEmail('example@user.com'); + $address2->setUserId($this->userId); $this->mapper->expects($this->at(0)) ->method('exists') - ->with($this->userId, $addresses[0]) + ->with($this->userId, 'user@example.com') ->will($this->returnValue(false)); $this->mapper->expects($this->at(1)) ->method('insert') - ->with($data[0]); + ->with($address1); $this->mapper->expects($this->at(2)) ->method('exists') - ->with($this->userId, $addresses[1]) + ->with($this->userId, 'example@user.com') ->will($this->returnValue(false)); $this->mapper->expects($this->at(3)) ->method('insert') - ->with($data[1]); + ->with($address2); $this->collector->addAddresses($addresses); } @@ -101,7 +102,7 @@ class AddressCollectorTest extends PHPUnit_Framework_TestCase { ->will($this->returnValue($mapperResult)); $result = $this->collector->searchAddress($term); - + $this->assertequals($mapperResult, $result); } diff --git a/tests/Service/Autocompletion/AutoCompleteServiceTest.php b/tests/Service/Autocompletion/AutoCompleteServiceTest.php index 27c054ed2..b79791ff7 100644 --- a/tests/Service/Autocompletion/AutoCompleteServiceTest.php +++ b/tests/Service/Autocompletion/AutoCompleteServiceTest.php @@ -21,8 +21,9 @@ namespace OCA\Mail\Tests\Service\Autocompletion; -use PHPUnit_Framework_TestCase; +use OCA\Mail\Db\CollectedAddress; use OCA\Mail\Service\AutoCompletion\AutoCompleteService; +use PHPUnit_Framework_TestCase; class AutoCompleteServiceTest extends PHPUnit_Framework_TestCase { @@ -48,12 +49,13 @@ class AutoCompleteServiceTest extends PHPUnit_Framework_TestCase { $term = 'jo'; $contactsResult = [ - ['id' => 12, 'label' => 'john doe', 'value' => 'john doe'], - ['id' => 13, 'label' => 'joe doe', 'value' => 'joe doe'], + ['id' => 12, 'label' => '"john doe" <john@doe.cz>', 'value' => '"john doe" <john@doe.cz>'], + ['id' => 13, 'label' => '"joe doe" <joe@doe.se>', 'value' => '"joe doe" <joe@doe.se>'], ]; - $john = new \OCA\Mail\Db\CollectedAddress(); + $john = new CollectedAddress(); $john->setId(1234); $john->setEmail('john@doe.com'); + $john->setDisplayName('John Doe'); $john->setUserId('testuser'); $collectedResult = [ $john, @@ -71,9 +73,9 @@ class AutoCompleteServiceTest extends PHPUnit_Framework_TestCase { $response = $this->service->findMatches($term); $expected = [ - ['id' => 12, 'label' => 'john doe', 'value' => 'john doe'], - ['id' => 13, 'label' => 'joe doe', 'value' => 'joe doe'], - ['id' => 1234, 'label' => 'john@doe.com', 'value' => 'john@doe.com'], + ['id' => 12, 'label' => '"john doe" <john@doe.cz>', 'value' => '"john doe" <john@doe.cz>'], + ['id' => 13, 'label' => '"joe doe" <joe@doe.se>', 'value' => '"joe doe" <joe@doe.se>'], + ['id' => 1234, 'label' => 'John Doe', 'value' => '"John Doe" <john@doe.com>'], ]; $this->assertEquals($expected, $response); } |