diff --git a/src/app/Console/Commands/User/ResyncCommand.php b/src/app/Console/Commands/User/ResyncCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/User/ResyncCommand.php @@ -0,0 +1,108 @@ +argument('user'); + $deleted_only = $this->option('deleted-only'); + $dry_run = $this->option('dry-run'); + $with_ldap = \config('app.with_ldap'); + + if (!empty($user)) { + if ($req_user = $this->getUser($user, true)) { + $users = [$req_user]; + } else { + $this->error("User not found."); + return 1; + } + } else { + $users = User::withTrashed(); + + if ($deleted_only) { + $users->whereNotNull('deleted_at') + ->where(function($query) { + $query->where('status', '&', User::STATUS_IMAP_READY)->orWhere('status', '&', User::STATUS_LDAP_READY); + }); + } + + $users = $users->orderBy('id')->cursor(); + } + + // TODO: Maybe we should also have account:resync, domain:resync, resource:resync and so on. + + foreach ($users as $user) { + if ($user->trashed()) { + if (($with_ldap && $user->isLdapReady()) || $user->isImapReady()) { + if ($dry_run) { + $this->info("{$user->email}: will be pushed"); + continue; + } + + if ($user->isDeleted()) { + // Remove the DELETED flag so the DeleteJob can do the work + $user->timestamps = false; + $user->update(['status' => $user->status ^ User::STATUS_DELETED]); + } + + // TODO: Do this not asyncronously as an option or when a signle user is requested? + \App\Jobs\User\DeleteJob::dispatch($user->id); + + $this->info("{$user->email}: pushed"); + } else { + // User properly deleted, no need to push. + // Here potentially we could connect to ldap/imap backend and check to be sure + // that the user is really deleted no matter what status it has in the database. + + $this->info("{$user->email}: in-sync"); + } + } else { + if (!$user->isActive() || ($with_ldap && !$user->isLdapReady()) || !$user->isImapReady()) { + if ($dry_run) { + $this->info("{$user->email}: will be pushed"); + continue; + } + + \App\Jobs\User\CreateJob::dispatch($user->id); + } else if (!empty($req_user)) { + if ($dry_run) { + $this->info("{$user->email}: will be pushed"); + continue; + } + + // We push the update only if a specific user is requested, + // We don't want to flood the database/backend with an update of all users + \App\Jobs\User\UpdateJob::dispatch($user->id); + + $this->info("{$user->email}: pushed"); + } else { + $this->info("{$user->email}: in-sync"); + } + } + } + } +} diff --git a/src/tests/Feature/Console/User/ResyncTest.php b/src/tests/Feature/Console/User/ResyncTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/User/ResyncTest.php @@ -0,0 +1,105 @@ +deleteTestUser('user@force-delete.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('user@force-delete.com'); + + parent::tearDown(); + } + + /** + * Test the command + */ + public function testHandle(): void + { + // Non-existing user + $code = \Artisan::call("user:resync unknown"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("User not found.", $output); + + $user = $this->getTestUser('user@force-delete.com'); + User::where('id', $user->id)->update([ + 'deleted_at' => now(), + 'status' => User::STATUS_DELETED | User::STATUS_IMAP_READY, + ]); + + Queue::fake(); + + // Test success (--dry-run) + $code = \Artisan::call("user:resync {$user->email} --dry-run"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("{$user->email}: will be pushed", $output); + $this->assertTrue($user->fresh()->isDeleted()); + + Queue::assertNothingPushed(); + + // Test success + $code = \Artisan::call("user:resync {$user->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("{$user->email}: pushed", $output); + $user->refresh(); + $this->assertFalse($user->isDeleted()); + $this->assertTrue($user->isImapReady()); + + Queue::assertPushed(\App\Jobs\User\DeleteJob::class, 1); + Queue::assertPushed(\App\Jobs\User\DeleteJob::class, function ($job) use ($user) { + $job_user_id = TestCase::getObjectProperty($job, 'userId'); + return $job_user_id === $user->id; + }); + + Queue::fake(); + User::withTrashed()->where('id', $user->id)->update(['status' => User::STATUS_DELETED]); + + // Test nothing to be done (deleted user) + $code = \Artisan::call("user:resync {$user->email}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("{$user->email}: in-sync", $output); + Queue::assertNothingPushed(); + + Queue::fake(); + User::withTrashed()->where('id', $user->id)->update([ + 'status' => User::STATUS_DELETED | User::STATUS_IMAP_READY + ]); + + // Remove all deleted users except one, to not interfere + User::withTrashed()->whereNotIn('id', [$user->id])->forceDelete(); + + // Test run for all deleted users + $code = \Artisan::call("user:resync --deleted-only"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("{$user->email}: pushed", $output); + + // TODO: Test other cases (non-deleted users) + } +}