forked from extern/egroupware
377 lines
11 KiB
PHP
377 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* CalDAV tests base class
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @author Ralf Becker
|
|
* @package api
|
|
* @subpackage caldav
|
|
* @copyright (c) 2020 by Ralf Becker <rb@egroupware.org>
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
*/
|
|
|
|
namespace EGroupware\Api;
|
|
|
|
// so tests can run standalone
|
|
require_once __DIR__.'/../src/loader/common.php'; // autoloader
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use GuzzleHttp\Client, GuzzleHttp\RequestOptions;
|
|
use Horde_Icalendar, Horde_Icalendar_Exception;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
/**
|
|
* Abstract CalDAVTest using GuzzleHttp\Client against EGroupware CalDAV/CardDAV server
|
|
*
|
|
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
|
|
*
|
|
* @package EGroupware\Api
|
|
*/
|
|
abstract class CalDAVTest extends TestCase
|
|
{
|
|
/**
|
|
* Base URL of CalDAV server
|
|
*/
|
|
const CALDAV_BASE = 'http://localhost/egroupware/groupdav.php';
|
|
|
|
/**
|
|
* Get full URL for a CalDAV path
|
|
*
|
|
* @param string $path CalDAV path
|
|
* @return string URL
|
|
*/
|
|
protected function url($path='/')
|
|
{
|
|
$base = self::CALDAV_BASE;
|
|
if (!empty($GLOBALS['EGW_DOMAIN']) && $GLOBALS['EGW_DOMAIN'] !== 'default')
|
|
{
|
|
$base = str_replace('localhost', $GLOBALS['EGW_DOMAIN'], $base);
|
|
}
|
|
return $base.$path;
|
|
}
|
|
|
|
/**
|
|
* Default options for GuzzleHttp\Client
|
|
*
|
|
* @var array
|
|
* @see http://docs.guzzlephp.org/en/v6/request-options.html
|
|
*/
|
|
protected $client_options = [
|
|
RequestOptions::HTTP_ERRORS => false, // return all HTTP status, not throwing exceptions
|
|
RequestOptions::HEADERS => [
|
|
'Cookie' => 'XDEBUG_SESSION=PHPSTORM',
|
|
//'User-Agent' => 'CalDAVSynchronizer',
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Get HTTP client for tests
|
|
*
|
|
* It will use by default the always existing user "demo" with password "guest" (use [] to NOT authenticate).
|
|
* Additional users need to be created with $this->createUser("name").
|
|
*
|
|
* @param string|array $user_or_options ='demo' string with account_lid of user for authentication or array of options
|
|
* @return Client
|
|
* @see http://docs.guzzlephp.org/en/v6/request-options.html
|
|
* @see http://docs.guzzlephp.org/en/v6/quickstart.html
|
|
*/
|
|
protected function getClient($user_or_options='demo')
|
|
{
|
|
if (!is_array($user_or_options))
|
|
{
|
|
$user_or_options = $this->auth($user_or_options);
|
|
}
|
|
return new Client(array_merge($this->client_options, $user_or_options));
|
|
}
|
|
|
|
/**
|
|
* Create a number of users with optional ACL rights too
|
|
*
|
|
* Example with boss granting secretary full rights on his calendar, plus one other user:
|
|
*
|
|
* $users = [
|
|
* 'boss' => [],
|
|
* 'secretary' => ['rights' => ['boss' => Acl::READ|Acl::ADD|Acl::EDIT|Acl::DELETE]],
|
|
* 'other' => [],
|
|
* ];
|
|
* self::createUsersACL($users);
|
|
*
|
|
* @param array& $users $account_lid => array with values for keys with (defaults) "firstname" ($_acount_lid), "lastname" ("User"),
|
|
* "email" ("$_account_lid@example.org"), "password" (random string), "primary_group" ("NoGroups" to not set rights)
|
|
* "rights" array with $grantee => $rights pairs (need to be created before!)
|
|
* @param string $app app to create the rights for, default "calendar"
|
|
* @throws \Exception
|
|
*/
|
|
protected function createUsersACL(array &$users, $app='calendar')
|
|
{
|
|
foreach($users as $user => $data)
|
|
{
|
|
$data['id'] = self::createUser($user, $data);
|
|
|
|
foreach($data['rights'] ?? [] as $grantee => $rights)
|
|
{
|
|
self::addAcl('calendar', $data['id'], $grantee, $rights);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Array to track created users for tearDown and authentication
|
|
*
|
|
* @var array $account_lid => array with other data pairs
|
|
*/
|
|
private static $created_users = [];
|
|
|
|
/**
|
|
* Create a user
|
|
*
|
|
* Created users are automatic deleted in tearDown() and can be passed to auth() or getClient() methods.
|
|
* Users have random passwords to force new/different sessions!
|
|
*
|
|
* @param string $_account_lid
|
|
* @param array& $data =[] values for keys with (defaults) "firstname" ($_acount_lid), "lastname" ("User"),
|
|
* "email" ("$_account_lid@example.org"), "password" (random string), "primary_group" ("NoGroups" to not set rights)
|
|
* on return: with defaults set
|
|
* @return int account_id of created user
|
|
* @throws \Exception
|
|
*/
|
|
protected static function createUser($_account_lid, array &$data=[])
|
|
{
|
|
// add some defaults
|
|
$data = array_merge([
|
|
'firstname' => ucfirst($_account_lid),
|
|
'lastname' => 'User',
|
|
'email' => $_account_lid.'@example.org',
|
|
'password' => 'secret',//Auth::randomstring(12),
|
|
'primary_group' => 'NoGroup',
|
|
], $data);
|
|
|
|
$data['id'] = self::getSetup()->add_account($_account_lid, $data['firstname'], $data['lastname'],
|
|
$data['password'], $data['primary_group'], false, $data['email']);
|
|
|
|
// give use run rights for CalDAV apps, as NoGroup does NOT!
|
|
self::addAcl(['groupdav','calendar','infolog','addressbook'], 'run', $data['id']);
|
|
|
|
self::$created_users[$_account_lid] = $data;
|
|
|
|
return $data['id'];
|
|
}
|
|
|
|
/**
|
|
* Get authentication information for given user to use
|
|
*
|
|
* @param string $_account_lid ='demo'
|
|
* @return array
|
|
*/
|
|
protected function auth($_account_lid='demo')
|
|
{
|
|
if ($_account_lid === 'demo')
|
|
{
|
|
$password = 'guest';
|
|
}
|
|
elseif (!isset(self::$created_users[$_account_lid]))
|
|
{
|
|
throw new \InvalidArgumentException("No user '$_account_lid' exist, need to create it with createUser('$_account_lid')");
|
|
}
|
|
else
|
|
{
|
|
$password = self::$created_users[$_account_lid]['password'];
|
|
}
|
|
return [RequestOptions::AUTH => [$_account_lid, $password]];
|
|
}
|
|
|
|
/**
|
|
* Tear down:
|
|
* - delete users created by createUser() incl. their ACL and data
|
|
*
|
|
* @ToDo: implement eg. with admin_cmd_delete_user to also delete ACL and data
|
|
*/
|
|
public static function tearDownAfterClass() : void
|
|
{
|
|
$setup = self::getSetup();
|
|
|
|
foreach(self::$created_users as $account_lid => $data)
|
|
{
|
|
// if ($id) $setup->accounts->delete($data['id']);
|
|
unset(self::$created_users[$account_lid]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add ACL rights
|
|
*
|
|
* @param string|array $apps app-names
|
|
* @param string $location eg. "run"
|
|
* @param int|string $account accountid or account_lid
|
|
* @param int $rights rights to set, default 1
|
|
*/
|
|
protected static function addAcl($apps, $location, $account, $rights=1)
|
|
{
|
|
return self::getSetup()->add_acl($apps, $location, $account, $rights);
|
|
}
|
|
|
|
/**
|
|
* Return instance of setup object eg. to create users
|
|
*
|
|
* @return \setup
|
|
*/
|
|
private static function getSetup()
|
|
{
|
|
static $setup=null;
|
|
if (!isset($setup))
|
|
{
|
|
if (!isset($_REQUEST['domain']))
|
|
{
|
|
$_REQUEST['domain'] = $GLOBALS['EGW_DOMAIN'] ?? 'default';
|
|
}
|
|
$_REQUEST['ConfigDomain'] = $_REQUEST['domain'];
|
|
|
|
$GLOBALS['egw_info'] = array(
|
|
'flags' => array(
|
|
'noheader' => True,
|
|
'nonavbar' => True,
|
|
'currentapp' => 'setup',
|
|
'noapi' => True
|
|
));
|
|
if (file_exists(__DIR__ . '/../../header.inc.php'))
|
|
{
|
|
include_once(__DIR__ . '/../../header.inc.php');
|
|
}
|
|
$setup = new \setup();
|
|
}
|
|
return $setup;
|
|
}
|
|
|
|
/**
|
|
* Check HTTP status in response
|
|
*
|
|
* @param int|array $expected one or more valid status codes
|
|
* @param ResponseInterface $response
|
|
* @param string $message ='' additional message to prefix result message
|
|
*/
|
|
protected function assertHttpStatus($expected, ResponseInterface $response, $message='')
|
|
{
|
|
$status = $response->getStatusCode();
|
|
$this->assertEquals(in_array($status, (array)$expected) ? $status : ((array)$expected)[0], $status,
|
|
(!empty($message) ? $message.': ' : ''). 'Expected HTTP status: '.json_encode($expected).
|
|
", Server returned: $status ".$response->getReasonPhrase());
|
|
}
|
|
|
|
/**
|
|
* Asserts an iCal file matches an expected one taking into account $_overwrites
|
|
*
|
|
* @param string $_expected
|
|
* @param string $_acctual
|
|
* @param string $_message
|
|
* @param array $_overwrites =[] eg. ['vEvent' => [['ATTENDEE' => ['mailto:boss@example.org' => ['PARTSTAT' => 'DECLINED']]]]]
|
|
* (first vEvent attendee with value 'mailto:boss@...' has param 'PARTSTAT=DECLINED')
|
|
* @throws Horde_Icalendar_Exception
|
|
*/
|
|
protected function assertIcal($_expected, $_acctual, $_message=null, $_overwrites=[])
|
|
{
|
|
// enable to see full iCals
|
|
//$this->assertEquals($_expected, (string)$_acctual, $_message.": iCal not byte-by-byte identical");
|
|
|
|
$expected = new Horde_Icalendar();
|
|
$expected->parsevCalendar($_expected);
|
|
$acctual = new Horde_Icalendar();
|
|
$acctual->parsevCalendar($_acctual);
|
|
|
|
if (($msgs = $this->checkComponentEqual($expected, $acctual, $_overwrites)))
|
|
{
|
|
$this->assertEquals($_expected, (string)$_acctual, ($_message ? $_message.":\n" : '').implode("\n", $msgs));
|
|
}
|
|
else
|
|
{
|
|
$this->assertTrue(true); // due to $_overwrite probable $_expected !== $_acctual
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check two iCal components are equal modulo overwrites / expected difference
|
|
*
|
|
* Only a whitelist of attributes per component are checked, see $component_attrs2check variable.
|
|
*
|
|
* @param Horde_Icalendar $_expected
|
|
* @param Horde_Icalendar $_acctual
|
|
* @param string $_message
|
|
* @param array $_overwrites =[] eg. ['ATTENDEE' => ['boss@example.org' => ['PARTSTAT' => 'DECLINED']]]
|
|
* @throws Horde_Icalendar_Exception
|
|
* @return array message(s) what's not equal
|
|
*/
|
|
protected function checkComponentEqual(Horde_Icalendar $_expected, Horde_Icalendar $_acctual, $_overwrites=[])
|
|
{
|
|
// only following attributes in these components are checked:
|
|
static $component_attrs2check = [
|
|
'vcalendar' => ['VERSION'],
|
|
'vTimeZone' => ['TZID'],
|
|
'vEvent' => ['UID', 'SUMMARY', 'LOCATION', 'DESCRIPTION', 'DTSTART', 'DTEND', 'ORGANIZER', 'ATTENDEE'],
|
|
];
|
|
|
|
if ($_expected->getType() !== $_acctual->getType())
|
|
{
|
|
return ["component type not equal"];
|
|
}
|
|
$msgs = [];
|
|
foreach ($component_attrs2check[$_expected->getType()] ?? [] as $attr)
|
|
{
|
|
$acctualAttrs = $_acctual->getAllAttributes($attr);
|
|
foreach($_expected->getAllAttributes($attr) as $expectedAttr)
|
|
{
|
|
$found = false;
|
|
foreach($acctualAttrs as $acctualAttr)
|
|
{
|
|
if (count($acctualAttrs) === 1 || $expectedAttr['value'] === $acctualAttr['value'])
|
|
{
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$found)
|
|
{
|
|
$msgs[] = "No $attr {$expectedAttr['value']} found";
|
|
continue;
|
|
}
|
|
// remove / ignore X-parameters, eg. X-EGROUPWARE-UID in ATTENDEE or ORGANIZER
|
|
$acctualAttr['params'] = array_filter($acctualAttr['params'], function ($key) {
|
|
return substr($key, 0, 2) !== 'X-';
|
|
}, ARRAY_FILTER_USE_KEY);
|
|
|
|
if (isset($_overwrites[$attr]) && is_scalar($_overwrites[$attr]))
|
|
{
|
|
$expectedAttr = [
|
|
'name' => $attr,
|
|
'value' => $_overwrites[$attr],
|
|
'values' => [$_overwrites[$attr]],
|
|
'params' => [],
|
|
];
|
|
}
|
|
elseif (isset($_overwrites[$attr]) && is_array($_overwrites[$attr]))
|
|
{
|
|
foreach ($_overwrites[$attr] as $value => $params)
|
|
{
|
|
if ($value === $expectedAttr['value'])
|
|
{
|
|
$expectedAttr['params'] = array_merge($expectedAttr['params'], $params);
|
|
}
|
|
}
|
|
}
|
|
if ($expectedAttr != $acctualAttr)
|
|
{
|
|
$this->assertEquals($expectedAttr, $acctualAttr, "$attr not equal");
|
|
$msgs[] = "$attr not equal";
|
|
}
|
|
}
|
|
}
|
|
// check sub-components, overrites use an index by type eg. 1. vEvent: ['vEvent'=>[[<overwrites for 1. vEvent]]]
|
|
$idx_by_type = [];
|
|
foreach($_expected->getComponents() as $idx => $component)
|
|
{
|
|
if (!isset($idx_by_type[$type = $component->getType()])) $idx_by_type[$type] = 0;
|
|
$msgs = array_merge($msgs, $this->checkComponentEqual($component, $_acctual->getComponent($idx),
|
|
$_overwrites[$type][$idx_by_type[$type]] ?? []));
|
|
$idx_by_type[$type]++;
|
|
}
|
|
return $msgs;
|
|
}
|
|
} |