diff --git a/src/app/Console/Commands/Domain/StatusCommand.php b/src/app/Console/Commands/Domain/StatusCommand.php index 440930e8..9007fc04 100644 --- a/src/app/Console/Commands/Domain/StatusCommand.php +++ b/src/app/Console/Commands/Domain/StatusCommand.php @@ -1,63 +1,40 @@ getDomain($this->argument('domain'), true); if (!$domain) { $this->error("Domain not found."); return 1; } - $statuses = [ - 'active' => Domain::STATUS_ACTIVE, - 'suspended' => Domain::STATUS_SUSPENDED, - 'deleted' => Domain::STATUS_DELETED, - 'confirmed' => Domain::STATUS_CONFIRMED, - 'verified' => Domain::STATUS_VERIFIED, - 'ldapReady' => Domain::STATUS_LDAP_READY, - ]; - - $domain_state = []; - - foreach ($statuses as $text => $bit) { - if ($text == 'deleted') { - $status = $domain->trashed(); - } else { - $status = $domain->{'is' . \ucfirst($text)}(); - } - - if ($status) { - $domain_state[] = "$text ($bit)"; - } - } - - $this->info("Status ({$domain->status}): " . \implode(', ', $domain_state)); + $this->info("Status ({$domain->status}): " . $domain->statusText()); } } diff --git a/src/app/Console/Commands/User/InfoCommand.php b/src/app/Console/Commands/User/InfoCommand.php new file mode 100644 index 00000000..4aedc290 --- /dev/null +++ b/src/app/Console/Commands/User/InfoCommand.php @@ -0,0 +1,59 @@ +getUser($this->argument('email'), true); + + if (!$user) { + $this->error('User not found.'); + return 1; + } + + $props = ['id', 'email', 'created_at', 'updated_at', 'deleted_at']; + + foreach ($props as $prop) { + if (!empty($user->{$prop})) { + $this->info("{$prop}: " . $user->{$prop}); + } + } + + $this->info("status: {$user->status} (" . $user->statusText() . ")"); + + $user->settings()->orderBy('key')->each( + function ($setting) { + if ($setting->value !== null) { + $this->info("{$setting->key}: " . \str_replace("\n", ' ', $setting->value)); + } + } + ); + + // TODO: Display additional info (maybe with --all option): + // - wallet balance + // - tenant ID (and name) + // - if not an account owner, owner ID/email + // - if signup code available, IP address (other headers) + } +} diff --git a/src/app/Console/Commands/User/StatusCommand.php b/src/app/Console/Commands/User/StatusCommand.php index 83c989be..178721b8 100644 --- a/src/app/Console/Commands/User/StatusCommand.php +++ b/src/app/Console/Commands/User/StatusCommand.php @@ -1,64 +1,40 @@ getUser($this->argument('user'), true); if (!$user) { $this->error("User not found."); return 1; } - $statuses = [ - 'active' => User::STATUS_ACTIVE, - 'suspended' => User::STATUS_SUSPENDED, - 'deleted' => User::STATUS_DELETED, - 'ldapReady' => User::STATUS_LDAP_READY, - 'imapReady' => User::STATUS_IMAP_READY, - 'degraded' => User::STATUS_DEGRADED, - 'restricted' => User::STATUS_RESTRICTED, - ]; - - $user_state = []; - - foreach ($statuses as $text => $bit) { - if ($text == 'deleted') { - $status = $user->trashed(); - } else { - $status = $user->{'is' . \ucfirst($text)}(); - } - - if ($status) { - $user_state[] = "$text ($bit)"; - } - } - - $this->info("Status ({$user->status}): " . \implode(', ', $user_state)); + $this->info("Status ({$user->status}): " . $user->statusText()); } } diff --git a/src/app/Console/Development/DomainStatus.php b/src/app/Console/Development/DomainStatus.php deleted file mode 100644 index 439409ed..00000000 --- a/src/app/Console/Development/DomainStatus.php +++ /dev/null @@ -1,75 +0,0 @@ -argument('domain'))->firstOrFail(); - - $statuses = [ - 'active' => Domain::STATUS_ACTIVE, - 'suspended' => Domain::STATUS_SUSPENDED, - 'deleted' => Domain::STATUS_DELETED, - 'ldapReady' => Domain::STATUS_LDAP_READY, - 'verified' => Domain::STATUS_VERIFIED, - 'confirmed' => Domain::STATUS_CONFIRMED, - ]; - - // I'd prefer "-state" and "+state" syntax, but it's not possible - $delete = false; - if ($update = $this->option('del')) { - $delete = true; - } elseif ($update = $this->option('add')) { - // do nothing - } - - if (!empty($update)) { - $map = \array_change_key_case($statuses); - $update = \strtolower($update); - - if (isset($map[$update])) { - if ($delete && $domain->status & $map[$update]) { - $domain->status ^= $map[$update]; - $domain->save(); - } elseif (!$delete && !($domain->status & $map[$update])) { - $domain->status |= $map[$update]; - $domain->save(); - } - } - } - - $domain_state = []; - foreach (\array_keys($statuses) as $state) { - $func = 'is' . \ucfirst($state); - if ($domain->$func()) { - $domain_state[] = $state; - } - } - - $this->info("Status: " . \implode(',', $domain_state)); - } -} diff --git a/src/app/Traits/StatusPropertyTrait.php b/src/app/Traits/StatusPropertyTrait.php index f4ecda6a..23adab5e 100644 --- a/src/app/Traits/StatusPropertyTrait.php +++ b/src/app/Traits/StatusPropertyTrait.php @@ -1,110 +1,129 @@ status & static::STATUS_ACTIVE) > 0; } /** * Returns whether this object is deleted. * * @return bool */ public function isDeleted(): bool { return defined('static::STATUS_DELETED') && ($this->status & static::STATUS_DELETED) > 0; } /** * Returns whether this object is registered in IMAP. * * @return bool */ public function isImapReady(): bool { return defined('static::STATUS_IMAP_READY') && ($this->status & static::STATUS_IMAP_READY) > 0; } /** * Returns whether this object is registered in LDAP. * * @return bool */ public function isLdapReady(): bool { return defined('static::STATUS_LDAP_READY') && ($this->status & static::STATUS_LDAP_READY) > 0; } /** * Returns whether this object is new. * * @return bool */ public function isNew(): bool { return defined('static::STATUS_NEW') && ($this->status & static::STATUS_NEW) > 0; } /** * Returns whether this object is suspended. * * @return bool */ public function isSuspended(): bool { return defined('static::STATUS_SUSPENDED') && ($this->status & static::STATUS_SUSPENDED) > 0; } + /** + * Returns object's statuses in a textual form + */ + public function statusText(): string + { + $reflection = new \ReflectionClass(get_class($this)); + $result = []; + + foreach ($reflection->getConstants() as $const => $value) { + if (str_starts_with($const, 'STATUS_') && ($this->status & $value) > 0) { + $result[] = Str::camel(strtolower(str_replace('STATUS_', '', $const))) . " ($value)"; + } + } + + return implode(', ', $result); + } + /** * Suspend this object. * * @return void */ public function suspend(): void { if (!defined('static::STATUS_SUSPENDED') || $this->isSuspended()) { return; } $this->status |= static::STATUS_SUSPENDED; $this->save(); } /** * Unsuspend this object. * * @return void */ public function unsuspend(): void { if (!defined('static::STATUS_SUSPENDED') || !$this->isSuspended()) { return; } $this->status ^= static::STATUS_SUSPENDED; $this->save(); } /** * Status property mutator * * @throws \Exception */ public function setStatusAttribute($status) { if ($status & ~$this->allowed_states) { throw new \Exception("Invalid status: {$status}"); } $this->attributes['status'] = $status; } } diff --git a/src/tests/Feature/Console/Domain/StatusTest.php b/src/tests/Feature/Console/Domain/StatusTest.php index b31c8d10..1db3424b 100644 --- a/src/tests/Feature/Console/Domain/StatusTest.php +++ b/src/tests/Feature/Console/Domain/StatusTest.php @@ -1,68 +1,65 @@ deleteTestDomain('domain-delete.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestDomain('domain-delete.com'); parent::tearDown(); } /** * Test the command */ public function testHandle(): void { Queue::fake(); // Non-existing domain $code = \Artisan::call("domain:status unknown.org"); $output = trim(\Artisan::output()); $this->assertSame(1, $code); $this->assertSame("Domain not found.", $output); // Existing domain $code = \Artisan::call("domain:status kolab.org"); $output = trim(\Artisan::output()); $this->assertSame(0, $code); if (\config('app.with_ldap')) { $this->assertSame("Status (114): active (2), confirmed (16), verified (32), ldapReady (64)", $output); } else { $this->assertSame("Status (50): active (2), confirmed (16), verified (32)", $output); } // Test deleted domain - $user = $this->getTestUser('john@kolab.org'); $domain = $this->getTestDomain('domain-delete.com', [ 'status' => \App\Domain::STATUS_NEW, 'type' => \App\Domain::TYPE_HOSTED, ]); - $package_domain = \App\Package::where('title', 'domain-hosting')->first(); - $domain->assignPackage($package_domain, $user); $domain->delete(); $code = \Artisan::call("domain:status {$domain->namespace}"); $output = trim(\Artisan::output()); $this->assertSame(0, $code); - $this->assertSame("Status (1): deleted (8)", $output); + $this->assertSame("Status (1): new (1)", $output); } } diff --git a/src/tests/Feature/Console/User/InfoTest.php b/src/tests/Feature/Console/User/InfoTest.php new file mode 100644 index 00000000..788ec6d4 --- /dev/null +++ b/src/tests/Feature/Console/User/InfoTest.php @@ -0,0 +1,59 @@ +deleteTestUser('user@force-delete.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('user@force-delete.com'); + + parent::tearDown(); + } + + /** + * Test the command + */ + public function testHandle(): void + { + Queue::fake(); + + // Non-existing user + $code = \Artisan::call("user:info unknown"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("User not found.", $output); + + // Test existing but soft-deleted user + $user = $this->getTestUser('user@force-delete.com', ['status' => \App\User::STATUS_NEW]); + $user->delete(); + + $code = \Artisan::call("user:info {$user->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertStringContainsString("id: {$user->id}", $output); + $this->assertStringContainsString("email: {$user->email}", $output); + $this->assertStringContainsString("created_at: {$user->created_at}", $output); + $this->assertStringContainsString("deleted_at: {$user->deleted_at}", $output); + $this->assertStringContainsString("status: {$user->status}", $output); + $this->assertStringContainsString("currency: CHF", $output); + } +} diff --git a/src/tests/Feature/Console/User/StatusTest.php b/src/tests/Feature/Console/User/StatusTest.php index 092323ac..c7caa4c1 100644 --- a/src/tests/Feature/Console/User/StatusTest.php +++ b/src/tests/Feature/Console/User/StatusTest.php @@ -1,68 +1,68 @@ deleteTestUser('user@force-delete.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('user@force-delete.com'); parent::tearDown(); } /** * Test command runs */ public function testHandle(): void { Queue::fake(); // Non-existing user $code = \Artisan::call("user:status unknown"); $output = trim(\Artisan::output()); $this->assertSame(1, $code); $this->assertSame("User not found.", $output); $user = $this->getTestUser( 'user@force-delete.com', ['status' => User::STATUS_NEW | User::STATUS_ACTIVE | User::STATUS_IMAP_READY | User::STATUS_LDAP_READY] ); // Existing user $code = \Artisan::call("user:status {$user->email}"); $output = trim(\Artisan::output()); $this->assertSame(0, $code); - $this->assertSame("Status (51): active (2), ldapReady (16), imapReady (32)", $output); + $this->assertSame("Status (51): new (1), active (2), ldapReady (16), imapReady (32)", $output); $user->status = User::STATUS_ACTIVE; $user->save(); $user->delete(); // Deleted user $code = \Artisan::call("user:status {$user->email}"); $output = trim(\Artisan::output()); $this->assertSame(0, $code); - $this->assertSame("Status (2): active (2), deleted (8)", $output); + $this->assertSame("Status (2): active (2)", $output); } } diff --git a/src/tests/Unit/DomainTest.php b/src/tests/Unit/DomainTest.php index e3b3034c..71f83947 100644 --- a/src/tests/Unit/DomainTest.php +++ b/src/tests/Unit/DomainTest.php @@ -1,140 +1,169 @@ 'test.com', 'status' => \array_sum($domainStatuses), 'type' => Domain::TYPE_EXTERNAL ] ); $domainStatuses = []; 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) ); if (\config('app.with_ldap')) { $this->assertTrue( $domain->isLdapReady() === in_array(Domain::STATUS_LDAP_READY, $domainStatuses) ); } $this->assertTrue( $domain->isVerified() === in_array(Domain::STATUS_VERIFIED, $domainStatuses) ); } } /** * Test basic Domain funtionality */ public function testDomainType(): void { $types = [ Domain::TYPE_PUBLIC, Domain::TYPE_HOSTED, Domain::TYPE_EXTERNAL, ]; $domains = \Tests\Utils::powerSet($types); foreach ($domains as $domain_types) { $domain = new Domain( [ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, 'type' => \array_sum($domain_types), ] ); $this->assertTrue($domain->isPublic() === in_array(Domain::TYPE_PUBLIC, $domain_types)); $this->assertTrue($domain->isHosted() === in_array(Domain::TYPE_HOSTED, $domain_types)); $this->assertTrue($domain->isExternal() === in_array(Domain::TYPE_EXTERNAL, $domain_types)); } } /** * Test domain hash generation */ public function testHash(): void { $domain = new Domain([ 'namespace' => 'test.com', 'status' => Domain::STATUS_NEW, ]); $hash_code = $domain->hash(); $this->assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $hash_code); $hash_text = $domain->hash(Domain::HASH_TEXT); $this->assertMatchesRegularExpression('/^kolab-verify=[a-f0-9]{32}$/', $hash_text); $this->assertSame($hash_code, str_replace('kolab-verify=', '', $hash_text)); $hash_cname = $domain->hash(Domain::HASH_CNAME); $this->assertSame('kolab-verify', $hash_cname); $hash_code2 = $domain->hash(Domain::HASH_CODE); $this->assertSame($hash_code, $hash_code2); } + + /** + * Test domain statusText() + */ + public function testStatusText(): void + { + $domain = new Domain(); + + $this->assertSame('', $domain->statusText()); + + $domain->status = Domain::STATUS_NEW + | Domain::STATUS_ACTIVE + | Domain::STATUS_SUSPENDED + | Domain::STATUS_DELETED + | Domain::STATUS_CONFIRMED + | Domain::STATUS_VERIFIED + | Domain::STATUS_LDAP_READY; + + $expected = [ + 'new (1)', + 'suspended (4)', + 'deleted (8)', + 'confirmed (16)', + 'verified (32)', + 'ldapReady (64)', + ]; + + $this->assertSame(implode(', ', $expected), $domain->statusText()); + } } diff --git a/src/tests/Unit/UserTest.php b/src/tests/Unit/UserTest.php index 6c680396..1bb014eb 100644 --- a/src/tests/Unit/UserTest.php +++ b/src/tests/Unit/UserTest.php @@ -1,139 +1,171 @@ 'user@email.com']); $user->password = 'test'; $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; $this->assertMatchesRegularExpression('/^\$2y\$04\$[0-9a-zA-Z\/.]{53}$/', $user->password); $this->assertSame($ssh512, $user->password_ldap); } /** * Test User password mutator */ public function testSetPasswordLdapAttribute(): void { $user = new User(['email' => 'user@email.com']); $user->password_ldap = 'test'; $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; $this->assertMatchesRegularExpression('/^\$2y\$04\$[0-9a-zA-Z\/.]{53}$/', $user->password); $this->assertSame($ssh512, $user->password_ldap); } /** * Test User password validation */ public function testPasswordValidation(): void { $user = new User(['email' => 'user@email.com']); $user->password = 'test'; $this->assertSame(true, $user->validateCredentials('user@email.com', 'test')); $this->assertSame(false, $user->validateCredentials('user@email.com', 'wrong')); $this->assertSame(true, $user->validateCredentials('User@Email.Com', 'test')); $this->assertSame(false, $user->validateCredentials('wrong', 'test')); // Ensure the fallback to the ldap_password works if the current password is empty $ssh512 = "{SSHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ" . "6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="; $ldapUser = new User(['email' => 'user2@email.com']); $ldapUser->setRawAttributes(['password' => '', 'password_ldap' => $ssh512, 'email' => 'user2@email.com']); $this->assertSame($ldapUser->password, ''); $this->assertSame($ldapUser->password_ldap, $ssh512); $this->assertSame(true, $ldapUser->validateCredentials('user2@email.com', 'test', false)); $ldapUser->delete(); } /** * Test User role mutator */ public function testSetRoleAttribute(): void { $user = new User(['email' => 'user@email.com']); $user->role = User::ROLE_ADMIN; $this->assertSame(User::ROLE_ADMIN, $user->role); $user->role = User::ROLE_RESELLER; $this->assertSame(User::ROLE_RESELLER, $user->role); $user->role = null; $this->assertSame(null, $user->role); $this->expectException(\Exception::class); $user->role = 'unknown'; } /** * Test basic User funtionality */ public function testStatus(): void { $statuses = [ User::STATUS_NEW, User::STATUS_ACTIVE, User::STATUS_SUSPENDED, User::STATUS_DELETED, User::STATUS_IMAP_READY, User::STATUS_LDAP_READY, User::STATUS_DEGRADED, User::STATUS_RESTRICTED, ]; $users = \Tests\Utils::powerSet($statuses); foreach ($users as $user_statuses) { $user = new User( [ 'email' => 'user@email.com', 'status' => \array_sum($user_statuses), ] ); $this->assertTrue($user->isNew() === in_array(User::STATUS_NEW, $user_statuses)); $this->assertTrue($user->isActive() === in_array(User::STATUS_ACTIVE, $user_statuses)); $this->assertTrue($user->isSuspended() === in_array(User::STATUS_SUSPENDED, $user_statuses)); $this->assertTrue($user->isDeleted() === in_array(User::STATUS_DELETED, $user_statuses)); $this->assertTrue($user->isLdapReady() === in_array(User::STATUS_LDAP_READY, $user_statuses)); $this->assertTrue($user->isImapReady() === in_array(User::STATUS_IMAP_READY, $user_statuses)); $this->assertTrue($user->isDegraded() === in_array(User::STATUS_DEGRADED, $user_statuses)); $this->assertTrue($user->isRestricted() === in_array(User::STATUS_RESTRICTED, $user_statuses)); } } /** * Test setStatusAttribute exception */ public function testStatusInvalid(): void { $this->expectException(\Exception::class); $user = new User( [ 'email' => 'user@email.com', 'status' => 1234567, ] ); } + + /** + * Test basic User funtionality + */ + public function testStatusText(): void + { + $user = new User(['email' => 'user@email.com']); + + $this->assertSame('', $user->statusText()); + + $user->status = User::STATUS_NEW + | User::STATUS_ACTIVE + | User::STATUS_SUSPENDED + | User::STATUS_DELETED + | User::STATUS_IMAP_READY + | User::STATUS_LDAP_READY + | User::STATUS_DEGRADED + | User::STATUS_RESTRICTED; + + $expected = [ + 'new (1)', + 'active (2)', + 'suspended (4)', + 'deleted (8)', + 'ldapReady (16)', + 'imapReady (32)', + 'degraded (64)', + 'restricted (128)', + ]; + + $this->assertSame(implode(', ', $expected), $user->statusText()); + } }