Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117719460
D970.1774888700.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
36 KB
Referenced Files
None
Subscribers
None
D970.1774888700.diff
View Options
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';
}
@@ -239,6 +246,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.
*
@@ -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($('<div class="invalid-feedback">')
- .text($.type(msg) === 'string' ? msg : msg.join('<br>')))
+ input.parent().find('.invalid-feedback').remove()
+ input.parent().append($('<div class="invalid-feedback">')
+ .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 @@
</div>
</div>
<p id="dashboard-nav">
+ <router-link class="link-profile" :to="{ name: 'profile' }">Your profile</router-link>
<router-link class="link-domains" :to="{ name: 'domains' }">Domains</router-link>
<router-link class="link-users" :to="{ name: 'users' }">User accounts</router-link>
</p>
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 @@
+<template>
+ <div class="container">
+ <div class="card" id="user-profile">
+ <div class="card-body">
+ <div class="card-title">Your profile</div>
+ <div class="card-text">
+ <form @submit.prevent="submit">
+ <div class="form-group row">
+ <label for="first_name" class="col-sm-4 col-form-label">First name</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="first_name" v-model="profile.first_name">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="last_name" class="col-sm-4 col-form-label">Last name</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="last_name" v-model="profile.last_name">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="phone" class="col-sm-4 col-form-label">Phone</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="phone" v-model="profile.phone">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="external_email" class="col-sm-4 col-form-label">External email</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="external_email" v-model="profile.external_email">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="billing_address" class="col-sm-4 col-form-label">Address</label>
+ <div class="col-sm-8">
+ <textarea class="form-control" id="billing_address" v-model="profile.billing_address"></textarea>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="country" class="col-sm-4 col-form-label">Country</label>
+ <div class="col-sm-8">
+ <select class="form-control custom-select" id="country" v-model="profile.country">
+ <option value="">-</option>
+ <option v-for="(item, code) in countries" :value="code">{{ item[1] }}</option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="password" class="col-sm-4 col-form-label">Password</label>
+ <div class="col-sm-8">
+ <input type="password" class="form-control" id="password" v-model="profile.password">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="password_confirmaton" class="col-sm-4 col-form-label">Confirm password</label>
+ <div class="col-sm-8">
+ <input type="password" class="form-control" id="password_confirmation" v-model="profile.password_confirmation">
+ </div>
+ </div>
+ <button class="btn btn-primary" type="submit">Submit</button>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ data() {
+ return {
+ profile: {},
+ countries: window.config.countries
+ }
+ },
+ created() {
+ this.profile = this.$store.state.authInfo.settings
+ },
+ mounted() {
+ $('#first_name').focus()
+ },
+ methods: {
+ submit() {
+ if (this.profile.country) {
+ this.profile.currency = this.countries[this.profile.country][0]
+ }
+
+ this.$root.clearFormValidation($('#user-profile form'))
+
+ axios.put('/api/v4/users/' + this.$store.state.authInfo.id, this.profile)
+ .then(response => {
+ delete this.profile.password
+ delete this.profile.password_confirm
+
+ if (response.data.status == 'success') {
+ this.$toastr('success', response.data.message)
+ }
+ })
+ }
+ }
+ }
+</script>
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'
@@ -54,6 +55,12 @@
name: 'password-reset',
component: PasswordResetComponent
},
+ {
+ path: '/profile',
+ name: 'profile',
+ component: UserProfileComponent,
+ meta: { requiresAuth: true }
+ },
{
path: '/signup/:param?',
name: 'signup',
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 @@
+<?php
+
+namespace Tests\Browser\Pages;
+
+use Laravel\Dusk\Browser;
+use Laravel\Dusk\Page;
+
+class UserProfile extends Page
+{
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '/profile';
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser
+ *
+ * @return void
+ */
+ public function assert(Browser $browser)
+ {
+ $browser->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 @@
+<?php
+
+namespace Tests\Browser;
+
+use App\User;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\UserProfile;
+use Tests\DuskTestCase;
+use Laravel\Dusk\Browser;
+use Illuminate\Foundation\Testing\DatabaseMigrations;
+
+class UserProfileTest extends DuskTestCase
+{
+ 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',
+ ];
+
+ /**
+ * {@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/<user-id>)
+ */
+ 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/<user-id>)
+ */
+ 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();
+ }
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Mar 30, 4:38 PM (5 d, 17 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18786273
Default Alt Text
D970.1774888700.diff (36 KB)
Attached To
Mode
D970: User profile (Bifrost#T331493)
Attached
Detach File
Event Timeline