Viewing File: /usr/local/cpanel/base/3rdparty/roundcube/plugins/carddav/tests/Unit/SyncHandlerRoundcubeTest.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 MStilkerich\Tests\RCMCardDAV\TestInfrastructure;
use PHPUnit\Framework\TestCase;
use MStilkerich\CardDavClient\AddressbookCollection;
use MStilkerich\RCMCardDAV\{Addressbook,DataConversion,SyncHandlerRoundcube};
use MStilkerich\RCMCardDAV\Db\AbstractDatabase;

/**
 * Tests for the SyncHandlerRoundcube class.
 *
 * Situations to consider in the test:
 *
 * GetExistingVCards: Provide array URI => ETAG
 *   - Case: Empty addressbook
 *   - Case: Addressbook with contacts AND vcard-style groups AND CATEGORIES-style groups [not contained]
 * Changes: URI, ETAG, ?Card
 *   - Card is NULL
 *   - New contact
 *     - with CATEGORIES
 *     - with PHOTO referenced by URI (no download from CardDAV server)
 *   - Updated contact, with CATEGORIES
 *      - New CATEGORIES
 *      - Removed CATEGORIES
 *      - A CATEGORIES-type group becomes empty during the sync
 *   - New group
 *      - Without members
 *      - With members provided in previous Change invocations
 *      - With members provided in later Change invocations
 *      - With members already locally available, but not changed
 *      - With duplicate members in a VCard-style group (part of increment1/nightwatch.vcf)
 *      - With duplicate members in a CATEGORIES-style group (part of increment1/cersei.vcf)
 *      - Error: With member UIDs that are unknown
 * Deleted: URI
 *      - Existing contact card
 *      - Existing group card
 *      - Unknown URI
 * Finalize:
 */
final class SyncHandlerRoundcubeTest extends TestCase
{
    /** @var JsonDatabase */
    private $db;

    public static function setUpBeforeClass(): void
    {
        $_SESSION['user_id'] = 105;
    }

    public function setUp(): void
    {
        $this->db = new JsonDatabase();
        TestInfrastructure::init($this->db);
    }

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

    /**
     * Tests that an empty local cache is properly reported as empty array by the sync handler.
     */
    public function testSyncHandlerProvidesExistingCacheStateEmpty(): void
    {
        [ $dc, $abook, $rcAbook ] = $this->initStubs();

        $synch = new SyncHandlerRoundcube($rcAbook, $dc, $abook);

        $cache = $synch->getExistingVCardETags();
        $this->assertCount(0, $cache);
    }

    /**
     * Tests that a non-empty local cache is properly reported by the sync handler.
     */
    public function testSyncHandlerProvidesExistingCacheState(): void
    {
        $db = $this->db;
        $db->importData('tests/Unit/data/syncHandlerTest/initial/db.json');

        [ $dc, $abook, $rcAbook ] = $this->initStubs();

        $synch = new SyncHandlerRoundcube($rcAbook, $dc, $abook);

        $cache = $synch->getExistingVCardETags();

        $centries = $db->get(['abook_id' => '42']);
        $gentries = $db->get(['abook_id' => '42', '!vcard' => null], ['id'], 'groups');
        $this->assertCount(count($centries) + count($gentries), $cache);

        // check that contact and group card both contained
        foreach (['nedstark', 'kings'] as $dataset) {
            $this->assertArrayHasKey("/book42/$dataset.vcf", $cache);
            $this->assertSame($cache["/book42/$dataset.vcf"], "etag@{$dataset}_1");
        }
    }

    /**
     * Tests an initial sync operation into an empty database.
     *
     * The initial data set contains everything that manifests in table columns outside vcard:
     * - Several contacts, including contacts with set ORG property
     * - CATEGORIES-type groups embedded in the contacts
     * - A KIND=group vcard that contains several of the contacts and invalid member references
     * - Use of a special label with the X-ABLABEL extension
     * - A company card using X-ABSHOWAS:COMPANY
     */
    public function testInitialSyncOnEmptyDatabase(): void
    {
        $vcfFiles = (glob("tests/Unit/data/syncHandlerTest/initial/*.vcf"));
        $this->assertIsArray($vcfFiles);
        $this->assertTrue(sort($vcfFiles));
        $this->initialSyncTestHelper($vcfFiles);
    }

