diff --git a/src/app/Backends/IMAP.php b/src/app/Backends/IMAP.php --- a/src/app/Backends/IMAP.php +++ b/src/app/Backends/IMAP.php @@ -130,7 +130,7 @@ $mailbox = self::toUTF7($name); self::createFolder($imap, $mailbox, true, $folderconfig['metadata']); } catch (\Exception $e) { - \Log::warning("Failed to create the default folder" . $e->getMessage()); + \Log::warning("Failed to create the default folder. " . $e->getMessage()); } } $imap->closeConnection(); diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -1112,8 +1112,6 @@ $roles = []; foreach ($user->entitlements as $entitlement) { - \Log::debug("Examining {$entitlement->sku->title}"); - switch ($entitlement->sku->title) { case "mailbox": break; diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js --- a/src/resources/js/user/routes.js +++ b/src/resources/js/user/routes.js @@ -18,17 +18,15 @@ const FileInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/Info') const FileListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/List') const PaymentStatusComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Payment/Status') +const PoliciesComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Policies') const ResourceInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/Info') const ResourceListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/List') const RoomInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/Info') const RoomListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/List') -const SettingsComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Settings') const SharedFolderInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/SharedFolder/Info') const SharedFolderListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/SharedFolder/List') const UserInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/Info') const UserListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/List') -const UserProfileComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/Profile') -const UserProfileDeleteComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/ProfileDelete') const WalletComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Wallet') const MeetComponent = () => import(/* webpackChunkName: "../user/meet" */ '../../vue/Meet/Room.vue') @@ -115,18 +113,6 @@ meta: { requiresAuth: true } }, { - path: '/profile', - name: 'profile', - component: UserProfileComponent, - meta: { requiresAuth: true } - }, - { - path: '/profile/delete', - name: 'profile-delete', - component: UserProfileDeleteComponent, - meta: { requiresAuth: true } - }, - { path: '/resource/:resource', name: 'resource', component: ResourceInfoComponent, @@ -151,10 +137,16 @@ meta: { requiresAuth: true, perm: 'rooms' } }, { + path: '/policies', + name: 'policies', + component: PoliciesComponent, + meta: { requiresAuth: true, perm: 'settings' } + }, + { path: '/settings', name: 'settings', - component: SettingsComponent, - meta: { requiresAuth: true, perm: 'settings' } + component: UserInfoComponent, + meta: { requiresAuth: true } }, { path: '/shared-folder/:folder', diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -86,9 +86,10 @@ 'domains' => "Domains", 'files' => "Files", 'invitations' => "Invitations", + 'myaccount' => "My account", + 'policies' => "Policies", 'profile' => "Your profile", 'resources' => "Resources", - 'settings' => "Settings", 'shared-folders' => "Shared folders", 'users' => "User accounts", 'wallet' => "Wallet", @@ -200,6 +201,7 @@ 'or' => "or", 'password' => "Password", 'password-confirm' => "Confirm Password", + 'personal' => "Personal information", 'phone' => "Phone", 'selectcountries' => "Select countries", 'settings' => "Settings", @@ -350,6 +352,12 @@ . " Enter the code we sent you, or click the link in the message.", ], + 'policies' => [ + 'password-policy' => "Password Policy", + 'password-retention' => "Password Retention", + 'password-max-age' => "Require a password change every", + ], + 'resource' => [ 'create' => "Create resource", 'delete' => "Delete resource", @@ -382,12 +390,6 @@ 'url' => "You can access the room at the URL below. Use this URL to invite people to join you. This room is only open when you (or another room moderator) is in attendance.", ], - 'settings' => [ - 'password-policy' => "Password Policy", - 'password-retention' => "Password Retention", - 'password-max-age' => "Require a password change every", - ], - 'shf' => [ 'aliases-none' => "This shared folder has no email aliases.", 'create' => "Create folder", @@ -474,7 +476,6 @@ 'degraded-warning' => "The account is degraded. Some features have been disabled.", 'degraded-hint' => "Please, make a payment.", 'delete' => "Delete user", - 'delete-account' => "Delete this account?", 'delete-email' => "Delete {email}", 'delete-text' => "Do you really want to delete this user permanently?" . " This will delete all account data and withdraw the permission to access the email account." @@ -507,7 +508,6 @@ 'pass-link-hint' => "Press Submit to activate the link", 'passwordpolicy' => "Password Policy", 'price' => "Price", - 'profile-title' => "Your profile", 'profile-delete' => "Delete account", 'profile-delete-title' => "Delete this account?", 'profile-delete-text1' => "This will delete the account as well as all domains, users and aliases associated with this account.", diff --git a/src/resources/lang/fr/ui.php b/src/resources/lang/fr/ui.php --- a/src/resources/lang/fr/ui.php +++ b/src/resources/lang/fr/ui.php @@ -418,7 +418,6 @@ 'org' => "Organisation", 'package' => "Paquet", 'price' => "Prix", - 'profile-title' => "Votre profile", 'profile-delete' => "Supprimer compte", 'profile-delete-title' => "Supprimer ce compte?", 'profile-delete-text1' => "Cela supprimera le compte ainsi que tous les domaines, utilisateurs et alias associés à ce compte.", diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss --- a/src/resources/themes/app.scss +++ b/src/resources/themes/app.scss @@ -378,8 +378,8 @@ // Some icons are too big, scale them down &.link-companionapp, &.link-domains, + &.link-policies, &.link-resources, - &.link-settings, &.link-wallet, &.link-invitations { svg { @@ -389,6 +389,7 @@ &.link-distlists, &.link-files, + &.link-settings, &.link-shared-folders { svg { transform: scale(0.9); diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue --- a/src/resources/vue/Dashboard.vue +++ b/src/resources/vue/Dashboard.vue @@ -3,8 +3,8 @@
- - {{ $t('dashboard.profile') }} + + {{ $t('dashboard.myaccount') }} {{ $t('dashboard.domains') }} @@ -36,8 +36,8 @@ {{ $t('dashboard.files') }} {{ $t('dashboard.beta') }} - - {{ $t('dashboard.settings') }} + + {{ $t('dashboard.policies') }} {{ $t('dashboard.webmail') }} @@ -64,6 +64,7 @@ require('@fortawesome/free-solid-svg-icons/faGear').definition, require('@fortawesome/free-solid-svg-icons/faGlobe').definition, require('@fortawesome/free-solid-svg-icons/faMobileScreen').definition, + require('@fortawesome/free-solid-svg-icons/faShieldHalved').definition, require('@fortawesome/free-solid-svg-icons/faSliders').definition, require('@fortawesome/free-solid-svg-icons/faUserGear').definition, require('@fortawesome/free-solid-svg-icons/faUsers').definition, diff --git a/src/resources/vue/Settings.vue b/src/resources/vue/Policies.vue rename from src/resources/vue/Settings.vue rename to src/resources/vue/Policies.vue --- a/src/resources/vue/Settings.vue +++ b/src/resources/vue/Policies.vue @@ -1,14 +1,14 @@ @@ -159,33 +221,63 @@ }, data() { return { + countries: window.config.countries, + isSelf: false, passwordLinkCode: '', passwordMode: '', user_id: null, user: { aliases: [], config: [] }, - status: {} + supportEmail: window.config['app.support_email'], + status: {}, + successRoute: { name: 'users' } } }, computed: { - isSelf: function () { - return this.user_id == this.$root.authInfo.id + deleteButton: function () { + return { + className: 'btn-danger modal-action', + dismiss: 'modal', + label: this.isSelf ? 'user.profile-delete' : 'btn.delete', + icon: 'trash-can' + } + }, + isController: function () { + return this.$root.hasPermission('users') }, passwordLink: function () { return this.$root.appUrl + '/password-reset/' + this.passwordLinkCode + }, + tabs: function () { + let tabs = ['form.general'] + + if (this.user_id === 'new') { + return tabs + } + + if (this.isController) { + tabs.push('form.settings') + } + + tabs.push('form.personal') + + return tabs } }, created() { - this.user_id = this.$route.params.user + if (this.$route.name === 'settings') { + this.user_id = this.$root.authInfo.id + this.successRoute = null + } else { + this.user_id = this.$route.params.user + } + + this.isSelf = this.user_id == this.$root.authInfo.id if (this.user_id !== 'new') { axios.get('/api/v4/users/' + this.user_id, { loader: true }) .then(response => { - this.user = response.data - this.user.first_name = response.data.settings.first_name - this.user.last_name = response.data.settings.last_name - this.user.organization = response.data.settings.organization + this.user = { ...response.data, ...response.data.settings } this.status = response.data.statusInfo - this.passwordLinkCode = this.user.passwordLinkCode }) .catch(this.$root.errorHandler) @@ -250,14 +342,21 @@ submit() { this.$root.clearFormValidation($('#general form')) + let props = this.isController ? ['aliases'] : [] + if (this.user_id === 'new') { + props = props.concat(['email', 'first_name', 'last_name', 'organization']) + } + let method = 'post' let location = '/api/v4/users' - let post = this.$root.pick(this.user, ['aliases', 'email', 'first_name', 'last_name', 'organization']) + let post = this.$root.pick(this.user, props) if (this.user_id !== 'new') { method = 'put' location += '/' + this.user_id - post.skus = this.$refs.skus.getSkus() + if (this.$refs.skus) { + post.skus = this.$refs.skus.getSkus() + } } else { post.package = $('#user-packages input:checked').val() } @@ -276,7 +375,27 @@ } this.$toast.success(response.data.message) - this.$router.push({ name: 'users' }) + if (this.successRoute) { + this.$router.push(this.successRoute) + } + }) + }, + submitPersonalSettings() { + this.$root.clearFormValidation($('#personal form')) + + let post = this.$root.pick(this.user, ['first_name', 'last_name', 'organization', 'phone', + 'country', 'external_email', 'billing_address']) + + axios.put('/api/v4/users' + '/' + this.user_id, post) + .then(response => { + if (response.data.statusInfo) { + this.$root.authInfo.statusInfo = response.data.statusInfo + } + + this.$toast.success(response.data.message) + if (this.successRoute) { + this.$router.push(this.successRoute) + } }) }, submitSettings() { @@ -305,18 +424,14 @@ .then(response => { if (response.data.status == 'success') { this.$toast.success(response.data.message) - this.$router.push({ name: 'users' }) + + if (this.isSelf) { + this.$root.logoutUser() + } else { + this.$router.push(this.successRoute) + } } }) - }, - showDeleteConfirmation() { - if (this.user_id == this.$root.authInfo.id) { - // Deleting self, redirect to /profile/delete page - this.$router.push({ name: 'profile-delete' }) - } else { - // Display the warning - this.$refs.deleteWarning.show() - } } } } diff --git a/src/resources/vue/User/Profile.vue b/src/resources/vue/User/Profile.vue deleted file mode 100644 --- a/src/resources/vue/User/Profile.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - diff --git a/src/resources/vue/User/ProfileDelete.vue b/src/resources/vue/User/ProfileDelete.vue deleted file mode 100644 --- a/src/resources/vue/User/ProfileDelete.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/src/tests/Browser/DomainTest.php b/src/tests/Browser/DomainTest.php --- a/src/tests/Browser/DomainTest.php +++ b/src/tests/Browser/DomainTest.php @@ -13,7 +13,6 @@ use Tests\Browser\Pages\DomainList; use Tests\Browser\Pages\Home; use Tests\TestCaseDusk; -use Illuminate\Foundation\Testing\DatabaseMigrations; class DomainTest extends TestCaseDusk { @@ -223,7 +222,7 @@ ->on(new Home()) ->submitLogon('jack@kolab.org', 'simple123', true) ->on(new Dashboard()) - ->assertVisible('@links a.link-profile') + ->assertVisible('@links a.link-settings') ->assertMissing('@links a.link-domains') ->assertMissing('@links a.link-users') ->assertMissing('@links a.link-wallet'); diff --git a/src/tests/Browser/LogonTest.php b/src/tests/Browser/LogonTest.php --- a/src/tests/Browser/LogonTest.php +++ b/src/tests/Browser/LogonTest.php @@ -7,9 +7,7 @@ use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; -use Tests\Browser\Pages\UserProfile; use Tests\TestCaseDusk; -use Illuminate\Foundation\Testing\DatabaseMigrations; class LogonTest extends TestCaseDusk { @@ -133,7 +131,7 @@ ->submitLogon('john@kolab.org', 'simple123', true) // Checks if we're really on Dashboard page ->on(new Dashboard()) - ->assertVisible('@links a.link-profile') + ->assertVisible('@links a.link-settings') ->assertVisible('@links a.link-domains') ->assertVisible('@links a.link-users') ->assertVisible('@links a.link-wallet') @@ -278,15 +276,17 @@ public function testAfterLogonRedirect(): void { $this->browse(function (Browser $browser) { - // User is logged in - $browser->visit(new UserProfile()); - - // Test redirect if the token is invalid - $browser->script("localStorage.setItem('token', '123')"); - $browser->refresh() + // User is logged in, visit the My account page + $browser->visit('/settings') + // invalidate the session token + ->execScript("localStorage.setItem('token', '123')") + // refresh the page + ->refresh() ->on(new Home()) + // log in the user ->submitLogon('john@kolab.org', 'simple123', false) - ->waitForLocation('/profile'); + // wait for a "redirect" to the My account page + ->waitForLocation('/settings'); }); } } diff --git a/src/tests/Browser/Pages/Settings.php b/src/tests/Browser/Pages/Policies.php rename from src/tests/Browser/Pages/Settings.php rename to src/tests/Browser/Pages/Policies.php --- a/src/tests/Browser/Pages/Settings.php +++ b/src/tests/Browser/Pages/Policies.php @@ -4,7 +4,7 @@ use Laravel\Dusk\Page; -class Settings extends Page +class Policies extends Page { /** * Get the URL for the page. @@ -13,7 +13,7 @@ */ public function url(): string { - return '/settings'; + return '/policies'; } /** @@ -38,7 +38,7 @@ { return [ '@app' => '#app', - '@form' => '#settings form', + '@form' => '#policies form', ]; } } diff --git a/src/tests/Browser/Pages/UserInfo.php b/src/tests/Browser/Pages/UserInfo.php --- a/src/tests/Browser/Pages/UserInfo.php +++ b/src/tests/Browser/Pages/UserInfo.php @@ -25,7 +25,7 @@ */ public function assert($browser) { - $browser->waitFor('@form') + $browser->waitFor('@general form') ->waitUntilMissing('.app-loader'); } @@ -38,11 +38,11 @@ { return [ '@app' => '#app', - '@form' => '#user-info form', '@nav' => 'ul.nav-tabs', '@packages' => '#user-packages', '@settings' => '#settings', '@general' => '#general', + '@personal' => '#personal', '@skus' => '#user-skus', '@status' => '#status-box', ]; diff --git a/src/tests/Browser/Pages/UserProfile.php b/src/tests/Browser/Pages/UserProfile.php deleted file mode 100644 --- a/src/tests/Browser/Pages/UserProfile.php +++ /dev/null @@ -1,45 +0,0 @@ -assertPathIs($this->url()) - ->waitUntilMissing('@app .app-loader') - ->assertSeeIn('#user-profile .card-title', 'Your profile'); - } - - /** - * Get the element shortcuts for the page. - * - * @return array - */ - public function elements(): array - { - return [ - '@app' => '#app', - '@form' => '#user-profile form', - ]; - } -} diff --git a/src/tests/Browser/PasswordResetTest.php b/src/tests/Browser/PasswordResetTest.php --- a/src/tests/Browser/PasswordResetTest.php +++ b/src/tests/Browser/PasswordResetTest.php @@ -10,7 +10,6 @@ use Tests\Browser\Pages\Home; use Tests\Browser\Pages\PasswordReset; use Tests\TestCaseDusk; -use Illuminate\Foundation\Testing\DatabaseMigrations; class PasswordResetTest extends TestCaseDusk { diff --git a/src/tests/Browser/SettingsTest.php b/src/tests/Browser/PoliciesTest.php copy from src/tests/Browser/SettingsTest.php copy to src/tests/Browser/PoliciesTest.php --- a/src/tests/Browser/SettingsTest.php +++ b/src/tests/Browser/PoliciesTest.php @@ -7,24 +7,24 @@ use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; -use Tests\Browser\Pages\Settings; +use Tests\Browser\Pages\Policies; use Tests\TestCaseDusk; -class SettingsTest extends TestCaseDusk +class PoliciesTest extends TestCaseDusk { /** - * Test settings page (unauthenticated) + * Test Policies page (unauthenticated) */ - public function testSettingsUnauth(): void + public function testPoliciesUnauth(): void { // Test that the page requires authentication $this->browse(function (Browser $browser) { - $browser->visit('/settings')->on(new Home()); + $browser->visit('/policies')->on(new Home()); }); } /** - * Test settings "box" on Dashboard + * Test Policies "box" on Dashboard */ public function testDashboard(): void { @@ -33,8 +33,8 @@ $browser->visit(new Home()) ->submitLogon('jack@kolab.org', 'simple123', true) ->on(new Dashboard()) - ->assertMissing('@links .link-settings') - ->visit('/settings') + ->assertMissing('@links .link-policies') + ->visit('/policies') ->assertErrorPage(403) ->within(new Menu(), function (Browser $browser) { $browser->clickMenuItem('logout'); @@ -45,25 +45,25 @@ ->on(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) ->on(new Dashboard()) - ->assertSeeIn('@links .link-settings svg + span', 'Settings'); + ->assertSeeIn('@links .link-policies svg + span', 'Policies'); }); } /** - * Test Settings page + * Test Policies page * * @depends testDashboard */ - public function testSettings(): void + public function testPolicies(): void { $john = $this->getTestUser('john@kolab.org'); $john->setSetting('password_policy', 'min:5,max:100,lower'); $john->setSetting('max_password_age', null); $this->browse(function (Browser $browser) { - $browser->click('@links .link-settings') - ->on(new Settings()) - ->assertSeeIn('#settings .card-title', 'Settings') + $browser->click('@links .link-policies') + ->on(new Policies()) + ->assertSeeIn('#policies .card-title', 'Policies') // Password policy ->assertSeeIn('@form .row:nth-child(1) > label', 'Password Policy') ->with('@form #password_policy', function (Browser $browser) { diff --git a/src/tests/Browser/SettingsTest.php b/src/tests/Browser/SettingsTest.php --- a/src/tests/Browser/SettingsTest.php +++ b/src/tests/Browser/SettingsTest.php @@ -2,16 +2,51 @@ namespace Tests\Browser; +use App\User; use Tests\Browser; -use Tests\Browser\Components\Menu; +use Tests\Browser\Components\Dialog; +use Tests\Browser\Components\ListInput; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; use Tests\Browser\Pages\Home; -use Tests\Browser\Pages\Settings; +use Tests\Browser\Pages\UserInfo; use Tests\TestCaseDusk; class SettingsTest extends TestCaseDusk { + private $profile = [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'currency' => 'USD', + 'country' => 'US', + 'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005", + 'external_email' => 'john.doe.external@gmail.com', + 'phone' => '+1 509-248-1111', + 'organization' => 'Kolab Developers', + ]; + + /** + * {@inheritDoc} + */ + public function setUp(): void + { + parent::setUp(); + + User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); + $this->deleteTestUser('profile-delete@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); + $this->deleteTestUser('profile-delete@kolabnow.com'); + + parent::tearDown(); + } + /** * Test settings page (unauthenticated) */ @@ -24,97 +59,194 @@ } /** - * Test settings "box" on Dashboard + * Test settings page (wallet controller) */ - public function testDashboard(): void + public function testSettingsController(): void { $this->browse(function (Browser $browser) { - // Test a user that is not an account owner + $user = $this->getTestUser('john@kolab.org'); + $user->setSetting('password_policy', 'min:10,upper,digit'); + $browser->visit(new Home()) - ->submitLogon('jack@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertMissing('@links .link-settings') - ->visit('/settings') - ->assertErrorPage(403) - ->within(new Menu(), function (Browser $browser) { - $browser->clickMenuItem('logout'); - }); - - // Test the account owner - $browser->waitForLocation('/login') - ->on(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) ->on(new Dashboard()) - ->assertSeeIn('@links .link-settings svg + span', 'Settings'); + ->assertSeeIn('@links .link-settings', 'My account') + ->click('@links .link-settings') + ->on(new UserInfo()) + ->assertSeeIn('#user-info button.button-delete', 'Delete account') + ->assertSeeIn('#user-info .card-title', 'My account') + ->assertSeeIn('@nav #tab-general', 'General') + ->with('@general', function (Browser $browser) use ($user) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'Status (Customer No.)') + ->assertSeeIn('div.row:nth-child(1) #status', 'Active') + ->assertSeeIn('div.row:nth-child(1) #userid', "({$user->id})") + ->assertSeeIn('div.row:nth-child(2) label', 'Email') + ->assertValue('div.row:nth-child(2) input[type=text]', $user->email) + ->assertDisabled('div.row:nth-child(2) input[type=text]') + ->assertSeeIn('div.row:nth-child(3) label', 'Email Aliases') + ->assertVisible('div.row:nth-child(3) .list-input') + ->with(new ListInput('#aliases'), function (Browser $browser) { + $browser->assertListInputValue(['john.doe@kolab.org']) + ->assertValue('@input', ''); + }) + ->assertSeeIn('div.row:nth-child(4) label', 'Password') + ->assertValue('div.row:nth-child(4) input#password', '') + ->assertValue('div.row:nth-child(4) input#password_confirmation', '') + ->assertAttribute('#password', 'placeholder', 'Password') + ->assertAttribute('#password_confirmation', 'placeholder', 'Confirm Password') + ->assertMissing('div.row:nth-child(4) .btn-group') + ->assertMissing('div.row:nth-child(4) #password-link') + ->assertSeeIn('div.row:nth-child(5) label', 'Subscriptions') + ->assertVisible('div.row:nth-child(5) table'); + }) + ->assertSeeIn('@nav #tab-settings', 'Settings') + ->click('@nav #tab-settings') + ->with('@settings', function (Browser $browser) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting') + ->click('div.row:nth-child(1) input[type=checkbox]'); + }) + ->assertSeeIn('@nav #tab-personal', 'Personal information') + ->click('@nav #tab-personal') + ->with('@personal', function (Browser $browser) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'First Name') + ->assertValue('div.row:nth-child(1) input[type=text]', $this->profile['first_name']) + ->assertSeeIn('div.row:nth-child(2) label', 'Last Name') + ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['last_name']) + ->assertSeeIn('div.row:nth-child(3) label', 'Organization') + ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['organization']) + ->assertSeeIn('div.row:nth-child(4) label', 'Phone') + ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['phone']) + ->assertSeeIn('div.row:nth-child(5) label', 'External Email') + ->assertValue('div.row:nth-child(5) input[type=text]', $this->profile['external_email']) + ->assertSeeIn('div.row:nth-child(6) label', 'Address') + ->assertValue('div.row:nth-child(6) textarea', $this->profile['billing_address']) + ->assertSeeIn('div.row:nth-child(7) label', 'Country') + ->assertValue('div.row:nth-child(7) select', $this->profile['country']) + // Set some fields and submit + ->type('#first_name', 'Arnie') + ->vueClear('#last_name') + ->click('button[type=submit]'); + }) + ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); }); } /** - * Test Settings page - * - * @depends testDashboard + * Test settings page (non-controller user) */ - public function testSettings(): void + public function testProfileNonController(): void { - $john = $this->getTestUser('john@kolab.org'); - $john->setSetting('password_policy', 'min:5,max:100,lower'); - $john->setSetting('max_password_age', null); + $user = $this->getTestUser('john@kolab.org'); + $user->setSetting('password_policy', 'min:10,upper,digit'); + // Test acting as non-controller $this->browse(function (Browser $browser) { - $browser->click('@links .link-settings') - ->on(new Settings()) - ->assertSeeIn('#settings .card-title', 'Settings') - // Password policy - ->assertSeeIn('@form .row:nth-child(1) > label', 'Password Policy') - ->with('@form #password_policy', function (Browser $browser) { - $browser->assertElementsCount('li', 7) - ->assertSeeIn('li:nth-child(1) label', 'Minimum password length') - ->assertChecked('li:nth-child(1) input[type=checkbox]') - ->assertDisabled('li:nth-child(1) input[type=checkbox]') - ->assertValue('li:nth-child(1) input[type=text]', '5') - ->assertSeeIn('li:nth-child(2) label', 'Maximum password length') - ->assertChecked('li:nth-child(2) input[type=checkbox]') - ->assertDisabled('li:nth-child(2) input[type=checkbox]') - ->assertValue('li:nth-child(2) input[type=text]', '100') - ->assertSeeIn('li:nth-child(3) label', 'Password contains a lower-case character') - ->assertChecked('li:nth-child(3) input[type=checkbox]') - ->assertMissing('li:nth-child(3) input[type=text]') - ->assertSeeIn('li:nth-child(4) label', 'Password contains an upper-case character') - ->assertNotChecked('li:nth-child(4) input[type=checkbox]') - ->assertMissing('li:nth-child(4) input[type=text]') - ->assertSeeIn('li:nth-child(5) label', 'Password contains a digit') - ->assertNotChecked('li:nth-child(5) input[type=checkbox]') - ->assertMissing('li:nth-child(5) input[type=text]') - ->assertSeeIn('li:nth-child(6) label', 'Password contains a special character') - ->assertNotChecked('li:nth-child(6) input[type=checkbox]') - ->assertMissing('li:nth-child(6) input[type=text]') - ->assertSeeIn('li:nth-child(7) label', 'Password cannot be the same as the last') - ->assertNotChecked('li:nth-child(7) input[type=checkbox]') - ->assertMissing('li:nth-child(7) input[type=text]') - ->assertSelected('li:nth-child(7) select', 3) - ->assertSelectHasOptions('li:nth-child(7) select', [1,2,3,4,5,6]) - // Change the policy - ->type('li:nth-child(1) input[type=text]', '11') - ->type('li:nth-child(2) input[type=text]', '120') - ->click('li:nth-child(3) input[type=checkbox]') - ->click('li:nth-child(4) input[type=checkbox]'); + $browser->visit(new Home()) + ->submitLogon('jack@kolab.org', 'simple123', true) + ->on(new Dashboard()) + ->assertSeeIn('@links .link-settings', 'My account') + ->click('@links .link-settings') + ->on(new UserInfo()) + ->assertMissing('#user-info button.button-delete') + ->assertSeeIn('#user-info .card-title', 'My account') + ->assertSeeIn('@nav #tab-general', 'General') + ->with('@general', function (Browser $browser) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'Email') + ->assertValue('div.row:nth-child(1) input[type=text]', 'jack@kolab.org') + ->assertSeeIn('div.row:nth-child(2) label', 'Password') + ->assertValue('div.row:nth-child(2) input#password', '') + ->assertValue('div.row:nth-child(2) input#password_confirmation', '') + ->assertAttribute('#password', 'placeholder', 'Password') + ->assertAttribute('#password_confirmation', 'placeholder', 'Confirm Password') + ->assertMissing('div.row:nth-child(2) .btn-group') + ->assertMissing('div.row:nth-child(2) #password-link') + ->assertMissing('div.row:nth-child(3)') + ->whenAvailable('#password_policy', function (Browser $browser) { + $browser->assertElementsCount('li', 3) + ->assertMissing('li:nth-child(1) svg.text-success') + ->assertSeeIn('li:nth-child(1) small', "Minimum password length: 10 characters") + ->assertMissing('li:nth-child(2) svg.text-success') + ->assertSeeIn('li:nth-child(2) small', "Password contains an upper-case character") + ->assertMissing('li:nth-child(3) svg.text-success') + ->assertSeeIn('li:nth-child(3) small', "Password contains a digit"); + }); + }) + ->assertMissing('@nav #tab-settings') + ->assertSeeIn('@nav #tab-personal', 'Personal information') + ->click('@nav #tab-personal') + ->with('@personal', function (Browser $browser) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'First Name') + ->assertValue('div.row:nth-child(1) input[type=text]', 'Jack') + ->assertSeeIn('div.row:nth-child(2) label', 'Last Name') + ->assertValue('div.row:nth-child(2) input[type=text]', 'Daniels') + ->assertSeeIn('div.row:nth-child(3) label', 'Organization') + ->assertSeeIn('div.row:nth-child(4) label', 'Phone') + ->assertSeeIn('div.row:nth-child(5) label', 'External Email') + ->assertSeeIn('div.row:nth-child(6) label', 'Address') + ->assertSeeIn('div.row:nth-child(7) label', 'Country') + ->click('button[type=submit]'); }) - ->assertSeeIn('@form .row:nth-child(2) > label', 'Password Retention') - ->with('@form #password_retention', function (Browser $browser) { - $browser->assertElementsCount('li', 1) - ->assertSeeIn('li:nth-child(1) label', 'Require a password change every') - ->assertNotChecked('li:nth-child(1) input[type=checkbox]') - ->assertSelected('li:nth-child(1) select', 3) - ->assertSelectHasOptions('li:nth-child(1) select', [3, 6, 9, 12]) - // change the policy - ->check('li:nth-child(1) input[type=checkbox]') - ->select('li:nth-child(1) select', 6); + ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); + }); + + $user = $this->getTestUser('profile-delete@kolabnow.com', ['password' => 'simple123']); + $oldpassword = $user->password; + + // Test password change + $this->browse(function (Browser $browser) use ($user) { + $browser->visit(new Home()) + ->submitLogon($user->email, 'simple123', true) + ->on(new Dashboard()) + ->click('@links .link-settings') + ->on(new UserInfo()) + ->assertSeeIn('@nav #tab-general', 'General') + ->with('@general', function (Browser $browser) { + $browser + ->type('input#password', '12345678') + ->type('input#password_confirmation', '12345678') + ->click('button[type=submit]'); }) - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.'); + ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); }); - $this->assertSame('min:11,max:120,upper', $john->getSetting('password_policy')); - $this->assertSame('6', $john->getSetting('max_password_age')); + $this->assertTrue($oldpassword != $user->fresh()->password); + } + + /** + * Test deleting an account + */ + public function testAccountDelete(): void + { + $this->browse(function (Browser $browser) { + $user = $this->getTestUser('profile-delete@kolabnow.com', ['password' => 'simple123']); + + $browser->visit(new Home()) + ->submitLogon('profile-delete@kolabnow.com', 'simple123', true) + ->on(new Dashboard()) + ->assertSeeIn('@links .link-settings', 'My account') + ->click('@links .link-settings') + ->on(new UserInfo()) + ->assertSeeIn('#user-info button.button-delete', 'Delete account') + ->click('#user-info button.button-delete') + ->with(new Dialog('#delete-warning'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Delete this account?') + ->assertSeeIn('@body', 'This will delete the account as well as all domains') + ->assertSeeIn('@body strong', 'This operation is irreversible') + ->assertFocused('@button-cancel') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Delete account') + ->click('@button-cancel'); + }) + ->waitUntilMissing('#delete-warning') + ->click('#user-info button.button-delete') + ->with(new Dialog('#delete-warning'), function (Browser $browser) { + $browser->click('@button-action'); + }) + ->waitUntilMissing('#delete-warning') + ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.') + ->on(new Home()); + + $this->assertTrue($user->fresh()->trashed()); + }); } } diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php --- a/src/tests/Browser/SignupTest.php +++ b/src/tests/Browser/SignupTest.php @@ -17,7 +17,6 @@ use Tests\Browser\Pages\PaymentStatus; use Tests\Browser\Pages\Signup; use Tests\TestCaseDusk; -use Illuminate\Foundation\Testing\DatabaseMigrations; class SignupTest extends TestCaseDusk { @@ -410,7 +409,7 @@ ->waitUntilMissing('.app-loader') ->on(new Dashboard()) ->assertUser('signuptestdusk@' . \config('app.domain')) - ->assertVisible('@links a.link-profile') + ->assertVisible('@links a.link-settings') ->assertMissing('@links a.link-domains') ->assertVisible('@links a.link-users') ->assertVisible('@links a.link-wallet'); @@ -511,7 +510,7 @@ ->waitUntilMissing('.app-loader') ->on(new Dashboard()) ->assertUser('admin@user-domain-signup.com') - ->assertVisible('@links a.link-profile') + ->assertVisible('@links a.link-settings') ->assertVisible('@links a.link-domains') ->assertVisible('@links a.link-users') ->assertVisible('@links a.link-wallet'); diff --git a/src/tests/Browser/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php deleted file mode 100644 --- a/src/tests/Browser/UserProfileTest.php +++ /dev/null @@ -1,228 +0,0 @@ - 'John', - 'last_name' => 'Doe', - 'currency' => 'USD', - 'country' => 'US', - 'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005", - 'external_email' => 'john.doe.external@gmail.com', - 'phone' => '+1 509-248-1111', - 'organization' => 'Kolab Developers', - ]; - - /** - * {@inheritDoc} - */ - public function setUp(): void - { - parent::setUp(); - - User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); - $this->deleteTestUser('profile-delete@kolabnow.com'); - } - - /** - * {@inheritDoc} - */ - public function tearDown(): void - { - User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); - $this->deleteTestUser('profile-delete@kolabnow.com'); - - parent::tearDown(); - } - - /** - * Test profile page (unauthenticated) - */ - public function testProfileUnauth(): void - { - // Test that the page requires authentication - $this->browse(function (Browser $browser) { - $browser->visit('/profile')->on(new Home()); - }); - } - - /** - * Test profile page - */ - public function testProfile(): void - { - $user = $this->getTestUser('john@kolab.org'); - $user->setSetting('password_policy', 'min:10,upper,digit'); - - $this->browse(function (Browser $browser) { - $browser->visit(new Home()) - ->submitLogon('john@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertSeeIn('@links .link-profile', 'Your profile') - ->click('@links .link-profile') - ->on(new UserProfile()) - ->assertSeeIn('#user-profile .profile-delete', 'Delete account') - ->whenAvailable('@form', function (Browser $browser) { - $user = User::where('email', 'john@kolab.org')->first(); - // Assert form content - $browser->assertFocused('div.row:nth-child(2) input') - ->assertSeeIn('div.row:nth-child(1) label', 'Customer No.') - ->assertSeeIn('div.row:nth-child(1) .form-control-plaintext', $user->id) - ->assertSeeIn('div.row:nth-child(2) label', 'First Name') - ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name']) - ->assertSeeIn('div.row:nth-child(3) label', 'Last Name') - ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name']) - ->assertSeeIn('div.row:nth-child(4) label', 'Organization') - ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization']) - ->assertSeeIn('div.row:nth-child(5) label', 'Phone') - ->assertValue('div.row:nth-child(5) input[type=text]', $this->profile['phone']) - ->assertSeeIn('div.row:nth-child(6) label', 'External Email') - ->assertValue('div.row:nth-child(6) input[type=text]', $this->profile['external_email']) - ->assertSeeIn('div.row:nth-child(7) label', 'Address') - ->assertValue('div.row:nth-child(7) textarea', $this->profile['billing_address']) - ->assertSeeIn('div.row:nth-child(8) label', 'Country') - ->assertValue('div.row:nth-child(8) select', $this->profile['country']) - ->assertSeeIn('div.row:nth-child(9) label', 'Password') - ->assertValue('div.row:nth-child(9) input#password', '') - ->assertValue('div.row:nth-child(9) input#password_confirmation', '') - ->assertAttribute('#password', 'placeholder', 'Password') - ->assertAttribute('#password_confirmation', 'placeholder', 'Confirm Password') - ->whenAvailable('#password_policy', function (Browser $browser) { - $browser->assertElementsCount('li', 3) - ->assertMissing('li:nth-child(1) svg.text-success') - ->assertSeeIn('li:nth-child(1) small', "Minimum password length: 10 characters") - ->assertMissing('li:nth-child(2) svg.text-success') - ->assertSeeIn('li:nth-child(2) small', "Password contains an upper-case character") - ->assertMissing('li:nth-child(3) svg.text-success') - ->assertSeeIn('li:nth-child(3) small', "Password contains a digit"); - }) - ->assertSeeIn('button[type=submit]', 'Submit'); - - // Test password policy checking - $browser->type('#password', '1A') - ->whenAvailable('#password_policy', function (Browser $browser) { - $browser->waitFor('li:nth-child(2) svg.text-success') - ->waitFor('li:nth-child(3) svg.text-success') - ->assertMissing('li:nth-child(1) svg.text-success'); - }) - ->vueClear('#password'); - - // Test form error handling - $browser->type('#phone', 'aaaaaa') - ->type('#external_email', 'bbbbb') - ->click('button[type=submit]') - ->waitFor('#phone + .invalid-feedback') - ->assertSeeIn('#phone + .invalid-feedback', 'The phone format is invalid.') - ->assertSeeIn( - '#external_email + .invalid-feedback', - 'The external email must be a valid email address.' - ) - ->assertFocused('#phone') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->clearToasts(); - - // Clear all fields and submit - // FIXME: Should any of these fields be required? - $browser->vueClear('#first_name') - ->vueClear('#last_name') - ->vueClear('#organization') - ->vueClear('#phone') - ->vueClear('#external_email') - ->vueClear('#billing_address') - ->click('button[type=submit]') - ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); - }) - // On success we're redirected to Dashboard - ->on(new Dashboard()); - }); - } - - /** - * Test profile of non-controller user - */ - public function testProfileNonController(): void - { - $user = $this->getTestUser('john@kolab.org'); - $user->setSetting('password_policy', 'min:10,upper,digit'); - - // Test acting as non-controller - $this->browse(function (Browser $browser) { - $browser->visit('/logout') - ->visit(new Home()) - ->submitLogon('jack@kolab.org', 'simple123', true) - ->on(new Dashboard()) - ->assertSeeIn('@links .link-profile', 'Your profile') - ->click('@links .link-profile') - ->on(new UserProfile()) - ->assertMissing('#user-profile .profile-delete') - ->whenAvailable('@form', function (Browser $browser) { - // TODO: decide on what fields the non-controller user should be able - // to see/change - }) - // Check that the account policy is used - ->whenAvailable('#password_policy', function (Browser $browser) { - $browser->assertElementsCount('li', 3) - ->assertMissing('li:nth-child(1) svg.text-success') - ->assertSeeIn('li:nth-child(1) small', "Minimum password length: 10 characters") - ->assertMissing('li:nth-child(2) svg.text-success') - ->assertSeeIn('li:nth-child(2) small', "Password contains an upper-case character") - ->assertMissing('li:nth-child(3) svg.text-success') - ->assertSeeIn('li:nth-child(3) small', "Password contains a digit"); - }); - - // Test that /profile/delete page is not accessible - $browser->visit('/profile/delete') - ->assertErrorPage(403); - }); - } - - /** - * Test profile delete page - */ - public function testProfileDelete(): void - { - $user = $this->getTestUser('profile-delete@kolabnow.com', ['password' => 'simple123']); - - $this->browse(function (Browser $browser) use ($user) { - $browser->visit('/logout') - ->on(new Home()) - ->submitLogon('profile-delete@kolabnow.com', 'simple123', true) - ->on(new Dashboard()) - ->assertSeeIn('@links .link-profile', 'Your profile') - ->click('@links .link-profile') - ->on(new UserProfile()) - ->click('#user-profile .profile-delete') - ->waitForLocation('/profile/delete') - ->assertSeeIn('#user-delete .card-title', 'Delete this account?') - ->assertSeeIn('#user-delete .button-cancel', 'Cancel') - ->assertSeeIn('#user-delete .card-text', 'This operation is irreversible') - ->assertFocused('#user-delete .button-cancel') - ->click('#user-delete .button-cancel') - ->waitForLocation('/profile') - ->on(new UserProfile()); - - // Test deleting the user - $browser->click('#user-profile .profile-delete') - ->waitForLocation('/profile/delete') - ->click('#user-delete .button-delete') - ->waitForLocation('/login') - ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.'); - - $this->assertTrue($user->fresh()->trashed()); - }); - } - - // TODO: Test that Ned (John's "delegatee") can delete himself - // TODO: Test that Ned (John's "delegatee") can/can't delete John ? -} diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php --- a/src/tests/Browser/UsersTest.php +++ b/src/tests/Browser/UsersTest.php @@ -19,7 +19,6 @@ use Tests\Browser\Pages\UserList; use Tests\Browser\Pages\Wallet as WalletPage; use Tests\TestCaseDusk; -use Illuminate\Foundation\Testing\DatabaseMigrations; class UsersTest extends TestCaseDusk { @@ -28,6 +27,11 @@ 'last_name' => 'Doe', 'organization' => 'Kolab Developers', 'limit_geo' => null, + 'currency' => 'USD', + 'country' => 'US', + 'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005", + 'external_email' => 'john.doe.external@gmail.com', + 'phone' => '+1 509-248-1111', ]; /** @@ -89,9 +93,9 @@ } /** - * Test user account editing page (not profile page) + * Test user page - General tab */ - public function testInfo(): void + public function testUserGeneralTab(): void { $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); @@ -106,45 +110,28 @@ ->submitLogon('john@kolab.org', 'simple123', false) ->on(new UserInfo()) ->assertSeeIn('#user-info .card-title', 'User account') + ->assertSeeIn('@nav #tab-general', 'General') ->with('@general', function (Browser $browser) { - // Assert form content + // Assert the General tab content $browser->assertSeeIn('div.row:nth-child(1) label', 'Status') ->assertSeeIn('div.row:nth-child(1) #status', 'Active') - ->assertFocused('div.row:nth-child(2) input') - ->assertSeeIn('div.row:nth-child(2) label', 'First Name') - ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name']) - ->assertSeeIn('div.row:nth-child(3) label', 'Last Name') - ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name']) - ->assertSeeIn('div.row:nth-child(4) label', 'Organization') - ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization']) - ->assertSeeIn('div.row:nth-child(5) label', 'Email') - ->assertValue('div.row:nth-child(5) input[type=text]', 'john@kolab.org') - ->assertDisabled('div.row:nth-child(5) input[type=text]') - ->assertSeeIn('div.row:nth-child(6) label', 'Email Aliases') - ->assertVisible('div.row:nth-child(6) .list-input') + ->assertSeeIn('div.row:nth-child(2) label', 'Email') + ->assertValue('div.row:nth-child(2) input[type=text]', 'john@kolab.org') + ->assertDisabled('div.row:nth-child(2) input[type=text]') + ->assertSeeIn('div.row:nth-child(3) label', 'Email Aliases') + ->assertVisible('div.row:nth-child(3) .list-input') ->with(new ListInput('#aliases'), function (Browser $browser) { $browser->assertListInputValue(['john.doe@kolab.org']) ->assertValue('@input', ''); }) - ->assertSeeIn('div.row:nth-child(7) label', 'Password') - ->assertValue('div.row:nth-child(7) input#password', '') - ->assertValue('div.row:nth-child(7) input#password_confirmation', '') + ->assertSeeIn('div.row:nth-child(4) label', 'Password') + ->assertValue('div.row:nth-child(4) input#password', '') + ->assertValue('div.row:nth-child(4) input#password_confirmation', '') ->assertAttribute('#password', 'placeholder', 'Password') ->assertAttribute('#password_confirmation', 'placeholder', 'Confirm Password') - ->assertMissing('div.row:nth-child(7) .btn-group') - ->assertMissing('div.row:nth-child(7) #password-link') - ->assertSeeIn('button[type=submit]', 'Submit') - // Clear some fields and submit - ->vueClear('#first_name') - ->vueClear('#last_name') - ->click('button[type=submit]'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') - ->on(new UserList()) - ->click('@table tr:nth-child(3) a') - ->on(new UserInfo()) - ->assertSeeIn('#user-info .card-title', 'User account') - ->with('@general', function (Browser $browser) { + ->assertMissing('div.row:nth-child(4) .btn-group') + ->assertMissing('div.row:nth-child(4) #password-link'); + // Test error handling (password) $browser->type('#password', 'aaaaaA') ->vueClear('#password_confirmation') @@ -176,17 +163,13 @@ }) ->scrollTo('button[type=submit]')->pause(500) ->click('button[type=submit]') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error'); - - $browser->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->assertFormError(2, 'The specified alias is invalid.', false); - }); - - // Test adding aliases - $browser->with(new ListInput('#aliases'), function (Browser $browser) { - $browser->removeListEntry(2) - ->addListEntry('john.test@kolab.org'); - }) + ->assertToast(Toast::TYPE_ERROR, 'Form validation error') + ->with(new ListInput('#aliases'), function (Browser $browser) { + $browser->assertFormError(2, 'The specified alias is invalid.', false) + // Test adding aliases + ->removeListEntry(2) + ->addListEntry('john.test@kolab.org'); + }) ->click('button[type=submit]') ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); }) @@ -199,8 +182,8 @@ // Test subscriptions $browser->with('@general', function (Browser $browser) { - $browser->assertSeeIn('div.row:nth-child(8) label', 'Subscriptions') - ->assertVisible('@skus.row:nth-child(8)') + $browser->assertSeeIn('div.row:nth-child(5) label', 'Subscriptions') + ->assertVisible('@skus.row:nth-child(5)') ->with('@skus', function ($browser) { $browser->assertElementsCount('tbody tr', 5) // Mailbox SKU @@ -301,7 +284,7 @@ ->with('@general', function (Browser $browser) use ($jack, $john, $code) { // Test displaying an existing password reset link $link = Browser::$baseUrl . '/password-reset/' . $code->short_code . '-' . $code->code; - $browser->assertSeeIn('div.row:nth-child(7) label', 'Password') + $browser->assertSeeIn('div.row:nth-child(4) label', 'Password') ->assertMissing('#password') ->assertMissing('#password_confirmation') ->assertMissing('#pass-mode-link:checked') @@ -356,11 +339,76 @@ } /** - * Test user settings tab + * Test user page - General tab + * + * @depends testUserGeneralTab + */ + public function testUserPersonalTab(): void + { + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + $jack = $this->getTestUser('jack@kolab.org'); + $jack->setSetting('organization', null); + + // Test the account controller + $browser->visit('/user/' . $john->id) + ->on(new UserInfo()) + ->assertSeeIn('@nav #tab-personal', 'Personal information') + ->click('#tab-personal') + ->with('@personal', function (Browser $browser) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'First Name') + ->assertValue('div.row:nth-child(1) input[type=text]', $this->profile['first_name']) + ->assertSeeIn('div.row:nth-child(2) label', 'Last Name') + ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['last_name']) + ->assertSeeIn('div.row:nth-child(3) label', 'Organization') + ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['organization']) + ->assertSeeIn('div.row:nth-child(4) label', 'Phone') + ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['phone']) + ->assertSeeIn('div.row:nth-child(5) label', 'External Email') + ->assertValue('div.row:nth-child(5) input[type=text]', $this->profile['external_email']) + ->assertSeeIn('div.row:nth-child(6) label', 'Address') + ->assertValue('div.row:nth-child(6) textarea', $this->profile['billing_address']) + ->assertSeeIn('div.row:nth-child(7) label', 'Country') + ->assertValue('div.row:nth-child(7) select', $this->profile['country']) + // Set some fields and submit + ->type('#first_name', 'Arnie') + ->vueClear('#last_name') + ->click('button[type=submit]'); + }) + ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') + ->on(new UserList()); + + $this->assertSame('Arnie', $john->getSetting('first_name')); + $this->assertSame(null, $john->getSetting('last_name')); + + // Test the non-controller user + $browser->visit('/user/' . $jack->id) + ->on(new UserInfo()) + ->click('#tab-personal') + ->with('@personal', function (Browser $browser) { + $browser->assertSeeIn('div.row:nth-child(1) label', 'First Name') + ->assertValue('div.row:nth-child(1) input[type=text]', 'Jack') + ->assertSeeIn('div.row:nth-child(2) label', 'Last Name') + ->assertValue('div.row:nth-child(2) input[type=text]', 'Daniels') + ->assertSeeIn('div.row:nth-child(3) label', 'Organization') + ->assertValue('div.row:nth-child(3) input[type=text]', '') + // Set some fields and submit + ->type('#organization', 'Test') + ->click('button[type=submit]'); + }) + ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.') + ->on(new UserList()); + + $this->assertSame('Test', $jack->getSetting('organization')); + }); + } + + /** + * Test user page - Settings tab * - * @depends testInfo + * @depends testUserPersonalTab */ - public function testUserSettings(): void + public function testUserSettingsTab(): void { $john = $this->getTestUser('john@kolab.org'); $john->setSetting('greylist_enabled', null); @@ -370,11 +418,9 @@ $this->browse(function (Browser $browser) use ($john) { $browser->visit('/user/' . $john->id) ->on(new UserInfo()) - ->assertElementsCount('@nav a', 2) - ->assertSeeIn('@nav #tab-general', 'General') ->assertSeeIn('@nav #tab-settings', 'Settings') ->click('@nav #tab-settings') - ->with('#settings form', function (Browser $browser) { + ->with('@settings', function (Browser $browser) { $browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting') ->assertMissing('div.row:nth-child(2)') // guam and geo-lockin settings are hidden ->click('div.row:nth-child(1) input[type=checkbox]:checked') @@ -391,7 +437,7 @@ $browser->refresh() ->on(new UserInfo()) ->click('@nav #tab-settings') - ->with('#settings form', function (Browser $browser) use ($john) { + ->with('@settings', function (Browser $browser) use ($john) { $browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting') ->assertSeeIn('div.row:nth-child(2) label', 'IMAP proxy') ->assertNotChecked('div.row:nth-child(2) input') @@ -425,8 +471,6 @@ /** * Test user adding page - * - * @depends testInfo */ public function testNewUser(): void { @@ -439,6 +483,8 @@ ->click('button.user-new') ->on(new UserInfo()) ->assertSeeIn('#user-info .card-title', 'New user account') + ->assertMissing('@nav #tab-settings') + ->assertMissing('@nav #tab-personal') ->with('@general', function (Browser $browser) { // Assert form content $browser->assertFocused('div.row:nth-child(1) input')