diff --git a/src/app/Http/Controllers/API/UsersController.php b/src/app/Http/Controllers/API/UsersController.php --- a/src/app/Http/Controllers/API/UsersController.php +++ b/src/app/Http/Controllers/API/UsersController.php @@ -75,18 +75,7 @@ public function info() { $user = $this->guard()->user(); - $response = $user->toArray(); - - // Settings - // TODO: It might be reasonable to limit the list of settings here to these - // that are safe and are used in the UI - $response['settings'] = []; - foreach ($user->settings as $item) { - $response['settings'][$item->key] = $item->value; - } - - // Status info - $response['statusInfo'] = self::statusInfo($user); + $response = $this->userResponse($user); return response()->json($response); } @@ -112,7 +101,6 @@ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } - $credentials = $request->only('email', 'password'); if ($token = $this->guard()->attempt($credentials)) { @@ -181,10 +169,12 @@ $user = User::find($id); if (empty($user)) { - return $this->errorResponse(404); + return $this->errorResponse(404); } - return response()->json($user); + $response = $this->userResponse($user); + + return response()->json($response); } /** @@ -345,4 +335,29 @@ return $current_user->id == $user_id; } + + /** + * Create a response data array for specified user. + * + * @param \App\User $user User object + * + * @return array Response data + */ + protected function userResponse(User $user): array + { + $response = $user->toArray(); + + // Settings + // TODO: It might be reasonable to limit the list of settings here to these + // that are safe and are used in the UI + $response['settings'] = []; + foreach ($user->settings as $item) { + $response['settings'][$item->key] = $item->value; + } + + // Status info + $response['statusInfo'] = self::statusInfo($user); + + return $response; + } } diff --git a/src/resources/vue/components/User/Info.vue b/src/resources/vue/components/User/Info.vue --- a/src/resources/vue/components/User/Info.vue +++ b/src/resources/vue/components/User/Info.vue @@ -4,6 +4,47 @@
User account
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
@@ -15,7 +56,7 @@ data() { return { user_id: null, - user: null + user: {} } }, created() { @@ -23,11 +64,31 @@ axios.get('/api/v4/users/' + this.user_id) .then(response => { this.user = response.data + this.user.first_name = response.data.settings.first_name + this.user.last_name = response.data.settings.last_name }) .catch(this.$root.errorHandler) } else { this.$root.errorPage(404) } + }, + mounted() { + $('#first_name').focus() + }, + methods: { + submit() { + this.$root.clearFormValidation($('#user-info form')) + + axios.put('/api/v4/users/' + this.user_id, this.user) + .then(response => { + delete this.user.password + delete this.user.password_confirm + + if (response.data.status == 'success') { + this.$toastr('success', response.data.message) + } + }) + } } } diff --git a/src/tests/Browser/Pages/UserInfo.php b/src/tests/Browser/Pages/UserInfo.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/Pages/UserInfo.php @@ -0,0 +1,45 @@ +waitFor('@form') + ->assertSeeIn('#user-info .card-title', 'User account'); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements(): array + { + return [ + '@app' => '#app', + '@form' => '#user-info form', + ]; + } +} diff --git a/src/tests/Browser/Pages/UserList.php b/src/tests/Browser/Pages/UserList.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/Pages/UserList.php @@ -0,0 +1,46 @@ +assertPathIs($this->url()) + ->waitUntilMissing('@app .app-loader') + ->assertSeeIn('#user-list .card-title', 'User Accounts'); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements(): array + { + return [ + '@app' => '#app', + '@table' => '#user-list table', + ]; + } +} diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/UsersTest.php @@ -0,0 +1,142 @@ + 'John', + 'last_name' => 'Doe', + ]; + + /** + * {@inheritDoc} + */ + public function setUp(): void + { + parent::setUp(); + + User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); + + parent::tearDown(); + } + + /** + * Test user info page (unauthenticated) + */ + public function testInfoUnauth(): void + { + // Test that the page requires authentication + $this->browse(function (Browser $browser) { + $user = User::where('email', 'john@kolab.org')->first(); + + $browser->visit('/user/' . $user->id)->on(new Home()); + }); + } + + /** + * Test users list page (unauthenticated) + */ + public function testListUnauth(): void + { + // Test that the page requires authentication + $this->browse(function (Browser $browser) { + $browser->visit('/users')->on(new Home()); + }); + } + + /** + * Test users list page + */ + public function testList(): void + { + // Test that the page requires authentication + $this->browse(function (Browser $browser) { + $browser->visit(new Home()) + ->submitLogon('john@kolab.org', 'simple123', true) + ->on(new Dashboard()) + ->assertSeeIn('@links .link-users', 'User accounts') + ->click('@links .link-users') + ->on(new UserList()) + ->whenAvailable('@table', function ($browser) { + $this->assertCount(1, $browser->elements('tbody tr')); + $browser->assertSeeIn('tbody tr td a', 'john@kolab.org'); + }); + }); + } + + /** + * Test profile page + * + * @depends testList + */ + public function testInfo(): void + { + $this->browse(function (Browser $browser) { + $browser->on(new UserList()) + ->click('@table tr:first-child a') + ->on(new UserInfo()) + ->whenAvailable('@form', function (Browser $browser) { + // Assert form content + $browser->assertFocused('div.row:nth-child(1) input') + ->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', 'Email') + ->assertValue('div.row:nth-child(3) input[type=text]', 'john@kolab.org') +// ->assertDisabled('div.row:nth-child(3) input') + ->assertSeeIn('div.row:nth-child(4) label', 'Password') + ->assertValue('div.row:nth-child(4) input[type=password]', '') + ->assertSeeIn('div.row:nth-child(5) label', 'Confirm password') + ->assertValue('div.row:nth-child(5) input[type=password]', '') + ->assertSeeIn('button[type=submit]', 'Submit'); + + // Clear some fields and submit + $browser->type('#first_name', '') + ->type('#last_name', '') + ->click('button[type=submit]'); + }) + ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) { + $browser->assertToastTitle('') + ->assertToastMessage('User data updated successfully') + ->closeToast(); + }); + + // Test error handling + $browser->with('@form', function (Browser $browser) { + $browser->type('#password', 'aaaaaa') + ->type('#password_confirmation', '') + ->click('button[type=submit]') + ->waitFor('#password + .invalid-feedback') + ->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.') + ->assertFocused('#password'); + }) + ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) { + $browser->assertToastTitle('Error') + ->assertToastMessage('Form validation error') + ->closeToast(); + }); + + // TODO: Test password change + }); + } +}