diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -399,7 +399,7 @@ public function wallet(): ?Wallet { // Note: Not all domains have a entitlement/wallet - $entitlement = $this->entitlement()->first(); + $entitlement = $this->entitlement()->withTrashed()->first(); return $entitlement ? $entitlement->wallet : null; } diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php --- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php @@ -29,32 +29,34 @@ } } elseif (strpos($search, '@')) { // Search by email - $user = User::where('email', $search)->first(); - if ($user) { - $result->push($user); - } else { + $result = User::withTrashed()->where('email', $search) + ->orderBy('email')->get(); + + if ($result->isEmpty()) { // Search by an alias $user_ids = UserAlias::where('alias', $search)->get()->pluck('user_id'); - if ($user_ids->isEmpty()) { - // Search by an external email - $user_ids = UserSetting::where('key', 'external_email') - ->where('value', $search)->get()->pluck('user_id'); - } + + // Search by an external email + $ext_user_ids = UserSetting::where('key', 'external_email') + ->where('value', $search)->get()->pluck('user_id'); + + $user_ids = $user_ids->merge($ext_user_ids)->unique(); if (!$user_ids->isEmpty()) { - $result = User::whereIn('id', $user_ids)->orderBy('email')->get(); + $result = User::withTrashed()->whereIn('id', $user_ids) + ->orderBy('email')->get(); } } } elseif (is_numeric($search)) { // Search by user ID - if ($user = User::find($search)) { + if ($user = User::withTrashed()->find($search)) { $result->push($user); } } elseif (!empty($search)) { // Search by domain - if ($domain = Domain::where('namespace', $search)->first()) { + if ($domain = Domain::withTrashed()->where('namespace', $search)->first()) { if ($wallet = $domain->wallet()) { - $result->push($wallet->owner); + $result->push($wallet->owner()->withTrashed()->first()); } } } diff --git a/src/resources/vue/Admin/Dashboard.vue b/src/resources/vue/Admin/Dashboard.vue --- a/src/resources/vue/Admin/Dashboard.vue +++ b/src/resources/vue/Admin/Dashboard.vue @@ -15,17 +15,23 @@ Primary Email ID + Created + Deleted - - + + - {{ user.email }} + {{ user.email }} + {{ user.email }} - {{ user.id }} + {{ user.id }} + {{ user.id }} + {{ toDate(user.created_at) }} + {{ toDate(user.deleted_at) }} @@ -65,7 +71,7 @@ axios.get('/api/v4/users', { params: { search: this.search } }) .then(response => { - if (response.data.count == 1) { + if (response.data.count == 1 && !response.data.list[0].isDeleted) { this.$router.push({ name: 'user', params: { user: response.data.list[0].id } }) return } @@ -77,6 +83,11 @@ this.users = response.data.list }) .catch(this.$root.errorHandler) + }, + toDate(datetime) { + if (datetime) { + return datetime.split(' ')[0] + } } } } diff --git a/src/tests/Browser.php b/src/tests/Browser.php --- a/src/tests/Browser.php +++ b/src/tests/Browser.php @@ -119,7 +119,24 @@ { $element = $this->resolver->findOrFail($selector); - Assert::assertTrue(strpos($element->getText(), $text) !== false, "No expected text in [$selector]"); + if ($text === '') { + Assert::assertTrue((string) $element->getText() === $text, "Element's text is not empty [$selector]"); + } else { + Assert::assertTrue(strpos($element->getText(), $text) !== false, "No expected text in [$selector]"); + } + + return $this; + } + + /** + * Assert that the given element contains specified text, + * no matter it's displayed or not - using a regular expression. + */ + public function assertTextRegExp($selector, $regexp) + { + $element = $this->resolver->findOrFail($selector); + + Assert::assertRegExp($regexp, $element->getText(), "No expected text in [$selector]"); return $this; } diff --git a/src/tests/Browser/Admin/DashboardTest.php b/src/tests/Browser/Admin/DashboardTest.php --- a/src/tests/Browser/Admin/DashboardTest.php +++ b/src/tests/Browser/Admin/DashboardTest.php @@ -2,12 +2,12 @@ namespace Tests\Browser\Admin; +use Illuminate\Support\Facades\Queue; use Tests\Browser; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; use Tests\TestCaseDusk; -use Illuminate\Foundation\Testing\DatabaseMigrations; class DashboardTest extends TestCaseDusk { @@ -21,6 +21,9 @@ $jack = $this->getTestUser('jack@kolab.org'); $jack->setSetting('external_email', null); + + $this->deleteTestUser('test@testsearch.com'); + $this->deleteTestDomain('testsearch.com'); } /** @@ -31,6 +34,9 @@ $jack = $this->getTestUser('jack@kolab.org'); $jack->setSetting('external_email', null); + $this->deleteTestUser('test@testsearch.com'); + $this->deleteTestDomain('testsearch.com'); + parent::tearDown(); } @@ -60,9 +66,24 @@ $browser->type('@search input', 'john.doe.external@gmail.com') ->click('@search form button') ->assertToast(Toast::TYPE_INFO, '2 user accounts have been found.') - ->whenAvailable('@search table', function (Browser $browser) { - $browser->assertElementsCount('tbody tr', 2); - // TODO: Assert table content + ->whenAvailable('@search table', function (Browser $browser) use ($john, $jack) { + $browser->assertElementsCount('tbody tr', 2) + ->with('tbody tr:first-child', function (Browser $browser) use ($jack) { + $browser->assertSeeIn('td:nth-child(1) a', $jack->email) + ->assertSeeIn('td:nth-child(2) a', $jack->id) + ->assertVisible('td:nth-child(3)') + ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') + ->assertVisible('td:nth-child(4)') + ->assertText('td:nth-child(4)', ''); + }) + ->with('tbody tr:last-child', function (Browser $browser) use ($john) { + $browser->assertSeeIn('td:nth-child(1) a', $john->email) + ->assertSeeIn('td:nth-child(2) a', $john->id) + ->assertVisible('td:nth-child(3)') + ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') + ->assertVisible('td:nth-child(4)') + ->assertText('td:nth-child(4)', ''); + }); }); // Test search with single record result -> redirect to user page @@ -76,4 +97,44 @@ }); }); } + + /** + * Test user search deleted user/domain + */ + public function testSearchDeleted(): void + { + $this->browse(function (Browser $browser) { + $browser->visit(new Home()) + ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) + ->on(new Dashboard()) + ->assertFocused('@search input') + ->assertMissing('@search table'); + + // Deleted users/domains + $domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]); + $user = $this->getTestUser('test@testsearch.com'); + $plan = \App\Plan::where('title', 'group')->first(); + $user->assignPlan($plan, $domain); + $user->setAliases(['alias@testsearch.com']); + Queue::fake(); + $user->delete(); + + // Test search with multiple results + $browser->type('@search input', 'testsearch.com') + ->click('@search form button') + ->assertToast(Toast::TYPE_INFO, '1 user accounts have been found.') + ->whenAvailable('@search table', function (Browser $browser) use ($user) { + $browser->assertElementsCount('tbody tr', 1) + ->assertVisible('tbody tr:first-child.text-secondary') + ->with('tbody tr:first-child', function (Browser $browser) use ($user) { + $browser->assertSeeIn('td:nth-child(1) span', $user->email) + ->assertSeeIn('td:nth-child(2) span', $user->id) + ->assertVisible('td:nth-child(3)') + ->assertTextRegExp('td:nth-child(3)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/') + ->assertVisible('td:nth-child(4)') + ->assertTextRegExp('td:nth-child(4)', '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/'); + }); + }); + }); + } } diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php --- a/src/tests/Feature/Controller/Admin/UsersTest.php +++ b/src/tests/Feature/Controller/Admin/UsersTest.php @@ -18,6 +18,8 @@ self::useAdminUrl(); $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); + $this->deleteTestUser('test@testsearch.com'); + $this->deleteTestDomain('testsearch.com'); $jack = $this->getTestUser('jack@kolab.org'); $jack->setSetting('external_email', null); @@ -29,6 +31,8 @@ public function tearDown(): void { $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); + $this->deleteTestUser('test@testsearch.com'); + $this->deleteTestDomain('testsearch.com'); $jack = $this->getTestUser('jack@kolab.org'); $jack->setSetting('external_email', null); @@ -146,6 +150,48 @@ $this->assertSame(0, $json['count']); $this->assertCount(0, $json['list']); + + // Deleted users/domains + $domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]); + $user = $this->getTestUser('test@testsearch.com'); + $plan = \App\Plan::where('title', 'group')->first(); + $user->assignPlan($plan, $domain); + $user->setAliases(['alias@testsearch.com']); + Queue::fake(); + $user->delete(); + + $response = $this->actingAs($admin)->get("api/v4/users?search=test@testsearch.com"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame($user->id, $json['list'][0]['id']); + $this->assertSame($user->email, $json['list'][0]['email']); + $this->assertTrue($json['list'][0]['isDeleted']); + + $response = $this->actingAs($admin)->get("api/v4/users?search=alias@testsearch.com"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame($user->id, $json['list'][0]['id']); + $this->assertSame($user->email, $json['list'][0]['email']); + $this->assertTrue($json['list'][0]['isDeleted']); + + $response = $this->actingAs($admin)->get("api/v4/users?search=testsearch.com"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame($user->id, $json['list'][0]['id']); + $this->assertSame($user->email, $json['list'][0]['email']); + $this->assertTrue($json['list'][0]['isDeleted']); } /**