Page MenuHomePhorge

D1135.1775516450.diff
No OneTemporary

Authored By
Unknown
Size
60 KB
Referenced Files
None
Subscribers
None

D1135.1775516450.diff

diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php
@@ -2,6 +2,54 @@
namespace App\Http\Controllers\API\V4\Admin;
+use App\Domain;
+use App\User;
+
class DomainsController extends \App\Http\Controllers\API\V4\DomainsController
{
+ /**
+ * Search for domains
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $search = trim(request()->input('search'));
+ $owner = trim(request()->input('owner'));
+ $result = collect([]);
+
+ if ($owner) {
+ if ($owner = User::find($owner)) {
+ foreach ($owner->wallets as $wallet) {
+ $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get();
+
+ foreach ($entitlements as $entitlement) {
+ $domain = $entitlement->entitleable;
+ $result->push($domain);
+ }
+ }
+
+ $result = $result->sortBy('namespace');
+ }
+ } elseif (!empty($search)) {
+ if ($domain = Domain::where('namespace', $search)->first()) {
+ $result->push($domain);
+ }
+ }
+
+ // Process the result
+ $result = $result->map(function ($domain) {
+ $data = $domain->toArray();
+ $data = array_merge($data, self::domainStatuses($domain));
+ return $data;
+ });
+
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'message' => \trans('app.search-foundxdomains', ['x' => count($result)]),
+ ];
+
+ return response()->json($result);
+ }
}
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
@@ -16,9 +16,14 @@
public function index()
{
$search = trim(request()->input('search'));
+ $owner = trim(request()->input('owner'));
$result = collect([]);
- if (strpos($search, '@')) {
+ if ($owner) {
+ if ($owner = User::find($owner)) {
+ $result = $owner->users(false)->orderBy('email')->get();
+ }
+ } elseif (strpos($search, '@')) {
// Search by email
if ($user = User::findByEmail($search, false)) {
$result->push($user);
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -52,13 +52,15 @@
*/
public function index()
{
- $response = [];
- $skus = Sku::select()->get();
+ // Note: Order by title for consistent ordering in tests
+ $skus = Sku::select()->orderBy('title')->get();
// Note: we do not limit the result to active SKUs only.
// It's because we might need users assigned to old SKUs,
// we need to display these old SKUs on the entitlements list
+ $response = [];
+
foreach ($skus as $sku) {
if ($data = $this->skuElement($sku)) {
$response[] = $data;
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
@@ -361,7 +361,7 @@
$response = array_merge($response, self::userStatuses($user));
// Add discount info to wallet object output
- $map_func = function ($wallet) {
+ $map_func = function ($wallet) use ($user) {
$result = $wallet->toArray();
if ($wallet->discount) {
@@ -369,6 +369,10 @@
$result['discount_description'] = $wallet->discount->description;
}
+ if ($wallet->user_id != $user->id) {
+ $result['user_email'] = $wallet->owner->email;
+ }
+
return $result;
};
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -499,16 +499,18 @@
/**
* Return users controlled by the current user.
*
- * Users assigned to wallets the current user controls or owns.
+ * @param bool $with_accounts Include users assigned to wallets
+ * the current user controls but not owns.
*
* @return \Illuminate\Database\Eloquent\Builder Query builder
*/
- public function users()
+ public function users($with_accounts = true)
{
- $wallets = array_merge(
- $this->wallets()->pluck('id')->all(),
- $this->accounts()->pluck('wallet_id')->all()
- );
+ $wallets = $this->wallets()->pluck('id')->all();
+
+ if ($with_accounts) {
+ $wallets = array_merge($wallets, $this->accounts()->pluck('wallet_id')->all());
+ }
return $this->select(['users.*', 'entitlements.wallet_id'])
->distinct()
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -95,7 +95,8 @@
$countries = include resource_path('countries.php');
$env['countries'] = $countries ?: [];
- $env['jsapp'] = strpos(request()->getHttpHost(), 'admin.') === 0 ? 'admin.js' : 'user.js';
+ $env['isAdmin'] = strpos(request()->getHttpHost(), 'admin.') === 0;
+ $env['jsapp'] = $env['isAdmin'] ? 'admin.js' : 'user.js';
return $env;
}
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -40,7 +40,6 @@
'balance' => 'integer',
];
- protected $guarded = ['balance'];
/**
* Add a controller to this wallet.
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -180,7 +180,10 @@
startLoading() {
this.isLoading = true
// Lock the UI with the 'loading...' element
- $('#app').append($('<div class="app-loader"><div class="spinner-border" role="status"><span class="sr-only">Loading</span></div></div>'))
+ let loading = $('#app > .app-loader').show()
+ if (!loading.length) {
+ $('#app').append($('<div class="app-loader"><div class="spinner-border" role="status"><span class="sr-only">Loading</span></div></div>'))
+ }
},
// Hide "loading" overlay
stopLoading() {
diff --git a/src/resources/js/routes-admin.js b/src/resources/js/routes-admin.js
--- a/src/resources/js/routes-admin.js
+++ b/src/resources/js/routes-admin.js
@@ -4,10 +4,10 @@
Vue.use(VueRouter)
import DashboardComponent from '../vue/Admin/Dashboard'
+import DomainComponent from '../vue/Admin/Domain'
import Error404Component from '../vue/404'
import LoginComponent from '../vue/Login'
import LogoutComponent from '../vue/Logout'
-import PasswordResetComponent from '../vue/PasswordReset'
import UserComponent from '../vue/Admin/User'
import store from './store'
@@ -23,6 +23,12 @@
component: DashboardComponent,
meta: { requiresAuth: true }
},
+ {
+ path: '/domain/:domain',
+ name: 'domain',
+ component: DomainComponent,
+ meta: { requiresAuth: true }
+ },
{
path: '/login',
name: 'login',
@@ -33,11 +39,6 @@
name: 'logout',
component: LogoutComponent
},
- {
- path: '/password-reset/:code?',
- name: 'password-reset',
- component: PasswordResetComponent
- },
{
path: '/user/:user',
name: 'user',
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -25,5 +25,6 @@
'user-create-success' => 'User created successfully.',
'user-delete-success' => 'User deleted successfully.',
+ 'search-foundxdomains' => ':x domains have been found.',
'search-foundxusers' => ':x user accounts have been found.',
];
diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss
--- a/src/resources/sass/app.scss
+++ b/src/resources/sass/app.scss
@@ -113,17 +113,18 @@
}
}
-table.form-list {
- td {
- border: 0;
+tfoot.table-fake-body {
+ background-color: #f8f8f8;
+ color: grey;
+ text-align: center;
+ height: 8em;
- &:first-child {
- padding-left: 0;
- }
+ td {
+ vertical-align: middle;
+ }
- &:last-child {
- padding-right: 0;
- }
+ tbody:not(:empty) + & {
+ display: none;
}
}
@@ -137,6 +138,20 @@
td.price {
text-align: right;
}
+
+ &.form-list {
+ td {
+ border: 0;
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+ }
}
ul.status-list {
diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Admin/Domain.vue
@@ -0,0 +1,69 @@
+<template>
+ <div v-if="domain" class="container">
+ <div class="card" id="domain-info">
+ <div class="card-body">
+ <div class="card-title">{{ domain.namespace }}</div>
+ <div class="card-text">
+ <form>
+ <div class="form-group row mb-0">
+ <label for="domainid" class="col-sm-4 col-form-label">ID <span class="text-muted">(Created at)</span></label>
+ <div class="col-sm-8">
+ <span class="form-control-plaintext" id="domainid">
+ {{ domain.id }} <span class="text-muted">({{ domain.created_at }})</span>
+ </span>
+ </div>
+ </div>
+ <div class="form-group row mb-0">
+ <label for="first_name" class="col-sm-4 col-form-label">Status</label>
+ <div class="col-sm-8">
+ <span class="form-control-plaintext" id="status">
+ <span :class="$root.domainStatusClass(domain)">{{ $root.domainStatusText(domain) }}</span>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ <ul class="nav nav-tabs mt-3" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="tab-config" href="#domain-config" role="tab" aria-controls="domain-config" aria-selected="true">
+ Configuration
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane show active" id="domain-config" role="tabpanel" aria-labelledby="tab-config">
+ <div class="card-body">
+ <div class="card-text">
+ <p>Domain DNS verification sample:</p>
+ <p><pre id="dns-verify">{{ domain.dns.join("\n") }}</pre></p>
+ <p>Domain DNS configuration sample:</p>
+ <p><pre id="dns-config">{{ domain.config.join("\n") }}</pre></p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ domain: null
+ }
+ },
+ created() {
+ const domain_id = this.$route.params.domain;
+
+ axios.get('/api/v4/domains/' + domain_id)
+ .then(response => {
+ this.domain = response.data
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ }
+ }
+</script>
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -4,11 +4,29 @@
<div class="card-body">
<div class="card-title">{{ user.email }}</div>
<div class="card-text">
- <form @submit.prevent="submit">
+ <form>
+ <div v-if="user.wallet.user_id != user.id" class="form-group row mb-0">
+ <label for="manager" class="col-sm-4 col-form-label">Managed by</label>
+ <div class="col-sm-8">
+ <span class="form-control-plaintext" id="manager">
+ <router-link :to="{ path: '/user/' + user.wallet.user_id }">{{ user.wallet.user_email }}</router-link>
+ </span>
+ </div>
+ </div>
+ <div class="form-group row mb-0">
+ <label for="userid" class="col-sm-4 col-form-label">ID <span class="text-muted">(Created at)</span></label>
+ <div class="col-sm-8">
+ <span class="form-control-plaintext" id="userid">
+ {{ user.id }} <span class="text-muted">({{ user.created_at }})</span>
+ </span>
+ </div>
+ </div>
<div class="form-group row mb-0">
- <label for="first_name" class="col-sm-4 col-form-label">Status</label>
+ <label for="status" class="col-sm-4 col-form-label">Status</label>
<div class="col-sm-8">
- <span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
+ <span class="form-control-plaintext" id="status">
+ <span :class="$root.userStatusClass(user)">{{ $root.userStatusText(user) }}</span>
+ </span>
</div>
</div>
<div class="form-group row mb-0" v-if="user.first_name">
@@ -32,7 +50,10 @@
<div class="form-group row mb-0">
<label for="external_email" class="col-sm-4 col-form-label">External email</label>
<div class="col-sm-8">
- <span class="form-control-plaintext" id="external_email">{{ user.external_email }}</span>
+ <span class="form-control-plaintext" id="external_email">
+ <a v-if="user.external_email" :href="'mailto:' + user.external_email">{{ user.external_email }}</a>
+ <button type="button" class="btn btn-secondary btn-sm">Edit</button>
+ </span>
</div>
</div>
<div class="form-group row mb-0" v-if="user.billing_address">
@@ -51,29 +72,198 @@
</div>
</div>
</div>
+ <ul class="nav nav-tabs mt-3" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="tab-finances" href="#user-finances" role="tab" aria-controls="user-finances" aria-selected="true">
+ Finances
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-aliases" href="#user-aliases" role="tab" aria-controls="user-aliases" aria-selected="false">
+ Aliases ({{ user.aliases.length }})
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-subscriptions" href="#user-subscriptions" role="tab" aria-controls="user-subscriptions" aria-selected="false">
+ Subscriptions ({{ skus.length }})
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-domains" href="#user-domains" role="tab" aria-controls="user-domains" aria-selected="false">
+ Domains ({{ domains.length }})
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-users" href="#user-users" role="tab" aria-controls="user-users" aria-selected="false">
+ Users ({{ users.length }})
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane show active" id="user-finances" role="tabpanel" aria-labelledby="tab-finances">
+ <div class="card-body">
+ <div class="card-title">Account balance <span :class="balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(balance) }}</strong></span></div>
+ <div class="card-text">
+ <form>
+ <div class="form-group row mb-0">
+ <label for="first_name" class="col-sm-2 col-form-label">Discount:</label>
+ <div class="col-sm-10">
+ <span class="form-control-plaintext" id="discount">
+ <span>{{ wallet_discount ? (wallet_discount + '% - ' + wallet_discount_description) : 'none' }}</span>
+ <button type="button" class="btn btn-secondary btn-sm">Edit</button>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="user-aliases" role="tabpanel" aria-labelledby="tab-aliases">
+ <div class="card-body">
+ <div class="card-text">
+ <table class="table table-sm table-hover">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Email address</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(alias, index) in user.aliases" :id="'alias' + index" :key="index">
+ <td>{{ alias }}</td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td>This user has no email aliases.</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="user-subscriptions" role="tabpanel" aria-labelledby="tab-subscriptions">
+ <div class="card-body">
+ <div class="card-text">
+ <table class="table table-sm table-hover">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Subscription</th>
+ <th scope="col">Price</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(sku, sku_id) in skus" :id="'sku' + sku_id" :key="sku_id">
+ <td>{{ sku.name }}</td>
+ <td>{{ sku.price }}</td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td colspan="2">This user has no subscriptions.</td>
+ </tr>
+ </tfoot>
+ </table>
+ <small v-if="discount > 0" class="hint">
+ <hr class="m-0">
+ &sup1; applied discount: {{ discount }}% - {{ discount_description }}
+ </small>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="user-domains" role="tabpanel" aria-labelledby="tab-domains">
+ <div class="card-body">
+ <div class="card-text">
+ <table class="table table-sm table-hover">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="domain in domains" :id="'domain' + domain.id" :key="domain.id">
+ <td>
+ <svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
+ <router-link :to="{ path: '/domain/' + domain.id }">{{ domain.namespace }}</router-link>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td>There are no domains in this account.</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="user-users" role="tabpanel" aria-labelledby="tab-users">
+ <div class="card-body">
+ <div class="card-text">
+ <table class="table table-sm table-hover">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Primary Email</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="item in users" :id="'user' + item.id" :key="item.id">
+ <td>
+ <svg-icon icon="user" :class="$root.userStatusClass(item)" :title="$root.userStatusText(item)"></svg-icon>
+ <router-link :to="{ path: '/user/' + item.id }">{{ item.email }}</router-link>
+ </td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td>There are no users in this account.</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
<script>
export default {
+ beforeRouteUpdate (to, from, next) {
+ // An event called when the route that renders this component has changed,
+ // but this component is reused in the new route.
+ // Required to handle links from /user/XXX to /user/YYY
+ next()
+ this.$parent.routerReload()
+ },
data() {
return {
+ balance: 0,
discount: 0,
discount_description: '',
- user: {},
- skus: []
+ wallet_discount: 0,
+ wallet_discount_description: '',
+ domains: [],
+ skus: [],
+ users: [],
+ user: {
+ aliases: [],
+ wallet: {},
+ skus: {},
+ }
}
},
created() {
- let user_id = this.$route.params.user
+ const user_id = this.$route.params.user
this.$root.startLoading()
axios.get('/api/v4/users/' + user_id)
.then(response => {
+ this.$root.stopLoading()
+
this.user = response.data
- let keys = ['first_name', 'last_name', 'external_email', 'billing_address']
+ let keys = ['first_name', 'last_name', 'external_email', 'billing_address', 'phone']
let country = this.user.settings.country
if (country) {
@@ -85,16 +275,67 @@
this.discount = this.user.wallet.discount
this.discount_description = this.user.wallet.discount_description
- this.$root.stopLoading()
+ // TODO: currencies, multi-wallets, accounts
+ this.user.wallets.forEach(wallet => {
+ this.balance += wallet.balance
+ })
+
+ this.wallet_discount = this.user.wallets[0].discount
+ this.wallet_discount_description = this.user.wallets[0].discount_description
+
+ // Create subscriptions list
+ axios.get('/api/v4/skus')
+ .then(response => {
+ // "merge" SKUs with user entitlement-SKUs
+ response.data.forEach(sku => {
+ if (sku.id in this.user.skus) {
+ let count = this.user.skus[sku.id].count
+ let item = {
+ id: sku.id,
+ name: sku.name,
+ price: this.price(sku.cost, count - sku.units_free)
+ }
+
+ if (sku.range) {
+ item.name += ' ' + count + ' ' + sku.range.unit
+ }
+
+ this.skus.push(item)
+ }
+ })
+ })
+
+ // Fetch users
+ // TODO: Multiple wallets
+ axios.get('/api/v4/users?owner=' + user_id)
+ .then(response => {
+ this.users = response.data.list.filter(user => {
+ return user.id != user_id;
+ })
+ })
+
+ // Fetch domains
+ axios.get('/api/v4/domains?owner=' + user_id)
+ .then(response => {
+ this.domains = response.data.list
+ })
})
.catch(this.$root.errorHandler)
},
mounted() {
+ $(this.$el).find('ul.nav-tabs a').on('click', e => {
+ e.preventDefault()
+ $(e.target).tab('show')
+ })
},
methods: {
price(cost, units = 1) {
let index = ''
+ if (units < 0) {
+ units = 1
+ }
+
if (this.discount) {
cost = Math.floor(cost * ((100 - this.discount) / 100))
index = '\u00B9'
diff --git a/src/resources/vue/App.vue b/src/resources/vue/App.vue
--- a/src/resources/vue/App.vue
+++ b/src/resources/vue/App.vue
@@ -1,12 +1,13 @@
<template>
- <router-view v-if="!isLoading"></router-view>
+ <router-view v-if="!isLoading && !routerReloading"></router-view>
</template>
<script>
export default {
data() {
return {
- isLoading: true
+ isLoading: true,
+ routerReloading: false
}
},
mounted() {
@@ -34,5 +35,16 @@
this.isLoading = false
}
},
+ methods: {
+ routerReload() {
+ // Together with beforeRouteUpdate even on a route component
+ // allows us to force reload the component. So it is possible
+ // to jump from/to page that uses currently loaded component.
+ this.routerReloading = true
+ this.$nextTick().then(() => {
+ this.routerReloading = false
+ })
+ }
+ }
}
</script>
diff --git a/src/resources/vue/Login.vue b/src/resources/vue/Login.vue
--- a/src/resources/vue/Login.vue
+++ b/src/resources/vue/Login.vue
@@ -23,7 +23,7 @@
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required v-model="password">
</div>
</div>
- <div class="form-group pt-3">
+ <div class="form-group pt-3" v-if="!isAdmin">
<label for="secondfactor" class="sr-only">2FA</label>
<div class="input-group">
<span class="input-group-prepend">
@@ -43,7 +43,7 @@
</div>
</div>
<div class="mt-1">
- <router-link :to="{ name: 'password-reset' }">Forgot password?</router-link>
+ <router-link v-if="!isAdmin" :to="{ name: 'password-reset' }" id="forgot-password">Forgot password?</router-link>
</div>
</div>
</template>
@@ -56,6 +56,7 @@
email: '',
password: '',
secondFactor: '',
+ isAdmin: window.config.isAdmin,
loginError: false
}
},
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -65,7 +65,7 @@
<span class="input-group-text">@</span>
</span>
<input v-if="is_domain" type="text" class="form-control rounded-right" id="signup_domain" required v-model="domain" placeholder="Domain">
- <select v-if="!is_domain" class="custom-select rounded-right" id="signup_domain" required v-model="domain"></select>
+ <select v-else class="custom-select rounded-right" id="signup_domain" required v-model="domain"></select>
</div>
</div>
<div class="form-group">
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -158,7 +158,7 @@
discount: 0,
discount_description: '',
user_id: null,
- user: {},
+ user: { aliases: [] },
packages: [],
package_id: null,
skus: []
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -12,6 +12,22 @@
*/
class Browser extends \Laravel\Dusk\Browser
{
+ /**
+ * Assert element's attribute value
+ */
+ public function assertAttribute($selector, $name, $value)
+ {
+ $element = $this->resolver->findOrFail($selector);
+
+ Assert::assertEquals(
+ $element->getAttribute($name),
+ $value,
+ "Failed asserting value of [$selector][$name] attribute"
+ );
+
+ return $this;
+ }
+
/**
* Assert number of (visible) elements
*/
diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Admin/DomainTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Tests\Browser\Admin;
+
+use App\Discount;
+use Tests\Browser;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Admin\Domain as DomainPage;
+use Tests\Browser\Pages\Admin\User as UserPage;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\TestCaseDusk;
+use Illuminate\Foundation\Testing\DatabaseMigrations;
+
+class DomainTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useAdminUrl();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ /**
+ * Test domain info page (unauthenticated)
+ */
+ public function testDomainUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $domain = $this->getTestDomain('kolab.org');
+ $browser->visit('/domain/' . $domain->id)->on(new Home());
+ });
+ }
+
+ /**
+ * Test domain info page
+ */
+ public function testDomainInfo(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $domain = $this->getTestDomain('kolab.org');
+ $domain_page = new DomainPage($domain->id);
+ $john = $this->getTestUser('john@kolab.org');
+ $user_page = new UserPage($john->id);
+
+ // Goto the domain page
+ $browser->visit(new Home())
+ ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true)
+ ->on(new Dashboard())
+ ->visit($user_page)
+ ->on($user_page)
+ ->pause(500)
+ ->click('@nav #tab-domains')
+ ->click('@user-domains table tbody tr:first-child td a');
+
+ $browser->on($domain_page)
+ ->assertSeeIn('@domain-info .card-title', 'kolab.org')
+ ->with('@domain-info form', function (Browser $browser) use ($domain) {
+ $browser->assertElementsCount('.row', 2)
+ ->assertSeeIn('.row:nth-child(1) label', 'ID (Created at)')
+ ->assertSeeIn('.row:nth-child(1) #domainid', "{$domain->id} ({$domain->created_at})")
+ ->assertSeeIn('.row:nth-child(2) label', 'Status')
+ ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active');
+ });
+
+ // Some tabs are loaded in background, wait a second
+ $browser->pause(500)
+ ->assertElementsCount('@nav a', 1);
+
+ // Assert Configuration tab
+ $browser->assertSeeIn('@nav #tab-config', 'Configuration')
+ ->with('@domain-config', function (Browser $browser) {
+ $browser->assertSeeIn('pre#dns-verify', 'kolab-verify.kolab.org.')
+ ->assertSeeIn('pre#dns-config', 'kolab.org.');
+ });
+ });
+ }
+}
diff --git a/src/tests/Browser/Admin/LogonTest.php b/src/tests/Browser/Admin/LogonTest.php
--- a/src/tests/Browser/Admin/LogonTest.php
+++ b/src/tests/Browser/Admin/LogonTest.php
@@ -27,10 +27,12 @@
public function testLogonMenu(): void
{
$this->browse(function (Browser $browser) {
- $browser->visit(new Home());
- $browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
- });
+ $browser->visit(new Home())
+ ->with(new Menu(), function ($browser) {
+ $browser->assertMenuItems(['signup', 'explore', 'blog', 'support', 'webmail']);
+ })
+ ->assertMissing('@second-factor-input')
+ ->assertMissing('@forgot-password');
});
}
diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -0,0 +1,342 @@
+<?php
+
+namespace Tests\Browser\Admin;
+
+use App\Discount;
+use Tests\Browser;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Admin\User as UserPage;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\TestCaseDusk;
+use Illuminate\Foundation\Testing\DatabaseMigrations;
+
+class UserTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useAdminUrl();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSettings([
+ 'phone' => '+48123123123',
+ ]);
+
+ $wallet = $john->wallets()->first();
+ $wallet->discount()->dissociate();
+ $wallet->balance = 0;
+ $wallet->save();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $john->setSettings([
+ 'phone' => null,
+ ]);
+
+ $wallet = $john->wallets()->first();
+ $wallet->discount()->dissociate();
+ $wallet->balance = 0;
+ $wallet->save();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test user info page (unauthenticated)
+ */
+ public function testUserUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $jack = $this->getTestUser('jack@kolab.org');
+ $browser->visit('/user/' . $jack->id)->on(new Home());
+ });
+ }
+
+ /**
+ * Test user info page
+ */
+ public function testUserInfo(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $jack = $this->getTestUser('jack@kolab.org');
+ $page = new UserPage($jack->id);
+
+ $browser->visit(new Home())
+ ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true)
+ ->on(new Dashboard())
+ ->visit($page)
+ ->on($page);
+
+ // Assert main info box content
+ $browser->assertSeeIn('@user-info .card-title', $jack->email)
+ ->with('@user-info form', function (Browser $browser) use ($jack) {
+ $browser->assertElementsCount('.row', 7)
+ ->assertSeeIn('.row:nth-child(1) label', 'Managed by')
+ ->assertSeeIn('.row:nth-child(1) #manager a', 'john@kolab.org')
+ ->assertSeeIn('.row:nth-child(2) label', 'ID (Created at)')
+ ->assertSeeIn('.row:nth-child(2) #userid', "{$jack->id} ({$jack->created_at})")
+ ->assertSeeIn('.row:nth-child(3) label', 'Status')
+ ->assertSeeIn('.row:nth-child(3) #status span.text-success', 'Active')
+ ->assertSeeIn('.row:nth-child(4) label', 'First name')
+ ->assertSeeIn('.row:nth-child(4) #first_name', 'Jack')
+ ->assertSeeIn('.row:nth-child(5) label', 'Last name')
+ ->assertSeeIn('.row:nth-child(5) #last_name', 'Daniels')
+ ->assertSeeIn('.row:nth-child(6) label', 'External email')
+ ->assertMissing('.row:nth-child(6) #external_email a')
+ ->assertSeeIn('.row:nth-child(7) label', 'Country')
+ ->assertSeeIn('.row:nth-child(7) #country', 'United States of America');
+ });
+
+ // Some tabs are loaded in background, wait a second
+ $browser->pause(500)
+ ->assertElementsCount('@nav a', 5);
+
+ // Assert Finances tab
+ $browser->assertSeeIn('@nav #tab-finances', 'Finances')
+ ->with('@user-finances', function (Browser $browser) {
+ $browser->assertSeeIn('.card-title', 'Account balance')
+ ->assertSeeIn('.card-title .text-success', '0,00 CHF')
+ ->with('form', function (Browser $browser) {
+ $browser->assertElementsCount('.row', 1)
+ ->assertSeeIn('.row:nth-child(1) label', 'Discount')
+ ->assertSeeIn('.row:nth-child(1) #discount span', 'none');
+ });
+ });
+
+ // Assert Aliases tab
+ $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)')
+ ->click('@nav #tab-aliases')
+ ->whenAvailable('@user-aliases', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 1)
+ ->assertSeeIn('table tbody tr:first-child td:first-child', 'jack.daniels@kolab.org')
+ ->assertMissing('table tfoot');
+ });
+
+ // Assert Subscriptions tab
+ $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)')
+ ->click('@nav #tab-subscriptions')
+ ->with('@user-subscriptions', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 3)
+ ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
+ ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF')
+ ->assertMissing('table tfoot');
+ });
+
+ // Assert Domains tab
+ $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)')
+ ->click('@nav #tab-domains')
+ ->with('@user-domains', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 0)
+ ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.');
+ });
+
+ // Assert Users tab
+ $browser->assertSeeIn('@nav #tab-users', 'Users (0)')
+ ->click('@nav #tab-users')
+ ->with('@user-users', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 0)
+ ->assertSeeIn('table tfoot tr td', 'There are no users in this account.');
+ });
+ });
+ }
+
+ /**
+ * Test user info page (continue)
+ *
+ * @depends testUserInfo
+ */
+ public function testUserInfo2(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $john = $this->getTestUser('john@kolab.org');
+ $page = new UserPage($john->id);
+ $discount = Discount::where('code', 'TEST')->first();
+ $wallet = $john->wallet();
+ $wallet->discount()->associate($discount);
+ $wallet->debit(2010);
+ $wallet->save();
+
+ // Click the managed-by link on Jack's page
+ $browser->click('@user-info #manager a')
+ ->on($page);
+
+ // Assert main info box content
+ $browser->assertSeeIn('@user-info .card-title', $john->email)
+ ->with('@user-info form', function (Browser $browser) use ($john) {
+ $ext_email = $john->getSetting('external_email');
+
+ $browser->assertElementsCount('.row', 8)
+ ->assertSeeIn('.row:nth-child(1) label', 'ID (Created at)')
+ ->assertSeeIn('.row:nth-child(1) #userid', "{$john->id} ({$john->created_at})")
+ ->assertSeeIn('.row:nth-child(2) label', 'Status')
+ ->assertSeeIn('.row:nth-child(2) #status span.text-success', 'Active')
+ ->assertSeeIn('.row:nth-child(3) label', 'First name')
+ ->assertSeeIn('.row:nth-child(3) #first_name', 'John')
+ ->assertSeeIn('.row:nth-child(4) label', 'Last name')
+ ->assertSeeIn('.row:nth-child(4) #last_name', 'Doe')
+ ->assertSeeIn('.row:nth-child(5) label', 'Phone')
+ ->assertSeeIn('.row:nth-child(5) #phone', $john->getSetting('phone'))
+ ->assertSeeIn('.row:nth-child(6) label', 'External email')
+ ->assertSeeIn('.row:nth-child(6) #external_email a', $ext_email)
+ ->assertAttribute('.row:nth-child(6) #external_email a', 'href', "mailto:$ext_email")
+ ->assertSeeIn('.row:nth-child(7) label', 'Address')
+ ->assertSeeIn('.row:nth-child(7) #billing_address', $john->getSetting('billing_address'))
+ ->assertSeeIn('.row:nth-child(8) label', 'Country')
+ ->assertSeeIn('.row:nth-child(8) #country', 'United States of America');
+ });
+
+ // Some tabs are loaded in background, wait a second
+ $browser->pause(500)
+ ->assertElementsCount('@nav a', 5);
+
+ // Assert Finances tab
+ $browser->assertSeeIn('@nav #tab-finances', 'Finances')
+ ->with('@user-finances', function (Browser $browser) {
+ $browser->assertSeeIn('.card-title', 'Account balance')
+ ->assertSeeIn('.card-title .text-danger', '-20,10 CHF')
+ ->with('form', function (Browser $browser) {
+ $browser->assertElementsCount('.row', 1)
+ ->assertSeeIn('.row:nth-child(1) label', 'Discount')
+ ->assertSeeIn('.row:nth-child(1) #discount span', '10% - Test voucher');
+ });
+ });
+
+ // Assert Aliases tab
+ $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)')
+ ->click('@nav #tab-aliases')
+ ->whenAvailable('@user-aliases', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 1)
+ ->assertSeeIn('table tbody tr:first-child td:first-child', 'john.doe@kolab.org')
+ ->assertMissing('table tfoot');
+ });
+
+ // Assert Subscriptions tab
+ $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (3)')
+ ->click('@nav #tab-subscriptions')
+ ->with('@user-subscriptions', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 3)
+ ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
+ ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹')
+ ->assertMissing('table tfoot')
+ ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
+ });
+
+ // Assert Domains tab
+ $browser->assertSeeIn('@nav #tab-domains', 'Domains (1)')
+ ->click('@nav #tab-domains')
+ ->with('@user-domains table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 1)
+ ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'kolab.org')
+ ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success')
+ ->assertMissing('tfoot');
+ });
+
+ // Assert Users tab
+ $browser->assertSeeIn('@nav #tab-users', 'Users (3)')
+ ->click('@nav #tab-users')
+ ->with('@user-users table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 3)
+ ->assertSeeIn('tbody tr:nth-child(1) td:first-child a', 'jack@kolab.org')
+ ->assertVisible('tbody tr:nth-child(1) td:first-child svg.text-success')
+ ->assertSeeIn('tbody tr:nth-child(2) td:first-child a', 'joe@kolab.org')
+ ->assertVisible('tbody tr:nth-child(2) td:first-child svg.text-success')
+ ->assertSeeIn('tbody tr:nth-child(3) td:first-child a', 'ned@kolab.org')
+ ->assertVisible('tbody tr:nth-child(3) td:first-child svg.text-success')
+ ->assertMissing('tfoot');
+ });
+ });
+
+ // Now we go to Ned's info page, he's a controller on John's wallet
+ $this->browse(function (Browser $browser) {
+ $ned = $this->getTestUser('ned@kolab.org');
+ $page = new UserPage($ned->id);
+
+ $browser->click('@user-users tbody tr:nth-child(3) td:first-child a')
+ ->on($page);
+
+ // Assert main info box content
+ $browser->assertSeeIn('@user-info .card-title', $ned->email)
+ ->with('@user-info form', function (Browser $browser) use ($ned) {
+ $browser->assertSeeIn('.row:nth-child(2) label', 'ID (Created at)')
+ ->assertSeeIn('.row:nth-child(2) #userid', "{$ned->id} ({$ned->created_at})");
+ });
+
+ // Some tabs are loaded in background, wait a second
+ $browser->pause(500)
+ ->assertElementsCount('@nav a', 5);
+
+ // Assert Finances tab
+ $browser->assertSeeIn('@nav #tab-finances', 'Finances')
+ ->with('@user-finances', function (Browser $browser) {
+ $browser->assertSeeIn('.card-title', 'Account balance')
+ ->assertSeeIn('.card-title .text-success', '0,00 CHF')
+ ->with('form', function (Browser $browser) {
+ $browser->assertElementsCount('.row', 1)
+ ->assertSeeIn('.row:nth-child(1) label', 'Discount')
+ ->assertSeeIn('.row:nth-child(1) #discount span', 'none');
+ });
+ });
+
+ // Assert Aliases tab
+ $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)')
+ ->click('@nav #tab-aliases')
+ ->whenAvailable('@user-aliases', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 0)
+ ->assertSeeIn('table tfoot tr td', 'This user has no email aliases.');
+ });
+
+ // Assert Subscriptions tab, we expect John's discount here
+ $browser->assertSeeIn('@nav #tab-subscriptions', 'Subscriptions (5)')
+ ->click('@nav #tab-subscriptions')
+ ->with('@user-subscriptions', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 5)
+ ->assertSeeIn('table tbody tr:nth-child(1) td:first-child', 'User Mailbox')
+ ->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:first-child', 'Storage Quota 2 GB')
+ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:first-child', 'Groupware Features')
+ ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(4) td:first-child', 'Activesync')
+ ->assertSeeIn('table tbody tr:nth-child(4) td:last-child', '0,90 CHF/month¹')
+ ->assertSeeIn('table tbody tr:nth-child(5) td:first-child', '2-Factor Authentication')
+ ->assertSeeIn('table tbody tr:nth-child(5) td:last-child', '0,00 CHF/month¹')
+ ->assertMissing('table tfoot')
+ ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher');
+ });
+
+ // We don't expect John's domains here
+ $browser->assertSeeIn('@nav #tab-domains', 'Domains (0)')
+ ->click('@nav #tab-domains')
+ ->with('@user-domains', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 0)
+ ->assertSeeIn('table tfoot tr td', 'There are no domains in this account.');
+ });
+
+ // We don't expect John's users here
+ $browser->assertSeeIn('@nav #tab-users', 'Users (0)')
+ ->click('@nav #tab-users')
+ ->with('@user-users', function (Browser $browser) {
+ $browser->assertElementsCount('table tbody tr', 0)
+ ->assertSeeIn('table tfoot tr td', 'There are no users in this account.');
+ });
+ });
+ }
+}
diff --git a/src/tests/Browser/Pages/Admin/Domain.php b/src/tests/Browser/Pages/Admin/Domain.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Admin/Domain.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Tests\Browser\Pages\Admin;
+
+use Laravel\Dusk\Page;
+
+class Domain extends Page
+{
+ protected $domainid;
+
+ /**
+ * Object constructor.
+ *
+ * @param int $domainid Domain Id
+ */
+ public function __construct($domainid)
+ {
+ $this->domainid = $domainid;
+ }
+
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '/domain/' . $this->domainid;
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser): void
+ {
+ $browser->waitForLocation($this->url())
+ ->waitFor('@domain-info');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@domain-info' => '#domain-info',
+ '@nav' => 'ul.nav-tabs',
+ '@domain-config' => '#domain-config',
+ ];
+ }
+}
diff --git a/src/tests/Browser/Pages/Admin/User.php b/src/tests/Browser/Pages/Admin/User.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Admin/User.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Tests\Browser\Pages\Admin;
+
+use Laravel\Dusk\Page;
+
+class User extends Page
+{
+ protected $userid;
+
+ /**
+ * Object constructor.
+ *
+ * @param int $userid User Id
+ */
+ public function __construct($userid)
+ {
+ $this->userid = $userid;
+ }
+
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '/user/' . $this->userid;
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser): void
+ {
+ $browser->waitForLocation($this->url())
+ ->waitFor('@user-info');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@user-info' => '#user-info',
+ '@nav' => 'ul.nav-tabs',
+ '@user-finances' => '#user-finances',
+ '@user-aliases' => '#user-aliases',
+ '@user-subscriptions' => '#user-subscriptions',
+ '@user-domains' => '#user-domains',
+ '@user-users' => '#user-users',
+ ];
+ }
+}
diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Admin/DomainsTest.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Tests\Feature\Controller\Admin;
+
+use Tests\TestCase;
+
+class DomainsTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useAdminUrl();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ parent::tearDown();
+ }
+
+ /**
+ * Test domains searching (/api/v4/domains)
+ */
+ public function testIndex(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $admin = $this->getTestUser('jeroen@jeroen.jeroen');
+
+ // Non-admin user
+ $response = $this->actingAs($john)->get("api/v4/domains");
+ $response->assertStatus(403);
+
+ // Search with no search criteria
+ $response = $this->actingAs($admin)->get("api/v4/domains");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertSame([], $json['list']);
+
+ // Search with no matches expected
+ $response = $this->actingAs($admin)->get("api/v4/domains?search=abcd12.org");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertSame([], $json['list']);
+
+ // Search by a domain name
+ $response = $this->actingAs($admin)->get("api/v4/domains?search=kolab.org");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame('kolab.org', $json['list'][0]['namespace']);
+
+ // Search by owner
+ $response = $this->actingAs($admin)->get("api/v4/domains?owner={$john->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame('kolab.org', $json['list'][0]['namespace']);
+
+ // Search by owner (Ned is a controller on John's wallets,
+ // here we expect only domains assigned to Ned's wallet(s))
+ $ned = $this->getTestUser('ned@kolab.org');
+ $response = $this->actingAs($admin)->get("api/v4/domains?owner={$ned->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertCount(0, $json['list']);
+ }
+}
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
@@ -119,5 +119,25 @@
$this->assertContains($user->email, $emails);
$this->assertContains($jack->email, $emails);
+
+ // Search by owner
+ $response = $this->actingAs($admin)->get("api/v4/users?owner={$user->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(4, $json['count']);
+ $this->assertCount(4, $json['list']);
+
+ // Search by owner (Ned is a controller on John's wallets,
+ // here we expect only users assigned to Ned's wallet(s))
+ $ned = $this->getTestUser('ned@kolab.org');
+ $response = $this->actingAs($admin)->get("api/v4/users?owner={$ned->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(0, $json['count']);
+ $this->assertCount(0, $json['list']);
}
}

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 6, 11:00 PM (4 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820304
Default Alt Text
D1135.1775516450.diff (60 KB)

Event Timeline