diff --git a/docker/proxy/rootfs/etc/nginx/nginx.conf b/docker/proxy/rootfs/etc/nginx/nginx.conf
index ded6543a..0574ee59 100644
--- a/docker/proxy/rootfs/etc/nginx/nginx.conf
+++ b/docker/proxy/rootfs/etc/nginx/nginx.conf
@@ -1,256 +1,258 @@
# For more information on configuration, see:
# * Official English Documentation: http://nginx.org/en/docs/
# * Official Russian Documentation: http://nginx.org/ru/docs/
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate SSL_CERTIFICATE_CERT;
ssl_certificate_key SSL_CERTIFICATE_KEY;
server_name APP_WEBSITE_DOMAIN;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass http://webapp:8000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_no_cache 1;
proxy_cache_bypass 1;
# Mostly for files, swoole has a 10MB limit
client_max_body_size 11m;
}
location /meetmedia {
proxy_pass https://meet:12443;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
location /meetmedia/api {
proxy_pass https://meet:12443;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_no_cache 1;
proxy_cache_bypass 1;
}
location /roundcubemail {
proxy_pass http://roundcube:80;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_no_cache 1;
proxy_cache_bypass 1;
}
location /chwala {
proxy_pass http://roundcube:80;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_no_cache 1;
proxy_cache_bypass 1;
}
location /Microsoft-Server-ActiveSync {
auth_request /auth;
#auth_request_set $auth_status $upstream_status;
proxy_pass http://roundcube:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_send_timeout 910s;
proxy_read_timeout 910s;
fastcgi_send_timeout 910s;
fastcgi_read_timeout 910s;
}
location ~* ^/\\.well-known/autoconfig {
proxy_pass http://roundcube:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location ~* ^/\\autodiscover/autodiscover.xml {
proxy_pass http://roundcube:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
- rewrite ^/\\.well-known/(caldav|carddav) https://\$server_name/iRony/ redirect;
+ location ~ ^/\\.well-known/(caldav|carddav)(.*)$ {
+ return 301 /iRony/$2;
+ }
location /iRony {
auth_request /auth;
#auth_request_set $auth_status $upstream_status;
proxy_pass http://roundcube:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location = /auth {
internal;
proxy_pass http://webapp:8000/api/webhooks/nginx-httpauth;
proxy_pass_request_body off;
proxy_set_header Host services.APP_WEBSITE_DOMAIN;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
}
mail {
server_name imap.hosted.com;
auth_http webapp:8000/api/webhooks/nginx;
auth_http_header Host services.APP_WEBSITE_DOMAIN;
proxy_pass_error_message on;
server {
listen 143;
protocol imap;
proxy on;
starttls on;
ssl_certificate SSL_CERTIFICATE_CERT;
ssl_certificate_key SSL_CERTIFICATE_KEY;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
}
# Roundcube specific imap endpoint with proxy-protocol enabled
server {
listen 144 proxy_protocol;
protocol imap;
auth_http webapp:8000/api/webhooks/nginx-roundcube;
proxy on;
starttls on;
ssl_certificate SSL_CERTIFICATE_CERT;
ssl_certificate_key SSL_CERTIFICATE_KEY;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
}
server {
listen 465 ssl;
protocol smtp;
proxy on;
ssl_certificate SSL_CERTIFICATE_CERT;
ssl_certificate_key SSL_CERTIFICATE_KEY;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
}
server {
listen 587;
protocol smtp;
proxy on;
starttls on;
ssl_certificate SSL_CERTIFICATE_CERT;
ssl_certificate_key SSL_CERTIFICATE_KEY;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
}
server {
listen 993 ssl;
protocol imap;
proxy on;
ssl_certificate SSL_CERTIFICATE_CERT;
ssl_certificate_key SSL_CERTIFICATE_KEY;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
}
}
diff --git a/src/app/Backends/DAV.php b/src/app/Backends/DAV.php
index 6bc36c98..992f2572 100644
--- a/src/app/Backends/DAV.php
+++ b/src/app/Backends/DAV.php
@@ -1,508 +1,508 @@
'urn:ietf:params:xml:ns:caldav',
self::TYPE_VTODO => 'urn:ietf:params:xml:ns:caldav',
self::TYPE_VCARD => 'urn:ietf:params:xml:ns:carddav',
];
protected $url;
protected $user;
protected $password;
protected $responseHeaders = [];
/**
* Object constructor
*/
public function __construct($user, $password)
{
- $this->url = \config('dav.uri');
+ $this->url = \config('services.dav.uri');
$this->user = $user;
$this->password = $password;
}
/**
* Discover DAV home (root) collection of a specified type.
*
* @param string $component Component to filter by (VEVENT, VTODO, VCARD)
*
* @return string|false Home collection location or False on error
*/
public function discover(string $component = self::TYPE_VEVENT)
{
$roots = [
self::TYPE_VEVENT => 'calendars',
self::TYPE_VTODO => 'calendars',
self::TYPE_VCARD => 'addressbooks',
];
$homes = [
self::TYPE_VEVENT => 'calendar-home-set',
self::TYPE_VTODO => 'calendar-home-set',
self::TYPE_VCARD => 'addressbook-home-set',
];
$path = parse_url($this->url, PHP_URL_PATH);
$body = ''
. ''
. ''
. ''
. ''
. '';
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
$headers = ['Depth' => 1, 'Prefer' => 'return-minimal'];
$response = $this->request('/' . $roots[$component], 'PROPFIND', $body, $headers);
if (empty($response)) {
\Log::error("Failed to get current-user-principal for {$component} from the DAV server.");
return false;
}
$elements = $response->getElementsByTagName('response');
foreach ($elements as $element) {
foreach ($element->getElementsByTagName('prop') as $prop) {
$principal_href = $prop->nodeValue;
break;
}
}
if (empty($principal_href)) {
\Log::error("No principal on the DAV server.");
return false;
}
if ($path && strpos($principal_href, $path) === 0) {
$principal_href = substr($principal_href, strlen($path));
}
$body = ''
. ''
. ''
. ''
. ''
. '';
$response = $this->request($principal_href, 'PROPFIND', $body);
if (empty($response)) {
\Log::error("Failed to get homes for {$component} from the DAV server.");
return false;
}
$root_href = false;
$elements = $response->getElementsByTagName('response');
foreach ($elements as $element) {
foreach ($element->getElementsByTagName('prop') as $prop) {
$root_href = $prop->nodeValue;
break;
}
}
if (!empty($root_href)) {
if ($path && strpos($root_href, $path) === 0) {
$root_href = substr($root_href, strlen($path));
}
}
return $root_href;
}
/**
* Check if we can connect to the DAV server
*
* @return bool True on success, False otherwise
*/
public static function healthcheck(): bool
{
// TODO
return true;
}
/**
* Get list of folders of specified type.
*
* @param string $component Component to filter by (VEVENT, VTODO, VCARD)
*
* @return false|array List of folders' metadata or False on error
*/
public function listFolders(string $component)
{
$root_href = $this->discover($component);
if ($root_href === false) {
return false;
}
$ns = 'xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"';
$props = '';
if ($component != self::TYPE_VCARD) {
$ns .= ' xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/" xmlns:k="Kolab:"';
$props = ''
. ''
. '';
}
$body = ''
. ''
. ''
. ''
. ''
. ''
. $props
. ''
. '';
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
$headers = ['Depth' => 1, 'Prefer' => 'return-minimal'];
$response = $this->request($root_href, 'PROPFIND', $body, $headers);
if (empty($response)) {
\Log::error("Failed to get folders list from the DAV server.");
return false;
}
$folders = [];
foreach ($response->getElementsByTagName('response') as $element) {
$folder = DAV\Folder::fromDomElement($element);
// Note: Addressbooks don't have 'type' specified
if (($component == self::TYPE_VCARD && in_array('addressbook', $folder->types))
|| in_array($component, $folder->components)
) {
$folders[] = $folder;
}
}
return $folders;
}
/**
* Create a DAV object in a folder
*
* @param DAV\CommonObject $object Object
*
* @return false|DAV\CommonObject Object on success, False on error
*/
public function create(DAV\CommonObject $object)
{
$headers = ['Content-Type' => $object->contentType];
$response = $this->request($object->href, 'PUT', $object, $headers);
if ($response !== false) {
if ($etag = $this->responseHeaders['etag']) {
if (preg_match('|^".*"$|', $etag)) {
$etag = substr($etag, 1, -1);
}
$object->etag = $etag;
}
return $object;
}
return false;
}
/**
* Update a DAV object in a folder
*
* @param DAV\CommonObject $object Object
*
* @return false|DAV\CommonObject Object on success, False on error
*/
public function update(DAV\CommonObject $object)
{
return $this->create($object);
}
/**
* Delete a DAV object from a folder
*
* @param string $location Object location
*
* @return bool True on success, False on error
*/
public function delete(string $location)
{
$response = $this->request($location, 'DELETE', '', ['Depth' => 1, 'Prefer' => 'return-minimal']);
return $response !== false;
}
/**
* Get all properties of a folder.
*
* @param string $location Object location
*
* @return false|DAV\Folder Folder metadata or False on error
*/
public function folderInfo(string $location)
{
$body = ''
. ''
. ''
. '';
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
$headers = ['Depth' => 1, 'Prefer' => 'return-minimal'];
$response = $this->request($location, 'PROPFIND', $body, $headers);
if (!empty($response) && ($element = $response->getElementsByTagName('response')->item(0))) {
return DAV\Folder::fromDomElement($element);
}
return false;
}
/**
* Search DAV objects in a folder.
*
* @param string $location Folder location
* @param string $component Object type (VEVENT, VTODO, VCARD)
*
* @return false|array Objects metadata on success, False on error
*/
public function search(string $location, string $component)
{
$queries = [
self::TYPE_VEVENT => 'calendar-query',
self::TYPE_VTODO => 'calendar-query',
self::TYPE_VCARD => 'addressbook-query',
];
$filter = '';
if ($component != self::TYPE_VCARD) {
$filter = ''
. ''
. '';
}
// TODO: Make filter an argument of this function to build all kind of queries.
// It probably should be a separate object e.g. DAV\Filter.
// TODO: List of object props to return should also be an argument, so we not only
// could fetch "an index" but also any of object's data.
$body = ''
.' '
. ''
. ''
. ''
. ($filter ? "$filter" : '')
. '';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
\Log::error("Failed to get objects from the DAV server.");
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->objectFromElement($element, $component);
}
return $objects;
}
/**
* Fetch DAV objects data from a folder
*
* @param string $location Folder location
* @param string $component Object type (VEVENT, VTODO, VCARD)
* @param array $hrefs List of objects' locations to fetch (empty for all objects)
*
* @return false|array Objects metadata on success, False on error
*/
public function getObjects(string $location, string $component, array $hrefs = [])
{
if (empty($hrefs)) {
return [];
}
$body = '';
foreach ($hrefs as $href) {
$body .= '' . $href . '';
}
$queries = [
self::TYPE_VEVENT => 'calendar-multiget',
self::TYPE_VTODO => 'calendar-multiget',
self::TYPE_VCARD => 'addressbook-multiget',
];
$types = [
self::TYPE_VEVENT => 'calendar-data',
self::TYPE_VTODO => 'calendar-data',
self::TYPE_VCARD => 'address-data',
];
$body = ''
.' '
. ''
. ''
. ''
. ''
. $body
. '';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
\Log::error("Failed to get objects from the DAV server.");
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->objectFromElement($element, $component);
}
return $objects;
}
/**
* Parse XML content
*/
protected function parseXML($xml)
{
$doc = new \DOMDocument('1.0', 'UTF-8');
if (stripos($xml, 'loadXML($xml)) {
throw new \Exception("Failed to parse XML");
}
$doc->formatOutput = true;
}
return $doc;
}
/**
* Parse request/response body for debug purposes
*/
protected function debugBody($body, $headers)
{
$head = '';
foreach ($headers as $header_name => $header_value) {
if (is_array($header_value)) {
$header_value = implode("\n\t", $header_value);
}
$head .= "{$header_name}: {$header_value}\n";
}
if (stripos($body, 'formatOutput = true;
$doc->preserveWhiteSpace = false;
if (!$doc->loadXML($body)) {
throw new \Exception("Failed to parse XML");
}
$body = $doc->saveXML();
}
return $head . "\n" . rtrim($body);
}
/**
* Create DAV\CommonObject from a DOMElement
*/
protected function objectFromElement($element, $component)
{
switch ($component) {
case self::TYPE_VEVENT:
$object = DAV\Vevent::fromDomElement($element);
break;
case self::TYPE_VTODO:
$object = DAV\Vtodo::fromDomElement($element);
break;
case self::TYPE_VCARD:
$object = DAV\Vcard::fromDomElement($element);
break;
default:
throw new \Exception("Unknown component: {$component}");
}
return $object;
}
/**
* Execute HTTP request to a DAV server
*/
protected function request($path, $method, $body = '', $headers = [])
{
$debug = \config('app.debug');
$url = $this->url;
$this->responseHeaders = [];
if ($path && ($rootPath = parse_url($url, PHP_URL_PATH)) && strpos($path, $rootPath) === 0) {
$path = substr($path, strlen($rootPath));
}
$url .= $path;
$client = Http::withBasicAuth($this->user, $this->password);
// $client = Http::withToken($token); // Bearer token
if ($body) {
if (!isset($headers['Content-Type'])) {
$headers['Content-Type'] = 'application/xml; charset=utf-8';
}
$client->withBody($body, $headers['Content-Type']);
}
if (!empty($headers)) {
$client->withHeaders($headers);
}
if ($debug) {
\Log::debug("C: {$method}: {$url}\n" . $this->debugBody($body, $headers));
}
$response = $client->send($method, $url);
$body = $response->body();
$code = $response->status();
if ($debug) {
\Log::debug("S: [{$code}]\n" . $this->debugBody($body, $response->headers()));
}
// Throw an exception if a client or server error occurred...
$response->throw();
$this->responseHeaders = $response->headers();
return $this->parseXML($body);
}
}
diff --git a/src/config/dav.php b/src/config/dav.php
deleted file mode 100644
index 23792ee0..00000000
--- a/src/config/dav.php
+++ /dev/null
@@ -1,5 +0,0 @@
- env('DAV_URI', 'http://kolab/dav'),
-];
diff --git a/src/config/services.php b/src/config/services.php
index 231466df..a531591d 100644
--- a/src/config/services.php
+++ b/src/config/services.php
@@ -1,59 +1,74 @@
[
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
],
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'sparkpost' => [
'secret' => env('SPARKPOST_SECRET'),
],
'payment_provider' => env('PAYMENT_PROVIDER', 'mollie'),
'mollie' => [
'key' => env('MOLLIE_KEY'),
],
'stripe' => [
'key' => env('STRIPE_KEY'),
'public_key' => env('STRIPE_PUBLIC_KEY'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
'coinbase' => [
'key' => env('COINBASE_KEY'),
'webhook_secret' => env('COINBASE_WEBHOOK_SECRET'),
'api_verify_tls' => env('COINBASE_VERIFY_TLS', true),
],
'openexchangerates' => [
'api_key' => env('OPENEXCHANGERATES_API_KEY', null),
- ]
+ ],
+ 'dav' => [
+ 'uri' => env('DAV_URI', 'https://proxy/'),
+ ],
+
+ 'activesync' => [
+ 'uri' => env('ACTIVESYNC_URI', 'https://proxy/Microsoft-Server-ActiveSync'),
+ ],
+
+ 'wopi' => [
+ 'uri' => env('WOPI_URI', 'http://roundcube/chwala/'),
+ ],
+
+ 'webmail' => [
+ 'uri' => env('WEBMAIL_URI', 'http://roundcube/roundcubemail/'),
+ ]
];
diff --git a/src/tests/Infrastructure/ActivesyncTest.php b/src/tests/Infrastructure/ActivesyncTest.php
new file mode 100644
index 00000000..6eb67b73
--- /dev/null
+++ b/src/tests/Infrastructure/ActivesyncTest.php
@@ -0,0 +1,184 @@
+loadXML($xml);
+ $encoder->encode($dom);
+ rewind($outputStream);
+ return stream_get_contents($outputStream);
+ }
+
+ private static function fromWbxml($binary)
+ {
+ $stream = fopen('php://memory', 'r+');
+ fwrite($stream, $binary);
+ rewind($stream);
+ $decoder = new \Syncroton_Wbxml_Decoder($stream);
+ return $decoder->decode();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ if (!self::$user) {
+ self::$user = $this->getTestUser('activesynctest@kolab.org', ['password' => 'simple123'], true);
+ }
+ if (!self::$deviceId) {
+ // By always creating a new device we force syncroton to initialize.
+ // Otherwise we work against uninitialized metadata (subscription states),
+ // because the account has been removed, but syncroton doesn't reinitalize the metadata for known devices.
+ self::$deviceId = (string) Str::uuid();
+ }
+ if (!self::$client) {
+ self::$client = new \GuzzleHttp\Client([
+ 'http_errors' => false, // No exceptions
+ 'base_uri' => \config("services.activesync.uri"),
+ 'verify' => false,
+ 'auth' => [self::$user->email, 'simple123'],
+ 'connect_timeout' => 10,
+ 'timeout' => 10,
+ 'headers' => [
+ "Content-Type" => "application/xml; charset=utf-8",
+ "Depth" => "1",
+ ]
+ ]);
+ }
+ }
+
+ public function testOptions()
+ {
+ $response = self::$client->request('OPTIONS', '');
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertStringContainsString('14', $response->getHeader('MS-Server-ActiveSync')[0]);
+ $this->assertStringContainsString('14.1', $response->getHeader('MS-ASProtocolVersions')[0]);
+ $this->assertStringContainsString('FolderSync', $response->getHeader('MS-ASProtocolCommands')[0]);
+ }
+
+ public function testList()
+ {
+ $user = self::$user;
+ $deviceId = self::$deviceId;
+ $request = <<
+
+
+ 0
+
+ EOF;
+ $body = self::toWbxml($request);
+ $response = self::$client->request(
+ 'POST',
+ "?Cmd=FolderSync&User={$user->email}&DeviceId={$deviceId}&DeviceType=iphone",
+ [
+ 'headers' => [
+ "Content-Type" => "application/vnd.ms-sync.wbxml",
+ 'MS-ASProtocolVersion' => "14.0"
+ ],
+ 'body' => $body
+ ]
+ );
+ $this->assertEquals(200, $response->getStatusCode());
+ $dom = self::fromWbxml($response->getBody());
+ $xml = $dom->saveXML();
+
+ $this->assertStringContainsString('INBOX', $xml);
+ // The hash is based on the name, so it's always the same
+ $inboxId = '38b950ebd62cd9a66929c89615d0fc04';
+ $this->assertStringContainsString($inboxId, $xml);
+ //TODO for this to work we need to create the default folders in IMAP::createUser
+ // $this->assertStringContainsString('Drafts', $result);
+ // $this->assertStringContainsString('Sent', $result);
+ // $this->assertStringContainsString('Trash', $result);
+ // $this->assertStringContainsString('Calendar', $result);
+ // $this->assertStringContainsString('Contacts', $result);
+
+ // Find the inbox for the next step
+ // $collectionIds = $dom->getElementsByTagName('ServerId');
+ // $inboxId = $collectionIds[0]->nodeValue;
+
+ return $inboxId;
+ }
+
+ /**
+ * @depends testList
+ */
+ public function testInitialSync($inboxId)
+ {
+ $user = self::$user;
+ $deviceId = self::$deviceId;
+ $request = <<
+
+
+
+
+ 0
+ {$inboxId}
+ 0
+ 0
+ 512
+
+ 0
+
+ 1
+ 1
+
+
+
+
+ 16
+
+ EOF;
+ $body = self::toWbxml($request);
+ $response = self::$client->request(
+ 'POST',
+ "?Cmd=Sync&User={$user->email}&DeviceId={$deviceId}&DeviceType=iphone",
+ [
+ 'headers' => [
+ "Content-Type" => "application/vnd.ms-sync.wbxml",
+ 'MS-ASProtocolVersion' => "14.0"
+ ],
+ 'body' => $body
+ ]
+ );
+ $this->assertEquals(200, $response->getStatusCode());
+ $dom = self::fromWbxml($response->getBody());
+ $status = $dom->getElementsByTagName('Status');
+ $this->assertEquals("1", $status[0]->nodeValue);
+ $collections = $dom->getElementsByTagName('Collection');
+ $this->assertEquals(1, $collections->length);
+ $collection = $collections->item(0);
+ $this->assertEquals("Class", $collection->childNodes->item(0)->nodeName);
+ $this->assertEquals("Email", $collection->childNodes->item(0)->nodeValue);
+ $this->assertEquals("SyncKey", $collection->childNodes->item(1)->nodeName);
+ $this->assertEquals("1", $collection->childNodes->item(1)->nodeValue);
+ $this->assertEquals("Status", $collection->childNodes->item(3)->nodeName);
+ $this->assertEquals("1", $collection->childNodes->item(3)->nodeValue);
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function testCleanup(): void
+ {
+ $this->deleteTestUser(self::$user->email);
+ }
+}
diff --git a/src/tests/Infrastructure/AutodiscoverTest.php b/src/tests/Infrastructure/AutodiscoverTest.php
new file mode 100644
index 00000000..f41a6ae1
--- /dev/null
+++ b/src/tests/Infrastructure/AutodiscoverTest.php
@@ -0,0 +1,92 @@
+ false, // No exceptions
+ 'base_uri' => "http://roundcube/",
+ 'verify' => false,
+ 'connect_timeout' => 10,
+ 'timeout' => 10,
+ ]);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ public function testWellKnownOutlook()
+ {
+ $body = <<
+
+ admin@example.local
+
+ http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
+
+
+
+ EOF;
+ $response = self::$client->request('POST', 'autodiscover/autodiscover.xml', [
+ 'headers' => [
+ "Content-Type" => "text/xml; charset=utf-8"
+ ],
+ 'body' => $body
+ ]);
+ $this->assertEquals($response->getStatusCode(), 200);
+ $data = $response->getBody();
+ $this->assertTrue(str_contains($data, 'example.local'));
+ $this->assertTrue(str_contains($data, 'admin@example.local'));
+ }
+
+ public function testWellKnownActivesync()
+ {
+ $body = <<
+
+ admin@example.local
+
+ http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006
+
+
+
+ EOF;
+ $response = self::$client->request('POST', 'autodiscover/autodiscover.xml', [
+ 'headers' => [
+ "Content-Type" => "text/xml; charset=utf-8"
+ ],
+ 'body' => $body
+ ]);
+ $this->assertEquals($response->getStatusCode(), 200);
+ $data = $response->getBody();
+ $this->assertTrue(str_contains($data, 'https://example.local/Microsoft-Server-ActiveSync'));
+ $this->assertTrue(str_contains($data, 'admin@example.local'));
+ }
+
+ public function testWellKnownMail()
+ {
+ $response = self::$client->request(
+ 'GET',
+ '.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=fred@example.com'
+ );
+ $this->assertEquals($response->getStatusCode(), 200);
+ }
+}
diff --git a/src/tests/Infrastructure/DavTest.php b/src/tests/Infrastructure/DavTest.php
new file mode 100644
index 00000000..72c92f64
--- /dev/null
+++ b/src/tests/Infrastructure/DavTest.php
@@ -0,0 +1,293 @@
+getTestUser('davtest@kolab.org', ['password' => 'simple123'], true);
+ }
+
+ if (!self::$client) {
+ self::$client = new \GuzzleHttp\Client([
+ 'http_errors' => false, // No exceptions
+ 'base_uri' => \config("services.dav.uri"),
+ 'verify' => false,
+ 'auth' => [self::$user->email, 'simple123'],
+ 'connect_timeout' => 10,
+ 'timeout' => 10,
+ 'headers' => [
+ "Content-Type" => "application/xml; charset=utf-8",
+ "Depth" => "1",
+ ]
+ ]);
+ }
+ }
+
+ public function testDiscoverPrincipal()
+ {
+ $user = self::$user;
+ $body = "";
+ $response = self::$client->request('PROPFIND', '/iRony/', ['body' => $body]);
+ $this->assertEquals(207, $response->getStatusCode());
+ $data = $response->getBody();
+ $this->assertStringContainsString("/iRony/principals/{$user->email}/", $data);
+ $this->assertStringContainsString('/iRony/calendars/', $data);
+ $this->assertStringContainsString('/iRony/addressbooks/', $data);
+ }
+
+ /**
+ * This codepath is triggerd by MacOS CalDAV when it tries to login.
+ * Verify we don't crash and end up with a 500 status code.
+ */
+ public function testFailingLogin()
+ {
+ $body = "";
+ $headers = [
+ "Content-Type" => "application/xml; charset=utf-8",
+ "Depth" => "1",
+ 'body' => $body,
+ 'auth' => ['invaliduser@kolab.org', 'invalid']
+ ];
+
+ $response = self::$client->request('PROPFIND', '/iRony/', $headers);
+ $this->assertEquals(403, $response->getStatusCode());
+ }
+
+ /**
+ * This codepath is triggerd by MacOS CardDAV when it tries to login.
+ * NOTE: This depends on the username_domain roundcube config option.
+ */
+ public function testShortlogin()
+ {
+ $this->markTestSkipped(
+ 'Shortlogins dont work with the nginx proxy.'
+ );
+ $body = "";
+ $response = self::$client->request('PROPFIND', '/iRony/', [
+ 'body' => $body,
+ 'auth' => ['davtest', 'simple123']
+ ]);
+ $this->assertEquals(207, $response->getStatusCode());
+ }
+
+ public function testDiscoverCalendarHomeset()
+ {
+ $user = self::$user;
+ $body = <<
+
+
+
+
+ EOF;
+
+ $response = self::$client->request('PROPFIND', '/iRony/', ['body' => $body]);
+ $this->assertEquals(207, $response->getStatusCode());
+ $data = $response->getBody();
+ $this->assertStringContainsString("/iRony/calendars/{$user->email}/", $data);
+ }
+
+ public function testDiscoverCalendars()
+ {
+ $user = self::$user;
+ $body = <<
+
+
+
+
+
+
+
+ EOF;
+
+ $response = self::$client->request('PROPFIND', "/iRony/calendars/{$user->email}", [
+ 'headers' => [
+ "Depth" => "infinity",
+ ],
+ 'body' => $body
+ ]);
+ $this->assertEquals(207, $response->getStatusCode());
+ $data = $response->getBody();
+ $this->assertStringContainsString("/iRony/calendars/{$user->email}/", $data);
+
+ $doc = new \DOMDocument('1.0', 'UTF-8');
+ $doc->loadXML($data);
+ $response = $doc->getElementsByTagName('response')->item(1);
+ $doc->getElementsByTagName('href')->item(0);
+
+ $this->assertEquals("d:href", $response->childNodes->item(0)->nodeName);
+ $href = $response->childNodes->item(0)->nodeValue;
+ return $href;
+ }
+
+ /**
+ * @depends testDiscoverCalendars
+ */
+ public function testPropfindCalendar($href)
+ {
+ $body = <<
+
+
+
+
+
+
+
+
+
+
+ EOF;
+
+ $response = self::$client->request('PROPFIND', $href, [
+ 'headers' => [
+ "Depth" => "0",
+ ],
+ 'body' => $body,
+ ]);
+ $this->assertEquals(207, $response->getStatusCode());
+ $data = $response->getBody();
+ $this->assertStringContainsString("$href", $data);
+ }
+
+ /**
+ * Thunderbird does this and relies on the WWW-Authenticate header response to
+ * start sending authenticated requests.
+ *
+ * @depends testDiscoverCalendars
+ */
+ public function testPropfindCalendarWithoutAuth($href)
+ {
+ $body = <<
+
+
+
+
+
+
+
+
+
+
+ EOF;
+
+ $response = self::$client->request('PROPFIND', $href, [
+ 'headers' => [
+ "Depth" => "0",
+ ],
+ 'body' => $body,
+ 'auth' => []
+ ]);
+ $this->assertEquals(401, $response->getStatusCode());
+ $this->assertStringContainsString('Basic realm=', $response->getHeader('WWW-Authenticate')[0]);
+ $data = $response->getBody();
+ $this->assertStringContainsString("Sabre\DAV\Exception\NotAuthenticated", $data);
+ }
+
+ /**
+ * Required for MacOS autoconfig
+ */
+ public function testOptions()
+ {
+ $user = self::$user;
+ $body = <<
+
+
+
+
+
+
+
+ EOF;
+
+ $response = self::$client->request('OPTIONS', "/iRony/principals/{$user->email}/", ['body' => $body]);
+ $this->assertEquals(200, $response->getStatusCode());
+ $this->assertStringContainsString('PROPFIND', $response->getHeader('Allow')[0]);
+ }
+
+ public function testWellKnown()
+ {
+ $user = self::$user;
+ $body = <<
+
+
+
+
+
+
+
+ EOF;
+
+ // The base URL needs to work as a redirect
+ $response = self::$client->request('PROPFIND', '/.well-known/caldav', [
+ 'headers' => [
+ "Depth" => "infinity",
+ ],
+ 'body' => $body,
+ 'allow_redirects' => false
+ ]);
+ $this->assertEquals(301, $response->getStatusCode());
+ $redirectTarget = $response->getHeader('location')[0];
+ $this->assertEquals(\config('services.dav.uri') . "iRony/", $redirectTarget);
+
+ // Follow the redirect
+ $response = self::$client->request('PROPFIND', $redirectTarget, [
+ 'headers' => [
+ "Depth" => "infinity",
+ ],
+ 'body' => $body,
+ 'allow_redirects' => false
+ ]);
+ $this->assertEquals(207, $response->getStatusCode());
+
+ // Any URL should result in a redirect to the same path
+ $response = self::$client->request('PROPFIND', "/.well-known/caldav/calendars/{$user->email}", [
+ 'headers' => [
+ "Depth" => "infinity",
+ ],
+ 'body' => $body,
+ 'allow_redirects' => false
+ ]);
+ $this->assertEquals(301, $response->getStatusCode());
+ $redirectTarget = $response->getHeader('location')[0];
+ //FIXME we have an extra slash that we don't technically want here
+ $this->assertEquals(\config('services.dav.uri') . "iRony//calendars/{$user->email}", $redirectTarget);
+
+ // Follow the redirect
+ $response = self::$client->request('PROPFIND', $redirectTarget, [
+ 'headers' => [
+ "Depth" => "infinity",
+ ],
+ 'body' => $body,
+ 'allow_redirects' => false
+ ]);
+ $this->assertEquals(207, $response->getStatusCode());
+ $data = $response->getBody();
+ $this->assertStringContainsString("/iRony/calendars/{$user->email}/", $data);
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function testCleanup(): void
+ {
+ $this->deleteTestUser(self::$user->email);
+ }
+}
diff --git a/src/tests/Infrastructure/RoundcubeTest.php b/src/tests/Infrastructure/RoundcubeTest.php
new file mode 100644
index 00000000..8c2133f0
--- /dev/null
+++ b/src/tests/Infrastructure/RoundcubeTest.php
@@ -0,0 +1,81 @@
+getTestUser('roundcubetesttest@kolab.org', ['password' => 'simple123'], true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ public function testLogin()
+ {
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/')
+ ->type('#rcmloginuser', self::$user->email)
+ ->type('#rcmloginpwd', "simple123")
+ ->press('#rcmloginsubmit')
+ ->waitFor('#logo')
+ ->waitUntil('!rcmail.busy')
+ ->assertSee('Inbox');
+
+ $browser->press('.contacts')
+ ->waitUntil('!rcmail.busy')
+ ->assertVisible('#directorylist')
+ ->assertVisible('.addressbook.personal')
+ ->assertSee('Contacts');
+
+ $browser->press('.button-calendar')
+ ->waitUntil('!rcmail.busy')
+ ->assertSee('Calendar');
+
+ //TODO requires the default folders to be created
+ // $browser->press('.button-files')
+ // ->waitUntil('!rcmail.busy')
+ // ->assertSeeIn('#files-folder-list', 'Files');
+
+ $browser->press('.button-notes')
+ ->waitUntil('!rcmail.busy')
+ ->assertSeeIn('#notebooks-content', 'Notes');
+
+ $browser->press('.button-tasklist')
+ ->waitUntil('!rcmail.busy')
+ ->assertSee('Tasks');
+
+ $browser->press('.settings')
+ ->waitUntil('!rcmail.busy')
+ ->assertSee('Activesync');
+ // print($browser->dump());
+ });
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function testCleanup(): void
+ {
+ $this->deleteTestUser(self::$user->email);
+ }
+}
diff --git a/src/tests/Infrastructure/WOPITest.php b/src/tests/Infrastructure/WOPITest.php
new file mode 100644
index 00000000..aab99cb5
--- /dev/null
+++ b/src/tests/Infrastructure/WOPITest.php
@@ -0,0 +1,65 @@
+getTestUser('wopitest@kolab.org', ['password' => 'simple123'], true);
+ }
+
+ if (!self::$client) {
+ self::$client = new \GuzzleHttp\Client([
+ 'base_uri' => \config('services.wopi.uri'),
+ 'verify' => false,
+ 'auth' => [self::$user->email, 'simple123'],
+ 'connect_timeout' => 10,
+ 'timeout' => 10
+ ]);
+ }
+ }
+
+ public function testAccess()
+ {
+ $response = self::$client->request('GET', 'api/?method=authenticate&version=4');
+ $this->assertEquals($response->getStatusCode(), 200);
+ $json = json_decode($response->getBody(), true);
+
+ $this->assertEquals('OK', $json['status']);
+ $token = $json['result']['token'];
+ $this->assertTrue(!empty($token));
+
+ //FIXME the session token doesn't seem to be required here?
+ $response = self::$client->request('GET', 'api/?method=mimetypes', [
+ 'headers' => [
+ 'X-Session_token' => $token
+ ]
+ ]);
+ $this->assertEquals($response->getStatusCode(), 200);
+ $json = json_decode($response->getBody(), true);
+ $this->assertEquals('OK', $json['status']);
+ $this->assertEquals('OK', $json['status']);
+ $this->assertContains('image/png', $json['result']['view']);
+ $this->assertArrayHasKey('text/plain', $json['result']['edit']);
+ }
+
+ /**
+ * @doesNotPerformAssertions
+ */
+ public function testCleanup(): void
+ {
+ $this->deleteTestUser(self::$user->email);
+ }
+}
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
index c0ae4778..fc623ca1 100644
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -1,753 +1,761 @@
'John',
'last_name' => 'Doe',
'organization' => 'Test Domain Owner',
];
/**
* Some users for the hosted domain, ultimately including the owner.
*
* @var \App\User[]
*/
protected $domainUsers = [];
/**
* A specific user that is a regular user in the hosted domain.
*
* @var ?\App\User
*/
protected $jack;
/**
* A specific user that is a controller on the wallet to which the hosted domain is charged.
*
* @var ?\App\User
*/
protected $jane;
/**
* A specific user that has a second factor configured.
*
* @var ?\App\User
*/
protected $joe;
/**
* One of the domains that is available for public registration.
*
* @var ?\App\Domain
*/
protected $publicDomain;
/**
* A newly generated user in a public domain.
*
* @var ?\App\User
*/
protected $publicDomainUser;
/**
* A placeholder for a password that can be generated.
*
* Should be generated with `\App\Utils::generatePassphrase()`.
*
* @var ?string
*/
protected $userPassword;
/**
* Register the beta entitlement for a user
*/
protected function addBetaEntitlement($user, $titles = []): void
{
// Add beta + $title entitlements
$beta_sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$user->assignSku($beta_sku);
if (!empty($titles)) {
Sku::withEnvTenantContext()->whereIn('title', (array) $titles)->get()
->each(function ($sku) use ($user) {
$user->assignSku($sku);
});
}
}
/**
* Assert that the entitlements for the user match the expected list of entitlements.
*
* @param \App\User|\App\Domain $object The object for which the entitlements need to be pulled.
* @param array $expected An array of expected \App\Sku titles.
*/
protected function assertEntitlements($object, $expected)
{
// Assert the user entitlements
$skus = $object->entitlements()->get()
->map(function ($ent) {
return $ent->sku->title;
})
->toArray();
sort($skus);
Assert::assertSame($expected, $skus);
}
/**
* Assert content of the SKU element in an API response
*
* @param string $sku_title The SKU title
* @param array $result The result to assert
* @param array $other Other items the SKU itself does not include
*/
protected function assertSkuElement($sku_title, $result, $other = []): void
{
$sku = Sku::withEnvTenantContext()->where('title', $sku_title)->first();
$this->assertSame($sku->id, $result['id']);
$this->assertSame($sku->title, $result['title']);
$this->assertSame($sku->name, $result['name']);
$this->assertSame($sku->description, $result['description']);
$this->assertSame($sku->cost, $result['cost']);
$this->assertSame($sku->units_free, $result['units_free']);
$this->assertSame($sku->period, $result['period']);
$this->assertSame($sku->active, $result['active']);
foreach ($other as $key => $value) {
$this->assertSame($value, $result[$key]);
}
$this->assertCount(8 + count($other), $result);
}
protected function backdateEntitlements($entitlements, $targetDate, $targetCreatedDate = null)
{
$wallets = [];
$ids = [];
foreach ($entitlements as $entitlement) {
$ids[] = $entitlement->id;
$wallets[] = $entitlement->wallet_id;
}
\App\Entitlement::whereIn('id', $ids)->update([
'created_at' => $targetCreatedDate ?: $targetDate,
'updated_at' => $targetDate,
]);
if (!empty($wallets)) {
$wallets = array_unique($wallets);
$owners = \App\Wallet::whereIn('id', $wallets)->pluck('user_id')->all();
\App\User::whereIn('id', $owners)->update([
'created_at' => $targetCreatedDate ?: $targetDate
]);
}
}
/**
* Removes all beta entitlements from the database
*/
protected function clearBetaEntitlements(): void
{
$beta_handlers = [
'App\Handlers\Beta',
];
$betas = Sku::whereIn('handler_class', $beta_handlers)->pluck('id')->all();
\App\Entitlement::whereIn('sku_id', $betas)->delete();
}
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
/**
* Create a set of transaction log entries for a wallet
*/
protected function createTestTransactions($wallet)
{
$result = [];
$date = Carbon::now();
$debit = 0;
$entitlementTransactions = [];
foreach ($wallet->entitlements as $entitlement) {
if ($entitlement->cost) {
$debit += $entitlement->cost;
$entitlementTransactions[] = $entitlement->createTransaction(
Transaction::ENTITLEMENT_BILLED,
$entitlement->cost
);
}
}
$transaction = Transaction::create(
[
'user_email' => 'jeroen@jeroen.jeroen',
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => Transaction::WALLET_DEBIT,
'amount' => $debit * -1,
'description' => 'Payment',
]
);
$result[] = $transaction;
Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]);
$transaction = Transaction::create(
[
'user_email' => null,
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => Transaction::WALLET_CREDIT,
'amount' => 2000,
'description' => 'Payment',
]
);
$transaction->created_at = $date->next(Carbon::MONDAY);
$transaction->save();
$result[] = $transaction;
$types = [
Transaction::WALLET_AWARD,
Transaction::WALLET_PENALTY,
];
// The page size is 10, so we generate so many to have at least two pages
$loops = 10;
while ($loops-- > 0) {
$type = $types[count($result) % count($types)];
$transaction = Transaction::create([
'user_email' => 'jeroen.@jeroen.jeroen',
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => $type,
'amount' => 11 * (count($result) + 1) * ($type == Transaction::WALLET_PENALTY ? -1 : 1),
'description' => 'TRANS' . $loops,
]);
$transaction->created_at = $date->next(Carbon::MONDAY);
$transaction->save();
$result[] = $transaction;
}
return $result;
}
/**
* Delete a test domain whatever it takes.
*
* @coversNothing
*/
protected function deleteTestDomain($name)
{
Queue::fake();
$domain = Domain::withTrashed()->where('namespace', $name)->first();
if (!$domain) {
return;
}
$job = new \App\Jobs\Domain\DeleteJob($domain->id);
$job->handle();
$domain->forceDelete();
}
/**
* Delete a test group whatever it takes.
*
* @coversNothing
*/
protected function deleteTestGroup($email)
{
Queue::fake();
$group = Group::withTrashed()->where('email', $email)->first();
if (!$group) {
return;
}
LDAP::deleteGroup($group);
$group->forceDelete();
}
/**
* Delete a test resource whatever it takes.
*
* @coversNothing
*/
protected function deleteTestResource($email)
{
Queue::fake();
$resource = Resource::withTrashed()->where('email', $email)->first();
if (!$resource) {
return;
}
LDAP::deleteResource($resource);
$resource->forceDelete();
}
/**
* Delete a test room whatever it takes.
*
* @coversNothing
*/
protected function deleteTestRoom($name)
{
Queue::fake();
$room = \App\Meet\Room::withTrashed()->where('name', $name)->first();
if (!$room) {
return;
}
$room->forceDelete();
}
/**
* Delete a test shared folder whatever it takes.
*
* @coversNothing
*/
protected function deleteTestSharedFolder($email)
{
Queue::fake();
$folder = SharedFolder::withTrashed()->where('email', $email)->first();
if (!$folder) {
return;
}
LDAP::deleteSharedFolder($folder);
$folder->forceDelete();
}
/**
* Delete a test user whatever it takes.
*
* @coversNothing
*/
protected function deleteTestUser($email)
{
Queue::fake();
$user = User::withTrashed()->where('email', $email)->first();
if (!$user) {
return;
}
- LDAP::deleteUser($user);
+ if (\config('app.with_imap')) {
+ IMAP::deleteUser($user);
+ }
+ if (\config('app.with_ldap')) {
+ LDAP::deleteUser($user);
+ }
$user->forceDelete();
}
/**
* Delete a test companion app whatever it takes.
*
* @coversNothing
*/
protected function deleteTestCompanionApp($deviceId)
{
Queue::fake();
$companionApp = CompanionApp::where('device_id', $deviceId)->first();
if (!$companionApp) {
return;
}
$companionApp->forceDelete();
}
/**
* Helper to access protected property of an object
*/
protected static function getObjectProperty($object, $property_name)
{
$reflection = new \ReflectionClass($object);
$property = $reflection->getProperty($property_name);
$property->setAccessible(true);
return $property->getValue($object);
}
/**
* Get Domain object by namespace, create it if needed.
* Skip LDAP jobs.
*
* @coversNothing
*/
protected function getTestDomain($name, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
return Domain::firstOrCreate(['namespace' => $name], $attrib);
}
/**
* Get Group object by email, create it if needed.
* Skip LDAP jobs.
*/
protected function getTestGroup($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
return Group::firstOrCreate(['email' => $email], $attrib);
}
/**
* Get Resource object by email, create it if needed.
* Skip LDAP jobs.
*/
protected function getTestResource($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
$resource = Resource::where('email', $email)->first();
if (!$resource) {
list($local, $domain) = explode('@', $email, 2);
$resource = new Resource();
$resource->email = $email;
$resource->domainName = $domain;
if (!isset($attrib['name'])) {
$resource->name = $local;
}
}
foreach ($attrib as $key => $val) {
$resource->{$key} = $val;
}
$resource->save();
return $resource;
}
/**
* Get Room object by name, create it if needed.
*
* @coversNothing
*/
protected function getTestRoom($name, $wallet = null, $attrib = [], $config = [], $title = null)
{
$attrib['name'] = $name;
$room = \App\Meet\Room::create($attrib);
if ($wallet) {
$room->assignToWallet($wallet, $title);
}
if (!empty($config)) {
$room->setConfig($config);
}
return $room;
}
/**
* Get SharedFolder object by email, create it if needed.
* Skip LDAP jobs.
*/
protected function getTestSharedFolder($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
$folder = SharedFolder::where('email', $email)->first();
if (!$folder) {
list($local, $domain) = explode('@', $email, 2);
$folder = new SharedFolder();
$folder->email = $email;
$folder->domainName = $domain;
if (!isset($attrib['name'])) {
$folder->name = $local;
}
}
foreach ($attrib as $key => $val) {
$folder->{$key} = $val;
}
$folder->save();
return $folder;
}
/**
* Get User object by email, create it if needed.
* Skip LDAP jobs.
*
* @coversNothing
*/
- protected function getTestUser($email, $attrib = [])
+ protected function getTestUser($email, $attrib = [], $createInBackends = false)
{
// Disable jobs (i.e. skip LDAP oprations)
- Queue::fake();
+ if (!$createInBackends) {
+ Queue::fake();
+ }
$user = User::firstOrCreate(['email' => $email], $attrib);
if ($user->trashed()) {
// Note: we do not want to use user restore here
User::where('id', $user->id)->forceDelete();
$user = User::create(['email' => $email] + $attrib);
}
return $user;
}
/**
* Get CompanionApp object by deviceId, create it if needed.
* Skip LDAP jobs.
*
* @coversNothing
*/
protected function getTestCompanionApp($deviceId, $user, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
$companionApp = CompanionApp::firstOrCreate(
[
'device_id' => $deviceId,
'user_id' => $user->id,
'notification_token' => '',
'mfa_enabled' => 1
],
$attrib
);
return $companionApp;
}
/**
* Call protected/private method of a class.
*
* @param object $object Instantiated object that we will run method on.
* @param string $methodName Method name to call
* @param array $parameters Array of parameters to pass into method.
*
* @return mixed Method return.
*/
protected function invokeMethod($object, $methodName, array $parameters = [])
{
$reflection = new \ReflectionClass(get_class($object));
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
return $method->invokeArgs($object, $parameters);
}
/**
* Extract content of an email message.
*
* @param \Illuminate\Mail\Mailable $mail Mailable object
*
* @return array Parsed message data:
* - 'plain': Plain text body
* - 'html: HTML body
* - 'subject': Mail subject
*/
protected function renderMail(\Illuminate\Mail\Mailable $mail): array
{
$mail->build(); // @phpstan-ignore-line
$result = $this->invokeMethod($mail, 'renderForAssertions');
return [
'plain' => $result[1],
'html' => $result[0],
'subject' => $mail->subject,
];
}
/**
* Reset a room after tests
*/
public function resetTestRoom(string $room_name = 'john', $config = [])
{
$room = \App\Meet\Room::where('name', $room_name)->first();
$room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
if ($room->session_id) {
$room->session_id = null;
$room->save();
}
if (!empty($config)) {
$room->setConfig($config);
}
return $room;
}
protected function setUpTest()
{
$this->userPassword = \App\Utils::generatePassphrase();
$this->domainHosted = $this->getTestDomain(
'test.domain',
[
'type' => \App\Domain::TYPE_EXTERNAL,
'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED
]
);
$this->getTestDomain(
'test2.domain2',
[
'type' => \App\Domain::TYPE_EXTERNAL,
'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED
]
);
$packageKolab = \App\Package::where('title', 'kolab')->first();
$this->domainOwner = $this->getTestUser('john@test.domain', ['password' => $this->userPassword]);
$this->domainOwner->assignPackage($packageKolab);
$this->domainOwner->setSettings($this->domainOwnerSettings);
$this->domainOwner->setAliases(['alias1@test2.domain2']);
// separate for regular user
$this->jack = $this->getTestUser('jack@test.domain', ['password' => $this->userPassword]);
// separate for wallet controller
$this->jane = $this->getTestUser('jane@test.domain', ['password' => $this->userPassword]);
$this->joe = $this->getTestUser('joe@test.domain', ['password' => $this->userPassword]);
$this->domainUsers[] = $this->jack;
$this->domainUsers[] = $this->jane;
$this->domainUsers[] = $this->joe;
$this->domainUsers[] = $this->getTestUser('jill@test.domain', ['password' => $this->userPassword]);
foreach ($this->domainUsers as $user) {
$this->domainOwner->assignPackage($packageKolab, $user);
}
$this->domainUsers[] = $this->domainOwner;
// assign second factor to joe
$this->joe->assignSku(Sku::where('title', '2fa')->first());
\App\Auth\SecondFactor::seed($this->joe->email);
usort(
$this->domainUsers,
function ($a, $b) {
return $a->email > $b->email;
}
);
$this->domainHosted->assignPackage(
\App\Package::where('title', 'domain-hosting')->first(),
$this->domainOwner
);
$wallet = $this->domainOwner->wallets()->first();
$wallet->addController($this->jane);
$this->publicDomain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first();
$this->publicDomainUser = $this->getTestUser(
'john@' . $this->publicDomain->namespace,
['password' => $this->userPassword]
);
$this->publicDomainUser->assignPackage($packageKolab);
}
public function tearDown(): void
{
foreach ($this->domainUsers as $user) {
if ($user == $this->domainOwner) {
continue;
}
$this->deleteTestUser($user->email);
}
if ($this->domainOwner) {
$this->deleteTestUser($this->domainOwner->email);
}
if ($this->domainHosted) {
$this->deleteTestDomain($this->domainHosted->namespace);
}
if ($this->publicDomainUser) {
$this->deleteTestUser($this->publicDomainUser->email);
}
parent::tearDown();
}
}