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(); } }