Viewing File: /usr/local/cpanel/base/3rdparty/roundcube/plugins/carddav/tests/Unit/AdminSettingsTest.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 MStilkerich\RCMCardDAV\RoundcubeLogger;
use MStilkerich\RCMCardDAV\Db\Database;
use MStilkerich\RCMCardDAV\Frontend\AdminSettings;
use MStilkerich\Tests\RCMCardDAV\TestInfrastructure;
use PHPUnit\Framework\TestCase;

final class AdminSettingsTest extends TestCase
{
    public static function setUpBeforeClass(): void
    {
        // needed for URL placeholder replacements when admin settings are read
        $_SESSION['username'] = 'user@example.com';
    }

    public function setUp(): void
    {
        $db = $this->createMock(Database::class);
        TestInfrastructure::init($db);
    }

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

    /**
     * @return array<string, array{string}>
     */
    public function configFileProvider(): array
    {
        $base = 'tests/Unit/data/adminSettingsTest';

        return [
            'Non-existent config file' => [ "$base/notExistent" ],
            'Valid config file with all settings' => [ "$base/fullconfig" ],
        ];
    }

    /**
     * Tests that config.inc.php file is correctly parsed.
     *
     * @dataProvider configFileProvider
     */
    public function testConfigFileParsedCorrectly(string $cfgFileBase): void
    {
        $expPrefs = TestInfrastructure::readJsonArray("$cfgFileBase.json");
        $loggerMock = $this->createMock(RoundcubeLogger::class);
        $loggerHttpMock = $this->createMock(RoundcubeLogger::class);

        if (isset($expPrefs["loglevel"])) {
            $loggerMock->expects($this->once())
                       ->method("setLogLevel")
                       ->with($this->equalTo($expPrefs['loglevel']));
        }
        if (isset($expPrefs["loglevel_http"])) {
            $loggerHttpMock->expects($this->once())
                           ->method("setLogLevel")
                           ->with($this->equalTo($expPrefs['loglevel_http']));
        }

        $admPrefs = new AdminSettings("$cfgFileBase.inc.php", $loggerMock, $loggerHttpMock);

        $this->assertSame($expPrefs['pwStoreScheme'], $admPrefs->pwStoreScheme);
        $this->assertSame($expPrefs['forbidCustomAddressbooks'], $admPrefs->forbidCustomAddressbooks);
        $this->assertSame($expPrefs['hidePreferences'], $admPrefs->hidePreferences);

        $this->assertSame($expPrefs["presets"], TestInfrastructure::getPrivateProperty($admPrefs, 'presets'));
        $this->assertSame(
            $expPrefs["specialAbookMatchers"],
            TestInfrastructure::getPrivateProperty($admPrefs, 'specialAbookMatchers')
        );
    }

    /**
     * Tests that getPreset() returns the expected preset data, merging account settings with those of extra
     * addressbooks.
     */
    public function testGetPresetReturnsAddressbookSpecificConfig(): void
    {
        $cfgFileBase = 'tests/Unit/data/adminSettingsTest/fullconfig';
        /** @var array<string, array<string, array>> */
        $expPrefs = TestInfrastructure::readJsonArray("$cfgFileBase-getPreset.json");

        $loggerMock = $this->createMock(RoundcubeLogger::class);
        $admPrefs = new AdminSettings("$cfgFileBase.inc.php", $loggerMock, $loggerMock);

        foreach ($expPrefs as $presetName => $presetBooks) {
            foreach ($presetBooks as $url => $presetExp) {
                if (strlen($url) == 0) {
                    $preset = $admPrefs->getPreset($presetName);
                    $presetUnknown = $admPrefs->getPreset($presetName, "http://not.a.known.url/of/an/abook");
                    $this->assertSame($presetExp, $presetUnknown, "Unknown abook URL should return base properties");
                } else {
                    $preset = $admPrefs->getPreset($presetName, $url);
                }

                // PHP arrays are ordered, including string keys
                ksort($presetExp);
                ksort($preset);
                $this->assertSame($presetExp, $preset);
            }
        }
    }

    public function testGetPresetThrowsExceptionForNonexistingPreset(): void
    {
        $cfgFileBase = 'tests/Unit/data/adminSettingsTest/fullconfig';
        $loggerMock = $this->createMock(RoundcubeLogger::class);
        $admPrefs = new AdminSettings("$cfgFileBase.inc.php", $loggerMock, $loggerMock);

        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Query for undefined preset Invalid Preset');
        $admPrefs->getPreset('Invalid Preset');
    }

