Viewing File: /usr/local/cpanel/base/3rdparty/roundcube/plugins/carddav/tests/Unit/DataConversionTest.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 Sabre\VObject;
use Sabre\VObject\Component\VCard;
use rcube_cache;
use MStilkerich\Tests\RCMCardDAV\{TestInfrastructure,TestLogger};
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use MStilkerich\CardDavClient\{Account,AddressbookCollection};
use MStilkerich\RCMCardDAV\{DataConversion,DelayedPhotoLoader,DelayedVCardExporter};
use MStilkerich\RCMCardDAV\Db\Database;

/**
 * XXX temporary workaround for vimeo/psalm#8980
 * @psalm-import-type SaveDataFromDC from DataConversion
 */

final class DataConversionTest extends TestCase
{
    /** @var rcube_cache & MockObject */
    private $cache;

    /** @var Database & MockObject */
    private $db;

    /** @var AddressbookCollection */
    private $abook;

    public static function setUpBeforeClass(): void
    {
    }

    public function setUp(): void
    {
        $_SESSION['user_id'] = 105;
        $abook = $this->createStub(AddressbookCollection::class);
        $abook->method('downloadResource')->will($this->returnCallback([Utils::class, 'downloadResource']));
        $this->abook = $abook;

        $this->db = $this->createMock(Database::class);
        $this->cache = $this->createMock(\rcube_cache::class);

        TestInfrastructure::init($this->db);
        TestInfrastructure::$infra->setCache($this->cache);
    }

    public function tearDown(): void
    {
        Utils::cleanupTempImages();
        TestInfrastructure::logger()->reset();
    }

    /**
     * @return array<string, list{string, string}>
     */
    private function vcardSamplesProvider(string $basedir): array
    {
        $vcfFiles = glob("$basedir/*.vcf");

        $result = [];
        foreach ($vcfFiles as $vcfFile) {
            $comp = pathinfo($vcfFile);
            $jsonFile = "{$comp["dirname"]}/{$comp["filename"]}.json";
            $result[$comp["basename"]] = [ $vcfFile, $jsonFile ];
        }

        return $result;
    }

    /**
     * @return array<string, list{string, string}>
     */
    public function vcardImportSamplesProvider(): array
    {
        return $this->vcardSamplesProvider('tests/Unit/data/vcardImport');
    }

    /**
     * Tests the conversion of VCards to roundcube's internal address data representation.
     *
     * @dataProvider vcardImportSamplesProvider
     */
    public function testCorrectConversionOfVcardToRoundcube(string $vcfFile, string $jsonFile): void
    {
        $logger = TestInfrastructure::logger();
        $dc = new DataConversion("42");
        $vcard = TestInfrastructure::readVCard($vcfFile);
        $saveDataExp = Utils::readSaveDataFromJson($jsonFile);
        $saveData = $dc->toRoundcube($vcard, $this->abook);
        Utils::compareSaveData($saveDataExp, $saveData, "Converted VCard does not result in expected roundcube data");
        $this->assertPhotoDownloadWarning($logger, $vcfFile);
    }

    /**
     * Tests that a new custom label is inserted into the database.
     */
    public function testNewCustomLabelIsInsertedToDatabase(): void
    {
        $db = $this->db;

        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([ ["typename" => "email", "subtype" => "Speciallabel"] ]));
        $db->expects($this->once())
            ->method("insert")
            ->with(
                $this->equalTo("xsubtypes"),
                $this->equalTo(["typename", "subtype", "abook_id"]),
                $this->equalTo([["email", "SpecialLabel", "42"]])
            )
            ->will($this->returnValue("49"));

