Skip to content

Commit 2eefbd2

Browse files
committed
Commands: Added testing for initial admin changes
- Also changed first-admin to initial. - Updated initial handling to not require email/name to be passed, using defaults instead. - Adds missing existing email use check.
1 parent a961552 commit 2eefbd2

File tree

2 files changed

+212
-26
lines changed

2 files changed

+212
-26
lines changed

app/Console/Commands/CreateAdminCommand.php

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use Illuminate\Support\Facades\Validator;
99
use Illuminate\Support\Str;
1010
use Illuminate\Validation\Rules\Password;
11-
use Illuminate\Validation\Rules\Unique;
1211

1312
class CreateAdminCommand extends Command
1413
{
@@ -23,7 +22,7 @@ class CreateAdminCommand extends Command
2322
{--password= : The password to assign to the new admin user}
2423
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}
2524
{--generate-password : Generate a random password for the new admin user}
26-
{--first-admin : Indicate if this should set/update the details of the initial admin user}';
25+
{--initial : Indicate if this should set/update the details of the initial admin user}';
2726

2827
/**
2928
* The console command description.
@@ -37,12 +36,12 @@ class CreateAdminCommand extends Command
3736
*/
3837
public function handle(UserRepo $userRepo): int
3938
{
40-
$firstAdminOnly = $this->option('first-admin');
39+
$initialAdminOnly = $this->option('initial');
4140
$shouldGeneratePassword = $this->option('generate-password');
42-
$details = $this->gatherDetails($shouldGeneratePassword);
41+
$details = $this->gatherDetails($shouldGeneratePassword, $initialAdminOnly);
4342

4443
$validator = Validator::make($details, [
45-
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
44+
'email' => ['required', 'email', 'min:5'],
4645
'name' => ['required', 'min:2'],
4746
'password' => ['required_without:external_auth_id', Password::default()],
4847
'external_auth_id' => ['required_without:password'],
@@ -58,13 +57,20 @@ public function handle(UserRepo $userRepo): int
5857

5958
$adminRole = Role::getSystemRole('admin');
6059

61-
if ($firstAdminOnly) {
62-
$handled = $this->handleFirstAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole);
63-
if ($handled) {
64-
return 0;
60+
if ($initialAdminOnly) {
61+
$handled = $this->handleInitialAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole);
62+
if ($handled !== null) {
63+
return $handled ? 0 : 1;
6564
}
6665
}
6766

67+
$emailUsed = $userRepo->getByEmail($details['email']) !== null;
68+
if ($emailUsed) {
69+
$this->error("Could not create admin account.");
70+
$this->error("An account with the email address \"{$details['email']}\" already exists.");
71+
return 1;
72+
}
73+
6874
$user = $userRepo->createWithoutActivity($validator->validated());
6975
$user->attachRole($adminRole);
7076
$user->email_confirmed = true;
@@ -80,14 +86,19 @@ public function handle(UserRepo $userRepo): int
8086
}
8187

8288
/**
83-
* Handle updates to the first admin if exists.
84-
* Returns true if the action has been handled (user updated or already a non-default admin user) otherwise
85-
* returns false if no action has been taken, and we therefore need to proceed with a normal account creation.
89+
* Handle updates to the original admin account if it exists.
90+
* Returns true if it's been successfully handled, false if unsuccessful, or null if not handled.
8691
*/
87-
protected function handleFirstAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): bool
92+
protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): bool|null
8893
{
8994
$defaultAdmin = $userRepo->getByEmail('[email protected]');
9095
if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) {
96+
if ($defaultAdmin->email !== $data['email'] && $userRepo->getByEmail($data['email']) !== null) {
97+
$this->error("Could not create admin account.");
98+
$this->error("An account with the email address \"{$data['email']}\" already exists.");
99+
return false;
100+
}
101+
91102
$userRepo->updateWithoutActivity($defaultAdmin, $data, true);
92103
if ($generatePassword) {
93104
$this->line($data['password']);
@@ -101,19 +112,27 @@ protected function handleFirstAdminIfExists(UserRepo $userRepo, array $data, boo
101112
return true;
102113
}
103114

104-
return false;
115+
return null;
105116
}
106117

107-
protected function gatherDetails(bool $generatePassword): array
118+
protected function gatherDetails(bool $generatePassword, bool $initialAdmin): array
108119
{
109120
$details = $this->snakeCaseOptions();
110121

111122
if (empty($details['email'])) {
112-
$details['email'] = $this->ask('Please specify an email address for the new admin user');
123+
if ($initialAdmin) {
124+
$details['email'] = '[email protected]';
125+
} else {
126+
$details['email'] = $this->ask('Please specify an email address for the new admin user');
127+
}
113128
}
114129

115130
if (empty($details['name'])) {
116-
$details['name'] = $this->ask('Please specify a name for the new admin user');
131+
if ($initialAdmin) {
132+
$details['name'] = 'Admin';
133+
} else {
134+
$details['name'] = $this->ask('Please specify a name for the new admin user');
135+
}
117136
}
118137

119138
if (empty($details['password'])) {

tests/Commands/CreateAdminCommandTest.php

Lines changed: 176 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@
22

33
namespace Tests\Commands;
44

5+
use BookStack\Users\Models\Role;
56
use BookStack\Users\Models\User;
7+
use Illuminate\Support\Facades\Artisan;
68
use Illuminate\Support\Facades\Auth;
9+
use Illuminate\Support\Facades\Hash;
710
use Tests\TestCase;
811

912
class CreateAdminCommandTest extends TestCase
1013
{
1114
public function test_standard_command_usage()
1215
{
1316
$this->artisan('bookstack:create-admin', [
14-
'--email' => '[email protected]',
15-
'--name' => 'Admin Test',
17+
'--email' => '[email protected]',
18+
'--name' => 'Admin Test',
1619
'--password' => 'testing-4',
1720
])->assertExitCode(0);
1821

1922
$this->assertDatabaseHas('users', [
2023
'email' => '[email protected]',
21-
'name' => 'Admin Test',
24+
'name' => 'Admin Test',
2225
]);
2326

2427
/** @var User $user */
@@ -30,14 +33,14 @@ public function test_standard_command_usage()
3033
public function test_providing_external_auth_id()
3134
{
3235
$this->artisan('bookstack:create-admin', [
33-
'--email' => '[email protected]',
34-
'--name' => 'Admin Test',
36+
'--email' => '[email protected]',
37+
'--name' => 'Admin Test',
3538
'--external-auth-id' => 'xX_admin_Xx',
3639
])->assertExitCode(0);
3740

3841
$this->assertDatabaseHas('users', [
39-
'email' => '[email protected]',
40-
'name' => 'Admin Test',
42+
'email' => '[email protected]',
43+
'name' => 'Admin Test',
4144
'external_auth_id' => 'xX_admin_Xx',
4245
]);
4346

@@ -50,14 +53,178 @@ public function test_password_required_if_external_auth_id_not_given()
5053
{
5154
$this->artisan('bookstack:create-admin', [
5255
'--email' => '[email protected]',
53-
'--name' => 'Admin Test',
56+
'--name' => 'Admin Test',
5457
])->expectsQuestion('Please specify a password for the new admin user (8 characters min)', 'hunter2000')
5558
->assertExitCode(0);
5659

5760
$this->assertDatabaseHas('users', [
5861
'email' => '[email protected]',
59-
'name' => 'Admin Test',
62+
'name' => 'Admin Test',
6063
]);
6164
$this->assertTrue(Auth::attempt(['email' => '[email protected]', 'password' => 'hunter2000']));
6265
}
66+
67+
public function test_generate_password_option()
68+
{
69+
$this->withoutMockingConsoleOutput()
70+
->artisan('bookstack:create-admin', [
71+
'--email' => '[email protected]',
72+
'--name' => 'Admin Test',
73+
'--generate-password' => true,
74+
]);
75+
76+
$output = trim(Artisan::output());
77+
$this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{32}$/', $output);
78+
79+
$user = User::query()->where('email', '=', '[email protected]')->first();
80+
$this->assertTrue(Hash::check($output, $user->password));
81+
}
82+
83+
public function test_initial_option_updates_default_admin()
84+
{
85+
$defaultAdmin = User::query()->where('email', '=', '[email protected]')->first();
86+
87+
$this->artisan('bookstack:create-admin', [
88+
'--email' => '[email protected]',
89+
'--name' => 'Admin Test',
90+
'--password' => 'testing-7',
91+
'--initial' => true,
92+
])->expectsOutput('The default admin user has been updated with the provided details!')
93+
->assertExitCode(0);
94+
95+
$defaultAdmin->refresh();
96+
97+
$this->assertEquals('[email protected]', $defaultAdmin->email);
98+
}
99+
100+
public function test_initial_option_does_not_update_if_only_non_default_admin_exists()
101+
{
102+
$defaultAdmin = User::query()->where('email', '=', '[email protected]')->first();
103+
$defaultAdmin->email = '[email protected]';
104+
$defaultAdmin->save();
105+
106+
$this->artisan('bookstack:create-admin', [
107+
'--email' => '[email protected]',
108+
'--name' => 'Admin Test',
109+
'--password' => 'testing-7',
110+
'--initial' => true,
111+
])->expectsOutput('Non-default admin user already exists. Skipping creation of new admin user.')
112+
->assertExitCode(0);
113+
114+
$defaultAdmin->refresh();
115+
116+
$this->assertEquals('[email protected]', $defaultAdmin->email);
117+
}
118+
119+
public function test_initial_option_updates_creates_new_admin_if_none_exists()
120+
{
121+
$adminRole = Role::getSystemRole('admin');
122+
$adminRole->users()->delete();
123+
$this->assertEquals(0, $adminRole->users()->count());
124+
125+
$this->artisan('bookstack:create-admin', [
126+
'--email' => '[email protected]',
127+
'--name' => 'My initial admin',
128+
'--password' => 'testing-7',
129+
'--initial' => true,
130+
])->expectsOutput("Admin account with email \"[email protected]\" successfully created!")
131+
->assertExitCode(0);
132+
133+
$this->assertEquals(1, $adminRole->users()->count());
134+
$this->assertDatabaseHas('users', [
135+
'email' => '[email protected]',
136+
'name' => 'My initial admin',
137+
]);
138+
}
139+
140+
public function test_initial_rerun_does_not_error_but_skips()
141+
{
142+
$adminRole = Role::getSystemRole('admin');
143+
$adminRole->users()->delete();
144+
145+
$this->artisan('bookstack:create-admin', [
146+
'--email' => '[email protected]',
147+
'--name' => 'My initial admin',
148+
'--password' => 'testing-7',
149+
'--initial' => true,
150+
])->expectsOutput("Admin account with email \"[email protected]\" successfully created!")
151+
->assertExitCode(0);
152+
153+
$this->artisan('bookstack:create-admin', [
154+
'--email' => '[email protected]',
155+
'--name' => 'My initial admin',
156+
'--password' => 'testing-7',
157+
'--initial' => true,
158+
])->expectsOutput("Non-default admin user already exists. Skipping creation of new admin user.")
159+
->assertExitCode(0);
160+
}
161+
162+
public function test_initial_option_creation_errors_if_email_already_exists()
163+
{
164+
$adminRole = Role::getSystemRole('admin');
165+
$adminRole->users()->delete();
166+
$editor = $this->users->editor();
167+
168+
$this->artisan('bookstack:create-admin', [
169+
'--email' => $editor->email,
170+
'--name' => 'My initial admin',
171+
'--password' => 'testing-7',
172+
'--initial' => true,
173+
])->expectsOutput("Could not create admin account.")
174+
->expectsOutput("An account with the email address \"{$editor->email}\" already exists.")
175+
->assertExitCode(1);
176+
}
177+
178+
public function test_initial_option_updating_errors_if_email_already_exists()
179+
{
180+
$editor = $this->users->editor();
181+
$defaultAdmin = User::query()->where('email', '=', '[email protected]')->first();
182+
$this->assertNotNull($defaultAdmin);
183+
184+
$this->artisan('bookstack:create-admin', [
185+
'--email' => $editor->email,
186+
'--name' => 'My initial admin',
187+
'--password' => 'testing-7',
188+
'--initial' => true,
189+
])->expectsOutput("Could not create admin account.")
190+
->expectsOutput("An account with the email address \"{$editor->email}\" already exists.")
191+
->assertExitCode(1);
192+
}
193+
194+
public function test_initial_option_does_not_require_name_or_email_to_be_passed()
195+
{
196+
$adminRole = Role::getSystemRole('admin');
197+
$adminRole->users()->delete();
198+
$this->assertEquals(0, $adminRole->users()->count());
199+
200+
$this->artisan('bookstack:create-admin', [
201+
'--generate-password' => true,
202+
'--initial' => true,
203+
])->assertExitCode(0);
204+
205+
$this->assertEquals(1, $adminRole->users()->count());
206+
$this->assertDatabaseHas('users', [
207+
'email' => '[email protected]',
208+
'name' => 'Admin',
209+
]);
210+
}
211+
212+
public function test_initial_option_updating_existing_user_with_generate_password_only_outputs_password()
213+
{
214+
$defaultAdmin = User::query()->where('email', '=', '[email protected]')->first();
215+
216+
$this->withoutMockingConsoleOutput()
217+
->artisan('bookstack:create-admin', [
218+
'--email' => '[email protected]',
219+
'--name' => 'Admin Test',
220+
'--generate-password' => true,
221+
'--initial' => true,
222+
]);
223+
224+
$output = Artisan::output();
225+
$this->assertMatchesRegularExpression('/^[a-zA-Z0-9]{32}$/', $output);
226+
227+
$defaultAdmin->refresh();
228+
$this->assertEquals('[email protected]', $defaultAdmin->email);
229+
}
63230
}

0 commit comments

Comments
 (0)