Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117753473
D2966.1775194412.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
38 KB
Referenced Files
None
Subscribers
None
D2966.1775194412.diff
View Options
diff --git a/src/.eslintrc.js b/src/.eslintrc.js
--- a/src/.eslintrc.js
+++ b/src/.eslintrc.js
@@ -13,6 +13,7 @@
"vue/html-indent": ["error", 4],
"vue/html-self-closing": "off",
"vue/max-attributes-per-line": "off",
+ "vue/no-unused-components": "off",
"vue/no-v-html": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline": "off"
diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php
--- a/src/app/Http/Controllers/API/V4/PaymentsController.php
+++ b/src/app/Http/Controllers/API/V4/PaymentsController.php
@@ -406,9 +406,10 @@
$exists = Payment::where('wallet_id', $wallet->id)
->where('type', PaymentProvider::TYPE_ONEOFF)
->whereIn('status', [
- PaymentProvider::STATUS_OPEN,
- PaymentProvider::STATUS_PENDING,
- PaymentProvider::STATUS_AUTHORIZED])
+ PaymentProvider::STATUS_OPEN,
+ PaymentProvider::STATUS_PENDING,
+ PaymentProvider::STATUS_AUTHORIZED
+ ])
->exists();
return response()->json([
@@ -437,9 +438,10 @@
$result = Payment::where('wallet_id', $wallet->id)
->where('type', PaymentProvider::TYPE_ONEOFF)
->whereIn('status', [
- PaymentProvider::STATUS_OPEN,
- PaymentProvider::STATUS_PENDING,
- PaymentProvider::STATUS_AUTHORIZED])
+ PaymentProvider::STATUS_OPEN,
+ PaymentProvider::STATUS_PENDING,
+ PaymentProvider::STATUS_AUTHORIZED
+ ])
->orderBy('created_at', 'desc')
->limit($pageSize + 1)
->offset($pageSize * ($page - 1))
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -76,12 +76,55 @@
public function index()
{
$user = $this->guard()->user();
+ $search = trim(request()->input('search'));
+ $page = intval(request()->input('page')) ?: 1;
+ $pageSize = 20;
+ $hasMore = false;
+
+ $result = $user->users();
+
+ // Search by user email, alias or name
+ if (strlen($search) > 0) {
+ // thanks to cloning we skip some extra queries in $user->users()
+ $allUsers1 = clone $result;
+ $allUsers2 = clone $result;
+
+ $result->whereLike('email', $search)
+ ->union(
+ $allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id')
+ ->whereLike('alias', $search)
+ )
+ ->union(
+ $allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id')
+ ->whereLike('value', $search)
+ ->whereIn('key', ['first_name', 'last_name'])
+ );
+ }
+
+ $result = $result->orderBy('email')
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ // Process the result
+ $result = $result->map(
+ function ($user) {
+ $data = $user->toArray();
+ $data = array_merge($data, self::userStatuses($user));
+ return $data;
+ }
+ );
- $result = $user->users()->orderBy('email')->get()->map(function ($user) {
- $data = $user->toArray();
- $data = array_merge($data, self::userStatuses($user));
- return $data;
- });
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ ];
return response()->json($result);
}
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -382,6 +382,7 @@
'reset-2fa' => "Reset 2-Factor Auth",
'reset-2fa-title' => "2-Factor Authentication Reset",
'title' => "User account",
+ 'search' => "User email address or name",
'search-pl' => "User ID, email or domain",
'skureq' => "{sku} requires {list}.",
'subscription' => "Subscription",
diff --git a/src/resources/vue/Reseller/Invitations.vue b/src/resources/vue/Reseller/Invitations.vue
--- a/src/resources/vue/Reseller/Invitations.vue
+++ b/src/resources/vue/Reseller/Invitations.vue
@@ -7,10 +7,7 @@
</div>
<div class="card-text">
<div class="mb-2 d-flex">
- <form @submit.prevent="searchInvitations" id="search-form" class="input-group" style="flex:1">
- <input class="form-control" type="text" :placeholder="$t('invitation.search')" v-model="search">
- <button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
- </form>
+ <list-search :placeholder="$t('invitation.search')" :on-search="searchInvitations"></list-search>
<div>
<button class="btn btn-success create-invite ms-1" @click="inviteUserDialog">
<svg-icon icon="envelope-open-text"></svg-icon> {{ $t('invitation.create') }}
@@ -47,15 +44,9 @@
</td>
</tr>
</tbody>
- <tfoot class="table-fake-body">
- <tr>
- <td colspan="3">{{ $t('invitation.empty-list') }}</td>
- </tr>
- </tfoot>
+ <list-foot :text="$t('invitation.empty-list')" colspan="3"></list-foot>
</table>
- <div class="text-center p-3" id="more-loader" v-if="hasMore">
- <button class="btn btn-secondary" @click="loadInvitations(true)">{{ $t('nav.more') }}</button>
- </div>
+ <list-more v-if="hasMore" :on-click="loadInvitations"></list-more>
</div>
</div>
</div>
@@ -96,21 +87,19 @@
import { Modal } from 'bootstrap'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faEnvelopeOpenText, faPaperPlane, faRedo } from '@fortawesome/free-solid-svg-icons'
+ import ListTools from '../Widgets/ListTools'
library.add(faEnvelopeOpenText, faPaperPlane, faRedo)
export default {
+ mixins: [ ListTools ],
data() {
return {
- invitations: [],
- hasMore: false,
- page: 1,
- search: ''
+ invitations: []
}
},
mounted() {
- this.$root.startLoading()
- this.loadInvitations(null, () => this.$root.stopLoading())
+ this.loadInvitations({ init: true })
$('#invite-create')[0].addEventListener('shown.bs.modal', event => {
$('input', event.target).first().focus()
@@ -179,54 +168,8 @@
this.dialog = new Modal(dialog)
this.dialog.show()
},
- loadInvitations(params, callback) {
- let loader
- let get = {}
-
- if (params) {
- if (params.reset) {
- this.invitations = []
- this.page = 0
- }
-
- get.page = params.page || (this.page + 1)
-
- if (typeof params === 'object' && 'search' in params) {
- get.search = params.search
- this.currentSearch = params.search
- } else {
- get.search = this.currentSearch
- }
-
- loader = $(get.page > 1 ? '#more-loader' : '#invitations-list tfoot td')
- } else {
- this.currentSearch = null
- }
-
- this.$root.addLoader(loader)
-
- axios.get('/api/v4/invitations', { params: get })
- .then(response => {
- this.$root.removeLoader(loader)
-
- // Note: In Vue we can't just use .concat()
- for (let i in response.data.list) {
- this.$set(this.invitations, this.invitations.length, response.data.list[i])
- }
- this.hasMore = response.data.hasMore
- this.page = response.data.page || 1
-
- if (callback) {
- callback()
- }
- })
- .catch(error => {
- this.$root.removeLoader(loader)
-
- if (callback) {
- callback()
- }
- })
+ loadInvitations(params) {
+ this.listSearch('invitations', '/api/v4/invitations', params)
},
resendInvite(id) {
axios.post('/api/v4/invitations/' + id + '/resend')
@@ -242,8 +185,8 @@
}
})
},
- searchInvitations() {
- this.loadInvitations({ reset: true, search: this.search })
+ searchInvitations(search) {
+ this.loadInvitations({ reset: true, search })
},
statusClass(invitation) {
if (invitation.isCompleted) {
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -4,12 +4,17 @@
<div class="card-body">
<div class="card-title">
{{ $t('user.list-title') }}
- <router-link class="btn btn-success float-end create-user" :to="{ path: 'user/new' }" tag="button">
- <svg-icon icon="user"></svg-icon> {{ $t('user.create') }}
- </router-link>
</div>
<div class="card-text">
- <table class="table table-sm table-hover">
+ <div class="mb-2 d-flex">
+ <list-search :placeholder="$t('user.search')" :on-search="searchUsers"></list-search>
+ <div>
+ <router-link class="btn btn-success ms-1 create-user" :to="{ path: 'user/new' }" tag="button">
+ <svg-icon icon="user"></svg-icon> {{ $t('user.create') }}
+ </router-link>
+ </div>
+ </div>
+ <table id="users-list" class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
@@ -23,12 +28,9 @@
</td>
</tr>
</tbody>
- <tfoot class="table-fake-body">
- <tr>
- <td>{{ $t('user.users-none') }}</td>
- </tr>
- </tfoot>
+ <list-foot :text="$t('user.users-none')"></list-foot>
</table>
+ <list-more v-if="hasMore" :on-click="loadUsers"></list-more>
</div>
</div>
</div>
@@ -36,22 +38,25 @@
</template>
<script>
+ import ListTools from '../Widgets/ListTools'
+
export default {
+ mixins: [ ListTools ],
data() {
return {
- users: [],
- current_user: null
+ users: []
}
},
- created() {
- this.$root.startLoading()
-
- axios.get('/api/v4/users')
- .then(response => {
- this.$root.stopLoading()
- this.users = response.data
- })
- .catch(this.$root.errorHandler)
+ mounted() {
+ this.loadUsers({ init: true })
+ },
+ methods: {
+ loadUsers(params) {
+ this.listSearch('users', '/api/v4/users', params)
+ },
+ searchUsers(search) {
+ this.loadUsers({ reset: true, search })
+ }
}
}
</script>
diff --git a/src/resources/vue/Widgets/ListTools.vue b/src/resources/vue/Widgets/ListTools.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/ListTools.vue
@@ -0,0 +1,116 @@
+<template>
+ <div></div>
+</template>
+
+<script>
+
+ const ListSearch = {
+ props: {
+ onSearch: { type: Function, default: () => {} },
+ placeholder: { type: String, default: '' }
+ },
+ data() {
+ return {
+ search: ''
+ }
+ },
+ template: `<form @submit.prevent="onSearch(search)" id="search-form" class="input-group" style="flex:1">
+ <input class="form-control" type="text" :placeholder="placeholder" v-model="search">
+ <button type="submit" class="btn btn-primary"><svg-icon icon="search"></svg-icon> {{ $t('btn.search') }}</button>
+ </form>`
+ }
+
+ const ListFoot = {
+ props: {
+ colspan: { type: Number, default: 1 },
+ text: { type: String, default: '' }
+ },
+ template: `<tfoot class="table-fake-body"><tr><td :colspan="colspan">{{ text }}</td></tr></tfoot>`
+ }
+
+ const ListMore = {
+ props: {
+ onClick: { type: Function, default: () => {} }
+ },
+ template: `<div class="text-center p-3 more-loader">
+ <button class="btn btn-secondary" @click="onClick({})">{{ $t('nav.more') }}</button>
+ </div>`
+ }
+
+ export default {
+ components: {
+ ListFoot,
+ ListMore,
+ ListSearch
+ },
+ data() {
+ return {
+ currentSearch: '',
+ hasMore: false,
+ page: 1
+ }
+ },
+ methods: {
+ listSearch(name, url, params) {
+ let loader
+ let get = {}
+
+ if (params) {
+ if (params.reset || params.init) {
+ this[name] = []
+ this.page = 0
+ }
+
+ get.page = params.page || (this.page + 1)
+
+ if ('search' in params) {
+ get.search = params.search
+ this.currentSearch = params.search
+ this.hasMore = false
+ } else {
+ get.search = this.currentSearch
+ }
+
+ if (!params.init) {
+ loader = $(this.$el).find('.more-loader')
+ if (!loader.length || get.page == 1) {
+ loader = $(this.$el).find('tfoot td')
+ }
+ }
+ } else {
+ this.currentSearch = null
+ }
+
+ if (params && params.init) {
+ this.$root.startLoading()
+ } else {
+ this.$root.addLoader(loader)
+ }
+
+ const finish = () => {
+ if (params && params.init) {
+ this.$root.stopLoading()
+ } else {
+ this.$root.removeLoader(loader)
+ }
+ }
+
+ axios.get(url, { params: get })
+ .then(response => {
+ // Note: In Vue we can't just use .concat()
+ for (let i in response.data.list) {
+ this.$set(this[name], this[name].length, response.data.list[i])
+ }
+
+ this.hasMore = response.data.hasMore
+ this.page = response.data.page || 1
+
+ finish()
+ })
+ .catch(error => {
+ finish()
+ })
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Widgets/PaymentLog.vue b/src/resources/vue/Widgets/PaymentLog.vue
--- a/src/resources/vue/Widgets/PaymentLog.vue
+++ b/src/resources/vue/Widgets/PaymentLog.vue
@@ -17,56 +17,28 @@
<td class="price text-success">{{ amount(payment) }}</td>
</tr>
</tbody>
- <tfoot class="table-fake-body">
- <tr>
- <td colspan="4">{{ $t('wallet.pending-payments-none') }}</td>
- </tr>
- </tfoot>
+ <list-foot :text="$t('wallet.pending-payments-none')" colspan="4"></list-foot>
</table>
- <div class="text-center p-3" id="payments-loader" v-if="hasMore">
- <button class="btn btn-secondary" @click="loadLog(true)">{{ $t('nav.more') }}</button>
- </div>
+ <list-more v-if="hasMore" :on-click="loadLog"></list-more>
</div>
</template>
<script>
+ import ListTools from './ListTools'
+
export default {
- props: {
- },
+ mixins: [ ListTools ],
data() {
return {
- payments: [],
- hasMore: false,
- page: 1
+ payments: []
}
},
mounted() {
- this.loadLog()
+ this.loadLog({ reset: true })
},
methods: {
- loadLog(more) {
- let loader = $(this.$el)
- let param = ''
-
- if (more) {
- param = '?page=' + (this.page + 1)
- loader = $('#payments-loader')
- }
-
- this.$root.addLoader(loader)
- axios.get('/api/v4/payments/pending' + param)
- .then(response => {
- this.$root.removeLoader(loader)
- // Note: In Vue we can't just use .concat()
- for (let i in response.data.list) {
- this.$set(this.payments, this.payments.length, response.data.list[i])
- }
- this.hasMore = response.data.hasMore
- this.page = response.data.page || 1
- })
- .catch(error => {
- this.$root.removeLoader(loader)
- })
+ loadLog(params) {
+ this.listSearch('payments', '/api/v4/payments/pending', params)
},
amount(payment) {
return this.$root.price(payment.amount, payment.currency)
diff --git a/src/resources/vue/Widgets/TransactionLog.vue b/src/resources/vue/Widgets/TransactionLog.vue
--- a/src/resources/vue/Widgets/TransactionLog.vue
+++ b/src/resources/vue/Widgets/TransactionLog.vue
@@ -26,62 +26,34 @@
<td :class="'price ' + className(transaction)">{{ amount(transaction) }}</td>
</tr>
</tbody>
- <tfoot class="table-fake-body">
- <tr>
- <td :colspan="isAdmin ? 5 : 4">{{ $t('wallet.transactions-none') }}</td>
- </tr>
- </tfoot>
+ <list-foot :text="$t('wallet.transactions-none')" :colspan="isAdmin ? 5 : 4"></list-foot>
</table>
- <div class="text-center p-3" id="transactions-loader" v-if="hasMore">
- <button class="btn btn-secondary" @click="loadLog(true)">{{ $t('nav.more') }}</button>
- </div>
+ <list-more v-if="hasMore" :on-click="loadLog"></list-more>
</div>
</template>
<script>
+ import ListTools from './ListTools'
+
export default {
+ mixins: [ ListTools ],
props: {
walletId: { type: String, default: null },
isAdmin: { type: Boolean, default: false },
},
data() {
return {
- transactions: [],
- hasMore: false,
- page: 1
+ transactions: []
}
},
mounted() {
- this.loadLog()
+ this.loadLog({ reset: true })
},
methods: {
- loadLog(more) {
- if (!this.walletId) {
- return
- }
-
- let loader = $(this.$el)
- let param = ''
-
- if (more) {
- param = '?page=' + (this.page + 1)
- loader = $('#transactions-loader')
+ loadLog(params) {
+ if (this.walletId) {
+ this.listSearch('transactions', '/api/v4/wallets/' + this.walletId + '/transactions', params)
}
-
- this.$root.addLoader(loader)
- axios.get('/api/v4/wallets/' + this.walletId + '/transactions' + param)
- .then(response => {
- this.$root.removeLoader(loader)
- // Note: In Vue we can't just use .concat()
- for (let i in response.data.list) {
- this.$set(this.transactions, this.transactions.length, response.data.list[i])
- }
- this.hasMore = response.data.hasMore
- this.page = response.data.page || 1
- })
- .catch(error => {
- this.$root.removeLoader(loader)
- })
},
loadTransaction(id) {
let record = $('#log' + id)
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -229,9 +229,7 @@
*/
public function vueClear($selector)
{
- if ($this->resolver->prefix != 'body') {
- $selector = $this->resolver->prefix . ' ' . $selector;
- }
+ $selector = $this->resolver->format($selector);
// The existing clear(), and type() with empty string do not work.
// We have to clear the field and dispatch 'input' event programatically.
diff --git a/src/tests/Browser/Pages/UserList.php b/src/tests/Browser/Pages/UserList.php
--- a/src/tests/Browser/Pages/UserList.php
+++ b/src/tests/Browser/Pages/UserList.php
@@ -39,6 +39,7 @@
{
return [
'@app' => '#app',
+ '@search' => '#search-form',
'@table' => '#user-list table',
];
}
diff --git a/src/tests/Browser/Reseller/InvitationsTest.php b/src/tests/Browser/Reseller/InvitationsTest.php
--- a/src/tests/Browser/Reseller/InvitationsTest.php
+++ b/src/tests/Browser/Reseller/InvitationsTest.php
@@ -51,7 +51,7 @@
->click('@links .link-invitations')
->on(new Invitations())
->assertElementsCount('@table tbody tr', 0)
- ->assertMissing('#more-loader')
+ ->assertMissing('.more-loader')
->assertSeeIn('@table tfoot td', "There are no invitations in the database.")
->assertSeeIn('@create-button', 'Create invite(s)');
@@ -177,7 +177,7 @@
$browser->visit(new Invitations())
// ->submitLogon('reseller@' . \config('app.domain'), \App\Utils::generatePassphrase(), true)
->assertElementsCount('@table tbody tr', 10)
- ->assertSeeIn('#more-loader button', 'Load more')
+ ->assertSeeIn('.more-loader button', 'Load more')
->with('@table tbody', function ($browser) use ($i1, $i2, $i3) {
$browser->assertSeeIn('tr:nth-child(1) td.email', $i1->email)
->assertText('tr:nth-child(1) td.email svg.text-danger title', 'Sending failed')
@@ -195,31 +195,31 @@
->assertVisible('tr:nth-child(4) td.buttons button.button-delete')
->assertVisible('tr:nth-child(4) td.buttons button.button-resend:disabled');
})
- ->click('#more-loader button')
+ ->click('.more-loader button')
->whenAvailable('@table tbody tr:nth-child(11)', function ($browser) use ($i11) {
$browser->assertSeeIn('td.email', $i11->email);
})
- ->assertMissing('#more-loader button');
+ ->assertMissing('.more-loader button');
// Test searching (by domain)
$browser->type('@search-input', 'ext.com')
->click('@search-button')
->waitUntilMissing('@table .app-loader')
->assertElementsCount('@table tbody tr', 3)
- ->assertMissing('#more-loader button')
+ ->assertMissing('.more-loader button')
// search by full email
->type('@search-input', 'email7@other.com')
->keys('@search-input', '{enter}')
->waitUntilMissing('@table .app-loader')
->assertElementsCount('@table tbody tr', 1)
->assertSeeIn('@table tbody tr:nth-child(1) td.email', 'email7@other.com')
- ->assertMissing('#more-loader button')
+ ->assertMissing('.more-loader button')
// reset search
->vueClear('#search-form input')
->keys('@search-input', '{enter}')
->waitUntilMissing('@table .app-loader')
->assertElementsCount('@table tbody tr', 10)
- ->assertVisible('#more-loader button');
+ ->assertVisible('.more-loader button');
});
}
}
diff --git a/src/tests/Browser/UserListTest.php b/src/tests/Browser/UserListTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/UserListTest.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Tests\Browser;
+
+use App\User;
+use Tests\Browser;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\UserInfo;
+use Tests\Browser\Pages\UserList;
+use Tests\TestCaseDusk;
+
+class UserListTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ /**
+ * Test users list page (unauthenticated)
+ */
+ public function testListUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/users')->on(new Home());
+ });
+ }
+
+ /**
+ * Test users list page
+ */
+ public function testList(): void
+ {
+ $this->browse(function (Browser $browser) {
+ // Test that the page requires authentication
+ // Test the list
+ $browser->visit(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', true)
+ ->on(new Dashboard())
+ ->assertSeeIn('@links .link-users', 'User accounts')
+ ->click('@links .link-users')
+ ->on(new UserList())
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')
+ ->assertElementsCount('tbody tr', 4)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org')
+ ->assertMissing('tfoot');
+ });
+
+ // Test searching
+ $browser->assertValue('@search input', '')
+ ->assertAttribute('@search input', 'placeholder', 'User email address or name')
+ ->assertSeeIn('@search button', 'Search')
+ ->type('@search input', 'jo')
+ ->click('@search button')
+ ->waitUntilMissing('@app .app-loader')
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')
+ ->assertElementsCount('tbody tr', 2)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'joe@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'john@kolab.org')
+ ->assertMissing('tfoot');
+ })
+ // test empty result
+ ->type('@search input', 'jojo')
+ ->click('@search button')
+ ->waitUntilMissing('@app .app-loader')
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tfoot tr')
+ ->assertSeeIn('tfoot tr', "There are no users in this account.");
+ })
+ // reset search
+ ->vueClear('@search input')
+ ->keys('@search input', '{enter}')
+ ->waitUntilMissing('@app .app-loader')
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 4);
+ });
+
+ // TODO: Test paging
+
+ $browser->click('@table tr:nth-child(3)')
+ ->on(new UserInfo())
+ ->assertSeeIn('#user-info .card-title', 'User account')
+ ->with('@general', function (Browser $browser) {
+ $browser->assertValue('#email', 'john@kolab.org');
+ });
+ });
+ }
+}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -87,65 +87,18 @@
parent::tearDown();
}
- /**
- * Test user info page (unauthenticated)
- */
- public function testInfoUnauth(): void
- {
- // Test that the page requires authentication
- $this->browse(function (Browser $browser) {
- $user = User::where('email', 'john@kolab.org')->first();
-
- $browser->visit('/user/' . $user->id)->on(new Home());
- });
- }
-
- /**
- * Test users list page (unauthenticated)
- */
- public function testListUnauth(): void
- {
- // Test that the page requires authentication
- $this->browse(function (Browser $browser) {
- $browser->visit('/users')->on(new Home());
- });
- }
-
- /**
- * Test users list page
- */
- public function testList(): void
- {
- // Test that the page requires authentication
- $this->browse(function (Browser $browser) {
- $browser->visit(new Home())
- ->submitLogon('john@kolab.org', 'simple123', true)
- ->on(new Dashboard())
- ->assertSeeIn('@links .link-users', 'User accounts')
- ->click('@links .link-users')
- ->on(new UserList())
- ->whenAvailable('@table', function (Browser $browser) {
- $browser->waitFor('tbody tr')
- ->assertElementsCount('tbody tr', 4)
- ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org')
- ->assertMissing('tfoot');
- });
- });
- }
-
/**
* Test user account editing page (not profile page)
- *
- * @depends testList
*/
public function testInfo(): void
{
$this->browse(function (Browser $browser) {
- $browser->on(new UserList())
- ->click('@table tr:nth-child(3) a')
+ $user = User::where('email', 'john@kolab.org')->first();
+
+ // Test that the page requires authentication
+ $browser->visit('/user/' . $user->id)
+ ->on(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', false)
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@general', function (Browser $browser) {
@@ -366,7 +319,7 @@
/**
* Test user adding page
*
- * @depends testList
+ * @depends testInfo
*/
public function testNewUser(): void
{
@@ -746,7 +699,7 @@
/**
* Test beta entitlements
*
- * @depends testList
+ * @depends testInfo
*/
public function testBetaEntitlements(): void
{
diff --git a/src/tests/Browser/WalletTest.php b/src/tests/Browser/WalletTest.php
--- a/src/tests/Browser/WalletTest.php
+++ b/src/tests/Browser/WalletTest.php
@@ -38,7 +38,6 @@
$john = $this->getTestUser('john@kolab.org');
Wallet::where('user_id', $john->id)->update(['balance' => 0]);
-
parent::tearDown();
}
@@ -219,7 +218,7 @@
$browser->waitUntilMissing('.app-loader')
->assertElementsCount('table tbody tr', 10)
->assertMissing('table td.email')
- ->assertSeeIn('#transactions-loader button', 'Load more');
+ ->assertSeeIn('.more-loader button', 'Load more');
foreach ($pages[0] as $idx => $transaction) {
$selector = 'table tbody tr:nth-child(' . ($idx + 1) . ')';
@@ -231,10 +230,10 @@
}
// Load the next page
- $browser->click('#transactions-loader button')
+ $browser->click('.more-loader button')
->waitUntilMissing('.app-loader')
->assertElementsCount('table tbody tr', 12)
- ->assertMissing('#transactions-loader button');
+ ->assertMissing('.more-loader button');
$debitEntry = null;
foreach ($pages[1] as $idx => $transaction) {
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -196,35 +196,76 @@
$json = $response->json();
- $this->assertCount(0, $json);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(0, $json['count']);
+ $this->assertCount(0, $json['list']);
$response = $this->actingAs($john)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(4, $json);
- $this->assertSame($jack->email, $json[0]['email']);
- $this->assertSame($joe->email, $json[1]['email']);
- $this->assertSame($john->email, $json[2]['email']);
- $this->assertSame($ned->email, $json[3]['email']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(4, $json['count']);
+ $this->assertCount(4, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+ $this->assertSame($joe->email, $json['list'][1]['email']);
+ $this->assertSame($john->email, $json['list'][2]['email']);
+ $this->assertSame($ned->email, $json['list'][3]['email']);
// Values below are tested by Unit tests
- $this->assertArrayHasKey('isDeleted', $json[0]);
- $this->assertArrayHasKey('isSuspended', $json[0]);
- $this->assertArrayHasKey('isActive', $json[0]);
- $this->assertArrayHasKey('isLdapReady', $json[0]);
- $this->assertArrayHasKey('isImapReady', $json[0]);
+ $this->assertArrayHasKey('isDeleted', $json['list'][0]);
+ $this->assertArrayHasKey('isSuspended', $json['list'][0]);
+ $this->assertArrayHasKey('isActive', $json['list'][0]);
+ $this->assertArrayHasKey('isLdapReady', $json['list'][0]);
+ $this->assertArrayHasKey('isImapReady', $json['list'][0]);
$response = $this->actingAs($ned)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(4, $json);
- $this->assertSame($jack->email, $json[0]['email']);
- $this->assertSame($joe->email, $json[1]['email']);
- $this->assertSame($john->email, $json[2]['email']);
- $this->assertSame($ned->email, $json[3]['email']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(4, $json['count']);
+ $this->assertCount(4, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+ $this->assertSame($joe->email, $json['list'][1]['email']);
+ $this->assertSame($john->email, $json['list'][2]['email']);
+ $this->assertSame($ned->email, $json['list'][3]['email']);
+
+ // Search by user email
+ $response = $this->actingAs($john)->get("/api/v4/users?search=jack@k");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+
+ // Search by alias
+ $response = $this->actingAs($john)->get("/api/v4/users?search=monster");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($joe->email, $json['list'][0]['email']);
+
+ // Search by name
+ $response = $this->actingAs($john)->get("/api/v4/users?search=land");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($ned->email, $json['list'][0]['email']);
+
+ // TODO: Test paging
}
/**
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 5:33 AM (3 d, 16 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822715
Default Alt Text
D2966.1775194412.diff (38 KB)
Attached To
Mode
D2966: User list search/paging
Attached
Detach File
Event Timeline