diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -103,7 +103,7 @@ */ public function isActive(): bool { - return ($this->status & self::STATUS_ACTIVE) == true; + return ($this->status & self::STATUS_ACTIVE) > 0; } /** @@ -113,7 +113,7 @@ */ public function isConfirmed(): bool { - return ($this->status & self::STATUS_CONFIRMED) == true; + return ($this->status & self::STATUS_CONFIRMED) > 0; } /** @@ -123,7 +123,7 @@ */ public function isDeleted(): bool { - return ($this->status & self::STATUS_DELETED) == true; + return ($this->status & self::STATUS_DELETED) > 0; } /** @@ -133,7 +133,7 @@ */ public function isExternal(): bool { - return ($this->type & self::TYPE_EXTERNAL) == true; + return ($this->type & self::TYPE_EXTERNAL) > 0; } /** @@ -143,7 +143,7 @@ */ public function isHosted(): bool { - return ($this->type & self::TYPE_HOSTED) == true; + return ($this->type & self::TYPE_HOSTED) > 0; } /** @@ -153,7 +153,7 @@ */ public function isNew(): bool { - return ($this->status & self::STATUS_NEW) == true; + return ($this->status & self::STATUS_NEW) > 0; } /** @@ -163,7 +163,7 @@ */ public function isPublic(): bool { - return ($this->type & self::TYPE_PUBLIC) == true; + return ($this->type & self::TYPE_PUBLIC) > 0; } /** @@ -173,7 +173,7 @@ */ public function isLdapReady(): bool { - return ($this->status & self::STATUS_LDAP_READY) == true; + return ($this->status & self::STATUS_LDAP_READY) > 0; } /** @@ -183,7 +183,7 @@ */ public function isSuspended(): bool { - return ($this->status & self::STATUS_SUSPENDED) == true; + return ($this->status & self::STATUS_SUSPENDED) > 0; } /** @@ -194,7 +194,7 @@ */ public function isVerified(): bool { - return ($this->status & self::STATUS_VERIFIED) == true; + return ($this->status & self::STATUS_VERIFIED) > 0; } /** diff --git a/src/app/Http/Controllers/API/DomainsController.php b/src/app/Http/Controllers/API/DomainsController.php --- a/src/app/Http/Controllers/API/DomainsController.php +++ b/src/app/Http/Controllers/API/DomainsController.php @@ -51,7 +51,7 @@ // Only owner (or admin) has access to the domain if (!self::hasAccess($domain)) { - return abort(403); + return $this->errorResponse(403); } if (!$domain->confirm()) { @@ -113,7 +113,7 @@ // Only owner (or admin) has access to the domain if (!self::hasAccess($domain)) { - return abort(403); + return $this->errorResponse(403); } $response = $domain->toArray(); 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 @@ -77,6 +77,15 @@ $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); return response()->json($response); @@ -165,16 +174,14 @@ */ public function show($id) { - $user = Auth::user(); - - if (!$user) { - return abort(403); + if (!$this->hasAccess($id)) { + return $this->errorResponse(403); } - // TODO: check whether or not the user is allowed - // for now, only allow self. - if ($user->id != $id) { - return abort(404); + $user = User::find($id); + + if (empty($user)) { + return $this->errorResponse(404); } return response()->json($user); @@ -212,7 +219,7 @@ if (!$domain->isPublic()) { $steps['domain-new'] = true; $steps['domain-ldap-ready'] = 'isLdapReady'; -// $steps['domain-verified'] = 'isVerified'; + $steps['domain-verified'] = 'isVerified'; $steps['domain-confirmed'] = 'isConfirmed'; } @@ -240,6 +247,79 @@ } /** + * Create a new user record. + * + * @param \Illuminate\Http\Request $request The API request. + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function store(Request $request) + { + // TODO + } + + /** + * Update user data. + * + * @param \Illuminate\Http\Request $request The API request. + * @params string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function update(Request $request, $id) + { + if (!$this->hasAccess($id)) { + return $this->errorResponse(403); + } + + $user = User::find($id); + + if (empty($user)) { + return $this->errorResponse(404); + } + + $rules = [ + 'external_email' => 'nullable|email', + 'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', + 'first_name' => 'string|nullable|max:512', + 'last_name' => 'string|nullable|max:512', + 'billing_address' => 'string|nullable|max:1024', + 'country' => 'string|nullable|alpha|size:2', + 'currency' => 'string|nullable|alpha|size:3', + ]; + + if (!empty($request->password) || !empty($request->password_confirmation)) { + $rules['password'] = 'required|min:4|max:2048|confirmed'; + } + + // Validate input + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + + // Update user settings + $settings = $request->only(array_keys($rules)); + unset($settings['password']); + + if (!empty($settings)) { + $user->setSettings($settings); + } + + // Update user password + if (!empty($rules['password'])) { + $user->password = $request->password; + $user->save(); + } + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-update-success'), + ]); + } + + /** * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\Guard @@ -248,4 +328,21 @@ { return Auth::guard(); } + + /** + * Check if the current user has access to the specified user + * + * @param int $user_id User identifier + * + * @return bool True if current user has access, False otherwise + */ + protected function hasAccess($user_id): bool + { + $current_user = $this->guard()->user(); + + // TODO: Admins, other users + // FIXME: This probably should be some kind of middleware/guard + + return $current_user->id == $user_id; + } } diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php --- a/src/app/Http/Controllers/Controller.php +++ b/src/app/Http/Controllers/Controller.php @@ -12,4 +12,33 @@ use AuthorizesRequests; use DispatchesJobs; use ValidatesRequests; + + + /** + * Common error response builder for API (JSON) responses + * + * @param int $code Error code + * @param string $message Error message + * + * @return \Illuminate\Http\JsonResponse + */ + protected function errorResponse(int $code, string $message = null) + { + $errors = [ + 400 => "Bad request", + 401 => "Unauthorized", + 403 => "Access denied", + 404 => "Not found", + 422 => "Input validation error", + 405 => "Method not allowed", + 500 => "Internal server error", + ]; + + $response = [ + 'status' => 'error', + 'message' => $message ?: (isset($errors[$code]) ? $errors[$code] : "Server error"), + ]; + + return response()->json($response, $code); + } } diff --git a/src/app/Traits/UserSettingsTrait.php b/src/app/Traits/UserSettingsTrait.php --- a/src/app/Traits/UserSettingsTrait.php +++ b/src/app/Traits/UserSettingsTrait.php @@ -39,8 +39,8 @@ * $user->setSetting('locale', 'en'); * ``` * - * @param string $key Setting name - * @param string $value The new value for the setting. + * @param string $key Setting name + * @param string|null $value The new value for the setting. * * @return void */ @@ -73,16 +73,15 @@ $this->setCache(); } - private function storeSetting(string $key, $value) + private function storeSetting(string $key, $value): void { - $record = UserSetting::where(['user_id' => $this->id, 'key' => $key])->first(); - - if ($record) { - $record->value = $value; - $record->save(); + if ($value === null || $value === '') { + UserSetting::where(['user_id' => $this->id, 'key' => $key])->delete(); } else { - $data = new UserSetting(['key' => $key, 'value' => $value]); - $this->settings()->save($data); + UserSetting::updateOrCreate( + ['user_id' => $this->id, 'key' => $key], + ['value' => $value] + ); } } @@ -103,7 +102,9 @@ $cached = []; foreach ($this->settings()->get() as $entry) { - $cached[$entry->key] = $entry->value; + if ($entry->value !== null && $entry->value !== '') { + $cached[$entry->key] = $entry->value; + } } Cache::forever('user_settings_' . $this->id, $cached); diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -248,7 +248,7 @@ */ public function isActive(): bool { - return ($this->status & self::STATUS_ACTIVE) == true; + return ($this->status & self::STATUS_ACTIVE) > 0; } /** @@ -258,7 +258,7 @@ */ public function isDeleted(): bool { - return ($this->status & self::STATUS_DELETED) == true; + return ($this->status & self::STATUS_DELETED) > 0; } /** @@ -269,7 +269,7 @@ */ public function isImapReady(): bool { - return ($this->status & self::STATUS_IMAP_READY) == true; + return ($this->status & self::STATUS_IMAP_READY) > 0; } /** @@ -279,7 +279,7 @@ */ public function isLdapReady(): bool { - return ($this->status & self::STATUS_LDAP_READY) == true; + return ($this->status & self::STATUS_LDAP_READY) > 0; } /** @@ -289,7 +289,7 @@ */ public function isNew(): bool { - return ($this->status & self::STATUS_NEW) == true; + return ($this->status & self::STATUS_NEW) > 0; } /** @@ -299,7 +299,7 @@ */ public function isSuspended(): bool { - return ($this->status & self::STATUS_SUSPENDED) == true; + return ($this->status & self::STATUS_SUSPENDED) > 0; } /** diff --git a/src/app/UserSetting.php b/src/app/UserSetting.php --- a/src/app/UserSetting.php +++ b/src/app/UserSetting.php @@ -15,7 +15,7 @@ class UserSetting extends Model { protected $fillable = [ - 'key', 'value' + 'user_id', 'key', 'value' ]; /** diff --git a/src/database/seeds/UserSeeder.php b/src/database/seeds/UserSeeder.php --- a/src/database/seeds/UserSeeder.php +++ b/src/database/seeds/UserSeeder.php @@ -26,7 +26,7 @@ $john = User::create( [ - 'name' => "John Doe", + 'name' => 'John Doe', 'email' => 'john@kolab.org', 'password' => 'simple123', 'email_verified_at' => now() @@ -35,10 +35,13 @@ $john->setSettings( [ - "first_name" => "John", - "last_name" => "Doe", - "currency" => "USD", - "country" => "US" + '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', ] ); @@ -52,7 +55,7 @@ $jack = User::create( [ - 'name' => "Jack Daniels", + 'name' => 'Jack Daniels', 'email' => 'jack@kolab.org', 'password' => 'simple123', 'email_verified_at' => now() @@ -61,10 +64,10 @@ $jack->setSettings( [ - "first_name" => "Jack", - "last_name" => "Daniels", - "currency" => "USD", - "country" => "US" + 'first_name' => 'Jack', + 'last_name' => 'Daniels', + 'currency' => 'USD', + 'country' => 'US' ] ); 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 @@ -36,8 +36,9 @@ if (input.length) { input.addClass('is-invalid') - .parent().append($('
') - .text($.type(msg) === 'string' ? msg : msg.join('
'))) + input.parent().find('.invalid-feedback').remove() + input.parent().append($('
') + .text($.type(msg) === 'string' ? msg : msg.join(' '))) return false } 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 @@ -21,4 +21,5 @@ 'process-domain-confirmed' => 'Custom domain ownership verified', 'domain-verify-success' => 'Domain verified successfully', + 'user-update-success' => 'User data updated successfully', ]; 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 @@ -33,6 +33,8 @@ display: flex; justify-content: center; color: #636b6f; + z-index: 10; + background: white; .code { text-align: right; @@ -71,3 +73,8 @@ padding: 1rem; background-color: $menu-bg-color; } + +.card-title { + font-size: 1.2rem; + font-weight: bold; +} diff --git a/src/resources/vue/components/Dashboard.vue b/src/resources/vue/components/Dashboard.vue --- a/src/resources/vue/components/Dashboard.vue +++ b/src/resources/vue/components/Dashboard.vue @@ -15,6 +15,7 @@

+ Your profile Domains User accounts

diff --git a/src/resources/vue/components/User/Profile.vue b/src/resources/vue/components/User/Profile.vue new file mode 100644 --- /dev/null +++ b/src/resources/vue/components/User/Profile.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/resources/vue/js/routes.js b/src/resources/vue/js/routes.js --- a/src/resources/vue/js/routes.js +++ b/src/resources/vue/js/routes.js @@ -13,6 +13,7 @@ import SignupComponent from '../components/Signup' import UserInfoComponent from '../components/User/Info' import UserListComponent from '../components/User/List' +import UserProfileComponent from '../components/User/Profile' import store from './store' @@ -55,6 +56,12 @@ component: PasswordResetComponent }, { + path: '/profile', + name: 'profile', + component: UserProfileComponent, + meta: { requiresAuth: true } + }, + { path: '/signup/:param?', name: 'signup', component: SignupComponent diff --git a/src/tests/Browser/Components/Toast.php b/src/tests/Browser/Components/Toast.php --- a/src/tests/Browser/Components/Toast.php +++ b/src/tests/Browser/Components/Toast.php @@ -77,7 +77,7 @@ } /** - * lose the toast with a click + * Close the toast with a click */ public function closeToast(Browser $browser) { diff --git a/src/tests/Browser/Pages/UserProfile.php b/src/tests/Browser/Pages/UserProfile.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/Pages/UserProfile.php @@ -0,0 +1,46 @@ +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/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/UserProfileTest.php @@ -0,0 +1,127 @@ + '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', + ]; + + /** + * {@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 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 + { + $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()) + ->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', 'Phone') + ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['phone']) + ->assertSeeIn('div.row:nth-child(4) label', 'External email') + ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['external_email']) + ->assertSeeIn('div.row:nth-child(5) label', 'Address') + ->assertValue('div.row:nth-child(5) textarea', $this->profile['billing_address']) + ->assertSeeIn('div.row:nth-child(6) label', 'Country') + ->assertValue('div.row:nth-child(6) select', $this->profile['country']) + ->assertSeeIn('div.row:nth-child(7) label', 'Password') + ->assertValue('div.row:nth-child(7) input[type=password]', '') + ->assertSeeIn('div.row:nth-child(8) label', 'Confirm password') + ->assertValue('div.row:nth-child(8) input[type=password]', '') + ->assertSeeIn('button[type=submit]', 'Submit'); + + // Clear all fields and submit + // FIXME: Should any of these fields be required? + $browser->type('#first_name', '') + ->type('#last_name', '') + ->type('#phone', '') + ->type('#external_email', '') + ->type('#billing_address', '') + ->select('#country', '') + ->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('#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'); + }) + ->with(new Toast(Toast::TYPE_ERROR), function (Browser $browser) { + $browser->assertToastTitle('Error') + ->assertToastMessage('Form validation error') + ->closeToast(); + }); + }); + } +} diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -5,6 +5,7 @@ use App\Http\Controllers\API\UsersController; use App\Domain; use App\User; +use Illuminate\Support\Facades\Queue; use Illuminate\Support\Str; use Tests\TestCase; @@ -22,6 +23,9 @@ $this->deleteTestDomain('userscontroller.com'); } + /** + * {@inheritDoc} + */ public function tearDown(): void { $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); @@ -32,7 +36,7 @@ } /** - * Test fetching current user info + * Test fetching current user info (/api/auth/info) */ public function testInfo(): void { @@ -43,29 +47,27 @@ ]); $response = $this->actingAs($user)->get("api/auth/info"); + $response->assertStatus(200); + $json = $response->json(); - $response->assertStatus(200); $this->assertEquals($user->id, $json['id']); $this->assertEquals($user->email, $json['email']); $this->assertEquals(User::STATUS_NEW, $json['status']); $this->assertTrue(is_array($json['statusInfo'])); + $this->assertEquals($user->getSetting('country'), $json['settings']['country']); + $this->assertEquals($user->getSetting('currency'), $json['settings']['currency']); } public function testIndex(): void { - $userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com'); - - $response = $this->actingAs($userA, 'api')->get("/api/v4/users/{$userA->id}"); - - $response->assertStatus(200); - $response->assertJson(['id' => $userA->id]); - - $user = factory(User::class)->create(); - $response = $this->actingAs($user)->get("/api/v4/users/{$userA->id}"); - $response->assertStatus(404); + // TODO + $this->markTestIncomplete(); } + /** + * Test /api/auth/login + */ public function testLogin(): string { // Request with no data @@ -174,14 +176,14 @@ $user->status |= User::STATUS_ACTIVE; $user->save(); -// $domain->status |= Domain::STATUS_VERIFIED; + $domain->status |= Domain::STATUS_VERIFIED; $domain->type = Domain::TYPE_EXTERNAL; $domain->save(); $result = UsersController::statusInfo($user); $this->assertSame('active', $result['status']); - $this->assertCount(6, $result['process']); + $this->assertCount(7, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('user-ldap-ready', $result['process'][1]['label']); @@ -192,10 +194,10 @@ $this->assertSame(true, $result['process'][3]['state']); $this->assertSame('domain-ldap-ready', $result['process'][4]['label']); $this->assertSame(false, $result['process'][4]['state']); -// $this->assertSame('domain-verified', $result['process'][5]['label']); -// $this->assertSame(true, $result['process'][5]['state']); - $this->assertSame('domain-confirmed', $result['process'][5]['label']); - $this->assertSame(false, $result['process'][5]['state']); + $this->assertSame('domain-verified', $result['process'][5]['label']); + $this->assertSame(true, $result['process'][5]['state']); + $this->assertSame('domain-confirmed', $result['process'][6]['label']); + $this->assertSame(false, $result['process'][6]['state']); $user->status |= User::STATUS_DELETED; $user->save(); @@ -204,4 +206,123 @@ $this->assertSame('deleted', $result['status']); } + + /** + * Test fetching user data/profile (GET /api/v4/users/) + */ + public function testShow(): void + { + $userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com'); + + // Test getting profile of self + $response = $this->actingAs($userA, 'api')->get("/api/v4/users/{$userA->id}"); + + $response->assertStatus(200); + $response->assertJson(['id' => $userA->id]); + + // Test unauthorized access to a profile of other user + $user = $this->getTestUser('jack@kolab.org'); + $response = $this->actingAs($user)->get("/api/v4/users/{$userA->id}"); + $response->assertStatus(403); + + // TODO: Test authorized access to a profile of other user + $this->markTestIncomplete(); + } + + /** + * Test user creation (POST /api/v4/users) + */ + public function testStore(): void + { + // TODO + $this->markTestIncomplete(); + } + + /** + * Test user update (PUT /api/v4/users/) + */ + public function testUpdate(): void + { + $userA = $this->getTestUser('UsersControllerTest1@userscontroller.com'); + $userB = $this->getTestUser('jack@kolab.org'); + + // Test unauthorized update of other user profile + $response = $this->actingAs($userB)->get("/api/v4/users/{$userA->id}", []); + $response->assertStatus(403); + + // Test updating of self + $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", []); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('success', $json['status']); + $this->assertSame("User data updated successfully", $json['message']); + $this->assertCount(2, $json); + + // Test some invalid data + $post = ['password' => '12345678', 'currency' => 'invalid']; + $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); + $response->assertStatus(422); + + $json = $response->json(); + + $this->assertSame('error', $json['status']); + $this->assertCount(2, $json); + $this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]); + $this->assertSame('The currency must be 3 characters.', $json['errors']['currency'][0]); + + // Test full profile update including password + $post = [ + 'password' => 'simple', + 'password_confirmation' => 'simple', + 'first_name' => 'John2', + 'last_name' => 'Doe2', + 'phone' => '+123 123 123', + 'external_email' => 'external@gmail.com', + 'billing_address' => 'billing', + 'country' => 'CH', + 'currency' => 'CHF', + ]; + + $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); + $json = $response->json(); + + $response->assertStatus(200); + + $this->assertSame('success', $json['status']); + $this->assertSame("User data updated successfully", $json['message']); + $this->assertCount(2, $json); + $this->assertTrue($userA->password != $userA->fresh()->password); + unset($post['password'], $post['password_confirmation']); + foreach ($post as $key => $value) { + $this->assertSame($value, $userA->getSetting($key)); + } + + // Test unsetting values + $post = [ + 'first_name' => '', + 'last_name' => '', + 'phone' => '', + 'external_email' => '', + 'billing_address' => '', + 'country' => '', + 'currency' => '', + ]; + + $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); + $json = $response->json(); + + $response->assertStatus(200); + + $this->assertSame('success', $json['status']); + $this->assertSame("User data updated successfully", $json['message']); + $this->assertCount(2, $json); + foreach ($post as $key => $value) { + $this->assertNull($userA->getSetting($key)); + } + + // TODO: Test authorized update of other user + $this->markTestIncomplete(); + } }