<?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\DBInteroperability;
use MStilkerich\RCMCardDAV\Db\AbstractDatabase;
use PHPUnit\Framework\TestCase;
/**
* This class provides functionality to manage data in test databases.
*
* It allows to clear tables and insert rows into tables. For row insertion, it remembers the assigned row ID by the
* database and enables to resolve foreign key references in subsequently inserted rows.
*
* @psalm-type TestDataKeyRef = array{0: string, 1: int, 2?: string}
* @psalm-type TestDataRowWithKeyRef = list<null|string|TestDataKeyRef>
* @psalm-type TestDataRow = list<null|string>
* @psalm-type TestDataTableDef = list<string>
* @psalm-type TableName = string
* @psalm-type CacheKeyPrefix = string
*/
final class TestData
{
/** @var TestDataTableDef Column names of the users table */
public const USERS_COLUMNS = [ "username", "mail_host" ];
/** @var TestDataTableDef Column names of the carddav_accounts table */
public const ACCOUNTS_COLUMNS = [ "accountname", "username", "password", "discovery_url", "user_id" ];
/** @var TestDataTableDef Column names of the carddav_addressbooks table */
public const ADDRESSBOOKS_COLUMNS = [ "name", "url", "account_id", "sync_token" ];
/** @var TestDataTableDef Column names of the carddav_xsubtypes table */
public const XSUBTYPES_COLUMNS = [ "typename", "subtype", "abook_id" ];
/** @var TestDataTableDef Column names of the carddav_contacts table */
public const CONTACTS_COLUMNS = [
"abook_id", "name", "email", "firstname", "surname", "organization", "showas", "vcard", "etag", "uri", "cuid"
];
/** @var TestDataTableDef Column names of the carddav_groups table */
public const GROUPS_COLUMNS = [ "abook_id", "name", "vcard", "etag", "uri", "cuid" ];
/** @var TestDataTableDef Column names of the carddav_group_user table */
public const GROUP_USER_COLUMNS = [ "group_id", "contact_id" ];
/** @var TestDataTableDef Column names of the carddav_migrations table */
public const MIGRATIONS_COLUMNS = [ "filename" ];
/** @var array<string, list<TestDataRowWithKeyRef>> Data to initialize the tables with.
* Keys are table names, values are arrays of value arrays, each of which contains the data for one row.
* The value arrays must match the column descriptions in self::TABLES.
* To reference the primary key of a record in a different table, insert an array as value where the
* first value names the target table, the second value the index in the initialization data of that
* table of the record that should be referenced.
*/
public const INITDATA = [
"users" => [
["testuser@example.com", "mail.example.com"],
["otheruser@example.com", "mail.example.com"],
["userWithoutAddressbooks@example.com", "mail.example.com"],
],
"carddav_accounts" => [
[ "First Account", "u1", "p1", "https://contacts.example.com/", [ "users", 0 ] ],
[ "Second Account", "u2", "p2", "https://contacts.example.com/", [ "users", 0 ] ],
],
"carddav_addressbooks" => [
[ "Empty Addressbook", "https://contacts.example.com/u1/empty/", [ "carddav_accounts", 0 ], "" ],
[ "Small Addressbook", "https://contacts.example.com/u2/small/", [ "carddav_accounts", 1 ], "" ],
],
"carddav_contacts" => [
[
[ "carddav_addressbooks", 1 ],
"Max Mustermann",
"max@mustermann.com, max.mustermann@company.com",
"Max",
"Mustermann",
"Company",
"INDIVIDUAL",
"FIXME INVALID VCARD",
"ex-etag-123",
"/u2/small/maxmuster.vcf",
"2459ca8d-1b8e-465e-8e88-1034dc87c2ec"
],
[
[ "carddav_addressbooks", 1 ],
"Albert Wesker",
"aw@umbrella.com",
"Albert",
"Wesker",
"Umbrella",
"INDIVIDUAL",
"FIXME INVALID VCARD",
"ex-etag-123",
"/u2/small/wesker.vcf",
"uidWesker"
],
],
"carddav_groups" => [
[
[ "carddav_addressbooks", 1 ],
"Test Gruppe Vcard-style",
"FIXME INVALID VCARD",
"ex-etag-1234",
"/u2/small/testgroup.vcf",
"11b98f71-ada1-4a28-b6ab-28ad09be0203"
],
[
[ "carddav_addressbooks", 1 ],
"Test Gruppe CATEGORIES-style",
null,
null,
null,
null
],
],
"carddav_group_user" => [
[
[ "carddav_groups", 0 ],
[ "carddav_contacts", 1 ],
],
],
"carddav_xsubtypes" => [
[ "email" , "customMail", [ "carddav_addressbooks", 1 ] ],
[ "phone" , "customPhone", [ "carddav_addressbooks", 1 ] ],
[ "address" , "customAddress", [ "carddav_addressbooks", 1 ] ],
],
"carddav_migrations" => [
[ "0000-dbinit" ],
[ "0001-categories" ],
[ "0002-increasetextfieldlengths" ],
[ "0003-fixtimestampdefaultvalue" ],
[ "0004-fixtimestampdefaultvalue" ],
[ "0005-changemysqlut8toutf8mb4" ],
[ "0006-rmgroupsnotnull" ],
[ "0007-replaceurlplaceholders" ],
[ "0008-unifyindexes" ],
[ "0009-dropauthschemefield" ],
],
];
/** @var list<array{string, TestDataTableDef}> List of tables to initialize and their columns.
* Tables will be initialized in the order given here. Initialization data is taken from self::INITDATA.
*/
private const TABLES = [
// table name, table columns to insert
[ "users", self::USERS_COLUMNS ],
[ "carddav_accounts", self::ACCOUNTS_COLUMNS ],
[ "carddav_addressbooks", self::ADDRESSBOOKS_COLUMNS ],
[ "carddav_contacts", self::CONTACTS_COLUMNS ],
[ "carddav_groups", self::GROUPS_COLUMNS ],
[ "carddav_group_user", self::GROUP_USER_COLUMNS ],
[ "carddav_xsubtypes", self::XSUBTYPES_COLUMNS ],
[ "carddav_migrations", self::MIGRATIONS_COLUMNS ],
];
/**
* @var array<TableName, array<CacheKeyPrefix, list<string>>> Remember DB ids for inserted rows.
*/
private $idCache = [];
/** @var \rcube_db */
private $dbh;
/** @var string A prefix used in creating cache keys. Used to decouple indexes from multiple test data sets. */
private $cacheKeyPrefix = 'builtin';
public function __construct(\rcube_db $dbh)
{
$this->dbh = $dbh;
}
public function setDbHandle(\rcube_db $dbh): void
{
$this->dbh = $dbh;
}
/**
* Initializes the database with the test data.
*
* It initializes all tables listed in self::TABLES in the given order. Table data is cleared in reverse order
* listed before inserting of data is started.
*
* @param bool $skipInitData If true, only the users table is populated, the carddav tables are left empty.
*/
public function initDatabase(bool $skipInitData = false): void
{
foreach (array_column(array_reverse(self::TABLES), 0) as $tbl) {
$this->purgeTable($tbl);
}
$this->idCache = [];
$this->setCacheKeyPrefix('builtin');
foreach (self::TABLES as $tbldesc) {
[ $tbl, $cols ] = $tbldesc;
TestCase::assertArrayHasKey($tbl, self::INITDATA, "No init data for table $tbl");
if ($skipInitData && $tbl != "users") {
continue;
}
foreach (self::INITDATA[$tbl] as $row) {
$this->insertRow($tbl, $cols, $row);
}
}
$this->setUserId();
}
/**
* Sets the session variables for the test user (the first user inserted to the users table).
*/
private function setUserId(): void
{
if (
isset($this->idCache['users'])
&& isset($this->idCache['users']['builtin'])
&& isset($this->idCache['users']['builtin'][0])
) {
$userId = $this->idCache['users']['builtin'][0];
$_SESSION["user_id"] = $userId;
// we need to set these session variables in case the placeholder replacement functions for
// username/password are invoked by the test execution
$_SESSION["username"] = self::INITDATA['users'][0][0];
$_SESSION["password"] = \rcube::get_instance()->encrypt('test');
} else {
TestCase::assertTrue(false, "cannot determine user ID from the test data");
}
}
/**
* Inserts the given row with test data into the DB, and resolves foreign key references within the row.
*
* @param TestDataTableDef $cols
* @param TestDataRowWithKeyRef $row
* @param-out TestDataRow $row
* @return string ID of the inserted row
*/
public function insertRow(string $tbl, array $cols, array &$row): string
{
$dbh = $this->dbh;
$cols = array_map(
function (string $s) use ($dbh): string {
return $dbh->quote_identifier($s);
},
$cols
);
TestCase::assertCount(count($cols), $row, "Column count mismatch of $tbl row " . print_r($row, true));
$sql = "INSERT INTO " . $dbh->table_name($tbl)
. " (" . implode(",", $cols) . ") "
. "VALUES (" . implode(",", array_fill(0, count($cols), "?")) . ")";
$newrow = [];
foreach ($row as $val) {
if (is_array($val)) {
// resolve foreign key reference
[ $dtbl, $didx ] = $val;
$val = $this->getRowId($dtbl, $didx, $val[2] ?? null);
}
$newrow[] = $val;
}
$row = $newrow;
$dbh->query($sql, $row);
TestCase::assertNull($dbh->is_error(), "Error inserting row to $tbl: " . $dbh->is_error());
/** @psalm-var string|false */
$id = $dbh->insert_id($tbl);
if (is_string($id)) { // not all tables have an ID column
$this->idCache[$tbl][$this->cacheKeyPrefix][] = $id;
return $id;
}
return "";
}
/**
* Purges all rows from the given table.
*/
public function purgeTable(string $tbl): void
{
$dbh = $this->dbh;
$dbh->query("DELETE FROM " . $dbh->table_name($tbl));
TestCase::assertNull($dbh->is_error(), "Error clearing table $tbl " . $dbh->is_error());
unset($this->idCache[$tbl]);
}
public function setCacheKeyPrefix(string $prefix): void
{
$this->cacheKeyPrefix = $prefix;
}
public function getRowId(string $tbl, int $idx, ?string $prefix = null): string
{
if (!isset($prefix)) {
$prefix = $this->cacheKeyPrefix;
}
TestCase::assertTrue(
isset($this->idCache[$tbl][$prefix][$idx]),
"Reference to {$prefix}.{$tbl}[$idx] cannot be resolved"
);
return $this->idCache[$tbl][$prefix][$idx];
}
/**
* @param TestDataKeyRef $fkRef
*/
public function resolveFkRef(array $fkRef): string
{
[ $dtbl, $didx ] = $fkRef;
$prefix = $fkRef[2] ?? null;
return $this->getRowId($dtbl, $didx, $prefix);
}
/**
* Resolves foreign key references in a row of test data.
* @param TestDataRowWithKeyRef $row
* @return TestDataRow
*/
public function resolveFkRefsInRow(array $row): array
{
$result = [];
foreach ($row as $cell) {
if (is_array($cell)) {
$result[] = $this->resolveFkRef($cell);
} else {
$result[] = $cell;
}
}
return $result;
}
}
// vim: ts=4:sw=4:expandtab:fenc=utf8:ff=unix:tw=120