Page MenuHomePhorge

D2966.1775471289.diff
No OneTemporary

Authored By
Unknown
Size
38 KB
Referenced Files
None
Subscribers
None

D2966.1775471289.diff

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

Mime Type
text/plain
Expires
Mon, Apr 6, 10:28 AM (11 h, 20 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822715
Default Alt Text
D2966.1775471289.diff (38 KB)

Event Timeline