diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -256,7 +256,7 @@ if (!self::getUserEntry($ldap, $user->email, $dn)) { if (empty($dn)) { - self::throwException($ldap, "Failed to create user {$user->email} in LDAP"); + self::throwException($ldap, "Failed to create user {$user->email} in LDAP (" . __LINE__ . ")"); } self::setUserAttributes($user, $entry); diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -232,11 +232,11 @@ $allowed_values = [ self::STATUS_NEW, self::STATUS_ACTIVE, - self::STATUS_CONFIRMED, self::STATUS_SUSPENDED, self::STATUS_DELETED, - self::STATUS_LDAP_READY, + self::STATUS_CONFIRMED, self::STATUS_VERIFIED, + self::STATUS_LDAP_READY, ]; foreach ($allowed_values as $value) { @@ -250,6 +250,31 @@ throw new \Exception("Invalid domain status: {$status}"); } + if ($this->isPublic()) { + $this->attributes['status'] = $new_status; + return; + } + + if ($new_status & self::STATUS_CONFIRMED) { + // if we have confirmed ownership of or management access to the domain, then we have + // also confirmed the domain exists in DNS. + $new_status |= self::STATUS_VERIFIED; + $new_status |= self::STATUS_ACTIVE; + } + + if ($new_status & self::STATUS_DELETED && $new_status & self::STATUS_ACTIVE) { + $new_status ^= self::STATUS_ACTIVE; + } + + if ($new_status & self::STATUS_SUSPENDED && $new_status & self::STATUS_ACTIVE) { + $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) { + $new_status ^= self::STATUS_NEW; + } + $this->attributes['status'] = $new_status; } @@ -349,6 +374,15 @@ /** * 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 @@ -358,6 +392,11 @@ } $this->status ^= Domain::STATUS_SUSPENDED; + + if ($this->isConfirmed() && $this->isVerified()) { + $this->status |= Domain::STATUS_ACTIVE; + } + $this->save(); } diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php --- a/src/app/Observers/DomainObserver.php +++ b/src/app/Observers/DomainObserver.php @@ -26,7 +26,7 @@ $domain->namespace = \strtolower($domain->namespace); - $domain->status |= Domain::STATUS_NEW | Domain::STATUS_ACTIVE; + $domain->status |= Domain::STATUS_NEW; } /** diff --git a/src/phpunit.xml b/src/phpunit.xml --- a/src/phpunit.xml +++ b/src/phpunit.xml @@ -13,6 +13,10 @@ tests/Unit + + tests/Functional + + tests/Feature diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php --- a/src/tests/Browser/StatusTest.php +++ b/src/tests/Browser/StatusTest.php @@ -54,16 +54,20 @@ { // Unconfirmed domain and user $domain = Domain::where('namespace', 'kolab.org')->first(); + if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $john = $this->getTestUser('john@kolab.org'); + $john->created_at = Carbon::now(); + if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } + $john->save(); $this->browse(function ($browser) use ($john, $domain) { @@ -113,10 +117,13 @@ $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } + $john->created_at = Carbon::now()->subSeconds(3600); + if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } + $john->save(); $this->browse(function ($browser) use ($john, $domain) { @@ -153,6 +160,18 @@ $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); + // side-step + $this->assertFalse($domain->isNew()); + $this->assertTrue($domain->isActive()); + $this->assertTrue($domain->isLdapReady()); + $this->assertTrue($domain->isExternal()); + + $this->assertFalse($domain->isHosted()); + $this->assertFalse($domain->isConfirmed()); + $this->assertFalse($domain->isVerified()); + $this->assertFalse($domain->isSuspended()); + $this->assertFalse($domain->isDeleted()); + $this->browse(function ($browser) use ($domain) { // Test auto-refresh $browser->on(new Dashboard()) diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php --- a/src/tests/Feature/Controller/DomainsTest.php +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -229,14 +229,14 @@ $json = $response->json(); $this->assertTrue($json['isVerified']); - $this->assertFalse($json['isReady']); + $this->assertTrue($json['isReady']); $this->assertCount(4, $json['process']); $this->assertSame('domain-verified', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('domain-confirmed', $json['process'][3]['label']); - $this->assertSame(false, $json['process'][3]['state']); - $this->assertSame('error', $json['status']); - $this->assertSame('Failed to verify an ownership of a domain.', $json['message']); + $this->assertSame(true, $json['process'][3]['state']); + $this->assertSame('success', $json['status']); + $this->assertSame('Setup process finished successfully.', $json['message']); // TODO: Test completing all process steps } diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php --- a/src/tests/Feature/Controller/PaymentsStripeTest.php +++ b/src/tests/Feature/Controller/PaymentsStripeTest.php @@ -541,6 +541,7 @@ $wallet->setSetting('mandate_disabled', null); $wallet->balance = -2050; $wallet->save(); + $result = PaymentsController::topUpWallet($wallet); $this->assertFalse($result); $this->assertCount(1, $wallet->payments()->get()); diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php --- a/src/tests/Feature/Controller/WalletsTest.php +++ b/src/tests/Feature/Controller/WalletsTest.php @@ -89,7 +89,7 @@ public function testReceiptDownload(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $john = $this->getTestUser('john@klab.org'); + $john = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Unauth access not allowed @@ -132,7 +132,7 @@ public function testReceipts(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $john = $this->getTestUser('john@klab.org'); + $john = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->payments()->delete(); @@ -220,7 +220,7 @@ $package_kolab = \App\Package::where('title', 'kolab')->first(); $user = $this->getTestUser('wallets-controller@kolabnow.com'); $user->assignPackage($package_kolab); - $john = $this->getTestUser('john@klab.org'); + $john = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); // Unauth access not allowed diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php --- a/src/tests/Feature/DomainTest.php +++ b/src/tests/Feature/DomainTest.php @@ -65,7 +65,7 @@ $this->assertSame('gmail.com', $result->namespace); $this->assertSame($domain->id, $result->id); $this->assertSame($domain->type, $result->type); - $this->assertSame(Domain::STATUS_NEW | Domain::STATUS_ACTIVE, $result->status); + $this->assertSame(Domain::STATUS_NEW, $result->status); } /** diff --git a/src/tests/Functional/Methods/DomainTest.php b/src/tests/Functional/Methods/DomainTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Functional/Methods/DomainTest.php @@ -0,0 +1,114 @@ +domain = $this->getTestDomain( + 'test.domain', + [ + 'status' => \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED, + 'type' => \App\Domain::TYPE_EXTERNAL + ] + ); + } + + public function tearDown(): void + { + $this->deleteTestDomain('test.domain'); + + parent::tearDown(); + } + + /** + * Verify we can suspend an active domain. + */ + public function testSuspendForActiveDomain() + { + Queue::fake(); + + $this->domain->status |= \App\Domain::STATUS_ACTIVE; + + $this->assertFalse($this->domain->isSuspended()); + $this->assertTrue($this->domain->isActive()); + + $this->domain->suspend(); + + $this->assertTrue($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + } + + /** + * Verify we can unsuspend a suspended domain + */ + public function testUnsuspendForSuspendedDomain() + { + Queue::fake(); + + $this->domain->status |= \App\Domain::STATUS_SUSPENDED; + + $this->assertTrue($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + + $this->domain->unsuspend(); + + $this->assertFalse($this->domain->isSuspended()); + $this->assertTrue($this->domain->isActive()); + } + + /** + * Verify we can unsuspend a suspended domain that wasn't confirmed + */ + public function testUnsuspendForSuspendedUnconfirmedDomain() + { + Queue::fake(); + + $this->domain->status = \App\Domain::STATUS_NEW | \App\Domain::STATUS_SUSPENDED; + + $this->assertTrue($this->domain->isNew()); + $this->assertTrue($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + $this->assertFalse($this->domain->isConfirmed()); + $this->assertFalse($this->domain->isVerified()); + + $this->domain->unsuspend(); + + $this->assertTrue($this->domain->isNew()); + $this->assertFalse($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + $this->assertFalse($this->domain->isConfirmed()); + $this->assertFalse($this->domain->isVerified()); + } + + /** + * Verify we can unsuspend a suspended domain that was verified but not confirmed + */ + public function testUnsuspendForSuspendedVerifiedUnconfirmedDomain() + { + Queue::fake(); + + $this->domain->status = \App\Domain::STATUS_NEW | \App\Domain::STATUS_SUSPENDED | \App\Domain::STATUS_VERIFIED; + + $this->assertTrue($this->domain->isNew()); + $this->assertTrue($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + $this->assertFalse($this->domain->isConfirmed()); + $this->assertTrue($this->domain->isVerified()); + + $this->domain->unsuspend(); + + $this->assertTrue($this->domain->isNew()); + $this->assertFalse($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + $this->assertFalse($this->domain->isConfirmed()); + $this->assertTrue($this->domain->isVerified()); + } + +} diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -24,38 +24,59 @@ $domains = \App\Utils::powerSet($statuses); - foreach ($domains as $domain_statuses) { + foreach ($domains as $domainStatuses) { $domain = new Domain( [ 'namespace' => 'test.com', - 'status' => \array_sum($domain_statuses), + 'status' => \array_sum($domainStatuses), 'type' => Domain::TYPE_EXTERNAL ] ); - $this->assertTrue($domain->isNew() === in_array(Domain::STATUS_NEW, $domain_statuses)); - $this->assertTrue($domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domain_statuses)); - $this->assertTrue($domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domain_statuses)); - $this->assertTrue($domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domain_statuses)); - $this->assertTrue($domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domain_statuses)); - $this->assertTrue($domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domain_statuses)); - $this->assertTrue($domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domain_statuses)); - } - } + $domainStatuses = []; - /** - * Test setStatusAttribute exception - */ - public function testDomainStatusInvalid(): void - { - $this->expectException(\Exception::class); - - $domain = new Domain( - [ - 'namespace' => 'test.com', - 'status' => 1234567, - ] - ); + foreach ($statuses as $status) { + if ($domain->status & $status) { + $domainStatuses[] = $status; + } + } + + $this->assertSame($domain->status, \array_sum($domainStatuses)); + + // either one is true, but not both + $this->assertSame( + $domain->isNew() === in_array(Domain::STATUS_NEW, $domainStatuses), + $domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domainStatuses) + ); + + $this->assertTrue( + $domain->isNew() === in_array(Domain::STATUS_NEW, $domainStatuses) + ); + + $this->assertTrue( + $domain->isActive() === in_array(Domain::STATUS_ACTIVE, $domainStatuses) + ); + + $this->assertTrue( + $domain->isConfirmed() === in_array(Domain::STATUS_CONFIRMED, $domainStatuses) + ); + + $this->assertTrue( + $domain->isSuspended() === in_array(Domain::STATUS_SUSPENDED, $domainStatuses) + ); + + $this->assertTrue( + $domain->isDeleted() === in_array(Domain::STATUS_DELETED, $domainStatuses) + ); + + $this->assertTrue( + $domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domainStatuses) + ); + + $this->assertTrue( + $domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domainStatuses) + ); + } } /** diff --git a/src/tests/Unit/Methods/DomainTest.php b/src/tests/Unit/Methods/DomainTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Unit/Methods/DomainTest.php @@ -0,0 +1,159 @@ +domain = new \App\Domain(); + } + + /** + * Test lower-casing namespace attribute. + */ + public function testSetNamespaceAttributeLowercases() + { + $this->domain = new \App\Domain(); + + $this->domain->namespace = 'UPPERCASE'; + + $this->assertTrue($this->domain->namespace === 'uppercase'); + } + + /** + * Test setting the status to something invalid + */ + public function testSetStatusAttributeInvalid() + { + $this->expectException(\Exception::class); + + $this->domain->status = 123456; + } + + /** + * Test public domain. + */ + public function testSetStatusAttributeOnPublicDomain() + { + $this->domain->{'type'} = \App\Domain::TYPE_PUBLIC; + + $this->domain->status = 115; + + $this->assertTrue($this->domain->status == 115); + } + + /** + * Test status mutations + */ + public function testSetStatusAttributeActiveMakesForNotNew() + { + $this->domain->status = \App\Domain::STATUS_NEW; + + $this->assertTrue($this->domain->isNew()); + $this->assertFalse($this->domain->isActive()); + + $this->domain->status |= \App\Domain::STATUS_ACTIVE; + + $this->assertFalse($this->domain->isNew()); + $this->assertTrue($this->domain->isActive()); + } + + /** + * Verify setting confirmed sets verified. + */ + public function testSetStatusAttributeConfirmedMakesForVerfied() + { + $this->domain->status = \App\Domain::STATUS_CONFIRMED; + + $this->assertTrue($this->domain->isConfirmed()); + $this->assertTrue($this->domain->isVerified()); + } + + /** + * Verify setting confirmed sets active. + */ + public function testSetStatusAttributeConfirmedMakesForActive() + { + $this->domain->status = \App\Domain::STATUS_CONFIRMED; + + $this->assertTrue($this->domain->isConfirmed()); + $this->assertTrue($this->domain->isActive()); + } + + /** + * Verify setting deleted drops active. + */ + public function testSetStatusAttributeDeletedVoidsActive() + { + $this->domain->status = \App\Domain::STATUS_ACTIVE; + + $this->assertTrue($this->domain->isActive()); + $this->assertFalse($this->domain->isNew()); + $this->assertFalse($this->domain->isDeleted()); + + $this->domain->status |= \App\Domain::STATUS_DELETED; + + $this->assertFalse($this->domain->isActive()); + $this->assertFalse($this->domain->isNew()); + $this->assertTrue($this->domain->isDeleted()); + } + + /** + * Verify setting suspended drops active. + */ + public function testSetStatusAttributeSuspendedVoidsActive() + { + $this->domain->status = \App\Domain::STATUS_ACTIVE; + + $this->assertTrue($this->domain->isActive()); + $this->assertFalse($this->domain->isSuspended()); + + $this->domain->status |= \App\Domain::STATUS_SUSPENDED; + + $this->assertFalse($this->domain->isActive()); + $this->assertTrue($this->domain->isSuspended()); + } + + /** + * Verify we can suspend a suspended domain without disaster. + * + * This doesn't change anything to trigger a save. + */ + public function testSuspendForSuspendedDomain() + { + $this->domain->status = \App\Domain::STATUS_ACTIVE; + + $this->domain->status |= \App\Domain::STATUS_SUSPENDED; + + $this->assertTrue($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + + $this->domain->suspend(); + + $this->assertTrue($this->domain->isSuspended()); + $this->assertFalse($this->domain->isActive()); + } + + /** + * Verify we can unsuspend an active (unsuspended) domain + * + * This doesn't change anything to trigger a save. + */ + public function testUnsuspendForActiveDomain() + { + $this->domain->status = \App\Domain::STATUS_ACTIVE; + + $this->assertFalse($this->domain->isSuspended()); + $this->assertTrue($this->domain->isActive()); + + $this->domain->unsuspend(); + + $this->assertFalse($this->domain->isSuspended()); + $this->assertTrue($this->domain->isActive()); + } +}