    /**
     * @return array<string, array{
     *     callable(array):array|callable(array):string,
     *     callable(array):array,
     *     callable(AdminSettings, RoundcubeLogger, RoundcubeLogger):void,
     *     string
     * }>
     */
    public function errorsInAdminConfigProvider(): array
    {
        $ret = [
            'Non-array prefs' => [
                function (array $_prefs): string {
                    return 'not an array';
                },
                function (array $_expPrefs): array {
                    return [
                        'pwStoreScheme' => 'encrypted',
                        'forbidCustomAddressbooks' => false,
                        'hidePreferences' => false,
                        'presets' => [],
                    ];
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                    TestCase::assertSame(
                        5,
                        TestInfrastructure::getPrivateProperty($rcLogger, 'loglevel')
                    );
                },
                'prefs must be an array'
            ],
            'Invalid loglevel value' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['_GLOBAL']['loglevel'] = 'foo';
                    return $prefs;
                },
                function (array $expPrefs): array {
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                    TestCase::assertSame(
                        5,
                        TestInfrastructure::getPrivateProperty($rcLogger, 'loglevel')
                    );
                },
                'unknown loglevel'
            ],
            'Invalid loglevel value (HTTP)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['_GLOBAL']['loglevel_http'] = 'foo';
                    return $prefs;
                },
                function (array $expPrefs): array {
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $rcLoggerHttp): void {
                    TestCase::assertSame(
                        5,
                        TestInfrastructure::getPrivateProperty($rcLoggerHttp, 'loglevel')
                    );
                },
                'unknown loglevel'
            ],
            'Invalid loglevel type' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['_GLOBAL']['loglevel'] = 5;
                    return $prefs;
                },
                function (array $expPrefs): array {
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                    TestCase::assertSame(
                        5,
                        TestInfrastructure::getPrivateProperty($rcLogger, 'loglevel')
                    );
                },
                'unknown loglevel'
            ],
            'Invalid loglevel type (HTTP)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['_GLOBAL']['loglevel_http'] = 5;
                    return $prefs;
                },
                function (array $expPrefs): array {
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $rcLoggerHttp): void {
                    TestCase::assertSame(
                        5,
                        TestInfrastructure::getPrivateProperty($rcLoggerHttp, 'loglevel')
                    );
                },
                'unknown loglevel'
            ],
            'Invalid pwstore scheme' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['_GLOBAL']['pwstore_scheme'] = 'foo';
                    return $prefs;
                },
                function (array $expPrefs): array {
                    $expPrefs['pwStoreScheme'] = 'encrypted'; // default should be used
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "Invalid pwStoreScheme foo in config.inc.php - using default 'encrypted'"
            ],
            'Invalid preset key (empty string)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs[''] = [ 'accountname' => 'Invalid' ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "A preset key must be a non-empty string - ignoring preset"
            ],
            'Invalid preset key (integer)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs[0] = [ 'accountname' => 'Invalid' ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "A preset key must be a non-empty string - ignoring preset"
            ],
            'Invalid preset key (not an array)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['Invalid'] = false;
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "preset definition must be an array"
            ],
            'Invalid preset, mandatory attribute missing (extraabook.url)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['Invalid'] = [ 'accountname' => 'Test', 'extra_addressbooks' => [['active' => true]] ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "required setting url is not set"
            ],
            'Invalid preset, wrong type (extraabooks not an array)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['Invalid'] = [ 'accountname' => 'Test', 'extra_addressbooks' => "example.com" ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "setting extra_addressbooks must be an array"
            ],
            'Invalid preset, wrong type (extraabooks[x] not an array)' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['Invalid'] = [
                        'accountname' => 'Test',
                        'extra_addressbooks' => [
                            ['url' => 'foo.com'],
                            "example.com"
                        ]
                    ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "setting extra_addressbooks\[1\] must be an array"
            ],
            'Invalid preset referenced in collected senders' => [
                function (array $prefs): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['_GLOBAL']['collected_senders'] = [
                        'preset' => 'InvalidKey',
                    ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid special addressbook matcher must be ignored
                    if (
                        isset($expPrefs['specialAbookMatchers'])
                        && is_array($expPrefs['specialAbookMatchers'])
                        && isset($expPrefs['specialAbookMatchers']['collected_senders'])
                    ) {
                        unset($expPrefs['specialAbookMatchers']['collected_senders']);
                    }
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "Setting for collected_senders must include a valid preset attribute"
            ],
        ];

        foreach (['accountname', 'username', 'password', 'discovery_url', 'rediscover_time', 'refresh_time'] as $k) {
            $ret["Wrong type for string attribute ($k)"] = [
                function (array $prefs) use ($k): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['Invalid'] = [ 'accountname' => 'Invalid', $k => 1 ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "setting $k must be a string"
            ];
        }

        foreach (['rediscover_time', 'refresh_time'] as $timeStrAttr) {
            $ret["Wrong type for timestring attribute ($timeStrAttr)"] = [
                function (array $prefs) use ($timeStrAttr): array {
                    TestCase::assertIsArray($prefs['_GLOBAL']);
                    $prefs['Invalid'] = [ 'accountname' => 'Invalid', $timeStrAttr => 'foo' ];
                    return $prefs;
                },
                function (array $expPrefs): array {
                    // invalid preset must be ignored
                    return $expPrefs;
                },
                function (AdminSettings $_admPrefs, RoundcubeLogger $_rcLogger, RoundcubeLogger $_rcLoggerHttp): void {
                },
                "Time string foo could not be parsed"
            ];
        }

        foreach (['fixed'] as $strArrayAttr) {
            foreach ([ true, 'foo', 1, [ 'foo', 1 ] ] as $idx => $errVal) {
                $ret["Wrong type for string array attribute ($strArrayAttr $idx)"] = [
                    function (array $prefs) use ($strArrayAttr, $errVal): array {
                        TestCase::assertIsArray($prefs['_GLOBAL']);
                        $prefs['Invalid'] = [ 'accountname' => 'Invalid', $strArrayAttr => $errVal ];
                        return $prefs;
                    },
                    function (array $expPrefs): array {
                        // invalid preset must be ignored
                        return $expPrefs;
                    },
                    function (
                        AdminSettings $_admPrefs,
                        RoundcubeLogger $_rcLogger,
                        RoundcubeLogger $_rcLoggerHttp
                    ): void {
                    },
                    is_array($errVal) ? "must be string" : "setting $strArrayAttr must be array"
                ];
            }
        }

        return $ret;
    }

    /**
     * Tests that errors in the admin configuration are detected and, if possible, handled without a fatal error, i.e.
     * using roundcube should still be possible.
     *
     * If the error affects a single preset, the preset will be ignored, i.e. not present in the resulting preset list.
     * As a side effect, if the preset happened to work before, it may cause deletion of related addressbooks for users
     * that already had them added earlier. This is acceptable, since no data is lost (everything is on the CardDAV
     * server) and the preset will be added again. The only drawback is that some server traffic will be generated for
     * re-downloading the addressbook.
     *
     * The following errors are tested:
     *
     * - wrong data type for a global configuration setting - except bool, where we interpret different types according
     *                                                        to PHP's understanding of true/false
     * - wrong data type for a preset configuration setting - except bool, see above
     * - wrong value for a configuration setting (e.g. non-existent loglevel, invalid time string)
     * - wrong preset key referenced in a special addressbook matcher
     *
     * As basis, we use a valid configuration and inject one error at a time.
     *
     * @param callable(array):array|callable(array):string $modifyPrefsFunc
     * @param callable(array):array $modifyExpResultFunc
     * @param callable(AdminSettings, RoundcubeLogger, RoundcubeLogger):void $validateFunc
     *
     * @dataProvider errorsInAdminConfigProvider
     */
    public function testErrorsInAdminConfigAreDetected(
        $modifyPrefsFunc,
        $modifyExpResultFunc,
        $validateFunc,
        string $expLogMsg
    ): void {
        $prefs = TestInfrastructure::readPhpPrefsArray('tests/Unit/data/adminSettingsTest/fullconfig.inc.php');

        // modify prefs
        $prefs = $modifyPrefsFunc($prefs);

        // write modified prefs to temporary file
        $tmpfile = tempnam("testreports", "adminSettingsTest_");
        $this->assertIsString($tmpfile);
        file_put_contents($tmpfile, "<?php\n\$prefs = " . var_export($prefs, true) . ';');

        // load modified settings
        $rcubecfg = \rcube::get_instance()->config;
        $rcubecfg->set('log_dir', __DIR__ . '/../../testreports/');

        $this->assertNotFalse(file_put_contents('testreports/adminSettingsTest_log.log', ''));
        $logger = new RoundcubeLogger('adminSettingsTest_log');
        $loggerHttp = new RoundcubeLogger('adminSettingsTest_logHttp');

        $admPrefs = new AdminSettings($tmpfile, $logger, $loggerHttp);

        // compare to expected settings
        $expPrefs = TestInfrastructure::readJsonArray("tests/Unit/data/adminSettingsTest/fullconfig.json");
        $expPrefs = $modifyExpResultFunc($expPrefs);
        $this->assertSame($expPrefs['pwStoreScheme'], $admPrefs->pwStoreScheme);
        $this->assertSame($expPrefs['forbidCustomAddressbooks'], $admPrefs->forbidCustomAddressbooks);
        $this->assertSame($expPrefs['hidePreferences'], $admPrefs->hidePreferences);
        $this->assertSame($expPrefs["presets"], TestInfrastructure::getPrivateProperty($admPrefs, 'presets'));

        // extra validation function
        $validateFunc($admPrefs, $logger, $loggerHttp);

        // check that expected error message was logged
        $logEntries = file_get_contents('testreports/adminSettingsTest_log.log');
        $this->assertMatchesRegularExpression("/\[5 ERR\] .*$expLogMsg/", $logEntries, "expected error log not found");
    }

    /**
     * Delete temporary files from testErrorsInAdminConfigAreDetected
     */
    public static function cleanupTempConfigs(): void
    {
        $tmpfs = glob("testreports/adminSettingsTest_*");
        if (!empty($tmpfs)) {
            foreach ($tmpfs as $tmpf) {
                unlink($tmpf);
            }
        }
    }
}

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