]> BookStack Code Mirror - bookstack/blob - tests/Commands/RefreshAvatarCommandTest.php
feat: Artisan command for updating avatars for existing users
[bookstack] / tests / Commands / RefreshAvatarCommandTest.php
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Tests\Commands;
6
7 use BookStack\Console\Commands\RefreshAvatarCommand;
8 use BookStack\Uploads\UserAvatars;
9 use BookStack\Users\Models\User;
10 use GuzzleHttp\Psr7\Response;
11 use Illuminate\Database\Eloquent\Collection;
12 use Symfony\Component\Console\Command\Command;
13 use Tests\TestCase;
14
15 final class RefreshAvatarCommandTest extends TestCase
16 {
17     public function test_command_requires_email_or_id_option()
18     {
19         $this->artisan(RefreshAvatarCommand::class)
20             ->expectsOutput('Either a --id=<number> or --email=<email> option must be provided.')
21             ->assertExitCode(Command::FAILURE);
22     }
23
24     public function test_command_runs_with_provided_email()
25     {
26         $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);
27         config()->set(['services.disable_services' => false]);
28
29         /** @var User $user */
30         $user = User::query()->first();
31
32         /** @var UserAvatars $avatar */
33         $avatar = app()->make(UserAvatars::class);
34         $avatar->destroyAllForUser($user);
35
36         $this->assertFalse($user->avatar()->exists());
37         $this->artisan(RefreshAvatarCommand::class, ['--email' => $user->email])
38             ->expectsOutputToContain("- ID: {$user->id}")
39             ->expectsQuestion('Are you sure you want to proceed?', true)
40             ->expectsOutput('User avatar has been updated.')
41             ->assertExitCode(Command::SUCCESS);
42
43         $expectedUri = 'https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon';
44         $this->assertEquals($expectedUri, $requests->latestRequest()->getUri());
45
46         $user->refresh();
47         $this->assertTrue($user->avatar()->exists());
48     }
49
50     public function test_command_runs_with_provided_id()
51     {
52         $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);
53         config()->set(['services.disable_services' => false]);
54
55         /** @var User $user */
56         $user = User::query()->first();
57
58         /** @var UserAvatars $avatar */
59         $avatar = app()->make(UserAvatars::class);
60         $avatar->destroyAllForUser($user);
61
62         $this->assertFalse($user->avatar()->exists());
63         $this->artisan(RefreshAvatarCommand::class, ['--id' => $user->id])
64             ->expectsOutputToContain("- ID: {$user->id}")
65             ->expectsQuestion('Are you sure you want to proceed?', true)
66             ->expectsOutput('User avatar has been updated.')
67             ->assertExitCode(Command::SUCCESS);
68
69         $expectedUri = 'https://www.gravatar.com/avatar/' . md5(strtolower($user->email)) . '?s=500&d=identicon';
70         $this->assertEquals($expectedUri, $requests->latestRequest()->getUri());
71
72         $user->refresh();
73         $this->assertTrue($user->avatar()->exists());
74     }
75
76     public function test_command_runs_with_provided_id_error_upstream()
77     {
78         $requests = $this->mockHttpClient([new Response(404)]);
79         config()->set(['services.disable_services' => false]);
80
81         /** @var User $user */
82         $user = User::query()->first();
83         /** @var UserAvatars $avatar */
84         $avatar = app()->make(UserAvatars::class);
85         $avatar->assignToUserFromExistingData($user, $this->files->pngImageData(), 'png');
86
87         $oldId = $user->avatar->id ?? 0;
88
89         $this->artisan(RefreshAvatarCommand::class, ['--id' => $user->id])
90             ->expectsOutputToContain("- ID: {$user->id}")
91             ->expectsQuestion('Are you sure you want to proceed?', true)
92             ->expectsOutput('Could not update avatar please review logs.')
93             ->assertExitCode(Command::FAILURE);
94
95         $this->assertEquals(1, $requests->requestCount());
96
97         $user->refresh();
98         $newId = $user->avatar->id ?? $oldId;
99         $this->assertEquals($oldId, $newId);
100     }
101
102     public function test_saying_no_to_confirmation_does_not_refresh_avatar()
103     {
104         /** @var User $user */
105         $user = User::query()->first();
106
107         $this->assertFalse($user->avatar()->exists());
108         $this->artisan(RefreshAvatarCommand::class, ['--id' => $user->id])
109             ->expectsQuestion('Are you sure you want to proceed?', false)
110             ->assertExitCode(Command::FAILURE);
111         $this->assertFalse($user->avatar()->exists());
112     }
113
114     public function test_giving_non_existing_user_shows_error_message()
115     {
116         $this->artisan(RefreshAvatarCommand::class, ['--email' => '[email protected]'])
117             ->expectsOutput('A user where [email protected] could not be found.')
118             ->assertExitCode(Command::FAILURE);
119     }
120
121     public function test_command_runs_all_users_without_avatars_dry_run()
122     {
123         $users = User::query()->where('image_id', '=', 0)->get();
124
125         $this->artisan(RefreshAvatarCommand::class, ['--users-without-avatars' => true])
126             ->expectsOutput(count($users) . ' user(s) found without avatars.')
127             ->expectsOutput("ID {$users[0]->id} - ")
128             ->expectsOutput('Not updated')
129             ->expectsOutput('Dry run, no avatars have been updated')
130             ->assertExitCode(Command::SUCCESS);
131     }
132
133     public function test_command_runs_all_users_without_avatars_non_to_update()
134     {
135         config()->set(['services.disable_services' => false]);
136
137         /** @var UserAvatars $avatar */
138         $avatar = app()->make(UserAvatars::class);
139
140         /** @var Collection|User[] $users */
141         $users = User::query()->get();
142         $responses = [];
143         foreach ($users as $user) {
144             $avatar->fetchAndAssignToUser($user);
145             $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());
146         }
147         $requests = $this->mockHttpClient($responses);
148
149         $this->artisan(RefreshAvatarCommand::class, ['--users-without-avatars' => true, '-f' => true])
150             ->expectsOutput('0 user(s) found without avatars.')
151             ->expectsQuestion('Are you sure you want to refresh avatars of users that do not have one?', true)
152             ->assertExitCode(Command::SUCCESS);
153
154         $userWithAvatars = User::query()->where('image_id', '==', 0)->count();
155         $this->assertEquals(0, $userWithAvatars);
156         $this->assertEquals(0, $requests->requestCount());
157     }
158
159     public function test_command_runs_all_users_without_avatars()
160     {
161         config()->set(['services.disable_services' => false]);
162
163         /** @var UserAvatars $avatar */
164         $avatar = app()->make(UserAvatars::class);
165
166         /** @var Collection|User[] $users */
167         $users = User::query()->get();
168         foreach ($users as $user) {
169             $avatar->destroyAllForUser($user);
170         }
171
172         /** @var Collection|User[] $users */
173         $users = User::query()->where('image_id', '=', 0)->get();
174
175         $pendingCommand = $this->artisan(RefreshAvatarCommand::class, ['--users-without-avatars' => true, '-f' => true]);
176         $pendingCommand
177             ->expectsOutput($users->count() . ' user(s) found without avatars.')
178             ->expectsQuestion('Are you sure you want to refresh avatars of users that do not have one?', true);
179
180         $responses = [];
181         foreach ($users as $user) {
182             $pendingCommand->expectsOutput("ID {$user->id} - ");
183             $pendingCommand->expectsOutput('Updated');
184             $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());
185         }
186         $requests = $this->mockHttpClient($responses);
187
188         $pendingCommand->assertExitCode(Command::SUCCESS);
189         $pendingCommand->run();
190
191         $userWithAvatars = User::query()->where('image_id', '!=', 0)->count();
192         $this->assertEquals($users->count(), $userWithAvatars);
193         $this->assertEquals($users->count(), $requests->requestCount());
194     }
195
196     public function test_saying_no_to_confirmation_all_users_without_avatars()
197     {
198         $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);
199         config()->set(['services.disable_services' => false]);
200
201         /** @var UserAvatars $avatar */
202         $avatar = app()->make(UserAvatars::class);
203
204         /** @var Collection|User[] $users */
205         $users = User::query()->get();
206         foreach ($users as $user) {
207             $avatar->destroyAllForUser($user);
208         }
209
210         $this->artisan(RefreshAvatarCommand::class, ['--users-without-avatars' => true, '-f' => true])
211             ->expectsQuestion('Are you sure you want to refresh avatars of users that do not have one?', false)
212             ->assertExitCode(Command::SUCCESS);
213
214         $userWithAvatars = User::query()->where('image_id', '=', 0)->count();
215         $this->assertEquals($users->count(), $userWithAvatars);
216         $this->assertEquals(0, $requests->requestCount());
217     }
218
219     public function test_command_runs_all_users_dry_run()
220     {
221         $users = User::query()->where('image_id', '=', 0)->get();
222
223         $this->artisan(RefreshAvatarCommand::class, ['--all' => true])
224             ->expectsOutput(count($users) . ' user(s) found.')
225             ->expectsOutput("ID {$users[0]->id} - ")
226             ->expectsOutput('Not updated')
227             ->expectsOutput('Dry run, no avatars have been updated')
228             ->assertExitCode(Command::SUCCESS);
229     }
230
231     public function test_command_runs_update_all_users_avatar()
232     {
233         config()->set(['services.disable_services' => false]);
234
235         /** @var Collection|User[] $users */
236         $users = User::query()->get();
237
238         $pendingCommand = $this->artisan(RefreshAvatarCommand::class, ['--all' => true, '-f' => true]);
239         $pendingCommand
240             ->expectsOutput($users->count() . ' user(s) found.')
241             ->expectsQuestion('Are you sure you want to refresh avatars for ALL USERS?', true);
242
243         $responses = [];
244         foreach ($users as $user) {
245             $pendingCommand->expectsOutput("ID {$user->id} - ");
246             $pendingCommand->expectsOutput('Updated');
247             $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());
248         }
249         $requests = $this->mockHttpClient($responses);
250
251         $pendingCommand->assertExitCode(Command::SUCCESS);
252         $pendingCommand->run();
253
254         $userWithAvatars = User::query()->where('image_id', '!=', 0)->count();
255         $this->assertEquals($users->count(), $userWithAvatars);
256         $this->assertEquals($users->count(), $requests->requestCount());
257     }
258
259     public function test_command_runs_update_all_users_avatar_errors()
260     {
261         config()->set(['services.disable_services' => false]);
262
263         /** @var Collection|User[] $users */
264         $users = User::query()->get();
265
266         $pendingCommand = $this->artisan(RefreshAvatarCommand::class, ['--all' => true, '-f' => true]);
267         $pendingCommand
268             ->expectsOutput($users->count() . ' user(s) found.')
269             ->expectsQuestion('Are you sure you want to refresh avatars for ALL USERS?', true);
270
271         $responses = [];
272         foreach ($users as $key => $user) {
273             $pendingCommand->expectsOutput("ID {$user->id} - ");
274
275             if ($key == 1) {
276                 $pendingCommand->expectsOutput('Not updated');
277                 $responses[] = new Response(404);
278                 continue;
279             }
280
281             $pendingCommand->expectsOutput('Updated');
282             $responses[] = new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData());
283         }
284
285         $requests = $this->mockHttpClient($responses);
286
287         $pendingCommand->assertExitCode(Command::FAILURE);
288         $pendingCommand->run();
289
290         $userWithAvatars = User::query()->where('image_id', '!=', 0)->count();
291         $this->assertEquals($users->count() - 1, $userWithAvatars);
292         $this->assertEquals($users->count(), $requests->requestCount());
293     }
294
295     public function test_saying_no_to_confirmation_update_all_users_avatar()
296     {
297         $requests = $this->mockHttpClient([new Response(200, ['Content-Type' => 'image/png'], $this->files->pngImageData())]);
298         config()->set(['services.disable_services' => false]);
299
300         /** @var UserAvatars $avatar */
301         $avatar = app()->make(UserAvatars::class);
302
303         /** @var Collection|User[] $users */
304         $users = User::query()->get();
305         foreach ($users as $user) {
306             $avatar->destroyAllForUser($user);
307         }
308
309         $this->artisan(RefreshAvatarCommand::class, ['--all' => true, '-f' => true])
310             ->expectsQuestion('Are you sure you want to refresh avatars for ALL USERS?', false)
311             ->assertExitCode(Command::SUCCESS);
312
313         $userWithAvatars = User::query()->where('image_id', '=', 0)->count();
314         $this->assertEquals($users->count(), $userWithAvatars);
315         $this->assertEquals(0, $requests->requestCount());
316     }
317 }