diff --git a/src/app/Domain.php b/src/app/Domain.php index 7fa81a9a..23f41665 100644 --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -1,420 +1,422 @@ The attributes that should be cast */ protected $casts = [ 'created_at' => 'datetime:Y-m-d H:i:s', 'deleted_at' => 'datetime:Y-m-d H:i:s', 'updated_at' => 'datetime:Y-m-d H:i:s', + 'status' => 'integer', + 'type' => 'integer', ]; /** @var array The attributes that are mass assignable */ protected $fillable = ['namespace', 'status', 'type']; /** * Assign a package to a domain. The domain should not belong to any existing entitlements. * * @param \App\Package $package The package to assign. * @param \App\User $user The wallet owner. * * @return \App\Domain Self */ public function assignPackage($package, $user) { // If this domain is public it can not be assigned to a user. if ($this->isPublic()) { return $this; } // See if this domain is already owned by another user. $wallet = $this->wallet(); if ($wallet) { throw new \Exception("Domain {$this->namespace} is already assigned to {$wallet->owner->email}"); } return $this->assignPackageAndWallet($package, $user->wallets()->first()); } /** * Return list of public+active domain names (for current tenant) */ public static function getPublicDomains(): array { return self::withEnvTenantContext() ->where('type', '&', Domain::TYPE_PUBLIC) ->pluck('namespace')->all(); } /** * Returns whether this domain is confirmed the ownership of. * * @return bool */ public function isConfirmed(): bool { return ($this->status & self::STATUS_CONFIRMED) > 0; } /** * Returns whether this domain is registered with us. * * @return bool */ public function isExternal(): bool { return ($this->type & self::TYPE_EXTERNAL) > 0; } /** * Returns whether this domain is hosted with us. * * @return bool */ public function isHosted(): bool { return ($this->type & self::TYPE_HOSTED) > 0; } /** * Returns whether this domain is public. * * @return bool */ public function isPublic(): bool { return ($this->type & self::TYPE_PUBLIC) > 0; } /** * Returns whether this (external) domain has been verified * to exist in DNS. * * @return bool */ public function isVerified(): bool { return ($this->status & self::STATUS_VERIFIED) > 0; } /** * Ensure the namespace is appropriately cased. */ public function setNamespaceAttribute($namespace) { $this->attributes['namespace'] = strtolower($namespace); } /** * Domain status mutator * * @throws \Exception */ public function setStatusAttribute($status) { // Detect invalid flags if ($status & ~$this->allowed_states) { throw new \Exception("Invalid domain status: {$status}"); } $new_status = $status; if ($this->isPublic()) { $this->attributes['status'] = $new_status; return; } // if we have confirmed ownership of or management access to the domain, then we have // also confirmed the domain exists in DNS. if ($new_status & self::STATUS_CONFIRMED) { $new_status |= self::STATUS_VERIFIED | self::STATUS_ACTIVE; } // it can't be deleted-or-suspended and active if ($new_status & self::STATUS_DELETED || $new_status & self::STATUS_SUSPENDED) { $new_status &= ~self::STATUS_ACTIVE; } // if the domain is now active, it is not new anymore. if ($new_status & self::STATUS_ACTIVE) { $new_status &= ~self::STATUS_NEW; } $this->attributes['status'] = $new_status; } /** * Ownership verification by checking for a TXT (or CNAME) record * in the domain's DNS (that matches the verification hash). * * @return bool True if verification was successful, false otherwise * @throws \Exception Throws exception on DNS or DB errors */ public function confirm(): bool { if ($this->isConfirmed()) { return true; } $hash = $this->hash(self::HASH_TEXT); $confirmed = false; // Get DNS records and find a matching TXT entry $records = \dns_get_record($this->namespace, DNS_TXT); if ($records === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } foreach ($records as $record) { if ($record['txt'] === $hash) { $confirmed = true; break; } } // Get DNS records and find a matching CNAME entry // Note: some servers resolve every non-existing name // so we need to define left and right side of the CNAME record // i.e.: kolab-verify IN CNAME .domain.tld. if (!$confirmed) { $cname = $this->hash(self::HASH_CODE) . '.' . $this->namespace; $records = \dns_get_record('kolab-verify.' . $this->namespace, DNS_CNAME); if ($records === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } foreach ($records as $records) { if ($records['target'] === $cname) { $confirmed = true; break; } } } if ($confirmed) { $this->status |= Domain::STATUS_CONFIRMED; $this->save(); } return $confirmed; } /** * Generate a verification hash for this domain * * @param int $mod One of: HASH_CNAME, HASH_CODE (Default), HASH_TEXT * * @return string Verification hash */ public function hash($mod = null): string { $cname = 'kolab-verify'; if ($mod === self::HASH_CNAME) { return $cname; } $hash = \md5('hkccp-verify-' . $this->namespace); return $mod === self::HASH_TEXT ? "$cname=$hash" : $hash; } /** * Checks if there are any objects (users/aliases/groups) in a domain. * Note: Public domains are always reported not empty. * * @return bool True if there are no objects assigned, False otherwise */ public function isEmpty(): bool { if ($this->isPublic()) { return false; } // FIXME: These queries will not use indexes, so maybe we should consider // wallet/entitlements to search in objects that belong to this domain account? $suffix = '@' . $this->namespace; $suffixLen = strlen($suffix); return !( User::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() || UserAlias::whereRaw('substr(alias, ?) = ?', [-$suffixLen, $suffix])->exists() || Group::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() || Resource::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() || SharedFolder::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() ); } /** * Returns domain's namespace (required by the EntitleableTrait). * * @return string|null Domain namespace */ public function toString(): ?string { return $this->namespace; } /** * Unsuspend this domain. * * The domain is unsuspended through either of the following courses of actions; * * * The account balance has been topped up, or * * a suspected spammer has resolved their issues, or * * the command-line is triggered. * * Therefore, we can also confidently set the domain status to 'active' should the ownership of or management * access to have been confirmed before. * * @return void */ public function unsuspend(): void { if (!$this->isSuspended()) { return; } $this->status ^= Domain::STATUS_SUSPENDED; if ($this->isConfirmed() && $this->isVerified()) { $this->status |= Domain::STATUS_ACTIVE; } $this->save(); } /** * List the users of a domain, so long as the domain is not a public registration domain. * Note: It returns only users with a mailbox. * * @return \Illuminate\Support\Collection A collection of users */ public function users() { if ($this->isPublic()) { return collect([]); } $wallet = $this->wallet(); if (!$wallet) { return collect([]); } $mailboxSKU = Sku::withObjectTenantContext($this)->where('title', 'mailbox')->first(); if (!$mailboxSKU) { \Log::error("No mailbox SKU available."); return collect([]); } return User::select() ->whereExists(function ($query) use ($wallet, $mailboxSKU) { $query->select(DB::raw(1)) ->from('entitlements') ->whereColumn('entitleable_id', 'users.id') ->where('entitlements.wallet_id', $wallet->id) ->where('entitlements.entitleable_type', User::class) ->where('entitlements.sku_id', $mailboxSKU->id); }) ->get(); } /** * Verify if a domain exists in DNS * * @return bool True if registered, False otherwise * @throws \Exception Throws exception on DNS or DB errors */ public function verify(): bool { if ($this->isVerified()) { return true; } $records = \dns_get_record($this->namespace, DNS_ANY); if ($records === false) { throw new \Exception("Failed to get DNS record for {$this->namespace}"); } // It may happen that result contains other domains depending on the host DNS setup // that's why in_array() and not just !empty() if (in_array($this->namespace, array_column($records, 'host'))) { $this->status |= Domain::STATUS_VERIFIED; $this->save(); return true; } return false; } } diff --git a/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php b/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php index f949cf74..a84ebb86 100644 --- a/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php +++ b/src/tests/Feature/Console/Scalpel/Domain/CreateCommandTest.php @@ -1,65 +1,65 @@ deleteTestDomain('domain-delete.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestDomain('domain-delete.com'); parent::tearDown(); } /** * Test the command execution */ public function testHandle(): void { // Test --help argument $code = \Artisan::call("scalpel:domain:create --help"); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $this->assertStringContainsString('--namespace[=NAMESPACE]', $output); $this->assertStringContainsString('--type[=TYPE]', $output); $this->assertStringContainsString('--status[=STATUS]', $output); $this->assertStringContainsString('--tenant_id[=TENANT_ID]', $output); $tenant = \App\Tenant::orderBy('id', 'desc')->first(); // Test successful domain creation $code = \Artisan::call( "scalpel:domain:create" . " --namespace=domain-delete.com" - . " --type={Domain::TYPE_PUBLIC}" + . " --type=" . Domain::TYPE_PUBLIC . " --tenant_id={$tenant->id}" ); $output = trim(\Artisan::output()); $domain = $this->getTestDomain('domain-delete.com'); $this->assertSame(0, $code); $this->assertSame($output, (string) $domain->id); $this->assertSame('domain-delete.com', $domain->namespace); $this->assertSame(Domain::TYPE_PUBLIC, $domain->type); $this->assertSame($domain->tenant_id, $tenant->id); } } diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php index c6f5ec04..1d8bc74f 100644 --- a/src/tests/Feature/Controller/DomainsTest.php +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -1,620 +1,621 @@ deleteTestUser('test1@' . \config('app.domain')); $this->deleteTestUser('test2@' . \config('app.domain')); $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); Sku::where('title', 'test')->delete(); } public function tearDown(): void { $this->deleteTestUser('test1@' . \config('app.domain')); $this->deleteTestUser('test2@' . \config('app.domain')); $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); Sku::where('title', 'test')->delete(); $domain = $this->getTestDomain('kolab.org'); $domain->settings()->whereIn('key', ['spf_whitelist'])->delete(); parent::tearDown(); } /** * Test domain confirm request * @group skipci */ public function testConfirm(): void { Queue::fake(); $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('error', $json['status']); $this->assertEquals('Domain ownership confirmation failed.', $json['message']); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain ownership confirmed successfully.', $json['message']); $this->assertTrue(is_array($json['statusInfo'])); // Not authorized access $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(403); // Authorized access by additional account controller $domain = $this->getTestDomain('kolab.org'); $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); } /** * Test domain delete request (DELETE /api/v4/domains/) */ public function testDestroy(): void { Queue::fake(); $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $johns_domain = $this->getTestDomain('kolab.org'); $user1 = $this->getTestUser('test1@' . \config('app.domain')); $user2 = $this->getTestUser('test2@' . \config('app.domain')); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user1->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); // Not authorized access $response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(403); // Can't delete non-empty domain $response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}"); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('error', $json['status']); $this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']); // Successful deletion $response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain deleted successfully.', $json['message']); $this->assertTrue($domain->fresh()->trashed()); // Authorized access by additional account controller $this->deleteTestDomain('domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user1->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $user1->wallets()->first()->addController($user2); $response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain deleted successfully.', $json['message']); $this->assertTrue($domain->fresh()->trashed()); } /** * Test fetching domains list */ public function testIndex(): void { // User with no domains $user = $this->getTestUser('test1@domainscontroller.com'); $response = $this->actingAs($user)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("0 domains have been found.", $json['message']); $this->assertSame([], $json['list']); // User with custom domain(s) $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $response = $this->actingAs($john)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("1 domains have been found.", $json['message']); $this->assertCount(1, $json['list']); $this->assertSame('kolab.org', $json['list'][0]['namespace']); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json['list'][0]); $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isVerified', $json['list'][0]); $this->assertArrayHasKey('isSuspended', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); if (\config('app.with_ldap')) { $this->assertArrayHasKey('isLdapReady', $json['list'][0]); } else { $this->assertArrayNotHasKey('isLdapReady', $json['list'][0]); } $this->assertArrayHasKey('isReady', $json['list'][0]); $response = $this->actingAs($ned)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertCount(1, $json['list']); $this->assertSame('kolab.org', $json['list'][0]['namespace']); } /** * Test domain config update (POST /api/v4/domains//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); $domain->setSetting('spf_whitelist', null); // Test unknown domain id $post = ['spf_whitelist' => []]; $response = $this->actingAs($john)->post("/api/v4/domains/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['spf_whitelist' => []]; $response = $this->actingAs($jack)->post("/api/v4/domains/{$domain->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['grey' => 1]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['grey']); $this->assertNull($domain->fresh()->getSetting('spf_whitelist')); // Test some valid data $post = ['spf_whitelist' => ['.test.domain.com']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('Domain settings updated successfully.', $json['message']); $expected = \json_encode($post['spf_whitelist']); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); // Test input validation $post = ['spf_whitelist' => ['aaa']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( 'The entry format is invalid. Expected a domain name starting with a dot.', $json['errors']['spf_whitelist'][0] ); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); } /** * Test fetching domain info */ public function testShow(): void { $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); $discount = \App\Discount::withEnvTenantContext()->where('code', 'TEST')->first(); $wallet = $user->wallet(); $wallet->discount()->associate($discount); $wallet->save(); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals($domain->id, $json['id']); $this->assertEquals($domain->namespace, $json['namespace']); $this->assertEquals($domain->status, $json['status']); $this->assertEquals($domain->type, $json['type']); $this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']); $this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']); $this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']); $this->assertSame([], $json['config']['spf_whitelist']); $this->assertCount(4, $json['mx']); $this->assertTrue(strpos(implode("\n", $json['mx']), $domain->namespace) !== false); $this->assertCount(8, $json['dns']); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false); $this->assertTrue(is_array($json['statusInfo'])); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isVerified', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); if (\config('app.with_ldap')) { $this->assertArrayHasKey('isLdapReady', $json); } else { $this->assertArrayNotHasKey('isLdapReady', $json); } $this->assertArrayHasKey('isReady', $json); $this->assertCount(1, $json['skus']); $this->assertSame(1, $json['skus'][$sku_domain->id]['count']); $this->assertSame([0], $json['skus'][$sku_domain->id]['costs']); $this->assertSame($wallet->id, $json['wallet']['id']); $this->assertSame($wallet->balance, $json['wallet']['balance']); $this->assertSame($wallet->currency, $json['wallet']['currency']); $this->assertSame($discount->discount, $json['wallet']['discount']); $this->assertSame($discount->description, $json['wallet']['discount_description']); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); // Not authorized - Other account domain $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(403); $domain = $this->getTestDomain('kolab.org'); // Ned is an additional controller on kolab.org's wallet $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); // Jack has no entitlement/control over kolab.org $response = $this->actingAs($jack)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(403); } /** * Test fetching SKUs list for a domain (GET /domains//skus) */ public function testSkus(): void { $user = $this->getTestUser('john@kolab.org'); $domain = $this->getTestDomain('kolab.org'); // Unauth access not allowed $response = $this->get("api/v4/domains/{$domain->id}/skus"); $response->assertStatus(401); // Create an sku for another tenant, to make sure it is not included in the result $nsku = Sku::create([ 'title' => 'test', 'name' => 'Test', 'description' => '', 'active' => true, 'cost' => 100, 'handler_class' => 'App\Handlers\Domain', ]); $tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first(); $nsku->tenant_id = $tenant->id; $nsku->save(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSkuElement('domain-hosting', $json[0], [ 'prio' => 0, 'type' => 'domain', 'handler' => 'DomainHosting', 'enabled' => true, 'readonly' => true, ]); } /** * Test fetching domain status (GET /api/v4/domains//status) * and forcing setup process update (?refresh=1) * * @group dns */ public function testStatus(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/domains/{$domain->id}/status"); $response->assertStatus(403); $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); // Get domain status $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status"); $response->assertStatus(200); $json = $response->json(); + $withLdap = \config('app.with_ldap'); $this->assertFalse($json['isVerified']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isDone']); - $this->assertCount(3, $json['process']); - $this->assertSame('domain-verified', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); + $this->assertCount($withLdap ? 4 : 3, $json['process']); + $this->assertSame('domain-verified', $json['process'][$withLdap ? 2 : 1]['label']); + $this->assertSame(false, $json['process'][$withLdap ? 2 : 1]['state']); $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); // Now "reboot" the process and verify the domain $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isVerified']); $this->assertTrue($json['isReady']); $this->assertTrue($json['isDone']); - $this->assertCount(3, $json['process']); - $this->assertSame('domain-verified', $json['process'][1]['label']); - $this->assertSame(true, $json['process'][1]['state']); - $this->assertSame('domain-confirmed', $json['process'][2]['label']); - $this->assertSame(true, $json['process'][2]['state']); + $this->assertCount($withLdap ? 4 : 3, $json['process']); + $this->assertSame('domain-verified', $json['process'][$withLdap ? 2 : 1]['label']); + $this->assertSame(true, $json['process'][$withLdap ? 2 : 1]['state']); + $this->assertSame('domain-confirmed', $json['process'][$withLdap ? 3 : 2]['label']); + $this->assertSame(true, $json['process'][$withLdap ? 3 : 2]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); // TODO: Test completing all process steps } /** * Test domain creation (POST /api/v4/domains) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test empty request $response = $this->actingAs($john)->post("/api/v4/domains", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The namespace field is required.", $json['errors']['namespace'][0]); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['namespace']); $this->assertCount(2, $json); // Test access by user not being a wallet controller $post = ['namespace' => 'domainscontroller.com']; $response = $this->actingAs($jack)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['namespace' => '--']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is invalid.', $json['errors']['namespace'][0]); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['namespace']); // Test an existing domain $post = ['namespace' => 'kolab.org']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is not available.', $json['errors']['namespace']); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); // Missing package $post = ['namespace' => 'domainscontroller.com']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Package is required.", $json['errors']['package']); $this->assertCount(2, $json); // Invalid package $post['package'] = $package_kolab->id; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Invalid package selected.", $json['errors']['package']); $this->assertCount(2, $json); // Test full and valid data $post['package'] = $package_domain->id; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Domain created successfully.", $json['message']); $this->assertCount(2, $json); $domain = Domain::where('namespace', $post['namespace'])->first(); $this->assertInstanceOf(Domain::class, $domain); // Assert the new domain entitlements $this->assertEntitlements($domain, ['domain-hosting']); // Assert the wallet to which the new domain should be assigned to $wallet = $domain->wallet(); $this->assertSame($john->wallets->first()->id, $wallet->id); // Test re-creating a domain $domain->delete(); $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame("Domain created successfully.", $json['message']); $this->assertCount(2, $json); $domain = Domain::where('namespace', $post['namespace'])->first(); $this->assertInstanceOf(Domain::class, $domain); $this->assertEntitlements($domain, ['domain-hosting']); $wallet = $domain->wallet(); $this->assertSame($john->wallets->first()->id, $wallet->id); // Test creating a domain that is soft-deleted and belongs to another user $domain->delete(); $domain->entitlements()->withTrashed()->update(['wallet_id' => $jack->wallets->first()->id]); $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is not available.', $json['errors']['namespace']); // Test acting as account controller (not owner) $this->markTestIncomplete(); } }