From 15fe8beecc93dfe96327146e26eb4e95fc33a749 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Tue, 26 May 2026 13:23:31 +0200 Subject: [PATCH] add photo delete logic to user update endpoint --- .../Controllers/Api/V1/UserController.php | 25 ++++--- .../Requests/V1/User/UserUpdateRequest.php | 9 ++- .../Unit/Endpoint/Api/V1/UserEndpointTest.php | 69 +++++++++++++++++++ 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Api/V1/UserController.php b/app/Http/Controllers/Api/V1/UserController.php index 2ddc0e3f..95371bed 100644 --- a/app/Http/Controllers/Api/V1/UserController.php +++ b/app/Http/Controllers/Api/V1/UserController.php @@ -49,18 +49,23 @@ class UserController extends Controller throw new AuthorizationException; } - if ($request->getPhoto() !== null) { - $photo = Base64File::decode($request->getPhoto()); - assert($photo !== null); - $extension = Base64File::extension($photo['mime_type']); - assert($extension !== null); - - $previousPhotoPath = $user->profile_photo_path; - $photoPath = 'profile-photos/'.Str::uuid().'.'.$extension; + if ($request->hasPhotoKey()) { $photoDisk = (string) config('jetstream.profile_photo_disk', 'public'); + $previousPhotoPath = $user->profile_photo_path; + $newPhoto = $request->getPhoto(); - Storage::disk($photoDisk)->put($photoPath, $photo['data'], 'public'); - $user->profile_photo_path = $photoPath; + if ($newPhoto === null) { + $user->profile_photo_path = null; + } else { + $decoded = Base64File::decode($newPhoto); + assert($decoded !== null); + $extension = Base64File::extension($decoded['mime_type']); + assert($extension !== null); + + $photoPath = 'profile-photos/'.Str::uuid().'.'.$extension; + Storage::disk($photoDisk)->put($photoPath, $decoded['data'], 'public'); + $user->profile_photo_path = $photoPath; + } if ($previousPhotoPath !== null) { Storage::disk($photoDisk)->delete($previousPhotoPath); diff --git a/app/Http/Requests/V1/User/UserUpdateRequest.php b/app/Http/Requests/V1/User/UserUpdateRequest.php index d9ef8b91..caff5609 100644 --- a/app/Http/Requests/V1/User/UserUpdateRequest.php +++ b/app/Http/Requests/V1/User/UserUpdateRequest.php @@ -81,8 +81,15 @@ class UserUpdateRequest extends BaseFormRequest return $this->has('week_start') ? Weekday::from($this->input('week_start')) : null; } + public function hasPhotoKey(): bool + { + return $this->has('photo'); + } + public function getPhoto(): ?string { - return $this->has('photo') ? (string) $this->input('photo') : null; + $value = $this->input('photo'); + + return is_string($value) ? $value : null; } } diff --git a/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php b/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php index b2bb6404..8d971708 100644 --- a/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/UserEndpointTest.php @@ -354,6 +354,75 @@ class UserEndpointTest extends ApiEndpointTestAbstract $response->assertJsonValidationErrors(['photo']); } + public function test_update_with_null_photo_deletes_photo_file_and_clears_profile_photo_path(): void + { + // Arrange + $data = $this->createUserWithPermission(); + $photoDisk = (string) config('jetstream.profile_photo_disk', 'public'); + $photoPath = 'profile-photos/existing.png'; + Storage::fake($photoDisk); + Storage::disk($photoDisk)->put($photoPath, 'photo contents'); + $data->user->profile_photo_path = $photoPath; + $data->user->save(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [ + 'photo' => null, + ]); + + // Assert + $response->assertSuccessful(); + $user = $data->user->fresh(); + $this->assertNull($user->profile_photo_path); + Storage::disk($photoDisk)->assertMissing($photoPath); + } + + public function test_update_with_null_photo_is_a_noop_when_user_has_no_photo(): void + { + // Arrange + $data = $this->createUserWithPermission(); + $photoDisk = (string) config('jetstream.profile_photo_disk', 'public'); + Storage::fake($photoDisk); + $data->user->profile_photo_path = null; + $data->user->save(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [ + 'photo' => null, + ]); + + // Assert + $response->assertSuccessful(); + $user = $data->user->fresh(); + $this->assertNull($user->profile_photo_path); + } + + public function test_update_without_photo_key_leaves_existing_photo_untouched(): void + { + // Arrange + $data = $this->createUserWithPermission(); + $photoDisk = (string) config('jetstream.profile_photo_disk', 'public'); + $photoPath = 'profile-photos/existing.png'; + Storage::fake($photoDisk); + Storage::disk($photoDisk)->put($photoPath, 'photo contents'); + $data->user->profile_photo_path = $photoPath; + $data->user->save(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.users.update', $data->user->getKey()), [ + 'name' => 'Just A Name Change', + ]); + + // Assert + $response->assertSuccessful(); + $user = $data->user->fresh(); + $this->assertSame($photoPath, $user->profile_photo_path); + Storage::disk($photoDisk)->assertExists($photoPath); + } + public function test_delete_fails_if_given_user_is_not_the_authenticated_user(): void { // Arrange