diff --git a/src/app/Console/Commands/Domain/SuspendCommand.php b/src/app/Console/Commands/Domain/SuspendCommand.php --- a/src/app/Console/Commands/Domain/SuspendCommand.php +++ b/src/app/Console/Commands/Domain/SuspendCommand.php @@ -11,7 +11,7 @@ * * @var string */ - protected $signature = 'domain:suspend {domain}'; + protected $signature = 'domain:suspend {domain} {--comment=}'; /** * The console command description. @@ -35,5 +35,7 @@ } $domain->suspend(); + + \App\EventLog::createFor($domain, \App\EventLog::TYPE_SUSPENDED, $this->option('comment')); } } diff --git a/src/app/Console/Commands/Domain/UnsuspendCommand.php b/src/app/Console/Commands/Domain/UnsuspendCommand.php --- a/src/app/Console/Commands/Domain/UnsuspendCommand.php +++ b/src/app/Console/Commands/Domain/UnsuspendCommand.php @@ -11,7 +11,7 @@ * * @var string */ - protected $signature = 'domain:unsuspend {domain}'; + protected $signature = 'domain:unsuspend {domain} {--comment=}'; /** * The console command description. @@ -35,5 +35,7 @@ } $domain->unsuspend(); + + \App\EventLog::createFor($domain, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment')); } } diff --git a/src/app/Console/Commands/Group/SuspendCommand.php b/src/app/Console/Commands/Group/SuspendCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Group/SuspendCommand.php @@ -0,0 +1,41 @@ +getGroup($this->argument('group')); + + if (!$group) { + $this->error("Group not found."); + return 1; + } + + $group->suspend(); + + \App\EventLog::createFor($group, \App\EventLog::TYPE_SUSPENDED, $this->option('comment')); + } +} diff --git a/src/app/Console/Commands/Group/UnsuspendCommand.php b/src/app/Console/Commands/Group/UnsuspendCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Group/UnsuspendCommand.php @@ -0,0 +1,41 @@ +getGroup($this->argument('group')); + + if (!$group) { + $this->error("Group not found."); + return 1; + } + + $group->unsuspend(); + + \App\EventLog::createFor($group, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment')); + } +} diff --git a/src/app/Console/Commands/User/SuspendCommand.php b/src/app/Console/Commands/User/SuspendCommand.php --- a/src/app/Console/Commands/User/SuspendCommand.php +++ b/src/app/Console/Commands/User/SuspendCommand.php @@ -11,7 +11,7 @@ * * @var string */ - protected $signature = 'user:suspend {user}'; + protected $signature = 'user:suspend {user} {--comment=}'; /** * The console command description. @@ -35,5 +35,7 @@ } $user->suspend(); + + \App\EventLog::createFor($user, \App\EventLog::TYPE_SUSPENDED, $this->option('comment')); } } diff --git a/src/app/Console/Commands/User/UnsuspendCommand.php b/src/app/Console/Commands/User/UnsuspendCommand.php --- a/src/app/Console/Commands/User/UnsuspendCommand.php +++ b/src/app/Console/Commands/User/UnsuspendCommand.php @@ -11,7 +11,7 @@ * * @var string */ - protected $signature = 'user:unsuspend {user}'; + protected $signature = 'user:unsuspend {user} {--comment=}'; /** * The console command description. @@ -35,5 +35,7 @@ } $user->unsuspend(); + + \App\EventLog::createFor($user, \App\EventLog::TYPE_UNSUSPENDED, $this->option('comment')); } } diff --git a/src/app/EventLog.php b/src/app/EventLog.php new file mode 100644 --- /dev/null +++ b/src/app/EventLog.php @@ -0,0 +1,131 @@ + The attributes that are mass assignable */ + protected $fillable = [ + 'comment', + // extra event info (json) + 'data', + 'type', + // user, domain, etc. + 'object_id', + 'object_type', + // actor, if any + 'user_email', + ]; + + /** @var array Casts properties as type */ + protected $casts = [ + 'created_at' => 'datetime:Y-m-d H:i:s', + 'data' => 'array', + 'type' => 'integer', + ]; + + /** @var array The attributes that can be not set */ + protected $nullable = ['comment', 'data', 'user_email']; + + /** @var string Database table name */ + protected $table = 'eventlog'; + + /** @var bool Indicates if the model should be timestamped. */ + public $timestamps = false; + + + /** + * Create an eventlog object for a specified object. + * + * @param object $object Object (User, Domain, etc.) + * @param int $type Event type (use one of EventLog::TYPE_* consts) + * @param ?string $comment Event description + * @param ?array $data Extra information + * + * @return EventLog + */ + public static function createFor($object, int $type, string $comment = null, array $data = null): EventLog + { + $event = self::create([ + 'object_id' => $object->id, + 'object_type' => get_class($object), + 'type' => $type, + 'comment' => $comment, + 'data' => $data, + ]); + + return $event; + } + + /** + * Principally an object such as Domain, User, Group. + * Note that it may be trashed (soft-deleted). + * + * @return mixed + */ + public function object() + { + return $this->morphTo()->withTrashed(); // @phpstan-ignore-line + } + + /** + * Get an event type name. + * + * @return ?string Event type name + */ + public function eventName(): ?string + { + switch ($this->type) { + case self::TYPE_SUSPENDED: + return \trans('app.event-suspended'); + case self::TYPE_UNSUSPENDED: + return \trans('app.event-unsuspended'); + case self::TYPE_COMMENT: + return \trans('app.event-comment'); + default: + return null; + } + } + + /** + * Event type mutator + * + * @throws \Exception + */ + public function setTypeAttribute($type) + { + if (!is_numeric($type)) { + throw new \Exception("Expecting an event type to be numeric"); + } + + $type = (int) $type; + + if ($type < 0 || $type > 255) { + throw new \Exception("Expecting an event type between 0 and 255"); + } + + $this->attributes['type'] = $type; + } +} diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php @@ -3,8 +3,10 @@ namespace App\Http\Controllers\API\V4\Admin; use App\Domain; +use App\EventLog; use App\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; class DomainsController extends \App\Http\Controllers\API\V4\DomainsController { @@ -94,8 +96,16 @@ return $this->errorResponse(404); } + $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + $domain->suspend(); + EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, $request->comment); + return response()->json([ 'status' => 'success', 'message' => self::trans('app.domain-suspend-success'), @@ -118,8 +128,16 @@ return $this->errorResponse(404); } + $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + $domain->unsuspend(); + EventLog::createFor($domain, EventLog::TYPE_UNSUSPENDED, $request->comment); + return response()->json([ 'status' => 'success', 'message' => self::trans('app.domain-unsuspend-success'), diff --git a/src/app/Http/Controllers/API/V4/Admin/EventLogController.php b/src/app/Http/Controllers/API/V4/Admin/EventLogController.php new file mode 100644 --- /dev/null +++ b/src/app/Http/Controllers/API/V4/Admin/EventLogController.php @@ -0,0 +1,67 @@ +errorResponse(404); + } + + $object = (new $object_type)->find($object_id); + + if (!$this->checkTenant($object)) { + return $this->errorResponse(404); + } + + $page = intval($request->input('page')) ?: 1; + $pageSize = 20; + $hasMore = false; + + $result = EventLog::where('object_id', $object_id) + ->where('object_type', $object_type) + ->orderBy('created_at', 'desc') + ->limit($pageSize + 1) + ->offset($pageSize * ($page - 1)) + ->get(); + + if (count($result) > $pageSize) { + $result->pop(); + $hasMore = true; + } + + $result = $result->map(function ($event) { + return [ + 'id' => $event->id, + 'comment' => $event->comment, + 'createdAt' => $event->created_at->toDateTimeString(), + 'event' => $event->eventName(), + 'data' => $event->data, + 'user' => $event->user_email, + ]; + }); + + return response()->json([ + 'list' => $result, + 'count' => count($result), + 'hasMore' => $hasMore, + ]); + } +} diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php --- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php @@ -2,9 +2,11 @@ namespace App\Http\Controllers\API\V4\Admin; +use App\EventLog; use App\Group; use App\User; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; class GroupsController extends \App\Http\Controllers\API\V4\GroupsController { @@ -73,8 +75,16 @@ return $this->errorResponse(404); } + $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + $group->suspend(); + EventLog::createFor($group, EventLog::TYPE_SUSPENDED, $request->comment); + return response()->json([ 'status' => 'success', 'message' => self::trans('app.distlist-suspend-success'), @@ -97,8 +107,16 @@ return $this->errorResponse(404); } + $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + $group->unsuspend(); + EventLog::createFor($group, EventLog::TYPE_UNSUSPENDED, $request->comment); + return response()->json([ 'status' => 'success', 'message' => self::trans('app.distlist-unsuspend-success'), diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php --- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\API\V4\Admin; use App\Domain; +use App\EventLog; use App\Sku; use App\User; use App\Wallet; @@ -321,8 +322,16 @@ return $this->errorResponse(403); } + $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + $user->suspend(); + EventLog::createFor($user, EventLog::TYPE_SUSPENDED, $request->comment); + return response()->json([ 'status' => 'success', 'message' => self::trans('app.user-suspend-success'), @@ -349,8 +358,16 @@ return $this->errorResponse(403); } + $v = Validator::make($request->all(), ['comment' => 'nullable|string|max:1024']); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + $user->unsuspend(); + EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, $request->comment); + return response()->json([ 'status' => 'success', 'message' => self::trans('app.user-unsuspend-success'), diff --git a/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php b/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php new file mode 100644 --- /dev/null +++ b/src/app/Http/Controllers/API/V4/Reseller/EventLogController.php @@ -0,0 +1,7 @@ +userId}"); if ($code == 2) { \Log::info("Suspending user due to suspected abuse: {$this->userId} {$user->email}"); + \App\EventLog::createFor($user, \App\EventLog::TYPE_SUSPENDED, "Suspected spammer"); + $user->status |= \App\User::STATUS_SUSPENDED; } } 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 @@ -45,6 +45,9 @@ public function deleted(Domain $domain) { if ($domain->isForceDeleting()) { + // Remove EventLog records + \App\EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->delete(); + return; } diff --git a/src/app/Observers/EventLogObserver.php b/src/app/Observers/EventLogObserver.php new file mode 100644 --- /dev/null +++ b/src/app/Observers/EventLogObserver.php @@ -0,0 +1,24 @@ +user_email)) { + $eventlog->user_email = \App\Utils::userEmailOrNull(); + } + + if (!isset($eventlog->type)) { + throw new \Exception("Unset event type"); + } + } +} diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php --- a/src/app/Observers/GroupObserver.php +++ b/src/app/Observers/GroupObserver.php @@ -45,6 +45,9 @@ public function deleted(Group $group) { if ($group->isForceDeleting()) { + // Remove EventLog records + \App\EventLog::where('object_id', $group->id)->where('object_type', Group::class)->delete(); + return; } 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 @@ -224,7 +224,7 @@ } /** - * Remove entitleables/transactions related to the user (in user's wallets) + * Remove entities related to the user (in user's wallets), entitlements, transactions, etc. * * @param \App\User $user The user * @param bool $force Force-delete mode @@ -261,6 +261,9 @@ \App\Transaction::where('object_type', Wallet::class) ->whereIn('object_id', $wallets) ->delete(); + + // Remove EventLog records + \App\EventLog::where('object_id', $user->id)->where('object_type', User::class)->delete(); } // regardless of force delete, we're always purging whitelists... just in case 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 @@ -65,6 +65,7 @@ { \App\Domain::observe(\App\Observers\DomainObserver::class); \App\Entitlement::observe(\App\Observers\EntitlementObserver::class); + \App\EventLog::observe(\App\Observers\EventLogObserver::class); \App\Group::observe(\App\Observers\GroupObserver::class); \App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class); \App\Meet\Room::observe(\App\Observers\Meet\RoomObserver::class); diff --git a/src/database/migrations/2023_06_06_100000_create_eventlog_table.php b/src/database/migrations/2023_06_06_100000_create_eventlog_table.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2023_06_06_100000_create_eventlog_table.php @@ -0,0 +1,43 @@ +string('id', 36)->primary(); + $table->string('object_id', 36); + $table->string('object_type', 36); + $table->tinyInteger('type')->unsigned(); + $table->string('user_email')->nullable(); + $table->string('comment', 1024)->nullable(); + $table->text('data')->nullable(); // json + $table->timestamp('created_at')->useCurrent(); + + $table->index(['object_id', 'object_type', 'type']); + $table->index('created_at'); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('eventlog'); + } +}; 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 @@ -23,6 +23,10 @@ 'companion-create-success' => 'Companion app has been created.', 'companion-delete-success' => 'Companion app has been removed.', + 'event-suspended' => 'Suspended', + 'event-unsuspended' => 'Unsuspended', + 'event-comment' => 'Commented', + 'mandate-delete-success' => 'The auto-payment has been removed.', 'mandate-update-success' => 'The auto-payment has been updated.', diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -43,6 +43,12 @@ 'verify' => "Verify", ], + 'collection' => [ + 'create' => "Create collection", + 'new' => "New Collection", + 'name' => "Name", + ], + 'companion' => [ 'title' => "Companion Apps", 'companion' => "Companion App", @@ -170,12 +176,6 @@ . "to the file via a unique link.", ], - 'collection' => [ - 'create' => "Create collection", - 'new' => "New Collection", - 'name' => "Name", - ], - 'form' => [ 'acl' => "Access rights", 'acl-full' => "All", @@ -185,6 +185,7 @@ 'anyone' => "Anyone", 'code' => "Confirmation Code", 'config' => "Configuration", + 'comment' => "Comment", 'companion' => "Companion App", 'date' => "Date", 'description' => "Description", @@ -198,8 +199,10 @@ 'general' => "General", 'geolocation' => "Your current location: {location}", 'lastname' => "Last Name", + 'less' => "Less", 'name' => "Name", 'months' => "months", + 'more' => "More", 'none' => "none", 'norestrictions' => "No restrictions", 'or' => "or", @@ -245,6 +248,12 @@ 'it' => "Italian", ], + 'log' => [ + 'event' => "Event", + 'list-none' => "There's no events in the log", + 'history' => "History", + ], + 'login' => [ '2fa' => "Second factor code", '2fa_desc' => "Second factor code is optional for users with no 2-Factor Authentication setup.", diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss --- a/src/resources/themes/app.scss +++ b/src/resources/themes/app.scss @@ -205,6 +205,31 @@ width: 50px; } } + + &.eventlog { + .details, + .btn-less { + display: none; + } + + tr.open { + .btn-more { + display: none; + } + + .details { + display: block; + } + + .btn-less { + display: initial; + } + } + + td.description { + width: 98%; + } + } } .table > :not(:first-child) { diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue --- a/src/resources/vue/Admin/Distlist.vue +++ b/src/resources/vue/Admin/Distlist.vue @@ -1,6 +1,6 @@ diff --git a/src/resources/vue/Widgets/ListTools.vue b/src/resources/vue/Widgets/ListTools.vue --- a/src/resources/vue/Widgets/ListTools.vue +++ b/src/resources/vue/Widgets/ListTools.vue @@ -125,7 +125,6 @@ get.search = this.currentSearch } - if ('parent' in params) { get.parent = params.parent this.currentParent = params.parent diff --git a/src/resources/vue/Widgets/ModalDialog.vue b/src/resources/vue/Widgets/ModalDialog.vue --- a/src/resources/vue/Widgets/ModalDialog.vue +++ b/src/resources/vue/Widgets/ModalDialog.vue @@ -62,7 +62,7 @@ if (this.cancelFocus) { $(event.target).find('button.modal-cancel').focus() } else { - $(event.target).find('input,select').first().focus() + $(event.target).find('input,select,textarea').first().focus() } }) diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -235,6 +235,8 @@ Route::post('domains/{id}/suspend', [API\V4\Admin\DomainsController::class, 'suspend']); Route::post('domains/{id}/unsuspend', [API\V4\Admin\DomainsController::class, 'unsuspend']); + Route::get('eventlog/{type}/{id}', [API\V4\Admin\EventLogController::class, 'index']); + Route::apiResource('groups', API\V4\Admin\GroupsController::class); Route::post('groups/{id}/suspend', [API\V4\Admin\GroupsController::class, 'suspend']); Route::post('groups/{id}/unsuspend', [API\V4\Admin\GroupsController::class, 'unsuspend']); @@ -275,6 +277,8 @@ Route::post('domains/{id}/suspend', [API\V4\Reseller\DomainsController::class, 'suspend']); Route::post('domains/{id}/unsuspend', [API\V4\Reseller\DomainsController::class, 'unsuspend']); + Route::get('eventlog/{type}/{id}', [API\V4\Reseller\EventLogController::class, 'index']); + Route::apiResource('groups', API\V4\Reseller\GroupsController::class); Route::post('groups/{id}/suspend', [API\V4\Reseller\GroupsController::class, 'suspend']); Route::post('groups/{id}/unsuspend', [API\V4\Reseller\GroupsController::class, 'unsuspend']); diff --git a/src/tests/Browser/Admin/DistlistTest.php b/src/tests/Browser/Admin/DistlistTest.php --- a/src/tests/Browser/Admin/DistlistTest.php +++ b/src/tests/Browser/Admin/DistlistTest.php @@ -2,9 +2,11 @@ namespace Tests\Browser\Admin; +use App\EventLog; use App\Group; use Illuminate\Support\Facades\Queue; use Tests\Browser; +use Tests\Browser\Components\Dialog; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Admin\Distlist as DistlistPage; use Tests\Browser\Pages\Admin\User as UserPage; @@ -23,6 +25,7 @@ self::useAdminUrl(); $this->deleteTestGroup('group-test@kolab.org'); + Eventlog::query()->delete(); } /** @@ -31,6 +34,7 @@ public function tearDown(): void { $this->deleteTestGroup('group-test@kolab.org'); + Eventlog::query()->delete(); parent::tearDown(); } @@ -65,6 +69,12 @@ $group->save(); $group->setConfig(['sender_policy' => ['test1.com', 'test2.com']]); + $event1 = EventLog::createFor($group, EventLog::TYPE_SUSPENDED, 'Event 1'); + $event2 = EventLog::createFor($group, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']); + $event2->refresh(); + $event1->created_at = (clone $event2->created_at)->subDay(); + $event1->save(); + $distlist_page = new DistlistPage($group->id); $user_page = new UserPage($user->id); @@ -75,7 +85,7 @@ ->visit($user_page) ->on($user_page) ->click('@nav #tab-distlists') - ->pause(1000) + ->waitFor('@user-distlists table tbody') ->click('@user-distlists table tbody tr:first-child td a') ->on($distlist_page) ->assertSeeIn('@distlist-info .card-title', $group->email) @@ -91,14 +101,42 @@ ->assertSeeIn('.row:nth-child(4) #members', $group->members[0]) ->assertSeeIn('.row:nth-child(4) #members', $group->members[1]); }) - ->assertElementsCount('ul.nav-tabs', 1) - ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings') + ->assertElementsCount('ul.nav-tabs li', 2) + ->assertSeeIn('ul.nav-tabs #tab-settings', 'Settings') ->with('@distlist-settings form', function (Browser $browser) { $browser->assertElementsCount('.row', 1) ->assertSeeIn('.row:nth-child(1) label', 'Sender Access List') ->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com'); }); + // Assert History tab + $browser->assertSeeIn('ul.nav-tabs #tab-history', 'History') + ->click('ul.nav-tabs #tab-history') + ->whenAvailable('@distlist-history table', function (Browser $browser) use ($event1, $event2) { + $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2) + // row 1 + ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString()) + ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended') + ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment) + ->assertMissing('tr:nth-child(1) td:nth-child(3) div') + ->assertMissing('tr:nth-child(1) td:nth-child(3) pre') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less') + ->click('tr:nth-child(1) td:nth-child(3) .btn-more') + ->assertMissing('tr:nth-child(1) td:nth-child(3) div.email') + ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more') + ->click('tr:nth-child(1) td:nth-child(3) .btn-less') + ->assertMissing('tr:nth-child(1) td:nth-child(3) div') + ->assertMissing('tr:nth-child(1) td:nth-child(3) pre') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less') + // row 2 + ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString()) + ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended') + ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment) + ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-more') + ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-less'); + }); + // Test invalid group identifier $browser->visit('/distlist/abc')->assertErrorPage(404); }); @@ -112,6 +150,7 @@ public function testSuspendAndUnsuspend(): void { Queue::fake(); + Eventlog::query()->delete(); $this->browse(function (Browser $browser) { $user = $this->getTestUser('john@kolab.org'); @@ -125,14 +164,36 @@ ->assertMissing('@distlist-info #button-unsuspend') ->assertSeeIn('@distlist-info #status.text-success', 'Active') ->click('@distlist-info #button-suspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Suspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->type('textarea', 'test suspend') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list suspended successfully.') ->assertSeeIn('@distlist-info #status.text-warning', 'Suspended') - ->assertMissing('@distlist-info #button-suspend') - ->click('@distlist-info #button-unsuspend') + ->assertMissing('@distlist-info #button-suspend'); + + $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first(); + $this->assertSame('test suspend', $event->comment); + $this->assertEquals($group->id, $event->object_id); + + $browser->click('@distlist-info #button-unsuspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Unsuspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list unsuspended successfully.') ->assertSeeIn('@distlist-info #status.text-success', 'Active') ->assertVisible('@distlist-info #button-suspend') ->assertMissing('@distlist-info #button-unsuspend'); + + $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first(); + $this->assertSame(null, $event->comment); + $this->assertEquals($group->id, $event->object_id); }); } } diff --git a/src/tests/Browser/Admin/DomainTest.php b/src/tests/Browser/Admin/DomainTest.php --- a/src/tests/Browser/Admin/DomainTest.php +++ b/src/tests/Browser/Admin/DomainTest.php @@ -3,7 +3,9 @@ namespace Tests\Browser\Admin; use App\Domain; +use App\EventLog; use Tests\Browser; +use Tests\Browser\Components\Dialog; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Admin\Domain as DomainPage; use Tests\Browser\Pages\Admin\User as UserPage; @@ -24,6 +26,8 @@ $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); + Eventlog::query()->delete(); + self::useAdminUrl(); } @@ -38,6 +42,8 @@ $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); + Eventlog::query()->delete(); + parent::tearDown(); } @@ -66,6 +72,12 @@ $domain->setSetting('spf_whitelist', null); + $event1 = EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, 'Event 1'); + $event2 = EventLog::createFor($domain, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']); + $event2->refresh(); + $event1->created_at = (clone $event2->created_at)->subDay(); + $event1->save(); + // Goto the domain page $browser->visit(new Home()) ->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true) @@ -73,7 +85,7 @@ ->visit($user_page) ->on($user_page) ->click('@nav #tab-domains') - ->pause(1000) + ->waitFor('@user-domains table tbody') ->click('@user-domains table tbody tr:first-child td a'); $browser->on($domain_page) @@ -88,7 +100,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 2); + ->assertElementsCount('@nav a', 3); // Assert Configuration tab $browser->assertSeeIn('@nav #tab-config', 'Configuration') @@ -110,11 +122,40 @@ $domain->setSetting('spf_whitelist', json_encode(['.test1.com', '.test2.com'])); $browser->refresh() + ->on($domain_page) ->waitFor('@nav #tab-settings') ->click('@nav #tab-settings') - ->with('@domain-settings form', function (Browser $browser) { + ->whenAvailable('@domain-settings form', function (Browser $browser) { $browser->assertSeeIn('.row:first-child .form-control-plaintext', '.test1.com, .test2.com'); }); + + // Assert History tab + $browser->assertSeeIn('@nav #tab-history', 'History') + ->click('@nav #tab-history') + ->whenAvailable('@domain-history table', function (Browser $browser) use ($event1, $event2) { + $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2) + // row 1 + ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString()) + ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended') + ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment) + ->assertMissing('tr:nth-child(1) td:nth-child(3) div') + ->assertMissing('tr:nth-child(1) td:nth-child(3) pre') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less') + ->click('tr:nth-child(1) td:nth-child(3) .btn-more') + ->assertMissing('tr:nth-child(1) td:nth-child(3) div.email') + ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more') + ->click('tr:nth-child(1) td:nth-child(3) .btn-less') + ->assertMissing('tr:nth-child(1) td:nth-child(3) div') + ->assertMissing('tr:nth-child(1) td:nth-child(3) pre') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less') + // row 2 + ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString()) + ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended') + ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment) + ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-more') + ->assertMissing('tr:nth-child(2) td:nth-child(3) .btn-less'); + }); }); } @@ -125,6 +166,8 @@ */ public function testSuspendAndUnsuspend(): void { + EventLog::query()->delete(); + $this->browse(function (Browser $browser) { $sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $user = $this->getTestUser('test1@domainscontroller.com'); @@ -146,14 +189,36 @@ ->assertVisible('@domain-info #button-suspend') ->assertMissing('@domain-info #button-unsuspend') ->click('@domain-info #button-suspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Suspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->type('textarea', 'test suspend') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.') ->assertSeeIn('@domain-info #status span.text-warning', 'Suspended') - ->assertMissing('@domain-info #button-suspend') - ->click('@domain-info #button-unsuspend') + ->assertMissing('@domain-info #button-suspend'); + + $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first(); + $this->assertSame('test suspend', $event->comment); + $this->assertEquals($domain->id, $event->object_id); + + $browser->click('@domain-info #button-unsuspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Unsuspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.') ->assertSeeIn('@domain-info #status span.text-success', 'Active') ->assertVisible('@domain-info #button-suspend') ->assertMissing('@domain-info #button-unsuspend'); + + $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first(); + $this->assertSame(null, $event->comment); + $this->assertEquals($domain->id, $event->object_id); }); } } diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php --- a/src/tests/Browser/Admin/UserTest.php +++ b/src/tests/Browser/Admin/UserTest.php @@ -5,6 +5,7 @@ use App\Auth\SecondFactor; use App\Discount; use App\Entitlement; +use App\EventLog; use App\Sku; use App\User; use Tests\Browser; @@ -39,6 +40,8 @@ $wallet->save(); Entitlement::where('cost', '>=', 5000)->delete(); + Eventlog::query()->delete(); + $this->deleteTestGroup('group-test@kolab.org'); $this->deleteTestUser('userstest1@kolabnow.com'); } @@ -61,6 +64,8 @@ $wallet->save(); Entitlement::where('cost', '>=', 5000)->delete(); + Eventlog::query()->delete(); + $this->deleteTestGroup('group-test@kolab.org'); $this->deleteTestUser('userstest1@kolabnow.com'); @@ -92,6 +97,15 @@ 'guam_enabled' => null, ]); + $event1 = EventLog::createFor($jack, EventLog::TYPE_SUSPENDED, 'Event 1'); + $event2 = EventLog::createFor($jack, EventLog::TYPE_UNSUSPENDED, 'Event 2', ['test' => 'test-data']); + $event2->refresh(); + $event1->created_at = (clone $event2->created_at)->subDay(); + $event1->user_email = 'jeroen@jeroen.jeroen'; + $event1->save(); + $event2->user_email = 'test@test.com'; + $event2->save(); + $page = new UserPage($jack->id); $browser->visit(new Home()) @@ -123,7 +137,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 9); + ->assertElementsCount('@nav a', 10); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); @@ -205,6 +219,35 @@ ->assertSeeIn('.row:nth-child(3) #limit_geo', 'No restrictions') ->assertMissing('#limit_geo + button'); }); + + // Assert History tab + $browser->assertSeeIn('@nav #tab-history', 'History') + ->click('@nav #tab-history') + ->whenAvailable('@user-history table', function (Browser $browser) use ($event1, $event2) { + $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 2) + // row 1 + ->assertSeeIn('tr:nth-child(1) td:nth-child(1)', $event2->created_at->toDateTimeString()) + ->assertSeeIn('tr:nth-child(1) td:nth-child(2)', 'Unsuspended') + ->assertSeeIn('tr:nth-child(1) td:nth-child(3)', $event2->comment) + ->assertMissing('tr:nth-child(1) td:nth-child(3) div') + ->assertMissing('tr:nth-child(1) td:nth-child(3) pre') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less') + ->click('tr:nth-child(1) td:nth-child(3) .btn-more') + ->assertSeeIn('tr:nth-child(1) td:nth-child(3) div.email', $event2->user_email) + ->assertSeeIn('tr:nth-child(1) td:nth-child(3) pre', 'test-data') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-more') + ->click('tr:nth-child(1) td:nth-child(3) .btn-less') + ->assertMissing('tr:nth-child(1) td:nth-child(3) div') + ->assertMissing('tr:nth-child(1) td:nth-child(3) pre') + ->assertMissing('tr:nth-child(1) td:nth-child(3) .btn-less') + // row 2 + ->assertSeeIn('tr:nth-child(2) td:nth-child(1)', $event1->created_at->toDateTimeString()) + ->assertSeeIn('tr:nth-child(2) td:nth-child(2)', 'Suspended') + ->assertSeeIn('tr:nth-child(2) td:nth-child(3)', $event1->comment) + ->click('tr:nth-child(2) td:nth-child(3) .btn-more') + ->assertSeeIn('tr:nth-child(2) td:nth-child(3) div.email', $event1->user_email) + ->assertMissing('tr:nth-child(2) td:nth-child(3) pre'); + }); }); } @@ -260,7 +303,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 9); + ->assertElementsCount('@nav a', 10); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); @@ -351,6 +394,14 @@ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org') ->assertMissing('table tfoot'); }); + + // Assert History tab + $browser->assertSeeIn('@nav #tab-history', 'History') + ->click('@nav #tab-history') + ->whenAvailable('@user-history table', function (Browser $browser) { + $browser->assertElementsCount('tbody tr', 0) + ->assertSeeIn('tfoot tr td', "There's no events in the log"); + }); }); // Now we go to Ned's info page, he's a controller on John's wallet @@ -392,7 +443,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 9); + ->assertElementsCount('@nav a', 10); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); @@ -546,14 +597,34 @@ ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend') ->click('@user-info #button-suspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Suspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->type('textarea', 'test suspend') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.') ->assertSeeIn('@user-info #status span.text-warning', 'Suspended') - ->assertMissing('@user-info #button-suspend') - ->click('@user-info #button-unsuspend') + ->assertMissing('@user-info #button-suspend'); + + $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first(); + $this->assertSame('test suspend', $event->comment); + + $browser->click('@user-info #button-unsuspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Unsuspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.') ->assertSeeIn('@user-info #status span.text-success', 'Active') ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend'); + + $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first(); + $this->assertSame(null, $event->comment); }); } diff --git a/src/tests/Browser/Pages/Admin/Distlist.php b/src/tests/Browser/Pages/Admin/Distlist.php --- a/src/tests/Browser/Pages/Admin/Distlist.php +++ b/src/tests/Browser/Pages/Admin/Distlist.php @@ -38,6 +38,7 @@ public function assert($browser): void { $browser->waitForLocation($this->url()) + ->waitUntilMissing('@app .app-loader') ->waitFor('@distlist-info'); } @@ -52,6 +53,7 @@ '@app' => '#app', '@distlist-info' => '#distlist-info', '@distlist-settings' => '#settings', + '@distlist-history' => '#history', ]; } } diff --git a/src/tests/Browser/Pages/Admin/Domain.php b/src/tests/Browser/Pages/Admin/Domain.php --- a/src/tests/Browser/Pages/Admin/Domain.php +++ b/src/tests/Browser/Pages/Admin/Domain.php @@ -38,6 +38,7 @@ public function assert($browser): void { $browser->waitForLocation($this->url()) + ->waitUntilMissing('@app .app-loader') ->waitFor('@domain-info'); } @@ -54,6 +55,7 @@ '@nav' => 'ul.nav-tabs', '@domain-config' => '#config', '@domain-settings' => '#settings', + '@domain-history' => '#history', ]; } } diff --git a/src/tests/Browser/Pages/Admin/Resource.php b/src/tests/Browser/Pages/Admin/Resource.php --- a/src/tests/Browser/Pages/Admin/Resource.php +++ b/src/tests/Browser/Pages/Admin/Resource.php @@ -38,6 +38,7 @@ public function assert($browser): void { $browser->waitForLocation($this->url()) + ->waitUntilMissing('@app .app-loader') ->waitFor('@resource-info'); } diff --git a/src/tests/Browser/Pages/Admin/SharedFolder.php b/src/tests/Browser/Pages/Admin/SharedFolder.php --- a/src/tests/Browser/Pages/Admin/SharedFolder.php +++ b/src/tests/Browser/Pages/Admin/SharedFolder.php @@ -38,6 +38,7 @@ public function assert($browser): void { $browser->waitForLocation($this->url()) + ->waitUntilMissing('@app .app-loader') ->waitFor('@folder-info'); } diff --git a/src/tests/Browser/Pages/Admin/User.php b/src/tests/Browser/Pages/Admin/User.php --- a/src/tests/Browser/Pages/Admin/User.php +++ b/src/tests/Browser/Pages/Admin/User.php @@ -58,6 +58,7 @@ '@user-subscriptions' => '#subscriptions', '@user-distlists' => '#distlists', '@user-domains' => '#domains', + '@user-history' => '#history', '@user-resources' => '#resources', '@user-folders' => '#folders', '@user-users' => '#users', diff --git a/src/tests/Browser/Reseller/DistlistTest.php b/src/tests/Browser/Reseller/DistlistTest.php --- a/src/tests/Browser/Reseller/DistlistTest.php +++ b/src/tests/Browser/Reseller/DistlistTest.php @@ -2,9 +2,11 @@ namespace Tests\Browser\Reseller; +use App\EventLog; use App\Group; use Illuminate\Support\Facades\Queue; use Tests\Browser; +use Tests\Browser\Components\Dialog; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Admin\Distlist as DistlistPage; use Tests\Browser\Pages\Admin\User as UserPage; @@ -23,6 +25,7 @@ self::useResellerUrl(); $this->deleteTestGroup('group-test@kolab.org'); + Eventlog::query()->delete(); } /** @@ -31,6 +34,7 @@ public function tearDown(): void { $this->deleteTestGroup('group-test@kolab.org'); + Eventlog::query()->delete(); parent::tearDown(); } @@ -75,7 +79,7 @@ ->visit($user_page) ->on($user_page) ->click('@nav #tab-distlists') - ->pause(1000) + ->waitFor('@user-distlists table tbody') ->click('@user-distlists table tbody tr:first-child td a') ->on($distlist_page) ->assertSeeIn('@distlist-info .card-title', $group->email) @@ -91,13 +95,14 @@ ->assertSeeIn('.row:nth-child(4) #members', $group->members[0]) ->assertSeeIn('.row:nth-child(4) #members', $group->members[1]); }) - ->assertElementsCount('ul.nav-tabs', 1) - ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings') + ->assertElementsCount('ul.nav-tabs li', 2) + ->assertSeeIn('ul.nav-tabs #tab-settings', 'Settings') ->with('@distlist-settings form', function (Browser $browser) { $browser->assertElementsCount('.row', 1) ->assertSeeIn('.row:nth-child(1) label', 'Sender Access List') ->assertSeeIn('.row:nth-child(1) #sender_policy', 'test1.com, test2.com'); - }); + }) + ->assertSeeIn('ul.nav-tabs #tab-history', 'History'); // Test invalid group identifier $browser->visit('/distlist/abc')->assertErrorPage(404); @@ -112,6 +117,7 @@ public function testSuspendAndUnsuspend(): void { Queue::fake(); + Eventlog::query()->delete(); $this->browse(function (Browser $browser) { $user = $this->getTestUser('john@kolab.org'); @@ -125,14 +131,36 @@ ->assertMissing('@distlist-info #button-unsuspend') ->assertSeeIn('@distlist-info #status.text-success', 'Active') ->click('@distlist-info #button-suspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Suspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->type('textarea', 'test suspend') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list suspended successfully.') ->assertSeeIn('@distlist-info #status.text-warning', 'Suspended') - ->assertMissing('@distlist-info #button-suspend') - ->click('@distlist-info #button-unsuspend') + ->assertMissing('@distlist-info #button-suspend'); + + $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first(); + $this->assertSame('test suspend', $event->comment); + $this->assertEquals($group->id, $event->object_id); + + $browser->click('@distlist-info #button-unsuspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Unsuspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Distribution list unsuspended successfully.') ->assertSeeIn('@distlist-info #status.text-success', 'Active') ->assertVisible('@distlist-info #button-suspend') ->assertMissing('@distlist-info #button-unsuspend'); + + $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first(); + $this->assertSame(null, $event->comment); + $this->assertEquals($group->id, $event->object_id); }); } } diff --git a/src/tests/Browser/Reseller/DomainTest.php b/src/tests/Browser/Reseller/DomainTest.php --- a/src/tests/Browser/Reseller/DomainTest.php +++ b/src/tests/Browser/Reseller/DomainTest.php @@ -3,7 +3,9 @@ namespace Tests\Browser\Reseller; use App\Domain; +use App\EventLog; use Tests\Browser; +use Tests\Browser\Components\Dialog; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Admin\Domain as DomainPage; use Tests\Browser\Pages\Admin\User as UserPage; @@ -24,6 +26,8 @@ $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); + Eventlog::query()->delete(); + self::useResellerUrl(); } @@ -35,6 +39,8 @@ $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); + Eventlog::query()->delete(); + parent::tearDown(); } @@ -84,7 +90,9 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 2); + ->assertElementsCount('@nav a', 3) + ->assertSeeIn('@nav #tab-settings', 'Settings') + ->assertSeeIn('@nav #tab-history', 'History'); // Assert Configuration tab $browser->assertSeeIn('@nav #tab-config', 'Configuration') @@ -102,6 +110,8 @@ */ public function testSuspendAndUnsuspend(): void { + EventLog::query()->delete(); + $this->browse(function (Browser $browser) { $sku_domain = \App\Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $user = $this->getTestUser('test1@domainscontroller.com'); @@ -123,14 +133,36 @@ ->assertVisible('@domain-info #button-suspend') ->assertMissing('@domain-info #button-unsuspend') ->click('@domain-info #button-suspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Suspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->type('textarea', 'test suspend') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Domain suspended successfully.') ->assertSeeIn('@domain-info #status span.text-warning', 'Suspended') - ->assertMissing('@domain-info #button-suspend') - ->click('@domain-info #button-unsuspend') + ->assertMissing('@domain-info #button-suspend'); + + $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first(); + $this->assertSame('test suspend', $event->comment); + $this->assertEquals($domain->id, $event->object_id); + + $browser->click('@domain-info #button-unsuspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Unsuspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'Domain unsuspended successfully.') ->assertSeeIn('@domain-info #status span.text-success', 'Active') ->assertVisible('@domain-info #button-suspend') ->assertMissing('@domain-info #button-unsuspend'); + + $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first(); + $this->assertSame(null, $event->comment); + $this->assertEquals($domain->id, $event->object_id); }); } } diff --git a/src/tests/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php --- a/src/tests/Browser/Reseller/UserTest.php +++ b/src/tests/Browser/Reseller/UserTest.php @@ -4,6 +4,7 @@ use App\Auth\SecondFactor; use App\Discount; +use App\EventLog; use App\Sku; use App\User; use Tests\Browser; @@ -117,7 +118,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 9); + ->assertElementsCount('@nav a', 10); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); @@ -253,7 +254,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 9); + ->assertElementsCount('@nav a', 10); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); @@ -344,6 +345,9 @@ ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', 'folder-contact@kolab.org') ->assertMissing('table tfoot'); }); + + // Assert History tab + $browser->assertSeeIn('@nav #tab-history', 'History'); }); // Now we go to Ned's info page, he's a controller on John's wallet @@ -365,7 +369,7 @@ // Some tabs are loaded in background, wait a second $browser->pause(500) - ->assertElementsCount('@nav a', 9); + ->assertElementsCount('@nav a', 10); // Note: Finances tab is tested in UserFinancesTest.php $browser->assertSeeIn('@nav #tab-finances', 'Finances'); @@ -450,6 +454,9 @@ ->assertSeeIn('.row:nth-child(3) label', 'Geo-lockin') ->assertSeeIn('.row:nth-child(3) #limit_geo', 'No restrictions'); }); + + // Assert History tab + $browser->assertSeeIn('@nav #tab-history', 'History'); }); } @@ -513,6 +520,8 @@ */ public function testSuspendAndUnsuspend(): void { + EventLog::query()->delete(); + $this->browse(function (Browser $browser) { $john = $this->getTestUser('john@kolab.org'); @@ -520,14 +529,34 @@ ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend') ->click('@user-info #button-suspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Suspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->type('textarea', 'test suspend') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'User suspended successfully.') ->assertSeeIn('@user-info #status span.text-warning', 'Suspended') - ->assertMissing('@user-info #button-suspend') - ->click('@user-info #button-unsuspend') + ->assertMissing('@user-info #button-suspend'); + + $event = EventLog::where('type', EventLog::TYPE_SUSPENDED)->first(); + $this->assertSame('test suspend', $event->comment); + + $browser->click('@user-info #button-unsuspend') + ->with(new Dialog('#suspend-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Unsuspend') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-action'); + }) ->assertToast(Toast::TYPE_SUCCESS, 'User unsuspended successfully.') ->assertSeeIn('@user-info #status span.text-success', 'Active') ->assertVisible('@user-info #button-suspend') ->assertMissing('@user-info #button-unsuspend'); + + $event = EventLog::where('type', EventLog::TYPE_UNSUSPENDED)->first(); + $this->assertSame(null, $event->comment); }); } diff --git a/src/tests/Feature/Console/Group/SuspendTest.php b/src/tests/Feature/Console/Group/SuspendTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Group/SuspendTest.php @@ -0,0 +1,75 @@ +deleteTestGroup('group-test@kolabnow.com'); + EventLog::truncate(); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestGroup('group-test@kolabnow.com'); + EventLog::truncate(); + + parent::tearDown(); + } + + /** + * Test the command + */ + public function testHandle(): void + { + Queue::fake(); + + // Non-existing user + $code = \Artisan::call("group:suspend unknown"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Group not found.", $output); + + $group = $this->getTestGroup('group-test@kolabnow.com'); + + // Test success (no --comment) + $code = \Artisan::call("group:suspend {$group->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("", $output); + $this->assertTrue($group->fresh()->isSuspended()); + $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first(); + $this->assertSame(null, $event->comment); + $this->assertSame(EventLog::TYPE_SUSPENDED, $event->type); + + $group->unsuspend(); + EventLog::truncate(); + + // Test success (no --comment) + $code = \Artisan::call("group:suspend --comment=\"Test comment\" {$group->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("", $output); + $this->assertTrue($group->fresh()->isSuspended()); + $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first(); + $this->assertSame('Test comment', $event->comment); + $this->assertSame(EventLog::TYPE_SUSPENDED, $event->type); + } +} diff --git a/src/tests/Feature/Console/Group/UnsuspendTest.php b/src/tests/Feature/Console/Group/UnsuspendTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Group/UnsuspendTest.php @@ -0,0 +1,76 @@ +deleteTestGroup('group-test@kolabnow.com'); + EventLog::truncate(); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestGroup('group-test@kolabnow.com'); + EventLog::truncate(); + + parent::tearDown(); + } + + /** + * Test the command + */ + public function testHandle(): void + { + Queue::fake(); + + // Non-existing user + $code = \Artisan::call("group:unsuspend unknown"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Group not found.", $output); + + $group = $this->getTestGroup('group-test@kolabnow.com'); + $group->suspend(); + + // Test success (no --comment) + $code = \Artisan::call("group:unsuspend {$group->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("", $output); + $this->assertFalse($group->fresh()->isSuspended()); + $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first(); + $this->assertSame(null, $event->comment); + $this->assertSame(EventLog::TYPE_UNSUSPENDED, $event->type); + + $group->suspend(); + EventLog::truncate(); + + // Test success (no --comment) + $code = \Artisan::call("group:unsuspend --comment=\"Test comment\" {$group->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("", $output); + $this->assertFalse($group->fresh()->isSuspended()); + $event = EventLog::where('object_id', $group->id)->where('object_type', Group::class)->first(); + $this->assertSame('Test comment', $event->comment); + $this->assertSame(EventLog::TYPE_UNSUSPENDED, $event->type); + } +} diff --git a/src/tests/Feature/Controller/Admin/DomainsTest.php b/src/tests/Feature/Controller/Admin/DomainsTest.php --- a/src/tests/Feature/Controller/Admin/DomainsTest.php +++ b/src/tests/Feature/Controller/Admin/DomainsTest.php @@ -4,6 +4,7 @@ use App\Domain; use App\Entitlement; +use App\EventLog; use App\Sku; use Illuminate\Support\Facades\Queue; use Tests\TestCase; @@ -201,7 +202,7 @@ $this->assertFalse($domain->fresh()->isSuspended()); - // Test suspending the user + // Test suspending the domain $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/suspend", []); $response->assertStatus(200); @@ -210,7 +211,23 @@ $this->assertSame('success', $json['status']); $this->assertSame("Domain suspended successfully.", $json['message']); $this->assertCount(2, $json); + $this->assertTrue($domain->fresh()->isSuspended()); + + $domain->unsuspend(); + EventLog::truncate(); + + // Test suspending the domain with a comment + $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/suspend", ['comment' => 'Test']); + $response->assertStatus(200); + + $where = [ + 'object_id' => $domain->id, + 'object_type' => Domain::class, + 'type' => EventLog::TYPE_SUSPENDED, + 'comment' => 'Test' + ]; + $this->assertSame(1, EventLog::where($where)->count()); $this->assertTrue($domain->fresh()->isSuspended()); } @@ -234,7 +251,7 @@ $this->assertTrue($domain->fresh()->isSuspended()); - // Test suspending the user + // Test suspending the domain $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/unsuspend", []); $response->assertStatus(200); @@ -243,7 +260,23 @@ $this->assertSame('success', $json['status']); $this->assertSame("Domain unsuspended successfully.", $json['message']); $this->assertCount(2, $json); + $this->assertFalse($domain->fresh()->isSuspended()); + + $domain->suspend(); + EventLog::truncate(); + + // Test unsuspending the domain with a comment + $response = $this->actingAs($admin)->post("/api/v4/domains/{$domain->id}/unsuspend", ['comment' => 'Test']); + $response->assertStatus(200); + + $where = [ + 'object_id' => $domain->id, + 'object_type' => Domain::class, + 'type' => EventLog::TYPE_UNSUSPENDED, + 'comment' => 'Test' + ]; + $this->assertSame(1, EventLog::where($where)->count()); $this->assertFalse($domain->fresh()->isSuspended()); } } diff --git a/src/tests/Feature/Controller/Admin/EventLogTest.php b/src/tests/Feature/Controller/Admin/EventLogTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Controller/Admin/EventLogTest.php @@ -0,0 +1,97 @@ +delete(); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + EventLog::query()->delete(); + + parent::tearDown(); + } + + /** + * Test listing events for a user (GET /api/v4/eventlog/user/{user}) + */ + public function testUserLog(): void + { + $user = $this->getTestUser('john@kolab.org'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + + // Non-admin user + $response = $this->actingAs($user)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(403); + + // Admin user (unknown object type) + $response = $this->actingAs($admin)->get("api/v4/eventlog/eeee/{$user->id}"); + $response->assertStatus(404); + + // Admin user (empty list) + $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(0, $json['count']); + $this->assertSame([], $json['list']); + $this->assertFalse($json['hasMore']); + + // Non-empty list + $event1 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 1", ['test' => 'test1']); + $event1->created_at = now(); + $event1->save(); + $event2 = EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, "Event 2", ['test' => 'test2']); + $event2->created_at = (clone now())->subDay(); + $event2->save(); + + $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(2, $json['count']); + $this->assertCount(2, $json['list']); + $this->assertFalse($json['hasMore']); + $this->assertSame($event1->id, $json['list'][0]['id']); + $this->assertSame($event1->comment, $json['list'][0]['comment']); + $this->assertSame($event1->data, $json['list'][0]['data']); + $this->assertSame($admin->email, $json['list'][0]['user']); + $this->assertSame('Suspended', $json['list'][0]['event']); + $this->assertSame($event2->id, $json['list'][1]['id']); + $this->assertSame($event2->comment, $json['list'][1]['comment']); + $this->assertSame($event2->data, $json['list'][1]['data']); + $this->assertSame($admin->email, $json['list'][1]['user']); + $this->assertSame('Unsuspended', $json['list'][1]['event']); + + // A user in another tenant + $user = $this->getTestUser('user@sample-tenant.dev-local'); + $event3 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 3"); + + $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame($event3->id, $json['list'][0]['id']); + } +} diff --git a/src/tests/Feature/Controller/Admin/GroupsTest.php b/src/tests/Feature/Controller/Admin/GroupsTest.php --- a/src/tests/Feature/Controller/Admin/GroupsTest.php +++ b/src/tests/Feature/Controller/Admin/GroupsTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Controller\Admin; +use App\EventLog; use App\Group; use Illuminate\Support\Facades\Queue; use Tests\TestCase; @@ -184,7 +185,23 @@ $this->assertSame('success', $json['status']); $this->assertSame("Distribution list suspended successfully.", $json['message']); $this->assertCount(2, $json); + $this->assertTrue($group->fresh()->isSuspended()); + + $group->unsuspend(); + EventLog::truncate(); + + // Test suspending the group with a comment + $response = $this->actingAs($admin)->post("/api/v4/groups/{$group->id}/suspend", ['comment' => 'Test']); + $response->assertStatus(200); + + $where = [ + 'object_id' => $group->id, + 'object_type' => Group::class, + 'type' => EventLog::TYPE_SUSPENDED, + 'comment' => 'Test' + ]; + $this->assertSame(1, EventLog::where($where)->count()); $this->assertTrue($group->fresh()->isSuspended()); } @@ -221,7 +238,23 @@ $this->assertSame('success', $json['status']); $this->assertSame("Distribution list unsuspended successfully.", $json['message']); $this->assertCount(2, $json); + $this->assertFalse($group->fresh()->isSuspended()); + + $group->unsuspend(); + EventLog::truncate(); + + // Test unsuspending the group with a comment + $response = $this->actingAs($admin)->post("/api/v4/groups/{$group->id}/unsuspend", ['comment' => 'Test']); + $response->assertStatus(200); + + $where = [ + 'object_id' => $group->id, + 'object_type' => Group::class, + 'type' => EventLog::TYPE_UNSUSPENDED, + 'comment' => 'Test' + ]; + $this->assertSame(1, EventLog::where($where)->count()); $this->assertFalse($group->fresh()->isSuspended()); } } diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php --- a/src/tests/Feature/Controller/Admin/UsersTest.php +++ b/src/tests/Feature/Controller/Admin/UsersTest.php @@ -3,7 +3,9 @@ namespace Tests\Feature\Controller\Admin; use App\Auth\SecondFactor; +use App\EventLog; use App\Sku; +use App\User; use Illuminate\Support\Facades\Queue; use Tests\TestCase; @@ -503,7 +505,23 @@ $this->assertSame('success', $json['status']); $this->assertSame("User suspended successfully.", $json['message']); $this->assertCount(2, $json); + $this->assertTrue($user->fresh()->isSuspended()); + + $user->unsuspend(); + EventLog::truncate(); + + // Test suspending the user with a comment + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/suspend", ['comment' => 'Test']); + $response->assertStatus(200); + + $where = [ + 'object_id' => $user->id, + 'object_type' => User::class, + 'type' => EventLog::TYPE_SUSPENDED, + 'comment' => 'Test' + ]; + $this->assertSame(1, EventLog::where($where)->count()); $this->assertTrue($user->fresh()->isSuspended()); } @@ -534,7 +552,23 @@ $this->assertSame('success', $json['status']); $this->assertSame("User unsuspended successfully.", $json['message']); $this->assertCount(2, $json); + $this->assertFalse($user->fresh()->isSuspended()); + + $user->suspend(); + EventLog::truncate(); + + // Test suspending the user with a comment + $response = $this->actingAs($admin)->post("/api/v4/users/{$user->id}/unsuspend", ['comment' => 'Test']); + $response->assertStatus(200); + + $where = [ + 'object_id' => $user->id, + 'object_type' => User::class, + 'type' => EventLog::TYPE_UNSUSPENDED, + 'comment' => 'Test' + ]; + $this->assertSame(1, EventLog::where($where)->count()); $this->assertFalse($user->fresh()->isSuspended()); } diff --git a/src/tests/Feature/Controller/Reseller/EventLogTest.php b/src/tests/Feature/Controller/Reseller/EventLogTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Controller/Reseller/EventLogTest.php @@ -0,0 +1,106 @@ +delete(); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + EventLog::query()->delete(); + + parent::tearDown(); + } + + /** + * Test listing events for a user (GET /api/v4/eventlog/user/{user}) + */ + public function testUserLog(): void + { + $user = $this->getTestUser('john@kolab.org'); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + $reseller1 = $this->getTestUser('reseller@' . \config('app.domain')); + $reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local'); + + // Non-admin user + $response = $this->actingAs($user)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(403); + + // Admin user + $response = $this->actingAs($admin)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(403); + + // Reseller user (unknown object type) + $response = $this->actingAs($reseller1)->get("api/v4/eventlog/eeee/{$user->id}"); + $response->assertStatus(404); + + // Reseller user (empty list) + $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(0, $json['count']); + $this->assertSame([], $json['list']); + $this->assertFalse($json['hasMore']); + + // Non-empty list + $event1 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 1", ['test' => 'test1']); + $event1->created_at = now(); + $event1->save(); + $event2 = EventLog::createFor($user, EventLog::TYPE_UNSUSPENDED, "Event 2", ['test' => 'test2']); + $event2->created_at = (clone now())->subDay(); + $event2->save(); + + $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(2, $json['count']); + $this->assertCount(2, $json['list']); + $this->assertFalse($json['hasMore']); + $this->assertSame($event1->id, $json['list'][0]['id']); + $this->assertSame($event1->comment, $json['list'][0]['comment']); + $this->assertSame($event1->data, $json['list'][0]['data']); + $this->assertSame($reseller1->email, $json['list'][0]['user']); + $this->assertSame('Suspended', $json['list'][0]['event']); + $this->assertSame($event2->id, $json['list'][1]['id']); + $this->assertSame($event2->comment, $json['list'][1]['comment']); + $this->assertSame($event2->data, $json['list'][1]['data']); + $this->assertSame($reseller1->email, $json['list'][1]['user']); + $this->assertSame('Unsuspended', $json['list'][1]['event']); + + // A user in another tenant + $user = $this->getTestUser('user@sample-tenant.dev-local'); + $event3 = EventLog::createFor($user, EventLog::TYPE_SUSPENDED, "Event 3"); + + $response = $this->actingAs($reseller1)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(404); + + $response = $this->actingAs($reseller2)->get("api/v4/eventlog/user/{$user->id}"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame(1, $json['count']); + $this->assertCount(1, $json['list']); + $this->assertSame($event3->id, $json['list'][0]['id']); + } +} 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 @@ -4,6 +4,7 @@ use App\Domain; use App\Entitlement; +use App\EventLog; use App\Sku; use App\User; use App\Tenant; @@ -232,6 +233,29 @@ } /** + * Test eventlog on domain deletion + */ + public function testDeleteAndEventLog(): void + { + Queue::fake(); + + $domain = $this->getTestDomain('gmail.com', [ + 'status' => Domain::STATUS_NEW, + 'type' => Domain::TYPE_PUBLIC, + ]); + + EventLog::createFor($domain, EventLog::TYPE_SUSPENDED, 'test'); + + $domain->delete(); + + $this->assertCount(1, EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->get()); + + $domain->forceDelete(); + + $this->assertCount(0, EventLog::where('object_id', $domain->id)->where('object_type', Domain::class)->get()); + } + + /** * Test isEmpty() method */ public function testIsEmpty(): void diff --git a/src/tests/Feature/GroupTest.php b/src/tests/Feature/GroupTest.php --- a/src/tests/Feature/GroupTest.php +++ b/src/tests/Feature/GroupTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\Group; +use App\EventLog; use Illuminate\Support\Facades\Queue; use Tests\TestCase; @@ -133,6 +134,26 @@ } /** + * Test eventlog on group deletion + */ + public function testDeleteAndEventLog(): void + { + Queue::fake(); + + $group = $this->getTestGroup('group-test@kolabnow.com'); + + EventLog::createFor($group, EventLog::TYPE_SUSPENDED, 'test'); + + $group->delete(); + + $this->assertCount(1, EventLog::where('object_id', $group->id)->where('object_type', Group::class)->get()); + + $group->forceDelete(); + + $this->assertCount(0, EventLog::where('object_id', $group->id)->where('object_type', Group::class)->get()); + } + + /** * Tests for Group::emailExists() */ public function testEmailExists(): void 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 @@ -3,6 +3,7 @@ namespace Tests\Feature; use App\Domain; +use App\EventLog; use App\Group; use App\Package; use App\PackageSku; @@ -839,6 +840,26 @@ } /** + * Test eventlog on user deletion + */ + public function testDeleteAndEventLog(): void + { + Queue::fake(); + + $user = $this->getTestUser('user-test@' . \config('app.domain')); + + EventLog::createFor($user, EventLog::TYPE_SUSPENDED, 'test'); + + $user->delete(); + + $this->assertCount(1, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get()); + + $user->forceDelete(); + + $this->assertCount(0, EventLog::where('object_id', $user->id)->where('object_type', User::class)->get()); + } + + /** * Test user deletion vs. group membership */ public function testDeleteAndGroups(): void diff --git a/src/tests/Unit/EventLogTest.php b/src/tests/Unit/EventLogTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Unit/EventLogTest.php @@ -0,0 +1,29 @@ +expectException(\Exception::class); + $event->type = -1; + + $this->expectException(\Exception::class); + $event->type = 256; + + $this->expectException(\Exception::class); + $event->type = 'abc'; // @phpstan-ignore-line + + $event->type = 2; + $this->assertSame(20, $event->type); + } +}