    /**
     * Tests an initial sync operation into an empty database, giving cards in reverse order.
     *
     * This test ensures that either in this test or in testInitialSyncOnEmptyDatabase(), a KIND=group vcard will be
     * reported to the sync handler before all members are known to the sync handler. The sync handler must be able to
     * cope with this, i.e. check group memberships only after it has an up-to-date view of the contacts.
     */
    public function testInitialSyncOnEmptyDatabaseInReverseOrder(): void
    {
        $vcfFiles = (glob("tests/Unit/data/syncHandlerTest/initial/*.vcf"));
        $this->assertIsArray($vcfFiles);
        $this->assertTrue(rsort($vcfFiles));
        $this->initialSyncTestHelper($vcfFiles);
    }

    /**
     * @param list<string> $vcfFiles
     */
    private function initialSyncTestHelper(array $vcfFiles): void
    {
        $db = $this->db;
        $db->importData('tests/Unit/data/syncHandlerTest/initialdb.json');
        $logger = TestInfrastructure::logger();
        [ $dc, $abook, $rcAbook ] = $this->initStubs();

        $synch = new SyncHandlerRoundcube($rcAbook, $dc, $abook);

        // Report all VCards of the test DB as changed
        foreach ($vcfFiles as $vcfFile) {
            $base = basename($vcfFile, ".vcf");
            $synch->addressObjectChanged(
                "/book42/$base.vcf",
                "etag@{$base}_1",
                TestInfrastructure::readVCard($vcfFile)
            );
        }

        // simulate a VCard parse error
        $synch->addressObjectChanged("/book42/error.vcf", "etag@error_1", null);

        $synch->finalizeSync();

        // check emitted warnings
        $logger->expectMessage(
            "warning",
            "don't know how to interpret group membership: urn:unknown:9e1c1cf8-51f8-42b1-9314-44e34a7d148f"
        );
        $logger->expectMessage(
            "warning",
            "cannot find DB ID for group member: 11111111-2222-3333-4444-555555555555"
        );
        $logger->expectMessage(
            "error",
            "Card /book42/error.vcf changed, but error in retrieving address data (card ignored)"
        );

        $this->compareResultDb($db, "initial");
    }

    /**
     * Tests a subsequent sync to the database state of the initial sync.
     */
    public function testFollowupSync1(): void
    {
        $db = $this->db;
        $db->importData('tests/Unit/data/syncHandlerTest/initial/db.json');
        [ $dc, $abook, $rcAbook ] = $this->initStubs();

        $vcfFiles = (glob("tests/Unit/data/syncHandlerTest/increment1/*.vcf"));
        $this->assertIsArray($vcfFiles);

        // get list of cards to report as deleted
        $deleted = file(
            "tests/Unit/data/syncHandlerTest/increment1/remove.txt",
            FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
        );
        $this->assertIsArray($deleted);

        $synch = new SyncHandlerRoundcube($rcAbook, $dc, $abook);

        foreach ($deleted as $deletedCard) {
            $synch->addressObjectDeleted("/book42/$deletedCard");
        }

        // include deletion of an unknown card, should be gracefully ignored
        $synch->addressObjectDeleted("/book42/doesNotExist");

        // Report all VCards of the test DB as changed
        foreach ($vcfFiles as $vcfFile) {
            $base = basename($vcfFile, ".vcf");
            $synch->addressObjectChanged(
                "/book42/$base.vcf",
                "etag@{$base}_2",
                TestInfrastructure::readVCard($vcfFile)
            );
        }

        $synch->finalizeSync();

        $this->compareResultDb($db, "increment1");
    }

    private function compareResultDb(JsonDatabase $db, string $syncStage): void
    {
        $expDb = new JsonDatabase(["tests/Unit/data/syncHandlerTest/$syncStage/db.json"]);

        // Compare database with expected state
        $expDb->compareTables('contacts', $db);
        $expDb->compareTables('groups', $db);
        $expDb->compareTables('group_user', $db);
        $expDb->compareTables('xsubtypes', $db);
    }

    /**
     * @return array{0: DataConversion, 1: AddressbookCollection, 2: Addressbook}
     */
    private function initStubs(): array
    {
        $rcAbook = $this->createStub(Addressbook::class);
        $rcAbook->method('getId')->will($this->returnValue("42"));

        $abook = $this->createStub(AddressbookCollection::class);

        $cache = $this->createMock(\rcube_cache::class);
        TestInfrastructure::$infra->setCache($cache);

        $dc = new DataConversion("42");
        return [ $dc, $abook, $rcAbook ];
    }
}

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