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']);
}
/**