From eac86afa539278eb33fce7758ee600f1ca7e48c4 Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Fri, 7 Aug 2026 15:01:50 +0200 Subject: [PATCH] feat: create user command --- app/Console/Commands/CreateUser.php | 97 +++++++++++++++++++++++++ tests/Feature/CreateUserCommandTest.php | 45 ++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 app/Console/Commands/CreateUser.php create mode 100644 tests/Feature/CreateUserCommandTest.php diff --git a/app/Console/Commands/CreateUser.php b/app/Console/Commands/CreateUser.php new file mode 100644 index 0000000..148fca5 --- /dev/null +++ b/app/Console/Commands/CreateUser.php @@ -0,0 +1,97 @@ +option('name') ?: $this->ask('Name'))); + $email = strtolower(trim((string) ($this->option('email') ?: $this->ask('Email address')))); + $role = (string) ($this->option('role') ?: $this->choice( + 'Role', + array_column(UserRole::cases(), 'value'), + UserRole::USER->value, + )); + $locale = strtolower(trim((string) $this->option('locale'))); + + $attributesValidator = Validator::make([ + 'name' => $name, + 'email' => $email, + 'role' => $role, + 'locale' => $locale, + ], [ + 'name' => ['required', 'string', 'min:3', 'max:32', 'regex:/^[a-zA-Z0-9_.-]+$/', 'unique:users,name'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'role' => ['required', Rule::enum(UserRole::class)], + 'locale' => ['required', Rule::in(config('app.supported_locales', ['fr', 'en']))], + ]); + + if ($attributesValidator->fails()) { + foreach ($attributesValidator->errors()->all() as $error) { + $this->components->error($error); + } + + return self::FAILURE; + } + + $password = (string) $this->secret('Password'); + $passwordConfirmation = (string) $this->secret('Confirm password'); + + $passwordValidator = Validator::make([ + 'password' => $password, + 'password_confirmation' => $passwordConfirmation, + ], [ + 'password' => [ + 'required', + 'confirmed', + Password::min(8) + ->mixedCase() + ->letters() + ->numbers() + ->symbols(), + ], + ]); + + if ($passwordValidator->fails()) { + foreach ($passwordValidator->errors()->all() as $error) { + $this->components->error($error); + } + + return self::FAILURE; + } + + $trialDays = max(0, (int) config('billing.trial_days', 7)); + + $user = User::query()->create([ + 'name' => $name, + 'email' => $email, + 'email_verified_at' => $this->option('verified') ? now() : null, + 'locale' => $locale, + 'password' => Hash::make($password), + 'role' => UserRole::from($role), + 'trial_ends_at' => $trialDays > 0 ? now()->addDays($trialDays) : null, + ]); + + $this->components->info("User {$user->email} created with ID {$user->getKey()} and role {$user->role->value}."); + + return self::SUCCESS; + } +} diff --git a/tests/Feature/CreateUserCommandTest.php b/tests/Feature/CreateUserCommandTest.php new file mode 100644 index 0000000..68ba9a8 --- /dev/null +++ b/tests/Feature/CreateUserCommandTest.php @@ -0,0 +1,45 @@ +artisan('user:create', [ + '--name' => 'admin.bowli', + '--email' => 'ADMIN@EXAMPLE.COM', + '--role' => UserRole::ADMIN->value, + '--locale' => 'fr', + '--verified' => true, + ]) + ->expectsQuestion('Password', 'Strong-password1') + ->expectsQuestion('Confirm password', 'Strong-password1') + ->assertSuccessful(); + + $user = User::query()->where('email', 'admin@example.com')->firstOrFail(); + + expect($user->name)->toBe('admin.bowli') + ->and($user->role)->toBe(UserRole::ADMIN) + ->and($user->locale)->toBe('fr') + ->and($user->email_verified_at)->not->toBeNull() + ->and($user->trial_ends_at)->not->toBeNull() + ->and(Hash::check('Strong-password1', $user->password))->toBeTrue(); +}); + +it('rejects duplicate users', function () { + User::factory()->create([ + 'name' => 'existing.user', + 'email' => 'existing@example.com', + ]); + + $this->artisan('user:create', [ + '--name' => 'existing.user', + '--email' => 'existing@example.com', + '--role' => UserRole::USER->value, + ])->assertFailed(); + + expect(User::query()->count())->toBe(1); +});