diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php index e338a4ee..3013d2b9 100644 --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -1,510 +1,534 @@ namespace},{$hostedRootDN}"; $aci = [ '(targetattr = "*")' . '(version 3.0; acl "Deny Unauthorized"; deny (all)' . '(userdn != "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)") ' . 'AND NOT roledn = "ldap:///cn=kolab-admin,' . $mgmtRootDN . '";)', '(targetattr != "userPassword")' . '(version 3.0;acl "Search Access";allow (read,compare,search)' . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)");)', '(targetattr = "*")' . '(version 3.0;acl "Kolab Administrators";allow (all)' . '(roledn = "ldap:///cn=kolab-admin,' . $domainBaseDN . ' || ldap:///cn=kolab-admin,' . $mgmtRootDN . '");)' ]; $entry = [ 'aci' => $aci, 'associateddomain' => $domain->namespace, 'inetdomainbasedn' => $domainBaseDN, 'objectclass' => [ 'top', 'domainrelatedobject', 'inetdomain' ], ]; $dn = "associateddomain={$domain->namespace},{$config['domain_base_dn']}"; if (!$ldap->get_entry($dn)) { $ldap->add_entry($dn, $entry); } // create ou, roles, ous $entry = [ 'description' => $domain->namespace, 'objectclass' => [ 'top', 'organizationalunit' ], 'ou' => $domain->namespace, ]; $entry['aci'] = array( '(targetattr = "*")' . '(version 3.0;acl "Deny Unauthorized"; deny (all)' . '(userdn != "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)") ' . 'AND NOT roledn = "ldap:///cn=kolab-admin,' . $mgmtRootDN . '";)', '(targetattr != "userPassword")' . '(version 3.0;acl "Search Access";allow (read,compare,search,write)' . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)");)', '(targetattr = "*")' . '(version 3.0;acl "Kolab Administrators";allow (all)' . '(roledn = "ldap:///cn=kolab-admin,' . $domainBaseDN . ' || ldap:///cn=kolab-admin,' . $mgmtRootDN . '");)', '(target = "ldap:///ou=*,' . $domainBaseDN . '")' . '(targetattr="objectclass || aci || ou")' . '(version 3.0;acl "Allow Domain sub-OU Registration"; allow (add)' . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)', '(target = "ldap:///uid=*,ou=People,' . $domainBaseDN . '")(targetattr="*")' . '(version 3.0;acl "Allow Domain First User Registration"; allow (add)' . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)', '(target = "ldap:///cn=*,' . $domainBaseDN . '")(targetattr="objectclass || cn")' . '(version 3.0;acl "Allow Domain Role Registration"; allow (add)' . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)', ); if (!$ldap->get_entry($domainBaseDN)) { $ldap->add_entry($domainBaseDN, $entry); } foreach (['Groups', 'People', 'Resources', 'Shared Folders'] as $item) { if (!$ldap->get_entry("ou={$item},{$domainBaseDN}")) { $ldap->add_entry( "ou={$item},{$domainBaseDN}", [ 'ou' => $item, 'description' => $item, 'objectclass' => [ 'top', 'organizationalunit' ] ] ); } } foreach (['kolab-admin', 'billing-user'] as $item) { if (!$ldap->get_entry("cn={$item},{$domainBaseDN}")) { $ldap->add_entry( "cn={$item},{$domainBaseDN}", [ 'cn' => $item, 'description' => "{$item} role", 'objectclass' => [ 'top', 'ldapsubentry', 'nsmanagedroledefinition', 'nsroledefinition', 'nssimpleroledefinition' ] ] ); } } $ldap->close(); } /** * Create a user in LDAP. * * Only need to add user if in any of the local domains? Figure that out here for now. Should * have Context-Based Access Controls before the job is queued though, probably. * * Use one of three modes; * * 1) The authenticated user account. * * * Only valid if the authenticated user is a domain admin. * * We don't know the originating user here. * * We certainly don't have its password anymore. * * 2) The hosted kolab account. * * 3) The Directory Manager account. * * @param \App\User $user The user account to create. * * @return bool|void */ public static function createUser(User $user) { $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - list($_local, $_domain) = explode('@', $user->email, 2); - - $domain = $ldap->find_domain($_domain); - - if (!$domain) { - return false; - } - $entry = [ 'objectclass' => [ 'top', 'inetorgperson', 'inetuser', 'kolabinetorgperson', 'mailrecipient', 'person' ], 'mail' => $user->email, 'uid' => $user->email, 'nsroledn' => [] ]; - self::setUserAttributes($user, $entry); - - $base_dn = $ldap->domain_root_dn($_domain); - $dn = "uid={$user->email},ou=People,{$base_dn}"; + if (!self::getUserEntry($ldap, $user->email, $dn) && $dn) { + self::setUserAttributes($user, $entry); - if (!$ldap->get_entry($dn)) { $ldap->add_entry($dn, $entry); } $ldap->close(); } /** * Update a domain in LDAP. * * @param \App\Domain $domain The domain to update. * * @return void */ - public static function updateDomain($domain) + public static function updateDomain(Domain $domain) { $config = self::getConfig('admin'); $ldap = self::initLDAP($config); $ldapDomain = $ldap->find_domain($domain->namespace); $oldEntry = $ldap->get_entry($ldapDomain['dn']); $newEntry = $oldEntry; self::setDomainAttributes($domain, $newEntry); $ldap->modify_entry($ldapDomain['dn'], $oldEntry, $newEntry); $ldap->close(); } - public static function deleteDomain($domain) + /** + * Delete a domain from LDAP. + * + * @param \App\Domain $domain The domain to update. + * + * @return void + */ + public static function deleteDomain(Domain $domain) { $config = self::getConfig('admin'); $ldap = self::initLDAP($config); $hostedRootDN = \config('ldap.hosted.root_dn'); $mgmtRootDN = \config('ldap.admin.root_dn'); $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}"; if ($ldap->get_entry($domainBaseDN)) { $ldap->delete_entry_recursive($domainBaseDN); } if ($ldap_domain = $ldap->find_domain($domain->namespace)) { if ($ldap->get_entry($ldap_domain['dn'])) { $ldap->delete_entry($ldap_domain['dn']); } } $ldap->close(); } - public static function deleteUser($user) + /** + * Delete a user from LDAP. + * + * @param \App\User $user The user account to update. + * + * @return void + */ + public static function deleteUser(User $user) { $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - list($_local, $_domain) = explode('@', $user->email, 2); - - $domain = $ldap->find_domain($_domain); - - if (!$domain) { - $ldap->close(); - return false; + if (self::getUserEntry($ldap, $user->email, $dn)) { + $ldap->delete_entry($dn); } - $base_dn = $ldap->domain_root_dn($_domain); - $dn = "uid={$user->email},ou=People,{$base_dn}"; + $ldap->close(); + } - if (!$ldap->get_entry($dn)) { - $ldap->close(); - return false; - } + /** + * Get a user data from LDAP. + * + * @param string $email The user email. + * + * @return array|false|null + */ + public static function getUser(string $email) + { + $config = self::getConfig('admin'); + $ldap = self::initLDAP($config); - $ldap->delete_entry($dn); + $user = self::getUserEntry($ldap, $email, $dn, true); $ldap->close(); + + return $user; } /** * Update a user in LDAP. * * @param \App\User $user The user account to update. * - * @return bool|void + * @return false|void */ public static function updateUser(User $user) { $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - list($_local, $_domain) = explode('@', $user->email, 2); + $newEntry = $oldEntry = self::getUserEntry($ldap, $user->email, $dn, true); - $domain = $ldap->find_domain($_domain); + if ($oldEntry) { + self::setUserAttributes($user, $newEntry); - if (!$domain) { + $ldap->modify_entry($dn, $oldEntry, $newEntry); $ldap->close(); - return false; - } - - $base_dn = $ldap->domain_root_dn($_domain); - $dn = "uid={$user->email},ou=People,{$base_dn}"; - - $oldEntry = $ldap->get_entry($dn); - - if (!$oldEntry) { + } else { $ldap->close(); return false; } - - if (!array_key_exists('nsroledn', $oldEntry)) { - $roles = $ldap->get_entry_attributes($dn, ['nsroledn']); - if (!empty($roles)) { - $oldEntry['nsroledn'] = (array)$roles['nsroledn']; - } - } - - $newEntry = $oldEntry; - - self::setUserAttributes($user, $newEntry); - - $ldap->modify_entry($dn, $oldEntry, $newEntry); - - $ldap->close(); } /** * Initialize connection to LDAP */ private static function initLDAP(array $config, string $privilege = 'admin') { $ldap = new \Net_LDAP3($config); $ldap->connect(); $ldap->bind(\config("ldap.{$privilege}.bind_dn"), \config("ldap.{$privilege}.bind_pw")); // TODO: error handling return $ldap; } /** * Set domain attributes */ private static function setDomainAttributes(Domain $domain, array &$entry) { $entry['inetdomainstatus'] = $domain->status; } /** * Set common user attributes */ private static function setUserAttributes(User $user, array &$entry) { $firstName = $user->getSetting('first_name'); $lastName = $user->getSetting('last_name'); $cn = "unknown"; $displayname = ""; if ($firstName) { if ($lastName) { $cn = "{$firstName} {$lastName}"; $displayname = "{$lastName}, {$firstName}"; } else { $lastName = "unknown"; $cn = "{$firstName}"; $displayname = "{$firstName}"; } } else { $firstName = ""; if ($lastName) { $cn = "{$lastName}"; $displayname = "{$lastName}"; } else { $lastName = "unknown"; } } $entry['cn'] = $cn; $entry['displayname'] = $displayname; $entry['givenname'] = $firstName; $entry['sn'] = $lastName; $entry['userpassword'] = $user->password_ldap; $entry['inetuserstatus'] = $user->status; $entry['mailquota'] = 0; + $entry['alias'] = $user->aliases->pluck('alias')->toArray(); + $roles = []; foreach ($user->entitlements as $entitlement) { \Log::debug("Examining {$entitlement->sku->title}"); switch ($entitlement->sku->title) { case "mailbox": break; case "storage": $entry['mailquota'] += 1048576; break; default: $roles[] = $entitlement->sku->title; break; } } $hostedRootDN = \config('ldap.hosted.root_dn'); if (empty($roles)) { if (array_key_exists('nsroledn', $entry)) { unset($entry['nsroledn']); } return; } $entry['nsroledn'] = []; if (in_array("2fa", $roles)) { $entry['nsroledn'][] = "cn=2fa-user,{$hostedRootDN}"; } if (in_array("activesync", $roles)) { $entry['nsroledn'][] = "cn=activesync-user,{$hostedRootDN}"; } if (!in_array("groupware", $roles)) { $entry['nsroledn'][] = "cn=imap-user,{$hostedRootDN}"; } if (empty($entry['nsroledn'])) { unset($entry['nsroledn']); } } /** * Get LDAP configuration for specified access level */ private static function getConfig(string $privilege) { $config = [ 'domain_base_dn' => \config('ldap.domain_base_dn'), 'domain_filter' => \config('ldap.domain_filter'), 'domain_name_attribute' => \config('ldap.domain_name_attribute'), 'hosts' => \config('ldap.hosts'), 'sort' => false, 'vlv' => false, 'log_hook' => 'App\Backends\LDAP::logHook', ]; return $config; } + /** + * Get user entry from LDAP. + * + * @param \Net_LDAP3 $ldap Ldap connection + * @param string $email User email (uid) + * @param string $dn Reference to user DN + * @param bool $full Get extra attributes, e.g. nsroledn + * + * @return false|null|array User entry, False on error, NULL if not found + */ + protected static function getUserEntry($ldap, $email, &$dn = null, $full = false) + { + list($_local, $_domain) = explode('@', $email, 2); + + $domain = $ldap->find_domain($_domain); + + if (!$domain) { + return false; + } + + $base_dn = $ldap->domain_root_dn($_domain); + $dn = "uid={$email},ou=People,{$base_dn}"; + + $entry = $ldap->get_entry($dn); + + if ($entry && $full) { + if (!array_key_exists('nsroledn', $entry)) { + $roles = $ldap->get_entry_attributes($dn, ['nsroledn']); + if (!empty($roles)) { + $entry['nsroledn'] = (array) $roles['nsroledn']; + } + } + } + + return $entry ?: null; + } + /** * Logging callback */ public static function logHook($level, $msg): void { if ( ( $level == LOG_INFO || $level == LOG_DEBUG || $level == LOG_NOTICE ) && !\config('app.debug') ) { return; } switch ($level) { case LOG_CRIT: $function = 'critical'; break; case LOG_EMERG: $function = 'emergency'; break; case LOG_ERR: $function = 'error'; break; case LOG_ALERT: $function = 'alert'; break; case LOG_WARNING: $function = 'warning'; break; case LOG_INFO: $function = 'info'; break; case LOG_DEBUG: $function = 'debug'; break; case LOG_NOTICE: $function = 'notice'; break; default: $function = 'info'; } if (is_array($msg)) { $msg = implode("\n", $msg); } $msg = '[LDAP] ' . $msg; \Log::{$function}($msg); } } diff --git a/src/app/Observers/UserAliasObserver.php b/src/app/Observers/UserAliasObserver.php index 1e372317..e66d4332 100644 --- a/src/app/Observers/UserAliasObserver.php +++ b/src/app/Observers/UserAliasObserver.php @@ -1,88 +1,64 @@ alias = \strtolower($alias->alias); if (User::where('email', $alias->alias)->first()) { \Log::error("Failed creating alias {$alias->alias}. User exists."); return false; } } /** * Handle the user alias "created" event. * * @param \App\UserAlias $alias User email alias * * @return void */ public function created(UserAlias $alias) { \App\Jobs\UserUpdate::dispatch($alias->user); } /** * Handle the user setting "updated" event. * * @param \App\UserAlias $alias User email alias * * @return void */ public function updated(UserAlias $alias) { \App\Jobs\UserUpdate::dispatch($alias->user); } /** * Handle the user setting "deleted" event. * * @param \App\UserAlias $alias User email alias * * @return void */ public function deleted(UserAlias $alias) { \App\Jobs\UserUpdate::dispatch($alias->user); } - - /** - * Handle the user alias "restored" event. - * - * @param \App\UserAlias $alias User email alias - * - * @return void - */ - public function restored(UserAlias $alias) - { - // not used - } - - /** - * Handle the user alias "force deleted" event. - * - * @param \App\UserAlias $alias User email alias - * - * @return void - */ - public function forceDeleted(UserAlias $alias) - { - // not used - } } diff --git a/src/app/Traits/UserAliasesTrait.php b/src/app/Traits/UserAliasesTrait.php index d0323dff..a2591edd 100644 --- a/src/app/Traits/UserAliasesTrait.php +++ b/src/app/Traits/UserAliasesTrait.php @@ -1,41 +1,40 @@ 'some@other.erg']); + * $user = User::firstOrCreate(['email' => 'some@other.org']); * $user->setAliases(['alias1@other.org', 'alias2@other.org']); * ``` * * @param array $aliases An array of email addresses * * @return void */ public function setAliases(array $aliases): void { - $existing_aliases = $this->aliases()->get()->map(function ($alias) { - return $alias->alias; - })->toArray(); - $aliases = array_map('strtolower', $aliases); $aliases = array_unique($aliases); - foreach (array_diff($aliases, $existing_aliases) as $alias) { - $this->aliases()->create(['alias' => $alias]); + $existing_aliases = []; + + foreach ($this->aliases()->get() as $alias) { + if (!in_array($alias->alias, $aliases)) { + $alias->delete(); + } else { + $existing_aliases[] = $alias->alias; + } } - foreach (array_diff($existing_aliases, $aliases) as $alias) { - $this->aliases()->where('alias', $alias)->delete(); + foreach (array_diff($aliases, $existing_aliases) as $alias) { + $this->aliases()->create(['alias' => $alias]); } } } diff --git a/src/tests/Feature/Jobs/UserCreateTest.php b/src/tests/Feature/Jobs/UserCreateTest.php index 49287390..fa9b3a8d 100644 --- a/src/tests/Feature/Jobs/UserCreateTest.php +++ b/src/tests/Feature/Jobs/UserCreateTest.php @@ -1,45 +1,43 @@ deleteTestUser('new-job-user@' . \config('app.domain')); } public function tearDown(): void { $this->deleteTestUser('new-job-user@' . \config('app.domain')); parent::tearDown(); } /** * Test job handle * * @group ldap */ public function testHandle(): void { $user = $this->getTestUser('new-job-user@' . \config('app.domain')); $this->assertFalse($user->isLdapReady()); $job = new UserCreate($user); $job->handle(); $this->assertTrue($user->fresh()->isLdapReady()); } } diff --git a/src/tests/Feature/Jobs/UserUpdateTest.php b/src/tests/Feature/Jobs/UserUpdateTest.php new file mode 100644 index 00000000..120a0ffd --- /dev/null +++ b/src/tests/Feature/Jobs/UserUpdateTest.php @@ -0,0 +1,83 @@ +deleteTestUser('new-job-user@' . \config('app.domain')); + } + + public function tearDown(): void + { + $this->deleteTestUser('new-job-user@' . \config('app.domain')); + + parent::tearDown(); + } + + /** + * Test job handle + * + * @group ldap + */ + public function testHandle(): void + { + // Ignore any jobs created here (e.g. on setAliases() use) + Queue::fake(); + + $user = $this->getTestUser('new-job-user@' . \config('app.domain')); + + // Create the user in LDAP + $job = new \App\Jobs\UserCreate($user); + $job->handle(); + + // Test setting two aliases + $aliases = [ + 'new-job-user1@' . \config('app.domain'), + 'new-job-user2@' . \config('app.domain'), + ]; + $user->setAliases($aliases); + + $job = new UserUpdate($user->fresh()); + $job->handle(); + + $ldap_user = LDAP::getUser('new-job-user@' . \config('app.domain')); + + $this->assertSame($aliases, $ldap_user['alias']); + + // Test updating aliases list + $aliases = [ + 'new-job-user1@' . \config('app.domain'), + ]; + $user->setAliases($aliases); + + $job = new UserUpdate($user->fresh()); + $job->handle(); + + $ldap_user = LDAP::getUser('new-job-user@' . \config('app.domain')); + + $this->assertSame($aliases, (array) $ldap_user['alias']); + + // Test unsetting aliases list + $aliases = []; + $user->setAliases($aliases); + + $job = new UserUpdate($user->fresh()); + $job->handle(); + + $ldap_user = LDAP::getUser('new-job-user@' . \config('app.domain')); + + $this->assertTrue(empty($ldap_user['alias'])); + } +} diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php index 061781fd..8258eaf3 100644 --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -1,374 +1,381 @@ deleteTestUser('user-create-test@' . \config('app.domain')); $this->deleteTestUser('userdeletejob@kolabnow.com'); $this->deleteTestUser('UserAccountA@UserAccount.com'); $this->deleteTestUser('UserAccountB@UserAccount.com'); $this->deleteTestUser('UserAccountC@UserAccount.com'); $this->deleteTestDomain('UserAccount.com'); } public function tearDown(): void { $this->deleteTestUser('user-create-test@' . \config('app.domain')); $this->deleteTestUser('userdeletejob@kolabnow.com'); $this->deleteTestUser('UserAccountA@UserAccount.com'); $this->deleteTestUser('UserAccountB@UserAccount.com'); $this->deleteTestUser('UserAccountC@UserAccount.com'); $this->deleteTestDomain('UserAccount.com'); parent::tearDown(); } /** * Tests for User::assignPackage() */ public function testAssignPackage(): void { $this->markTestIncomplete(); } /** * Tests for User::assignPlan() */ public function testAssignPlan(): void { $this->markTestIncomplete(); } /** * Tests for User::assignSku() */ public function testAssignSku(): void { $this->markTestIncomplete(); } /** * Verify user creation process */ public function testUserCreateJob(): void { // Fake the queue, assert that no jobs were pushed... Queue::fake(); Queue::assertNothingPushed(); $user = User::create([ 'email' => 'user-create-test@' . \config('app.domain') ]); Queue::assertPushed(\App\Jobs\UserCreate::class, 1); Queue::assertPushed(\App\Jobs\UserCreate::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id && $job_user->email === $user->email; }); Queue::assertPushedWithChain(\App\Jobs\UserCreate::class, [ \App\Jobs\UserVerify::class, ]); /* FIXME: Looks like we can't really do detailed assertions on chained jobs Another thing to consider is if we maybe should run these jobs independently (not chained) and make sure there's no race-condition in status update Queue::assertPushed(\App\Jobs\UserVerify::class, 1); Queue::assertPushed(\App\Jobs\UserVerify::class, function ($job) use ($user) { $job_user = TestCase::getObjectProperty($job, 'user'); return $job_user->id === $user->id && $job_user->email === $user->email; }); */ } /** * Verify a wallet assigned a controller is among the accounts of the assignee. */ public function testListUserAccounts(): void { $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $this->assertTrue($userA->wallets()->count() == 1); $userA->wallets()->each( function ($wallet) use ($userB) { $wallet->addController($userB); } ); $this->assertTrue($userB->accounts()->get()[0]->id === $userA->wallets()->get()[0]->id); } public function testAccounts(): void { $this->markTestIncomplete(); } public function testCanDelete(): void { $this->markTestIncomplete(); } public function testCanRead(): void { $this->markTestIncomplete(); } public function testCanUpdate(): void { $this->markTestIncomplete(); } /** * Tests for User::domains() */ public function testDomains(): void { $user = $this->getTestUser('john@kolab.org'); $domains = []; foreach ($user->domains() as $domain) { $domains[] = $domain->namespace; } $this->assertContains(\config('app.domain'), $domains); $this->assertContains('kolab.org', $domains); // Jack is not the wallet controller, so for him the list should not // include John's domains, kolab.org specifically $user = $this->getTestUser('jack@kolab.org'); $domains = []; foreach ($user->domains() as $domain) { $domains[] = $domain->namespace; } $this->assertContains(\config('app.domain'), $domains); $this->assertNotContains('kolab.org', $domains); } public function testUserQuota(): void { // TODO: This test does not test much, probably could be removed // or moved to somewhere else, or extended with // other entitlements() related cases. $user = $this->getTestUser('john@kolab.org'); $storage_sku = \App\Sku::where('title', 'storage')->first(); $count = 0; foreach ($user->entitlements()->get() as $entitlement) { if ($entitlement->sku_id == $storage_sku->id) { $count += 1; } } $this->assertTrue($count == 2); } /** * Test user deletion */ public function testDelete(): void { Queue::fake(); $user = $this->getTestUser('userdeletejob@kolabnow.com'); $package = \App\Package::where('title', 'kolab')->first(); $user->assignPackage($package); $id = $user->id; $this->assertCount(4, $user->entitlements()->get()); $user->delete(); $this->assertCount(0, $user->entitlements()->get()); $this->assertTrue($user->fresh()->trashed()); $this->assertFalse($user->fresh()->isDeleted()); // Delete the user for real $job = new \App\Jobs\UserDelete($id); $job->handle(); $this->assertTrue(User::withTrashed()->where('id', $id)->first()->isDeleted()); $user->forceDelete(); $this->assertCount(0, User::withTrashed()->where('id', $id)->get()); // Test an account with users $userA = $this->getTestUser('UserAccountA@UserAccount.com'); $userB = $this->getTestUser('UserAccountB@UserAccount.com'); $userC = $this->getTestUser('UserAccountC@UserAccount.com'); $package_kolab = \App\Package::where('title', 'kolab')->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $domain = $this->getTestDomain('UserAccount.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_HOSTED, ]); $userA->assignPackage($package_kolab); $domain->assignPackage($package_domain, $userA); $userA->assignPackage($package_kolab, $userB); $userA->assignPackage($package_kolab, $userC); $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id); $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id); $entitlementsC = \App\Entitlement::where('entitleable_id', $userC->id); $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id); $this->assertSame(4, $entitlementsA->count()); $this->assertSame(4, $entitlementsB->count()); $this->assertSame(4, $entitlementsC->count()); $this->assertSame(1, $entitlementsDomain->count()); // Delete non-controller user $userC->delete(); $this->assertTrue($userC->fresh()->trashed()); $this->assertFalse($userC->fresh()->isDeleted()); $this->assertSame(0, $entitlementsC->count()); // Delete the controller (and expect "sub"-users to be deleted too) $userA->delete(); $this->assertSame(0, $entitlementsA->count()); $this->assertSame(0, $entitlementsB->count()); $this->assertSame(0, $entitlementsDomain->count()); $this->assertTrue($userA->fresh()->trashed()); $this->assertTrue($userB->fresh()->trashed()); $this->assertTrue($domain->fresh()->trashed()); $this->assertFalse($userA->isDeleted()); $this->assertFalse($userB->isDeleted()); $this->assertFalse($domain->isDeleted()); } /** * Tests for User::findByEmail() */ public function testFindByEmail(): void { $user = $this->getTestUser('john@kolab.org'); $result = User::findByEmail('john'); $this->assertNull($result); $result = User::findByEmail('non-existing@email.com'); $this->assertNull($result); $result = User::findByEmail('john@kolab.org'); $this->assertInstanceOf(User::class, $result); $this->assertSame($user->id, $result->id); // Use an alias $result = User::findByEmail('john.doe@kolab.org'); $this->assertInstanceOf(User::class, $result); $this->assertSame($user->id, $result->id); // TODO: searching by external email (setting) $this->markTestIncomplete(); } /** * Tests for UserAliasesTrait::setAliases() */ public function testSetAliases(): void { Queue::fake(); + Queue::assertNothingPushed(); $user = $this->getTestUser('UserAccountA@UserAccount.com'); $this->assertCount(0, $user->aliases->all()); // Add an alias $user->setAliases(['UserAlias1@UserAccount.com']); + Queue::assertPushed(\App\Jobs\UserUpdate::class, 1); + $aliases = $user->aliases()->get(); $this->assertCount(1, $aliases); $this->assertSame('useralias1@useraccount.com', $aliases[0]['alias']); // Add another alias $user->setAliases(['UserAlias1@UserAccount.com', 'UserAlias2@UserAccount.com']); + Queue::assertPushed(\App\Jobs\UserUpdate::class, 2); + $aliases = $user->aliases()->orderBy('alias')->get(); $this->assertCount(2, $aliases); $this->assertSame('useralias1@useraccount.com', $aliases[0]->alias); $this->assertSame('useralias2@useraccount.com', $aliases[1]->alias); // Remove an alias $user->setAliases(['UserAlias1@UserAccount.com']); + Queue::assertPushed(\App\Jobs\UserUpdate::class, 3); + $aliases = $user->aliases()->get(); $this->assertCount(1, $aliases); $this->assertSame('useralias1@useraccount.com', $aliases[0]['alias']); // Remove all aliases $user->setAliases([]); - $this->assertCount(0, $user->aliases()->get()); + Queue::assertPushed(\App\Jobs\UserUpdate::class, 4); - // TODO: Test that the changes are propagated to ldap + $this->assertCount(0, $user->aliases()->get()); } /** * Tests for UserSettingsTrait::setSettings() */ public function testSetSettings(): void { $this->markTestIncomplete(); } /** * Tests for User::users() */ public function testUsers(): void { $jack = $this->getTestUser('jack@kolab.org'); $joe = $this->getTestUser('joe@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $wallet = $john->wallets()->first(); $users = $john->users()->orderBy('email')->get(); $this->assertCount(4, $users); $this->assertEquals($jack->id, $users[0]->id); $this->assertEquals($joe->id, $users[1]->id); $this->assertEquals($john->id, $users[2]->id); $this->assertEquals($ned->id, $users[3]->id); $this->assertSame($wallet->id, $users[0]->wallet_id); $this->assertSame($wallet->id, $users[1]->wallet_id); $this->assertSame($wallet->id, $users[2]->wallet_id); $this->assertSame($wallet->id, $users[3]->wallet_id); $users = $jack->users()->orderBy('email')->get(); $this->assertCount(0, $users); $users = $ned->users()->orderBy('email')->get(); $this->assertCount(4, $users); } public function testWallets(): void { $this->markTestIncomplete(); } }