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($('
'))
+ let loading = $('#app > .app-loader').show()
+ if (!loading.length) {
+ $('#app').append($(''))
+ }
},
// 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 @@
+
+
+
+
+
{{ domain.namespace }}
+
+
+
+
+
+
+
+
+
Domain DNS verification sample:
+
{{ domain.dns.join("\n") }}
+
Domain DNS configuration sample:
+
{{ domain.config.join("\n") }}
+
+
+
+
+
+
+
+
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 @@
{{ user.email }}
+
+
+
+
+
Account balance {{ $root.price(balance) }}
+
+
+
+
+
+
+
+
+
+ Email address |
+
+
+
+
+ {{ alias }} |
+
+
+
+
+ This user has no email aliases. |
+
+
+
+
+
+
+
+
+
+
+
+
+ Subscription |
+ Price |
+
+
+
+
+ {{ sku.name }} |
+ {{ sku.price }} |
+
+
+
+
+ This user has no subscriptions. |
+
+
+
+
+
+ ¹ applied discount: {{ discount }}% - {{ discount_description }}
+
+
+
+
+
+
+
+
+
+
+ Name |
+
+
+
+
+
+
+ {{ domain.namespace }}
+ |
+
+
+
+
+ There are no domains in this account. |
+
+
+
+
+
+
+
+
+
+
+
+
+ Primary Email |
+
+
+
+
+
+
+ {{ item.email }}
+ |
+
+
+
+
+ There are no users in this account. |
+
+
+
+
+
+
+
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 @@
-
@@ -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 @@
@
-
+
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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+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']);
}
}