From 6f7e2fb599e380eccbbc5616762f45b6a291a514 Mon Sep 17 00:00:00 2001 From: Nick Zeng Date: Wed, 1 Apr 2026 13:52:12 +0800 Subject: [PATCH] update user list --- backend/app/Command/UserListCommand.php | 71 +++++++++ .../app/Command/UserResetPasswordCommand.php | 72 +++++++++ .../Unit/Command/UserListCommandTest.php | 86 ++++++++++ .../Command/UserResetPasswordCommandTest.php | 148 ++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 backend/app/Command/UserListCommand.php create mode 100644 backend/app/Command/UserResetPasswordCommand.php create mode 100644 backend/test/Cases/Unit/Command/UserListCommandTest.php create mode 100644 backend/test/Cases/Unit/Command/UserResetPasswordCommandTest.php diff --git a/backend/app/Command/UserListCommand.php b/backend/app/Command/UserListCommand.php new file mode 100644 index 0000000..f7e7956 --- /dev/null +++ b/backend/app/Command/UserListCommand.php @@ -0,0 +1,71 @@ +setDescription('List all system users'); + $this->addOption('role', 'r', InputOption::VALUE_OPTIONAL, 'Filter by role name (e.g. administrator, developer, accessor)'); + } + + public function handle(): int + { + $query = User::query()->with('role'); + + $role_name = $this->input->getOption('role'); + if ($role_name) { + $query->whereHas('role', fn ($q) => $q->where('name', $role_name)); + } + + $users = $query->orderBy('id')->get(); + + if ($users->isEmpty()) { + $this->info('No users found.'); + return 0; + } + + $rows = $users->map(fn (User $u) => [ + $u->id, + $u->username, + $u->email, + $u->role?->name ?? '-', + $this->formatStatus($u->status), + $u->created_at?->toDateTimeString(), + ])->toArray(); + + $this->table( + ['ID', 'Username', 'Email', 'Role', 'Status', 'Created At'], + $rows, + ); + + $this->info(sprintf('Total: %d user(s)', count($rows))); + + return 0; + } + + private function formatStatus(int $status): string + { + return match ($status) { + 1 => 'enabled', + 0 => 'disabled', + default => 'unknown', + }; + } +} diff --git a/backend/app/Command/UserResetPasswordCommand.php b/backend/app/Command/UserResetPasswordCommand.php new file mode 100644 index 0000000..5698dbb --- /dev/null +++ b/backend/app/Command/UserResetPasswordCommand.php @@ -0,0 +1,72 @@ +setDescription('Reset password for a specified user'); + $this->addArgument('identifier', InputArgument::REQUIRED, 'Username or user ID'); + $this->addOption('password', 'p', InputOption::VALUE_OPTIONAL, 'New password (min 6 characters). If omitted, will prompt interactively'); + } + + public function handle(): int + { + $identifier = $this->input->getArgument('identifier'); + + // 按 ID(纯数字)或 username 查找用户 + $user = is_numeric($identifier) + ? User::find((int) $identifier) + : User::query()->where('username', $identifier)->first(); + + if (! $user) { + $this->error(sprintf('User "%s" not found.', $identifier)); + return 1; + } + + // 获取密码:优先 --password 选项,否则交互式隐藏输入 + $new_password = $this->input->getOption('password'); + if ($new_password === null) { + $new_password = $this->secret('Enter new password'); + if ($new_password === null) { + $this->error('Password cannot be empty.'); + return 1; + } + } + + // 密码长度校验(bcrypt 截断超过 72 字节的输入) + if (strlen($new_password) < 6) { + $this->error('Password must be at least 6 characters.'); + return 1; + } + if (strlen($new_password) > 72) { + $this->error('Password must not exceed 72 characters (bcrypt limit).'); + return 1; + } + + // password mutator 自动调用 password_hash() + $user->password = $new_password; + $user->save(); + + $this->info(sprintf("Password for user '%s' has been reset.", $user->username)); + + return 0; + } +} diff --git a/backend/test/Cases/Unit/Command/UserListCommandTest.php b/backend/test/Cases/Unit/Command/UserListCommandTest.php new file mode 100644 index 0000000..6cee1ee --- /dev/null +++ b/backend/test/Cases/Unit/Command/UserListCommandTest.php @@ -0,0 +1,86 @@ + 0) { + $callback(); + return; + } + + $exception = null; + \Swoole\Coroutine\run(static function () use ($callback, &$exception): void { + try { + $callback(); + } catch (\Throwable $e) { + $exception = $e; + } + }); + if ($exception !== null) { + throw $exception; + } + } + + public function test_list_all_users_displays_table(): void + { + $this->runInCoroutine(function (): void { + $container = ApplicationContext::getContainer(); + $command = new UserListCommand($container); + $tester = new CommandTester($command); + $tester->execute([]); + + $output = $tester->getDisplay(); + $this->assertStringContainsString('ID', $output); + $this->assertStringContainsString('Username', $output); + $this->assertStringContainsString('Email', $output); + $this->assertStringContainsString('Role', $output); + $this->assertStringContainsString('Status', $output); + $this->assertStringContainsString('Total:', $output); + }); + } + + public function test_list_users_filtered_by_role(): void + { + $this->runInCoroutine(function (): void { + $container = ApplicationContext::getContainer(); + $command = new UserListCommand($container); + $tester = new CommandTester($command); + $tester->execute(['--role' => 'administrator']); + + $output = $tester->getDisplay(); + $this->assertTrue( + str_contains($output, 'administrator') || str_contains($output, 'No users found'), + 'Should display administrator users or empty message' + ); + }); + } + + public function test_list_users_with_nonexistent_role_shows_empty(): void + { + $this->runInCoroutine(function (): void { + $container = ApplicationContext::getContainer(); + $command = new UserListCommand($container); + $tester = new CommandTester($command); + $tester->execute(['--role' => 'nonexistent_role_xyz_123']); + + $output = $tester->getDisplay(); + $this->assertStringContainsString('No users found', $output); + }); + } +} diff --git a/backend/test/Cases/Unit/Command/UserResetPasswordCommandTest.php b/backend/test/Cases/Unit/Command/UserResetPasswordCommandTest.php new file mode 100644 index 0000000..cd2eb71 --- /dev/null +++ b/backend/test/Cases/Unit/Command/UserResetPasswordCommandTest.php @@ -0,0 +1,148 @@ + 0) { + $callback(); + return; + } + + $exception = null; + \Swoole\Coroutine\run(static function () use ($callback, &$exception): void { + try { + $callback(); + } catch (\Throwable $e) { + $exception = $e; + } + }); + if ($exception !== null) { + throw $exception; + } + } + + protected function tearDown(): void + { + if ($this->testUserId !== null) { + $this->runInCoroutine(function (): void { + User::query()->where('id', $this->testUserId)->delete(); + }); + $this->testUserId = null; + } + parent::tearDown(); + } + + private function createTestUser(): User + { + $role = Role::query()->first(); + $unique = 'test_reset_' . uniqid(); + $user = User::create([ + 'username' => $unique, + 'email' => $unique . '@test.com', + 'password' => 'old_password_123', + 'status' => 1, + 'role_id' => $role?->id, + 'ext' => [], + ]); + $this->testUserId = $user->id; + + return $user; + } + + public function test_reset_password_by_username(): void + { + $this->runInCoroutine(function (): void { + $test_user = $this->createTestUser(); + + $container = ApplicationContext::getContainer(); + $command = new UserResetPasswordCommand($container); + $tester = new CommandTester($command); + $tester->execute([ + 'identifier' => $test_user->username, + '--password' => 'new_password_123', + ]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('has been reset', $tester->getDisplay()); + + // 验证密码确实已更新 + $updated_user = User::find($test_user->id); + $this->assertTrue($updated_user->verifyPassword('new_password_123')); + }); + } + + public function test_reset_password_by_id(): void + { + $this->runInCoroutine(function (): void { + $test_user = $this->createTestUser(); + + $container = ApplicationContext::getContainer(); + $command = new UserResetPasswordCommand($container); + $tester = new CommandTester($command); + $tester->execute([ + 'identifier' => (string) $test_user->id, + '--password' => 'another_pwd_456', + ]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('has been reset', $tester->getDisplay()); + + $updated_user = User::find($test_user->id); + $this->assertTrue($updated_user->verifyPassword('another_pwd_456')); + }); + } + + public function test_user_not_found_shows_error(): void + { + $this->runInCoroutine(function (): void { + $container = ApplicationContext::getContainer(); + $command = new UserResetPasswordCommand($container); + $tester = new CommandTester($command); + $tester->execute([ + 'identifier' => 'nonexistent_user_xyz_999', + '--password' => 'some_password', + ]); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertStringContainsString('not found', $tester->getDisplay()); + }); + } + + public function test_password_too_short_shows_error(): void + { + $this->runInCoroutine(function (): void { + $test_user = $this->createTestUser(); + + $container = ApplicationContext::getContainer(); + $command = new UserResetPasswordCommand($container); + $tester = new CommandTester($command); + $tester->execute([ + 'identifier' => $test_user->username, + '--password' => '12345', + ]); + + $this->assertSame(1, $tester->getStatusCode()); + $this->assertStringContainsString('at least 6', $tester->getDisplay()); + }); + } +}