Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117428985
D1000.1774832932.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
58 KB
Referenced Files
None
Subscribers
None
D1000.1774832932.diff
View Options
diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/Storage.php
--- a/src/app/Handlers/Storage.php
+++ b/src/app/Handlers/Storage.php
@@ -6,7 +6,7 @@
{
public static function entitleableClass()
{
- return null;
+ return \App\User::class;
}
public static function preReq($entitlement, $object)
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
@@ -46,27 +46,47 @@
}
/**
- * Display a listing of the resources.
+ * Delete a user.
*
- * The user themself, and other user entitlements.
+ * @param int $id User identifier
*
- * @return \Illuminate\Http\JsonResponse
+ * @return \Illuminate\Http\JsonResponse The response
*/
- public function index()
+ public function destroy($id)
{
- $user = Auth::user();
+ $user = User::find($id);
+
+ if (empty($user)) {
+ return $this->errorResponse(404);
+ }
+
+ $current_user = $this->guard()->user();
- if (!$user) {
- return response()->json(['error' => 'unauthorized'], 401);
+ // User can't remove himself until he's the controller
+ if ($user->controller()->id != $current_user->id) {
+ return $this->errorResponse(403);
}
- $result = [$user];
+ $user->delete();
- $user->entitlements()->each(
- function ($entitlement) {
- $result[] = User::find($entitlement->user_id);
- }
- );
+ return response()->json([
+ 'status' => 'success',
+ 'message' => __('app.user-delete-success'),
+ ]);
+ }
+
+ /**
+ * Listing of users.
+ *
+ * The user-entitlements billed to the current user wallet(s)
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = $this->guard()->user();
+
+ $result = $user->users()->orderBy('email')->get();
return response()->json($result);
}
@@ -166,16 +186,18 @@
*/
public function show($id)
{
- if (!$this->hasAccess($id)) {
- return $this->errorResponse(403);
- }
-
$user = User::find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
+ $current_user = $this->guard()->user();
+
+ if ($current_user->id != $id && $user->controller()->id != $current_user->id) {
+ return $this->errorResponse(403);
+ }
+
$response = $this->userResponse($user);
return response()->json($response);
@@ -249,7 +271,9 @@
*/
public function store(Request $request)
{
- if ($this->guard()->user()->controller()->id !== $this->guard()->user()->id) {
+ $current_user = $this->guard()->user();
+
+ if ($current_user->controller()->id != $current_user->id) {
return $this->errorResponse(403);
}
@@ -300,16 +324,19 @@
*/
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);
}
+ $current_user = $this->guard()->user();
+
+ // TODO: Decide what attributes a user can change on his own profile
+ if ($current_user->id != $id && $user->controller()->id != $current_user->id) {
+ return $this->errorResponse(403);
+ }
+
if ($error_response = $this->validateUserRequest($request, $user, $settings)) {
return $error_response;
}
@@ -349,23 +376,6 @@
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;
- }
-
/**
* Create a response data array for specified user.
*
@@ -394,6 +404,10 @@
// Status info
$response['statusInfo'] = self::statusInfo($user);
+ // Information about wallets and controller for access checks
+ $response['wallets'] = $user->wallets->toArray();
+ $response['wallet'] = $user->wallet()->toArray();
+
return $response;
}
diff --git a/src/app/Jobs/DomainDelete.php b/src/app/Jobs/DomainDelete.php
--- a/src/app/Jobs/DomainDelete.php
+++ b/src/app/Jobs/DomainDelete.php
@@ -27,13 +27,13 @@
/**
* Create a new job instance.
*
- * @param Domain $domain The domain to delete.
+ * @param int $domain The ID of the domain to delete.
*
* @return void
*/
- public function __construct(Domain $domain)
+ public function __construct(int $domain_id)
{
- $this->domain = $domain;
+ $this->domain = Domain::withTrashed()->find($domain_id);
}
/**
@@ -43,6 +43,11 @@
*/
public function handle()
{
- LDAP::deleteDomain($this->domain);
+ if (!$this->domain->isDeleted()) {
+ LDAP::deleteDomain($this->domain);
+
+ $this->domain->status |= Domain::STATUS_DELETED;
+ $this->domain->save();
+ }
}
}
diff --git a/src/app/Jobs/UserDelete.php b/src/app/Jobs/UserDelete.php
--- a/src/app/Jobs/UserDelete.php
+++ b/src/app/Jobs/UserDelete.php
@@ -43,6 +43,11 @@
*/
public function handle()
{
- LDAP::deleteUser($this->user);
+ if (!$this->user->isDeleted()) {
+ LDAP::deleteUser($this->user);
+
+ $this->user->status |= User::STATUS_DELETED;
+ $this->user->save();
+ }
}
}
diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php
--- a/src/app/Observers/DomainObserver.php
+++ b/src/app/Observers/DomainObserver.php
@@ -3,6 +3,7 @@
namespace App\Observers;
use App\Domain;
+use Illuminate\Support\Facades\DB;
class DomainObserver
{
@@ -39,35 +40,47 @@
\App\Jobs\DomainCreate::dispatch($domain);
}
+ /**
+ * Handle the domain "deleting" event.
+ *
+ * @param \App\Domain $domain The domain.
+ *
+ * @return void
+ */
public function deleting(Domain $domain)
{
- //
+ // Entitlements do not have referential integrity on the entitled object, so this is our
+ // way of doing an onDelete('cascade') without the foreign key.
+ \App\Entitlement::where('entitleable_id', $domain->id)
+ ->where('entitleable_type', Domain::class)
+ ->delete();
}
/**
- * Handle the domain "updated" event.
+ * Handle the domain "deleted" event.
*
* @param \App\Domain $domain The domain.
*
* @return void
*/
- public function updated(Domain $domain)
+ public function deleted(Domain $domain)
{
- //
+ \App\Jobs\DomainDelete::dispatch($domain->id);
}
/**
- * Handle the domain "deleted" event.
+ * Handle the domain "updated" event.
*
* @param \App\Domain $domain The domain.
*
* @return void
*/
- public function deleted(Domain $domain)
+ public function updated(Domain $domain)
{
- \App\Jobs\DomainDelete::dispatch($domain);
+ //
}
+
/**
* Handle the domain "restored" event.
*
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -2,7 +2,10 @@
namespace App\Observers;
+use App\Entitlement;
+use App\Domain;
use App\User;
+use Illuminate\Support\Facades\DB;
class UserObserver
{
@@ -79,15 +82,57 @@
*/
public function deleting(User $user)
{
+ // TODO: Especially in tests we're doing delete() on a already deleted user.
+ // Should we escape here - for performance reasons?
+ // TODO: I think all of this should use database transactions
+
// Entitlements do not have referential integrity on the entitled object, so this is our
// way of doing an onDelete('cascade') without the foreign key.
- $entitlements = \App\Entitlement::where('entitleable_id', $user->id)
- ->where('entitleable_type', \App\User::class)->get();
+ Entitlement::where('entitleable_id', $user->id)
+ ->where('entitleable_type', User::class)
+ ->delete();
+
+ // Remove owned users/domains
+ $wallets = $user->wallets()->pluck('id')->all();
+ $assignments = Entitlement::whereIn('wallet_id', $wallets)->get();
+ $users = [];
+ $domains = [];
+ $entitlements = [];
- foreach ($entitlements as $entitlement) {
- $entitlement->delete();
+ foreach ($assignments as $entitlement) {
+ if ($entitlement->entitleable_type == Domain::class) {
+ $domains[] = $entitlement->entitleable_id;
+ } elseif ($entitlement->entitleable_type == User::class && $entitlement->entitleable_id != $user->id) {
+ $users[] = $entitlement->entitleable_id;
+ } else {
+ $entitlements[] = $entitlement->id;
+ }
+ }
+
+ $users = array_unique($users);
+ $domains = array_unique($domains);
+
+ // Note: Domains/users need to be deleted one by one to make sure
+ // events are fired and observers can do the proper cleanup.
+ // Entitlements have no delete event handlers as for now.
+ if (!empty($users)) {
+ foreach (User::whereIn('id', $users)->get() as $_user) {
+ $_user->delete();
+ }
}
+ if (!empty($domains)) {
+ foreach (Domain::whereIn('id', $domains)->get() as $_domain) {
+ $_domain->delete();
+ }
+ }
+
+ if (!empty($entitlements)) {
+ Entitlement::whereIn('id', $entitlements)->delete();
+ }
+
+ // FIXME: What do we do with user wallets?
+
\App\Jobs\UserDelete::dispatch($user->id);
}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -2,7 +2,6 @@
namespace App\Providers;
-use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -2,9 +2,11 @@
namespace App;
+use App\Entitlement;
use App\UserAlias;
use App\Traits\UserAliasesTrait;
use App\Traits\UserSettingsTrait;
+use App\Wallet;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -157,8 +159,10 @@
'entitleable_type' => User::class
])->first();
- if ($entitlement && $entitlement->owner_id != $this->id) {
- return $entitlement->owner;
+ // TODO: No entitlement should not happen, but in tests we have
+ // such cases, so we fallback to the user itself
+ if ($entitlement && $entitlement->wallet->owner_id != $this->id) {
+ return $entitlement->wallet->owner;
}
return $this;
@@ -360,6 +364,22 @@
return $this->hasMany('App\UserSetting', 'user_id');
}
+ /**
+ * Return users controlled by the current user.
+ *
+ * @return \Illuminate\Database\Eloquent\Builder Query builder
+ */
+ public function users()
+ {
+ $wallets = $this->wallets()->pluck('id')->all();
+
+ return $this->select('users.*', 'entitlements.wallet_id')
+ ->distinct()
+ ->leftJoin('entitlements', 'entitlements.entitleable_id', '=', 'users.id')
+ ->whereIn('entitlements.wallet_id', $wallets)
+ ->where('entitlements.entitleable_type', 'App\User');
+ }
+
/**
* Verification codes for this user.
*
@@ -370,6 +390,28 @@
return $this->hasMany('App\VerificationCode', 'user_id', 'id');
}
+ /**
+ * Returns the wallet by which the current user is controlled
+ *
+ * @return \App\Wallet A wallet object
+ */
+ public function wallet(): Wallet
+ {
+ // FIXME: This is most likely not the best way to do this
+ $entitlement = \App\Entitlement::where([
+ 'entitleable_id' => $this->id,
+ 'entitleable_type' => User::class
+ ])->first();
+
+ // TODO: No entitlement should not happen, but in tests we have
+ // such cases, so we fallback to the user's wallet in this case
+ if ($entitlement) {
+ return $entitlement->wallet;
+ }
+
+ return $this->wallets()->first();
+ }
+
/**
* Wallets this user owns.
*
diff --git a/src/composer.json b/src/composer.json
--- a/src/composer.json
+++ b/src/composer.json
@@ -22,6 +22,7 @@
"kolab/net_ldap3": "dev-master",
"laravel/framework": "5.8.*",
"laravel/tinker": "^1.0",
+ "morrislaptop/laravel-queue-clear": "^1.2",
"silviolleite/laravelpwa": "^1.0",
"spatie/laravel-translatable": "^4.2",
"swooletw/laravel-swoole": "^2.6",
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
@@ -109,6 +109,17 @@
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
},
+ isController(wallet_id) {
+ if (wallet_id && store.state.authInfo) {
+ for (let i = 0; i < store.state.authInfo.wallets.length; i++) {
+ if (wallet_id == store.state.authInfo.wallets[i].id) {
+ return true
+ }
+ }
+ }
+
+ return false
+ },
// Set user state to "logged in"
loginUser(token, dashboard) {
store.commit('logoutUser') // destroy old state data
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
@@ -20,7 +20,8 @@
'process-domain-verified' => 'Custom domain verified',
'process-domain-confirmed' => 'Custom domain ownership verified',
- 'domain-verify-success' => 'Domain verified successfully',
- 'user-update-success' => 'User data updated successfully',
- 'user-create-success' => 'User created successfully',
+ 'domain-verify-success' => 'Domain verified successfully.',
+ 'user-update-success' => 'User data updated successfully.',
+ 'user-create-success' => 'User created successfully.',
+ 'user-delete-success' => 'User deleted successfully.',
];
diff --git a/src/resources/lang/en/auth.php b/src/resources/lang/en/auth.php
--- a/src/resources/lang/en/auth.php
+++ b/src/resources/lang/en/auth.php
@@ -14,6 +14,6 @@
*/
'failed' => 'Invalid username or password.',
- 'throttle' => 'Too many login attempts. Please try again in :seconds seconds',
- 'logoutsuccess' => 'Successfully logged out',
+ 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
+ 'logoutsuccess' => 'Successfully logged out.',
];
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
@@ -108,7 +108,7 @@
// on new user redirect to users list
if (this.user_id === 'new') {
- this.$route.push({ name: 'users' })
+ this.$router.push({ name: 'users' })
}
})
}
diff --git a/src/resources/vue/components/User/List.vue b/src/resources/vue/components/User/List.vue
--- a/src/resources/vue/components/User/List.vue
+++ b/src/resources/vue/components/User/List.vue
@@ -13,15 +13,42 @@
</tr>
</thead>
<tbody>
- <tr v-for="user in users">
- <td><router-link :to="{ path: 'user/' + user.id }">{{ user.email }}</router-link></td>
- <td></td>
+ <tr v-for="user in users" :id="'user' + user.id">
+ <td>
+ <router-link :to="{ path: 'user/' + user.id }">{{ user.email }}</router-link>
+ </td>
+ <td>
+ <button v-if="$root.isController(user.wallet_id)" class="btn btn-danger button-delete"
+ @click="deleteUser(user.id)">Delete</button>
+ </td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
+
+ <div id="delete-warning" class="modal" tabindex="-1" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title"></h5>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <p>Do you really want to delete this user permanently?
+ This will delete all account data and withdraw the permission to access the email account.
+ Please note that this action cannot be undone.</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary modal-cancel" data-dismiss="modal">Cancel</button>
+ <button type="button" class="btn btn-danger modal-action" @click="deleteUser()">Delete</button>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
@@ -29,7 +56,8 @@
export default {
data() {
return {
- users: []
+ users: [],
+ current_user: null
}
},
created() {
@@ -38,6 +66,47 @@
this.users = response.data
})
.catch(this.$root.errorHandler)
+ },
+ methods: {
+ deleteUser(id) {
+ let dialog = $('#delete-warning').modal('hide')
+
+ // Delete the user from the confirm dialog
+ if (!id && this.current_user) {
+ id = this.current_user.id
+ axios.delete('/api/v4/users/' + id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toastr('success', response.data.message)
+ $('#user' + id).remove()
+ }
+ })
+
+ return
+ }
+
+
+ // Deleting self, redirect to /profile/delete page
+ if (id == this.$store.state.authInfo.id) {
+ this.$router.push({ name: 'profile-delete' })
+ return
+ }
+
+ // Display the warning
+ if (this.current_user = this.getUser(id)) {
+ dialog.find('.modal-title').text('Delete ' + this.current_user.email)
+ dialog.on('shown.bs.modal', () => {
+ dialog.find('button.modal-cancel').focus()
+ }).modal()
+ }
+ },
+ getUser(id) {
+ for (let i = 0; i < this.users.length; i++) {
+ if (this.users[i].id == id) {
+ return this.users[i]
+ }
+ }
+ }
}
}
</script>
diff --git a/src/resources/vue/components/User/Profile.vue b/src/resources/vue/components/User/Profile.vue
--- a/src/resources/vue/components/User/Profile.vue
+++ b/src/resources/vue/components/User/Profile.vue
@@ -56,7 +56,9 @@
<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>
+ <button class="btn btn-primary button-submit" type="submit">Submit</button>
+ <router-link v-if="$root.isController(wallet_id)" class="btn btn-danger button-delete"
+ to="/profile/delete" tag="button">Delete account</router-link>
</form>
</div>
</div>
@@ -69,10 +71,12 @@
data() {
return {
profile: {},
+ wallet_id: null,
countries: window.config.countries
}
},
created() {
+ this.wallet_id = this.$store.state.authInfo.wallet.id
this.profile = this.$store.state.authInfo.settings
},
mounted() {
diff --git a/src/resources/vue/components/User/ProfileDelete.vue b/src/resources/vue/components/User/ProfileDelete.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/components/User/ProfileDelete.vue
@@ -0,0 +1,47 @@
+<template>
+ <div class="container">
+ <div class="card" id="user-delete">
+ <div class="card-body">
+ <div class="card-title">Delete this account?</div>
+ <div class="card-text">
+ <p>This will delete the account as well as all domains, users and aliases associated with this account.
+ <strong>This operation is irreversible</strong>.</p>
+ <p>As you will not be able to recover anything after this point, please make sure
+ that you have migrated all data before proceeding.</p>
+ <p>As we always strive to improve, we would like to ask for 2 minutes of your time.
+ The best tool for improvement is feedback from users, and we would like to ask
+ for a few words about your reasons for leaving our service. Please send your feedback
+ to support@kolabnow.com.</p>
+ <p>Also feel free to contact Kolab Now Support at support@kolabnow.com with any questions
+ or concerns that you may have in this context.</p>
+ <button class="btn btn-secondary button-cancel" @click="$router.go(-1)">Cancel</button>
+ <button class="btn btn-danger button-delete" @click="deleteProfile">Delete account</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ created() {
+ if (!this.$root.isController(this.$store.state.authInfo.wallet.id)) {
+ this.$root.errorPage(403)
+ }
+ },
+ mounted() {
+ $('button.btn-secondary').focus()
+ },
+ methods: {
+ deleteProfile() {
+ axios.delete('/api/v4/users/' + this.$store.state.authInfo.id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$root.logoutUser()
+ 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
@@ -14,6 +14,7 @@
import UserInfoComponent from '../components/User/Info'
import UserListComponent from '../components/User/List'
import UserProfileComponent from '../components/User/Profile'
+import UserProfileDeleteComponent from '../components/User/ProfileDelete'
import store from './store'
@@ -61,6 +62,12 @@
component: UserProfileComponent,
meta: { requiresAuth: true }
},
+ {
+ path: '/profile/delete',
+ name: 'profile-delete',
+ component: UserProfileDeleteComponent,
+ meta: { requiresAuth: true }
+ },
{
path: '/signup/:param?',
name: 'signup',
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -4,7 +4,7 @@
use Facebook\WebDriver\WebDriverKeys;
use PHPUnit\Framework\Assert;
-use Tests\Browser\Components;
+use Tests\Browser\Components\Error;
/**
* Laravel Dusk Browser extensions
@@ -32,6 +32,18 @@
return $this;
}
+ /**
+ * Assert specified error page is displayed.
+ */
+ public function assertErrorPage(int $error_code)
+ {
+ $this->with(new Error($error_code), function ($browser) {
+ // empty, assertions will be made by the Error component itself
+ });
+
+ return $this;
+ }
+
/**
* Assert that the given element has specified class assigned.
*/
@@ -45,6 +57,16 @@
return $this;
}
+ /**
+ * Remove all toast messages
+ */
+ public function clearToasts()
+ {
+ $this->script("jQuery('.toast-container > *').remove()");
+
+ return $this;
+ }
+
/**
* Check if in Phone mode
*/
diff --git a/src/tests/Browser/Components/Error.php b/src/tests/Browser/Components/Dialog.php
copy from src/tests/Browser/Components/Error.php
copy to src/tests/Browser/Components/Dialog.php
--- a/src/tests/Browser/Components/Error.php
+++ b/src/tests/Browser/Components/Dialog.php
@@ -5,18 +5,14 @@
use Laravel\Dusk\Component as BaseComponent;
use PHPUnit\Framework\Assert as PHPUnit;
-class Error extends BaseComponent
+class Dialog extends BaseComponent
{
- protected $code;
- protected $message;
- protected $messages_map = [
- 404 => 'Not Found'
- ];
+ protected $selector;
- public function __construct($code)
+
+ public function __construct($selector)
{
- $this->code = $code;
- $this->message = $this->messages_map[$code];
+ $this->selector = trim($selector);
}
/**
@@ -26,7 +22,7 @@
*/
public function selector()
{
- return '#error-page';
+ return $this->selector;
}
/**
@@ -38,9 +34,7 @@
*/
public function assert($browser)
{
- $browser->waitFor($this->selector())
- ->assertSeeIn('@code', $this->code)
- ->assertSeeIn('@message', $this->message);
+ $browser->waitFor($this->selector() . '.modal.show');
}
/**
@@ -50,11 +44,11 @@
*/
public function elements()
{
- $selector = $this->selector();
-
return [
- '@code' => "$selector .code",
- '@message' => "$selector .message",
+ '@title' => '.modal-header .modal-title',
+ '@body' => '.modal-body',
+ '@button-action' => '.modal-footer button.modal-action',
+ '@button-cancel' => '.modal-footer button.modal-cancel',
];
}
}
diff --git a/src/tests/Browser/Components/Error.php b/src/tests/Browser/Components/Error.php
--- a/src/tests/Browser/Components/Error.php
+++ b/src/tests/Browser/Components/Error.php
@@ -10,7 +10,12 @@
protected $code;
protected $message;
protected $messages_map = [
- 404 => 'Not Found'
+ 400 => "Bad request",
+ 401 => "Unauthorized",
+ 403 => "Access denied",
+ 404 => "Not Found",
+ 405 => "Method not allowed",
+ 500 => "Internal server error",
];
public function __construct($code)
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
@@ -5,7 +5,6 @@
use App\Domain;
use App\User;
use Tests\Browser;
-use Tests\Browser\Components\Error;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\DomainInfo;
@@ -41,11 +40,7 @@
$browser->visit('/domain/123')
->on(new Home())
->submitLogon('john@kolab.org', 'simple123')
- // TODO: the check below could look simpler, but we can't
- // just remove the callback argument. We'll create
- // Browser wrapper in future, then we could create expectError() method
- ->with(new Error('404'), function ($browser) {
- });
+ ->assertErrorPage(404);
});
}
diff --git a/src/tests/Browser/Pages/Home.php b/src/tests/Browser/Pages/Home.php
--- a/src/tests/Browser/Pages/Home.php
+++ b/src/tests/Browser/Pages/Home.php
@@ -25,7 +25,7 @@
*/
public function assert($browser)
{
- $browser->assertPathIs($this->url())
+ $browser->waitForLocation($this->url())
->assertVisible('form.form-signin');
}
diff --git a/src/tests/Browser/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php
--- a/src/tests/Browser/UserProfileTest.php
+++ b/src/tests/Browser/UserProfileTest.php
@@ -31,6 +31,7 @@
parent::setUp();
User::where('email', 'john@kolab.org')->first()->setSettings($this->profile);
+ $this->deleteTestUser('profile-delete@kolabnow.com');
}
/**
@@ -39,6 +40,7 @@
public function tearDown(): void
{
User::where('email', 'john@kolab.org')->first()->setSettings($this->profile);
+ $this->deleteTestUser('profile-delete@kolabnow.com');
parent::tearDown();
}
@@ -66,6 +68,7 @@
->assertSeeIn('@links .link-profile', 'Your profile')
->click('@links .link-profile')
->on(new UserProfile())
+ ->assertSeeIn('#user-profile .button-delete', 'Delete account')
->whenAvailable('@form', function (Browser $browser) {
// Assert form content
$browser->assertFocused('div.row:nth-child(1) input')
@@ -103,7 +106,6 @@
->closeToast();
});
-
// Test error handling
$browser->with('@form', function (Browser $browser) {
$browser->type('#phone', 'aaaaaa')
@@ -124,4 +126,71 @@
});
});
}
+
+ /**
+ * Test profile of non-controller user
+ */
+ public function testProfileNonController(): void
+ {
+ // 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 .button-delete')
+ ->whenAvailable('@form', function (Browser $browser) {
+ // TODO: decide on what fields the non-controller user should be able
+ // to see/change
+ });
+
+ // 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())
+ ->clearToasts()
+ ->assertSeeIn('@links .link-profile', 'Your profile')
+ ->click('@links .link-profile')
+ ->on(new UserProfile())
+ ->click('#user-profile .button-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 .button-delete')
+ ->waitForLocation('/profile/delete')
+ ->click('#user-delete .button-delete')
+ ->waitForLocation('/login')
+ ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
+ $browser->assertToastTitle('')
+ ->assertToastMessage('User deleted successfully.')
+ ->closeToast();
+ });
+
+ $this->assertTrue($user->fresh()->trashed());
+ });
+ }
}
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
@@ -5,6 +5,7 @@
use App\User;
use App\UserAlias;
use Tests\Browser;
+use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
@@ -28,8 +29,7 @@
{
parent::setUp();
- // TODO: Use TestCase::deleteTestUser()
- User::withTrashed()->where('email', 'john.rambo@kolab.org')->forceDelete();
+ $this->deleteTestUser('julia.roberts@kolab.org');
$john = User::where('email', 'john@kolab.org')->first();
$john->setSettings($this->profile);
@@ -42,8 +42,7 @@
*/
public function tearDown(): void
{
- // TODO: Use TestCase::deleteTestUser()
- User::withTrashed()->where('email', 'john.rambo@kolab.org')->forceDelete();
+ $this->deleteTestUser('julia.roberts@kolab.org');
$john = User::where('email', 'john@kolab.org')->first();
$john->setSettings($this->profile);
@@ -75,8 +74,6 @@
$this->browse(function (Browser $browser) {
$browser->visit('/users')->on(new Home());
});
-
- // TODO: Test that jack@kolab.org can't access this page
}
/**
@@ -92,9 +89,12 @@
->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');
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'john@kolab.org')
+ ->assertVisible('tbody tr:nth-child(1) button.button-delete')
+ ->assertVisible('tbody tr:nth-child(2) button.button-delete');
});
});
}
@@ -108,7 +108,7 @@
{
$this->browse(function (Browser $browser) {
$browser->on(new UserList())
- ->click('@table tr:first-child a')
+ ->click('@table tr:last-child a')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@form', function (Browser $browser) {
@@ -120,7 +120,7 @@
->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')
-//TODO ->assertDisabled('div.row:nth-child(3) input')
+ ->assertDisabled('div.row:nth-child(3) input[type=text]')
->assertSeeIn('div.row:nth-child(4) label', 'Email aliases')
->assertVisible('div.row:nth-child(4) .listinput-widget')
->with(new ListInput('#aliases'), function (Browser $browser) {
@@ -226,7 +226,7 @@
->assertValue('div.row:nth-child(2) input[type=text]', '')
->assertSeeIn('div.row:nth-child(3) label', 'Email')
->assertValue('div.row:nth-child(3) input[type=text]', '')
- ->assertEnabled('div.row:nth-child(3) input')
+ ->assertEnabled('div.row:nth-child(3) input[type=text]')
->assertSeeIn('div.row:nth-child(4) label', 'Email aliases')
->assertVisible('div.row:nth-child(4) .listinput-widget')
->with(new ListInput('#aliases'), function (Browser $browser) {
@@ -263,7 +263,7 @@
// Test form error handling (aliases)
$browser->with('@form', function (Browser $browser) {
- $browser->type('#email', 'john.rambo@kolab.org')
+ $browser->type('#email', 'julia.roberts@kolab.org')
->type('#password_confirmation', 'simple123')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
@@ -285,7 +285,7 @@
$browser->with('@form', function (Browser $browser) {
$browser->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->removeListEntry(1)
- ->addListEntry('john.rambo2@kolab.org');
+ ->addListEntry('julia.roberts2@kolab.org');
})
->click('button[type=submit]');
})
@@ -293,13 +293,90 @@
$browser->assertToastTitle('')
->assertToastMessage('User created successfully')
->closeToast();
- });
-
- // TODO: assert redirect to users list
+ })
+ // check redirection to users list
+ ->waitForLocation('/users')
+ ->on(new UserList())
+ ->whenAvailable('@table', function (Browser $browser) {
+// TODO: This will not work until we handle entitlements on user creation
+// $browser->assertElementsCount('tbody tr', 3)
+// ->assertSeeIn('tbody tr:nth-child(3) a', 'julia.roberts@kolab.org');
+ });
- $john = User::where('email', 'john.rambo@kolab.org')->first();
- $alias = UserAlias::where('user_id', $john->id)->where('alias', 'john.rambo2@kolab.org')->first();
+ $julia = User::where('email', 'julia.roberts@kolab.org')->first();
+ $alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first();
$this->assertTrue(!empty($alias));
});
}
+
+ /**
+ * Test user delete
+ *
+ * @depends testNewUser
+ */
+ public function testDeleteUser(): void
+ {
+ // First create a new user
+ $john = $this->getTestUser('john@kolab.org');
+ $julia = $this->getTestUser('julia.roberts@kolab.org');
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $john->assignPackage($package_kolab, $julia);
+
+ // Test deleting non-controller user
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new UserList())
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 3)
+ ->assertSeeIn('tbody tr:nth-child(3) a', 'julia.roberts@kolab.org')
+ ->click('tbody tr:nth-child(3) button.button-delete');
+ })
+ ->with(new Dialog('#delete-warning'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Delete julia.roberts@kolab.org')
+ ->assertFocused('@button-cancel')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Delete')
+ ->click('@button-cancel');
+ })
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->click('tbody tr:nth-child(3) button.button-delete');
+ })
+ ->with(new Dialog('#delete-warning'), function (Browser $browser) {
+ $browser->click('@button-action');
+ })
+ ->with(new Toast(Toast::TYPE_SUCCESS), function (Browser $browser) {
+ $browser->assertToastTitle('')
+ ->assertToastMessage('User deleted successfully')
+ ->closeToast();
+ })
+ ->with('@table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'john@kolab.org');
+ });
+
+ $julia = User::where('email', 'julia.roberts@kolab.org')->first();
+ $this->assertTrue(empty($julia));
+
+ // Test clicking Delete on the controller record redirects to /profile/delete
+ $browser
+ ->with('@table', function (Browser $browser) {
+ $browser->click('tbody tr:nth-child(2) button.button-delete');
+ })
+ ->waitForLocation('/profile/delete');
+ });
+
+ // Test that non-controller user cannot see/delete himself on the users list
+ // Note: Access to /profile/delete page is tested in UserProfileTest.php
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/logout')
+ ->on(new Home())
+ ->submitLogon('jack@kolab.org', 'simple123', true)
+ ->visit(new UserList())
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 0);
+ });
+ });
+
+ // TODO: Test what happens with the logged in user session after he's been deleted by another user
+ }
}
diff --git a/src/tests/DuskTestCase.php b/src/tests/DuskTestCase.php
--- a/src/tests/DuskTestCase.php
+++ b/src/tests/DuskTestCase.php
@@ -22,7 +22,7 @@
return;
}
- $job = new \App\Jobs\DomainDelete($domain);
+ $job = new \App\Jobs\DomainDelete($domain->id);
$job->handle();
$domain->forceDelete();
diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php
--- a/src/tests/Feature/Controller/DomainsTest.php
+++ b/src/tests/Feature/Controller/DomainsTest.php
@@ -72,7 +72,7 @@
$json = $response->json();
$this->assertEquals('success', $json['status']);
- $this->assertEquals('Domain verified successfully', $json['message']);
+ $this->assertEquals('Domain verified successfully.', $json['message']);
}
/**
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
@@ -2,8 +2,8 @@
namespace Tests\Feature\Controller;
-use App\Http\Controllers\API\UsersController;
use App\Domain;
+use App\Http\Controllers\API\UsersController;
use App\User;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
@@ -19,6 +19,8 @@
parent::setUp();
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
+ $this->deleteTestUser('UsersControllerTest2@userscontroller.com');
+ $this->deleteTestUser('UsersControllerTest3@userscontroller.com');
$this->deleteTestUser('UserEntitlement2A@UserEntitlement.com');
$this->deleteTestUser('john2.doe2@kolab.org');
$this->deleteTestDomain('userscontroller.com');
@@ -30,6 +32,8 @@
public function tearDown(): void
{
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
+ $this->deleteTestUser('UsersControllerTest2@userscontroller.com');
+ $this->deleteTestUser('UsersControllerTest3@userscontroller.com');
$this->deleteTestUser('UserEntitlement2A@UserEntitlement.com');
$this->deleteTestUser('john2.doe2@kolab.org');
$this->deleteTestDomain('userscontroller.com');
@@ -63,10 +67,95 @@
// Note: Details of the content are tested in testUserResponse()
}
+ /**
+ * Test user deleting (DELETE /api/v4/users/<id>)
+ */
+ public function testDestroy(): void
+ {
+ // First create some users/accounts to delete
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $package_domain = \App\Package::where('title', 'domain-hosting')->first();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com');
+ $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com');
+ $domain = $this->getTestDomain('userscontroller.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_PUBLIC,
+ ]);
+ $user1->assignPackage($package_kolab);
+ $domain->assignPackage($package_domain, $user1);
+ $user1->assignPackage($package_kolab, $user2);
+ $user1->assignPackage($package_kolab, $user3);
+
+ // Test unauth access
+ $response = $this->delete("api/v4/users/{$user2->id}");
+ $response->assertStatus(401);
+
+ // Test access to other user/account
+ $response = $this->actingAs($john)->delete("api/v4/users/{$user2->id}");
+ $response->assertStatus(403);
+ $response = $this->actingAs($john)->delete("api/v4/users/{$user1->id}");
+ $response->assertStatus(403);
+ $json = $response->json();
+
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("Access denied", $json['message']);
+ $this->assertCount(2, $json);
+
+ // Test that non-controller cannot remove himself
+ $response = $this->actingAs($user3)->delete("api/v4/users/{$user3->id}");
+ $response->assertStatus(403);
+
+ // Test removing a non-controller user
+ $response = $this->actingAs($user1)->delete("api/v4/users/{$user3->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('User deleted successfully.', $json['message']);
+
+ // Test removing self (an account with users)
+ $response = $this->actingAs($user1)->delete("api/v4/users/{$user1->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals('success', $json['status']);
+ $this->assertEquals('User deleted successfully.', $json['message']);
+
+ // TODO: Support
+ }
+
+ /**
+ * Test user listing (GET /api/v4/users)
+ */
public function testIndex(): void
{
- // TODO
- $this->markTestIncomplete();
+ // Test unauth access
+ $response = $this->get("api/v4/users");
+ $response->assertStatus(401);
+
+ $jack = $this->getTestUser('jack@kolab.org');
+ $john = $this->getTestUser('john@kolab.org');
+
+ $response = $this->actingAs($jack)->get("/api/v4/users");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(0, $json);
+
+ $response = $this->actingAs($john)->get("/api/v4/users");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame($jack->email, $json[0]['email']);
+ $this->assertSame($john->email, $json[1]['email']);
}
/**
@@ -129,7 +218,7 @@
$json = $response->json();
$this->assertEquals('success', $json['status']);
- $this->assertEquals('Successfully logged out', $json['message']);
+ $this->assertEquals('Successfully logged out.', $json['message']);
// Check if it really destroyed the token?
$response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->get("api/auth/info");
@@ -217,13 +306,16 @@
public function testUserResponse(): void
{
$user = $this->getTestUser('john@kolab.org');
-
+ $wallet = $user->wallets()->first();
$result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]);
$this->assertEquals($user->id, $result['id']);
$this->assertEquals($user->email, $result['email']);
$this->assertEquals($user->status, $result['status']);
$this->assertTrue(is_array($result['statusInfo']));
+ $this->assertTrue(is_array($result['wallets']));
+ $this->assertCount(1, $result['wallets']);
+ $this->assertSame($wallet->id, $result['wallet']['id']);
$this->assertTrue(is_array($result['aliases']));
$this->assertCount(1, $result['aliases']);
@@ -338,7 +430,7 @@
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
- $this->assertSame("User created successfully", $json['message']);
+ $this->assertSame("User created successfully.", $json['message']);
$this->assertCount(2, $json);
$user = User::where('email', 'john2.doe2@kolab.org')->first();
@@ -376,7 +468,7 @@
$json = $response->json();
$this->assertSame('success', $json['status']);
- $this->assertSame("User data updated successfully", $json['message']);
+ $this->assertSame("User data updated successfully.", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
@@ -411,7 +503,7 @@
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
- $this->assertSame("User data updated successfully", $json['message']);
+ $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'], $post['aliases']);
@@ -441,7 +533,7 @@
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
- $this->assertSame("User data updated successfully", $json['message']);
+ $this->assertSame("User data updated successfully.", $json['message']);
$this->assertCount(2, $json);
unset($post['aliases']);
foreach ($post as $key => $value) {
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -163,4 +163,32 @@
$this->assertTrue($domain->confirm());
$this->assertTrue($domain->isConfirmed());
}
+
+ /**
+ * Test domain deletion
+ */
+ public function testDelete(): void
+ {
+ Queue::fake();
+
+ $domain = $this->getTestDomain('gmail.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_PUBLIC,
+ ]);
+
+ $domain->delete();
+
+ $this->assertTrue($domain->fresh()->trashed());
+ $this->assertFalse($domain->fresh()->isDeleted());
+
+ // Delete the domain for real
+ $job = new \App\Jobs\DomainDelete($domain->id);
+ $job->handle();
+
+ $this->assertTrue(Domain::withTrashed()->where('id', $domain->id)->first()->isDeleted());
+
+ $domain->forceDelete();
+
+ $this->assertCount(0, Domain::withTrashed()->where('id', $domain->id)->get());
+ }
}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature;
+use App\Domain;
use App\User;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
@@ -13,17 +14,21 @@
parent::setUp();
$this->deleteTestUser('user-create-test@' . \config('app.domain'));
+ $this->deleteTestUser('userdeletejob@kolabnow.com');
$this->deleteTestUser('UserAccountA@UserAccount.com');
$this->deleteTestUser('UserAccountB@UserAccount.com');
- $this->deleteTestUser('userdeletejob@kolabnow.com');
+ $this->deleteTestUser('UserAccountC@UserAccount.com');
+ $this->deleteTestDomain('UserAccount.com');
}
public function tearDown(): void
{
$this->deleteTestUser('user-create-test@' . \config('app.domain'));
+ $this->deleteTestUser('userdeletejob@kolabnow.com');
$this->deleteTestUser('UserAccountA@UserAccount.com');
$this->deleteTestUser('UserAccountB@UserAccount.com');
- $this->deleteTestUser('userdeletejob@kolabnow.com');
+ $this->deleteTestUser('UserAccountC@UserAccount.com');
+ $this->deleteTestDomain('UserAccount.com');
parent::tearDown();
}
@@ -115,6 +120,8 @@
$jack = $this->getTestUser('jack@kolab.org');
$this->assertSame($john->id, $jack->controller()->id);
+
+ // TODO: More sophisticated cases regarding wallets
}
/**
@@ -147,6 +154,10 @@
public function testUserQuota(): void
{
+ // TODO: This test does not test much, probably could be removed
+ // or moved to somewhere else, or extended with
+ // other entitlements() related cases.
+
$user = $this->getTestUser('john@kolab.org');
$storage_sku = \App\Sku::where('title', 'storage')->first();
@@ -164,26 +175,79 @@
/**
* Test user deletion
*/
- public function testUserDelete(): void
+ public function testDelete(): void
{
- $user = $this->getTestUser('userdeletejob@kolabnow.com');
+ Queue::fake();
+ $user = $this->getTestUser('userdeletejob@kolabnow.com');
$package = \App\Package::where('title', 'kolab')->first();
-
$user->assignPackage($package);
$id = $user->id;
+ $entitlements = \App\Entitlement::where('owner_id', $id)->get();
+ $this->assertCount(4, $entitlements);
+
$user->delete();
+ $entitlements = \App\Entitlement::where('owner_id', $id)->get();
+ $this->assertCount(0, $entitlements);
+ $this->assertTrue($user->fresh()->trashed());
+ $this->assertFalse($user->fresh()->isDeleted());
+
+ // Delete the user for real
$job = new \App\Jobs\UserDelete($id);
$job->handle();
+ $this->assertTrue(User::withTrashed()->where('id', $id)->first()->isDeleted());
+
$user->forceDelete();
- $entitlements = \App\Entitlement::where('owner_id', 'id')->get();
+ $this->assertCount(0, User::withTrashed()->where('id', $id)->get());
- $this->assertCount(0, $entitlements);
+ // Test an account with users
+ $userA = $this->getTestUser('UserAccountA@UserAccount.com');
+ $userB = $this->getTestUser('UserAccountB@UserAccount.com');
+ $userC = $this->getTestUser('UserAccountC@UserAccount.com');
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $package_domain = \App\Package::where('title', 'domain-hosting')->first();
+ $domain = $this->getTestDomain('UserAccount.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_HOSTED,
+ ]);
+ $userA->assignPackage($package_kolab);
+ $domain->assignPackage($package_domain, $userA);
+ $userA->assignPackage($package_kolab, $userB);
+ $userA->assignPackage($package_kolab, $userC);
+
+ $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id);
+ $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id);
+ $entitlementsC = \App\Entitlement::where('entitleable_id', $userC->id);
+ $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id);
+ $this->assertSame(4, $entitlementsA->count());
+ $this->assertSame(4, $entitlementsB->count());
+ $this->assertSame(4, $entitlementsC->count());
+ $this->assertSame(1, $entitlementsDomain->count());
+
+ // Delete non-controller user
+ $userC->delete();
+
+ $this->assertTrue($userC->fresh()->trashed());
+ $this->assertFalse($userC->fresh()->isDeleted());
+ $this->assertSame(0, $entitlementsC->count());
+
+ // Delete the controller (and expect "sub"-users to be deleted too)
+ $userA->delete();
+
+ $this->assertSame(0, $entitlementsA->count());
+ $this->assertSame(0, $entitlementsB->count());
+ $this->assertSame(0, $entitlementsDomain->count());
+ $this->assertTrue($userA->fresh()->trashed());
+ $this->assertTrue($userB->fresh()->trashed());
+ $this->assertTrue($domain->fresh()->trashed());
+ $this->assertFalse($userA->isDeleted());
+ $this->assertFalse($userB->isDeleted());
+ $this->assertFalse($domain->isDeleted());
}
/**
@@ -260,4 +324,26 @@
{
$this->markTestIncomplete();
}
+
+ /**
+ * Tests for User::users()
+ */
+ public function testUsers(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $wallet = $john->wallets()->first();
+
+ $users = $john->users()->orderBy('email')->get();
+
+ $this->assertCount(2, $users);
+ $this->assertEquals($jack->id, $users[0]->id);
+ $this->assertEquals($john->id, $users[1]->id);
+ $this->assertSame($wallet->id, $users[0]->wallet_id);
+ $this->assertSame($wallet->id, $users[1]->wallet_id);
+
+ $users = $jack->users()->orderBy('email')->get();
+
+ $this->assertCount(0, $users);
+ }
}
diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php
--- a/src/tests/TestCase.php
+++ b/src/tests/TestCase.php
@@ -19,7 +19,7 @@
return;
}
- $job = new \App\Jobs\DomainDelete($domain);
+ $job = new \App\Jobs\DomainDelete($domain->id);
$job->handle();
$domain->forceDelete();
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Mar 30, 1:08 AM (5 d, 19 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18722985
Default Alt Text
D1000.1774832932.diff (58 KB)
Attached To
Mode
D1000: Deleting users/accounts
Attached
Detach File
Event Timeline