Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117876227
D1135.1775336084.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
60 KB
Referenced Files
None
Subscribers
None
D1135.1775336084.diff
View Options
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'
@@ -24,6 +24,12 @@
meta: { requiresAuth: true }
},
{
+ path: '/domain/:domain',
+ name: 'domain',
+ component: DomainComponent,
+ meta: { requiresAuth: true }
+ },
+ {
path: '/login',
name: 'login',
component: LoginComponent
@@ -34,11 +40,6 @@
component: LogoutComponent
},
{
- path: '/password-reset/:code?',
- name: 'password-reset',
- component: PasswordResetComponent
- },
- {
path: '/user/:user',
name: 'user',
component: UserComponent,
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">
+ ¹ 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
@@ -13,6 +13,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
*/
public function assertElementsCount($selector, $expected_count, $visible = true)
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
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 8:54 PM (1 d, 8 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831135
Default Alt Text
D1135.1775336084.diff (60 KB)
Attached To
Mode
D1135: Support: User/Domain info pages
Attached
Detach File
Event Timeline