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($('
Loading
')) + let loading = $('#app > .app-loader').show() + if (!loading.length) { + $('#app').append($('
Loading
')) + } }, // 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 @@ + + + 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 }}
-
+ +
+ +
+ + {{ user.wallet.user_email }} + +
+
+
+ +
+ + {{ user.id }} ({{ user.created_at }}) + +
+
- +
- {{ $root.userStatusText(user) }} + + {{ $root.userStatusText(user) }} +
@@ -32,7 +50,10 @@
- {{ user.external_email }} + + {{ user.external_email }} + +
@@ -51,29 +72,198 @@
+ +
+
+
+
Account balance {{ $root.price(balance) }}
+
+ +
+ +
+ + {{ wallet_discount ? (wallet_discount + '% - ' + wallet_discount_description) : 'none' }} + + +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
Email address
{{ alias }}
This user has no email aliases.
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
SubscriptionPrice
{{ 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 @@ -
+
@@ -43,7 +43,7 @@
- Forgot password? + Forgot password?
@@ -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']); } }