        $dc = new DataConversion("42");
        $vcard = TestInfrastructure::readVCard("tests/Unit/data/vcardImport/XAbLabel.vcf");
        $dc->toRoundcube($vcard, $this->abook);
    }

    /**
     * Tests that known custom labels are offered in coltypes.
     */
    public function testKnownCustomLabelPresentedToRoundcube(): void
    {
        $db = $this->db;
        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([ ["typename" => "email", "subtype" => "SpecialLabel"] ]));

        $dc = new DataConversion("42");
        $coltypes = $dc->getColtypes();
        $this->assertTrue(isset($coltypes["email"]["subtypes"]));
        $this->assertContains("SpecialLabel", $coltypes["email"]["subtypes"], "SpecialLabel not contained in coltypes");
    }

    /**
     * Tests that a known custom label is not inserted into the database again.
     */
    public function testKnownCustomLabelIsNotInsertedToDatabase(): void
    {
        $db = $this->db;

        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([ ["typename" => "email", "subtype" => "SpecialLabel"] ]));
        $db->expects($this->never())
            ->method("insert");

        $dc = new DataConversion("42");
        $vcard = TestInfrastructure::readVCard("tests/Unit/data/vcardImport/XAbLabel.vcf");
        $dc->toRoundcube($vcard, $this->abook);
    }

    /**
     * @return array<string, list{string, string}>
     */
    public function vcardCreateSamplesProvider(): array
    {
        return $this->vcardSamplesProvider('tests/Unit/data/vcardCreate');
    }

    /**
     * Tests that a new VCard is created from Roundcube data properly.
     *
     * @dataProvider vcardCreateSamplesProvider
     */
    public function testCorrectCreationOfVcardFromRoundcube(string $vcfFile, string $jsonFile): void
    {
        $db = $this->db;
        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([
                ["typename" => "email", "subtype" => "SpecialLabel"],
                ["typename" => "phone", "subtype" => "0"],
                ["typename" => "website", "subtype" => "0"]
            ]));
        $dc = new DataConversion("42");
        $vcardExpected = TestInfrastructure::readVCard($vcfFile);
        $saveData = Utils::readSaveDataFromJson($jsonFile);
        $result = $dc->fromRoundcube($saveData);

        $this->compareVCards($vcardExpected, $result, true);
    }

    /**
     * Tests that errors in the save data are properly reported and handled.
     *
     * The offending parts of the save data should be dropped and an error message logged.
     */
    public function testErroneousAttributesInSaveDataAreIgnored(): void
    {
        $logger = TestInfrastructure::logger();
        $db = $this->db;
        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([]));
        $dc = new DataConversion("42");
        $vcardExpected = TestInfrastructure::readVCard('tests/Unit/data/singleTest/Errors.vcf');
        $saveData = Utils::readSaveDataFromJson('tests/Unit/data/singleTest/Errors.json');
        $result = $dc->fromRoundcube($saveData);

        $this->compareVCards($vcardExpected, $result, true);

        // check emitted warnings
        $logger->expectMessage("error", "save data nickname must be string");
    }

    /**
     * @return array<string, list{string, string}>
     */
    public function vcardUpdateSamplesProvider(): array
    {
        return $this->vcardSamplesProvider('tests/Unit/data/vcardUpdate');
    }

    /**
     * Tests that an existing VCard is updated from Roundcube data properly.
     *
     * @dataProvider vcardUpdateSamplesProvider
     */
    public function testCorrectUpdateOfVcardFromRoundcube(string $vcfFile, string $jsonFile): void
    {
        $db = $this->db;
        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([
                ["typename" => "email", "subtype" => "SpecialLabel"],
                ["typename" => "email", "subtype" => "SpecialLabel2"]
            ]));

        $dc = new DataConversion("42");
        $vcardOriginal = TestInfrastructure::readVCard($vcfFile);
        $vcardExpected = TestInfrastructure::readVCard("$vcfFile.new");
        $saveData = Utils::readSaveDataFromJson($jsonFile);

        $result = $dc->fromRoundcube($saveData, $vcardOriginal);
        $this->compareVCards($vcardExpected, $result, false);
    }

    /**
     * @return array<string, array{0: string, 1: bool, 2: bool}>
     */
    public function cachePhotosSamplesProvider(): array
    {
        return [
            "InlinePhoto.vcf" => ["tests/Unit/data/vcardImport/InlinePhoto", false, false],
            "UriPhotoCrop.vcf" => ["tests/Unit/data/vcardImport/UriPhotoCrop", true, true],
            "InvalidUriPhoto.vcf" => ["tests/Unit/data/vcardImport/InvalidUriPhoto", true, false],
            "UriPhoto.vcf" => ["tests/Unit/data/vcardImport/UriPhoto", true, true],
        ];
    }

    /**
     * Tests whether a PHOTO is stored/not stored to the roundcube cache as expected.
     *
     * @dataProvider cachePhotosSamplesProvider
     */
    public function testNewPhotoIsStoredToCacheIfNeeded(string $basename, bool $getExp, bool $storeExp): void
    {
        $logger = TestInfrastructure::logger();
        $cache = $this->cache;

        $vcard = TestInfrastructure::readVCard("$basename.vcf");
        $this->assertInstanceOf(VObject\Property::class, $vcard->PHOTO);

        $key = "photo_105_" . md5((string) $vcard->UID);
        $saveDataExpected = Utils::readSaveDataFromJson("$basename.json");

        // photo should be stored to cache if not already stored in vcard in the final form
        if ($getExp) {
            // simulate cache miss
            $cache->expects($this->once())
                  ->method("get")
                  ->with($this->equalTo($key))
                  ->will($this->returnValue(null));
        } else {
            $cache->expects($this->never())->method("get");
        }

        if ($storeExp) {
            $checkPhotoFn = function (array $cacheObj) use ($vcard, $saveDataExpected): bool {
                $this->assertNotNull($cacheObj['photoPropMd5']);
                $this->assertNotNull($cacheObj['photo']);
                $this->assertIsString($cacheObj['photo']);
                $this->assertSame(md5($vcard->PHOTO->serialize()), $cacheObj['photoPropMd5']);
                $this->assertTrue(isset($saveDataExpected["photo"]));
                $this->assertIsString($saveDataExpected["photo"]);
                Utils::comparePhoto($saveDataExpected["photo"], $cacheObj['photo']);
                return true;
            };
            $cache->expects($this->once())
               ->method("set")
               ->with(
                   $this->equalTo($key),
                   $this->callback($checkPhotoFn)
               )
               ->will($this->returnValue(true));
        } else {
            $cache->expects($this->never())->method("set");
        }

        $dc = new DataConversion("42");
        $saveData = $dc->toRoundcube($vcard, $this->abook);
        $this->assertTrue(isset($saveData['photo']));
        $this->assertTrue(isset($saveDataExpected["photo"]));
        $this->assertIsString($saveDataExpected["photo"]);
        Utils::comparePhoto($saveDataExpected["photo"], (string) $saveData["photo"]);

        $this->assertPhotoDownloadWarning($logger, $basename);
    }

    /**
     * Tests that a photo is retrieved from the roundcube cache if available, skipping processing.
     *
     * @dataProvider cachePhotosSamplesProvider
     */
    public function testPhotoIsUsedFromCacheIfAvailable(string $basename, bool $getExp, bool $storeExp): void
    {
        $cache = $this->cache;

        // we use this file as some placeholder for cached data that is not used in any of the vcards photos
        $cachedPhotoData = file_get_contents("tests/Unit/data/srv/pixel.jpg");
        $this->assertNotFalse($cachedPhotoData);

        $vcard = TestInfrastructure::readVCard("$basename.vcf");
        $this->assertInstanceOf(VObject\Property::class, $vcard->PHOTO);

        $key = "photo_105_" . md5((string) $vcard->UID);
        $saveDataExpected = Utils::readSaveDataFromJson("$basename.json");

        if ($getExp) {
            // simulate cache hit
            $cache->expects($this->once())
                  ->method("get")
                  ->with($this->equalTo($key))
                  ->will(
                      $this->returnValue([
                          'photoPropMd5' => md5($vcard->PHOTO->serialize()),
                          'photo' => $cachedPhotoData
                      ])
                  );
        } else {
            $cache->expects($this->never())->method("get");
        }

        // no cache update expected
        $cache->expects($this->never())->method("set");

        $dc = new DataConversion("42");
        $saveData = $dc->toRoundcube($vcard, $this->abook);

        $this->assertTrue(isset($saveData['photo']));
        if ($getExp) {
            Utils::comparePhoto($cachedPhotoData, (string) $saveData["photo"]);

            // If we fetch it a second time, we should hit the internal cache of the photo loader and no processing
            // occurs again
            $cache->expects($this->never())->method("get");
            Utils::comparePhoto($cachedPhotoData, (string) $saveData["photo"]);
        } else {
            $this->assertTrue(isset($saveDataExpected["photo"]));
            $this->assertIsString($saveDataExpected["photo"]);
            Utils::comparePhoto($saveDataExpected["photo"], (string) $saveData["photo"]);
        }
    }

    /**
     * Tests that an outdated photo in the cache is replaced by a newly processed one.
     *
     * @dataProvider cachePhotosSamplesProvider
     */
    public function testOutdatedPhotoIsReplacedInCache(string $basename, bool $getExp, bool $storeExp): void
    {
        $logger = TestInfrastructure::logger();
        $cache = $this->cache;

        // we use this file as some placeholder for cached data that is not used in any of the vcards photos
        $cachedPhotoData = file_get_contents("tests/Unit/data/srv/pixel.jpg");
        $this->assertNotFalse($cachedPhotoData);

        $vcard = TestInfrastructure::readVCard("$basename.vcf");
        $this->assertInstanceOf(VObject\Property::class, $vcard->PHOTO);

        $key = "photo_105_" . md5((string) $vcard->UID);
        $saveDataExpected = Utils::readSaveDataFromJson("$basename.json");

        if ($getExp) {
            // simulate cache hit with non-matching md5sum
            $cache->expects($this->once())
                  ->method("get")
                  ->with($this->equalTo($key))
                  ->will(
                      $this->returnValue([
                          'photoPropMd5' => md5("foo"), // will not match the current md5
                          'photo' => $cachedPhotoData
                      ])
                  );

            // expect that the old record is purged
            $cache->expects($this->once())
                  ->method("remove")
                  ->with($this->equalTo($key));
        } else {
            $cache->expects($this->never())->method("get");
        }

        // a new record should be inserted if photo requires caching
        if ($storeExp) {
            $checkPhotoFn = function (array $cacheObj) use ($vcard, $saveDataExpected): bool {
                $this->assertNotNull($cacheObj['photoPropMd5']);
                $this->assertNotNull($cacheObj['photo']);
                $this->assertIsString($cacheObj['photo']);
                $this->assertSame(md5($vcard->PHOTO->serialize()), $cacheObj['photoPropMd5']);
                $this->assertTrue(isset($saveDataExpected["photo"]));
                $this->assertIsString($saveDataExpected["photo"]);
                Utils::comparePhoto($saveDataExpected["photo"], $cacheObj['photo']);
                return true;
            };
            $cache->expects($this->once())
               ->method("set")
               ->with(
                   $this->equalTo($key),
                   $this->callback($checkPhotoFn)
               )
               ->will($this->returnValue(true));
        } else {
            $cache->expects($this->never())->method("set");
        }

        $dc = new DataConversion("42");
        $saveData = $dc->toRoundcube($vcard, $this->abook);
        $this->assertTrue(isset($saveData['photo']));
        $this->assertTrue(isset($saveDataExpected["photo"]));
        $this->assertIsString($saveDataExpected["photo"]);
        Utils::comparePhoto($saveDataExpected["photo"], (string) $saveData["photo"]);

        $this->assertPhotoDownloadWarning($logger, $basename);
    }

    /**
     * Tests that a delayed photo loader handles vcards lacking a PHOTO property.
     */
    public function testPhotoloaderHandlesVcardWithoutPhotoProperty(): void
    {
        $vcard = TestInfrastructure::readVCard("tests/Unit/data/vcardImport/AllAttr.vcf");
        $this->assertNull($vcard->PHOTO);

        $proxy = new DelayedPhotoLoader($vcard, $this->abook);
        $this->assertEquals("", $proxy);
    }

    /**
     * Tests that DelayedPhotoLoader logs an error in case rcube cache usage is attempted without user logged on.
     */
    public function testPhotoloaderHandlesUnauthenticatedUsageError(): void
    {
        $logger = TestInfrastructure::logger();
        unset($_SESSION['user_id']);

        $vcard = TestInfrastructure::readVCard("tests/Unit/data/vcardImport/UriPhoto.vcf");
        $this->assertNotNull($vcard->PHOTO);
        $proxy = new DelayedPhotoLoader($vcard, $this->abook);

        $this->assertEquals("", $proxy);
        $logger->expectMessage("error", "determineCacheKey: user must be logged on to use photo cache");
    }

    /**
     * Tests that DelayedPhotoLoader logs a warning if it encounters an unsupported PHOTO URI scheme
     */
    public function testPhotoloaderHandlesUnknownUriScheme(): void
    {
        $logger = TestInfrastructure::logger();

        $vcard = TestInfrastructure::readVCard("tests/Unit/data/vcardImport/UriPhoto.vcf");
        $this->assertNotNull($vcard->PHOTO);
        $vcard->PHOTO->setValue('ftp://localhost/raven.jpg');
        $proxy = new DelayedPhotoLoader($vcard, $this->abook);

        $this->assertEquals("", $proxy);
        $logger->expectMessage("warning", "Unsupported URI scheme ftp for PHOTO property");
    }

    /**
     * Tests that the function properly reports single-value attributes.
     */
    public function testSinglevalueAttributesReportedAsSuch(): void
    {
        $dc = new DataConversion("42");

        $knownSingle  = ['name', 'firstname', 'surname', 'middlename', 'prefix', 'suffix', 'nickname', 'jobtitle',
            'organization', 'department', 'assistant', 'manager', 'gender', 'maidenname', 'spouse', 'birthday',
            'anniversary', 'notes', 'photo'];

        foreach ($knownSingle as $singleAttr) {
            $this->assertFalse($dc->isMultivalueProperty($singleAttr), "Attribute $singleAttr expected to be single");
        }
    }

    /**
     * Tests that the data converter properly reports multi-value attributes.
     */
    public function testMultivalueAttributesReportedAsSuch(): void
    {
        $dc = new DataConversion("42");

        $knownMulti  = ['email', 'phone', 'address', 'website', 'im'];

        foreach ($knownMulti as $multiAttr) {
            $this->assertTrue($dc->isMultivalueProperty($multiAttr), "Attribute $multiAttr expected to be multi");
        }
    }

    /**
     * Tests that the data converter throws an exception when asked for the type of an unknown attribute.
     */
    public function testExceptionWhenAskedForTypeOfUnknownAttribute(): void
    {
        $dc = new DataConversion("42");

        $this->expectExceptionMessage('not a known roundcube contact property');
        $dc->isMultivalueProperty("unknown");
    }

    /**
     * @return array<string, list{string, string}>
     */
    public function vcardExportSamplesProvider(): array
    {
        return $this->vcardSamplesProvider('tests/Unit/data/vcardExport');
    }

    /**
     * Tests that a VCard converted to roundcube can be properly exported as vcard again.
     *
     *
     * @dataProvider vcardExportSamplesProvider
     */
    public function testCorrectExportOfVcardFromRoundcube(string $vcfFile, string $_jsonFileNotExisting): void
    {
        $logger = TestInfrastructure::logger();
        $db = $this->db;
        $db->expects($this->once())
            ->method("get")
            ->with(
                $this->equalTo(["abook_id" => "42"]),
                $this->equalTo(['typename', 'subtype']),
                $this->equalTo('xsubtypes')
            )
            ->will($this->returnValue([
                ["typename" => "email", "subtype" => "SpecialLabel"],
            ]));

        $dc = new DataConversion("42");
        $vcardOrig = TestInfrastructure::readVCard($vcfFile);
        $saveData = $dc->toRoundcube($vcardOrig, $this->abook);

        $this->assertIsString($saveData["vcard"] ?? null);
        $vcardOrig = $saveData["_carddav_vcard"] ?? null;
        $this->assertInstanceOf(VCard::class, $vcardOrig);

        /**
         * @psalm-var SaveDataFromDC $saveData XXX temporary workaround for vimeo/psalm#8980
         */
        $vcfExported = DataConversion::exportVCard($vcardOrig, $saveData);
        $this->assertPhotoDownloadWarning($logger, $vcfFile);
        $vcardExported = VObject\Reader::read($vcfExported);
        $this->assertInstanceOf(VCard::class, $vcardExported);

        $vcardExpected = TestInfrastructure::readVCard("$vcfFile.exported");
        $this->compareVCards($vcardExpected, $vcardExported, false);
    }

    private function compareVCards(VCard $vcardExpected, VCard $vcardRoundcube, bool $isNew): void
    {
        // These attributes are dynamically created / updated and therefore cannot be statically compared
        $noCompare = [ 'REV', 'PRODID' ];

        if ($isNew) {
            // new VCard will have UID assigned by carddavclient lib on store
            $noCompare[] = 'UID';
        }

        foreach ($noCompare as $property) {
            unset($vcardExpected->{$property});
            unset($vcardRoundcube->{$property});
        }

        /** @var VObject\Property[] */
        $propsExp = $vcardExpected->children();
        $propsExp = self::groupNodesByName($propsExp);
        /** @var VObject\Property[] */
        $propsRC = $vcardRoundcube->children();
        $propsRC = self::groupNodesByName($propsRC);

        // compare
        foreach ($propsExp as $name => $props) {
            TestCase::assertArrayHasKey($name, $propsRC, "Expected property $name missing from test vcard");
            self::compareNodeList("Property $name", $props, $propsRC[$name]);

            for ($i = 0; $i < count($props); ++$i) {
                TestCase::assertEqualsIgnoringCase(
                    $props[$i]->group,
                    $propsRC[$name][$i]->group,
                    "Property group name differs"
                );
                /** @psalm-var VObject\Parameter[] */
                $paramExp = $props[$i]->parameters();
                $paramExp = self::groupNodesByName($paramExp);
                /** @psalm-var VObject\Parameter[] */
                $paramRC = $propsRC[$name][$i]->parameters();
                $paramRC = self::groupNodesByName($paramRC);
                foreach ($paramExp as $pname => $params) {
                    self::compareNodeList("Parameter $name/$pname", $params, $paramRC[$pname]);
                    unset($paramRC[$pname]);
                }
                TestCase::assertEmpty($paramRC, "Prop $name has extra params: " . implode(", ", array_keys($paramRC)));
            }
            unset($propsRC[$name]);
        }

        TestCase::assertEmpty($propsRC, "VCard has extra properties: " . implode(", ", array_keys($propsRC)));
    }

    /**
     * Groups a list of VObject\Node by node name.
     *
     * @template T of VObject\Property|VObject\Parameter
     *
     * @param T[] $nodes
     * @return array<string, list<T>> Array with node names as keys, and arrays of nodes by that name as values.
     */
    private static function groupNodesByName(array $nodes): array
    {
        $res = [];
        foreach ($nodes as $n) {
            $res[$n->name][] = $n;
        }

        return $res;
    }

    /**
     * Compares to lists of VObject nodes with the same name.
     *
     * This can be two lists of property instances (e.g. EMAIL, TEL) or two lists of parameters (e.g. TYPE).
     *
     * @param string $dbgid Some string to identify property/parameter for error messages
     * @param VObject\Property[]|VObject\Parameter[] $exp Expected list of nodes
     * @param VObject\Property[]|VObject\Parameter[] $rc  List of nodes in the VCard produces by rcmcarddav
     */
    private static function compareNodeList(string $dbgid, array $exp, array $rc): void
    {
        TestCase::assertCount(count($exp), $rc, "Different amount of $dbgid");

        for ($i = 0; $i < count($exp); ++$i) {
            if ($dbgid == "Property PHOTO") {
                Utils::comparePhoto((string) $exp[$i]->getValue(), (string) $rc[$i]->getValue());
            } else {
                TestCase::assertSame($exp[$i]->getValue(), $rc[$i]->getValue(), "Nodes $dbgid differ");
            }
        }
    }

    /**
     * Asserts that a warning message concerning failure to download the photo has been issued for test cases that use
     * the InvalidUriPhoto.vcf data set.
     *
     * @param string Name of the vcffile used by the test. Assertion is only done if it contains InvalidUriPhoto.
     */
    private function assertPhotoDownloadWarning(TestLogger $logger, string $vcffile): void
    {
        if (strpos($vcffile, 'InvalidUriPhoto') !== false) {
            $logger->expectMessage(
                'warning',
                'downloadPhoto: Attempt to download photo from http://localhost/doesNotExist.jpg failed'
            );
        }
    }
}

// vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120
Back to Directory File Manager