diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 3cdd9f0d7..715cf18ec 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Auth; -use App\Jobs\DownloadUserAvatar; +use App\Jobs\UpdateUserAvatar; use App\Models\User; use App\Rules\Recaptcha; use App\Rules\Username; @@ -52,7 +52,7 @@ public function store(Request $request): RedirectResponse Auth::login($user); - dispatch(new DownloadUserAvatar($user)); + dispatch(new UpdateUserAvatar($user)); return redirect(route('profile.show', [ 'username' => $user->username, diff --git a/app/Http/Controllers/Profile/AvatarController.php b/app/Http/Controllers/Profile/AvatarController.php new file mode 100644 index 000000000..c9590df4c --- /dev/null +++ b/app/Http/Controllers/Profile/AvatarController.php @@ -0,0 +1,42 @@ +user())->as(User::class); + + $file = type($request->file('avatar'))->as(UploadedFile::class); + UpdateUserAvatar::dispatchSync($user, $file->getRealPath()); + + return to_route('profile.edit') + ->with('flash-message', 'Avatar updated.'); + } + + /** + * Delete the existing avatar. + */ + public function delete(Request $request): RedirectResponse + { + $user = type($request->user())->as(User::class); + + UpdateUserAvatar::dispatchSync($user); + + return to_route('profile.edit') + ->with('flash-message', 'Avatar deleted.'); + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index b43a05b64..234efc935 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers; use App\Http\Requests\ProfileUpdateRequest; -use App\Jobs\DownloadUserAvatar; use App\Jobs\IncrementViews; +use App\Jobs\UpdateUserAvatar; use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -51,7 +51,9 @@ public function update(ProfileUpdateRequest $request): RedirectResponse $user->save(); - dispatch(new DownloadUserAvatar($user)); + if (! $user->is_uploaded_avatar) { + UpdateUserAvatar::dispatch($user); + } session()->flash('flash-message', 'Profile updated.'); diff --git a/app/Http/Requests/UpdateUserAvatarRequest.php b/app/Http/Requests/UpdateUserAvatarRequest.php new file mode 100644 index 000000000..5a833b37c --- /dev/null +++ b/app/Http/Requests/UpdateUserAvatarRequest.php @@ -0,0 +1,25 @@ +> + */ + public function rules(): array + { + return [ + 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png', 'max:2048'], + ]; + } +} diff --git a/app/Jobs/DownloadUserAvatar.php b/app/Jobs/DownloadUserAvatar.php deleted file mode 100644 index 748f1e479..000000000 --- a/app/Jobs/DownloadUserAvatar.php +++ /dev/null @@ -1,78 +0,0 @@ -fetchNewAvatar(); - - $this->deleteExistingAvatar(); - - $this->user->update([ - 'avatar' => 'storage/'.$avatar, - 'avatar_updated_at' => now(), - ]); - } - - /** - * Fetch a new avatar for the user. - */ - private function fetchNewAvatar(): string - { - /** @var array $urls */ - $urls = $this->user->links->pluck('url')->values()->all(); - - $avatar = new Avatar($this->user->email, $urls); - $contents = app()->environment('testing') ? '...' : (string) file_get_contents($avatar->url()); - - $avatar = 'avatars/'.hash('sha256', random_int(0, PHP_INT_MAX).'@'.$this->user->id).'.png'; - - Storage::disk('public')->put($avatar, $contents, 'public'); - - return $avatar; - } - - /** - * Delete the current avatar. - */ - private function deleteExistingAvatar(): void - { - $avatar = $this->user->avatar; - - if (! $avatar) { - return; - } - - if (! Storage::disk('public')->exists(str_replace('storage/', '', $avatar))) { - return; - } - - Storage::disk('public')->delete(str_replace('storage/', '', $avatar)); - } -} diff --git a/app/Jobs/IncrementViews.php b/app/Jobs/IncrementViews.php index c32c1a544..9b9da02f9 100644 --- a/app/Jobs/IncrementViews.php +++ b/app/Jobs/IncrementViews.php @@ -25,7 +25,7 @@ final class IncrementViews implements ShouldQueue * * @param Collection|Collection $viewables */ - public function __construct(protected Collection $viewables, protected int|string $id) + public function __construct(private Collection $viewables, private int|string $id) { // } diff --git a/app/Jobs/UpdateUserAvatar.php b/app/Jobs/UpdateUserAvatar.php new file mode 100644 index 000000000..643bd3008 --- /dev/null +++ b/app/Jobs/UpdateUserAvatar.php @@ -0,0 +1,99 @@ +user->avatar) { + if ($disk->exists(str_replace('storage/', '', $this->user->avatar))) { + $disk->delete(str_replace('storage/', '', $this->user->avatar)); + } + } + + $file = $this->file !== null ? $this->file : (new Avatar($this->user->email))->url(); + + $contents = (string) file_get_contents($file); + + $avatar = 'avatars/'.hash('sha256', random_int(0, PHP_INT_MAX).'@'.$this->user->id).'.png'; + + Storage::disk('public')->put($avatar, $contents, 'public'); + + $this->resizer()->read($disk->path($avatar)) + ->resize(200, 200) + ->save(); + + $this->user->update([ + 'avatar' => "storage/$avatar", + 'avatar_updated_at' => now(), + 'is_uploaded_avatar' => $this->file !== null, + ]); + + $this->ensureFileIsDeleted(); + } + + /** + * Handle a job failure. + */ + public function failed(?Throwable $exception): void + { + $this->ensureFileIsDeleted(); + + $this->user->update([ + 'avatar' => null, + 'avatar_updated_at' => null, + 'is_uploaded_avatar' => false, + ]); + } + + /** + * Ensure the file is deleted. + */ + private function ensureFileIsDeleted(): void + { + if ($this->file !== null) { + File::delete($this->file); + } + } + + /** + * Creates a new image resizer. + */ + private function resizer(): ImageManager + { + return new ImageManager( + new Drivers\Gd\Driver(), + ); + } +} diff --git a/app/Livewire/Links/Create.php b/app/Livewire/Links/Create.php index a9b9fa65b..38e8b5d4d 100644 --- a/app/Livewire/Links/Create.php +++ b/app/Livewire/Links/Create.php @@ -4,7 +4,7 @@ namespace App\Livewire\Links; -use App\Jobs\DownloadUserAvatar; +use App\Jobs\UpdateUserAvatar; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -53,7 +53,9 @@ public function store(Request $request): void $user->links()->create($validated); - dispatch(new DownloadUserAvatar($user)); + if (! $user->is_uploaded_avatar) { + dispatch(new UpdateUserAvatar($user)); + } $this->description = ''; $this->url = ''; diff --git a/app/Livewire/Links/Index.php b/app/Livewire/Links/Index.php index f09d17967..c6b00f12a 100644 --- a/app/Livewire/Links/Index.php +++ b/app/Livewire/Links/Index.php @@ -4,7 +4,7 @@ namespace App\Livewire\Links; -use App\Jobs\DownloadUserAvatar; +use App\Jobs\UpdateUserAvatar; use App\Models\Link; use App\Models\User; use Illuminate\Auth\Access\AuthorizationException; @@ -42,24 +42,6 @@ public function click(int $linkId): void Cache::put($cacheKey, true, now()->addDay()); } - /** - * Reset the user's avatar. - */ - public function resetAvatar(): void - { - $user = type(auth()->user())->as(User::class); - - if (! $this->canResetAvatar($user)) { - $this->dispatch('notification.created', message: 'You have to wait 24 hours before resetting the avatar again.'); - - return; - } - - dispatch_sync(new DownloadUserAvatar($user)); - - $this->dispatch('notification.created', message: 'Avatar reset.'); - } - /** * Store the new order of the links. * @@ -93,10 +75,12 @@ public function destroy(int $linkId): void $this->authorize('delete', $link); - dispatch(new DownloadUserAvatar($user)); - $link->delete(); + if (! $user->is_uploaded_avatar) { + dispatch(new UpdateUserAvatar($user)); + } + $this->dispatch('notification.created', message: 'Link deleted.'); } @@ -120,7 +104,6 @@ public function render(): View return view('livewire.links.index', [ 'user' => $user, - 'canResetAvatar' => $this->canResetAvatar($user), 'questionsReceivedCount' => $user->questionsReceived() ->where('is_reported', false) ->where('is_ignored', false) @@ -134,15 +117,4 @@ public function render(): View })->values(), ]); } - - /** - * Determine if the user can reset the avatar. - */ - private function canResetAvatar(User $user): bool - { - return auth()->id() === $this->userId && ( - $user->avatar_updated_at === null - || $user->avatar_updated_at->diffInHours(now()) > 24 - ); - } } diff --git a/app/Models/User.php b/app/Models/User.php index ec1e8279e..22d0f7c08 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -20,7 +20,7 @@ /** * @property bool $prefers_anonymous_questions - * @property string $avatar + * @property string|null $avatar * @property string $avatar_url * @property string|null $bio * @property Carbon $created_at @@ -42,6 +42,7 @@ * @property ?Carbon $avatar_updated_at * @property string $username * @property int $views + * @property bool $is_uploaded_avatar * @property-read Collection $links * @property-read Collection $questionsReceived * @property-read Collection $questionsSent @@ -111,6 +112,14 @@ public function pinnedQuestion(): HasOne ->where('pinned', true); } + /** + * Get the user's avatar URL attribute. + */ + public function getAvatarUrlAttribute(): string + { + return $this->avatar ? asset($this->avatar) : asset('img/default-avatar.png'); + } + /** * Get the user's links sort attribute. * @@ -153,20 +162,6 @@ public function getLeftColorAttribute(): string ->value(); } - /** - * Get the user's avatar URL attribute. - */ - public function getAvatarUrlAttribute(): string - { - /** @var array $urls */ - $urls = $this->links->pluck('url')->values()->all(); - - return (new Avatar( - email: $this->email, - links: $urls, - ))->url(); - } - /** * Get the user's shape attribute. */ @@ -252,6 +247,7 @@ protected function casts(): array 'avatar_updated_at' => 'datetime', 'mail_preference_time' => UserMailPreference::class, 'views' => 'integer', + 'is_uploaded_avatar' => 'boolean', ]; } } diff --git a/app/Providers/PulseServiceProvider.php b/app/Providers/PulseServiceProvider.php index 2cf5164c1..43d1c0b14 100644 --- a/app/Providers/PulseServiceProvider.php +++ b/app/Providers/PulseServiceProvider.php @@ -21,7 +21,7 @@ public function boot(): void Pulse::user(fn (User $user): array => [ 'name' => $user->name, 'extra' => $user->email, - 'avatar' => $user->avatar, + 'avatar' => $user->avatar_url, ]); } } diff --git a/app/Services/Avatar.php b/app/Services/Avatar.php index bfe99aa4a..6affbb833 100644 --- a/app/Services/Avatar.php +++ b/app/Services/Avatar.php @@ -4,20 +4,13 @@ namespace App\Services; -use App\Contracts\Services\AvatarProvider; -use App\Services\AvatarProviders\GitHub; -use App\Services\AvatarProviders\Twitter; - final readonly class Avatar { /** * Create a new avatar for the given name and email address. - * - * @param array $links */ public function __construct( private string $email, - private array $links, ) { // } @@ -27,41 +20,8 @@ public function __construct( */ public function url(): string { - $url = "https://unavatar.io/$this->email"; - - $providers = [ - Twitter::class, - GitHub::class, - ]; - - $fallbacks = collect(); - $gravatarHash = hash('sha256', mb_strtolower($this->email)); - foreach ($this->links as $link) { - foreach ($providers as $provider) { - $provider = type(new $provider())->as(AvatarProvider::class); - - if ($provider->applicable($link)) { - $fallback = $provider->getUrl($link); - - if ($provider instanceof Twitter) { - $fallbacks->add($url); - - $url = $fallback; - } else { - $fallbacks->add($fallback); - } - } - } - } - - $fallbacks->add("https://gravatar.com/avatar/$gravatarHash?s=300"); - - $fallbacks = $fallbacks->unique(); - - return $url.$fallbacks - ->map(fn (string $url): string => "?fallback=$url") - ->implode(''); + return "https://gravatar.com/avatar/$gravatarHash?s=300"; } } diff --git a/composer.json b/composer.json index 25027ceff..0cee47943 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "require": { "php": "^8.3", "another-library/type-guard": "dev-main", + "intervention/image": "^3.5.1", "laravel/framework": "^11.4.0", "laravel/pennant": "^1.7", "laravel/pulse": "^1.0@beta", diff --git a/composer.lock b/composer.lock index d1b3f578e..0f66bde97 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fe14c16e963c1c2cc82b2554b30a59f0", + "content-hash": "94165c31555d8c86a68452486c2187ca", "packages": [ { "name": "another-library/type-guard", @@ -1339,6 +1339,142 @@ ], "time": "2023-12-03T19:50:20+00:00" }, + { + "name": "intervention/gif", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3", + "reference": "3a2b5f8a8856e8877cdab5c47e51aab2d4cb23a3", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.1.0" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-03-26T17:23:47+00:00" + }, + { + "name": "intervention/image", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "67be90e5700370c88833190d4edc07e4bb7d157b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/67be90e5700370c88833190d4edc07e4bb7d157b", + "reference": "67be90e5700370c88833190d4edc07e4bb7d157b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.0.1", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^1", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "PHP image manipulation", + "homepage": "https://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.5.1" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2024-03-22T07:12:19+00:00" + }, { "name": "laravel/framework", "version": "v11.4.0", diff --git a/database/migrations/2024_04_18_124813_add_is_uploaded_avatar_column_to_users_table.php b/database/migrations/2024_04_18_124813_add_is_uploaded_avatar_column_to_users_table.php new file mode 100644 index 000000000..213f11319 --- /dev/null +++ b/database/migrations/2024_04_18_124813_add_is_uploaded_avatar_column_to_users_table.php @@ -0,0 +1,20 @@ +boolean('is_uploaded_avatar')->default(false)->avatar_updated_at(); + }); + } +}; diff --git a/public/img/default-avatar.png b/public/img/default-avatar.png new file mode 100644 index 000000000..9d1717445 Binary files /dev/null and b/public/img/default-avatar.png differ diff --git a/resources/views/components/icons/camera.blade.php b/resources/views/components/icons/camera.blade.php new file mode 100644 index 000000000..ec7dd8435 --- /dev/null +++ b/resources/views/components/icons/camera.blade.php @@ -0,0 +1,11 @@ + + + + diff --git a/resources/views/layouts/components/head.blade.php b/resources/views/layouts/components/head.blade.php index bf345b211..dca88e81e 100644 --- a/resources/views/layouts/components/head.blade.php +++ b/resources/views/layouts/components/head.blade.php @@ -34,7 +34,7 @@ - + @elseif (request()->routeIs('questions.show')) @php $question = request()->route('question'); @@ -52,7 +52,7 @@ /> - + @if ($answer) {{ $question->to->name }}: "{{ $answer }}" / Pinkary diff --git a/resources/views/livewire/home/users.blade.php b/resources/views/livewire/home/users.blade.php index acdaac5db..a187b92b9 100644 --- a/resources/views/livewire/home/users.blade.php +++ b/resources/views/livewire/home/users.blade.php @@ -40,7 +40,7 @@ class="group flex items-center gap-3 rounded-2xl border border-slate-900 bg-slat
{{ $user->username }}
diff --git a/resources/views/livewire/links/index.blade.php b/resources/views/livewire/links/index.blade.php index dd54ba734..77c7b0656 100644 --- a/resources/views/livewire/links/index.blade.php +++ b/resources/views/livewire/links/index.blade.php @@ -44,20 +44,17 @@ class="flex size-10 items-center justify-center rounded-lg bg-slate-900 text-sla
{{ $user->username }} - @if ($canResetAvatar) + @if (auth()->user()?->is($user)) @endif
diff --git a/resources/views/livewire/notifications/index.blade.php b/resources/views/livewire/notifications/index.blade.php index fe09fae1b..d601a2afc 100644 --- a/resources/views/livewire/notifications/index.blade.php +++ b/resources/views/livewire/notifications/index.blade.php @@ -16,7 +16,7 @@
{{ $question->to->username }} @@ -35,7 +35,7 @@ class="{{ $question->to->is_company_verified ? 'rounded-md' : 'rounded-full' }}
+
+
+ @include('profile.partials.upload-profile-photo-form') +
+
+
@include('profile.partials.update-profile-information-form') diff --git a/resources/views/profile/partials/upload-profile-photo-form.blade.php b/resources/views/profile/partials/upload-profile-photo-form.blade.php new file mode 100644 index 000000000..fb12574f9 --- /dev/null +++ b/resources/views/profile/partials/upload-profile-photo-form.blade.php @@ -0,0 +1,59 @@ +
+
+

+ {{ __('Profile Photo') }} +

+ +

+ {{ __("Upload a profile photo to personalize your account.") }} +

+
+
+
+
+ {{ auth()->user()->name }} +
+
+ @if (auth()->user()->is_uploaded_avatar) +
+

+ {{ __("If you delete your uploaded avatar, we will try to fetch your image based in your email, links, etc.") }} +

+
+ @endif +
+ +
+ @csrf + @method('patch') + +
+ + + +
+ +
+ {{ __('Upload') }} + + @if (auth()->user()->is_uploaded_avatar) +
+ @csrf + @method('delete') + + + {{ __('Delete Uploaded Avatar') }} + +
+ @endif +
+
diff --git a/routes/web.php b/routes/web.php index 3e85f1865..e6c53c2e4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,6 +4,7 @@ use App\Http\Controllers\ChangelogController; use App\Http\Controllers\NotificationController; +use App\Http\Controllers\Profile\AvatarController; use App\Http\Controllers\Profile\Connect\GitHubController; use App\Http\Controllers\Profile\TimezoneController; use App\Http\Controllers\Profile\VerifiedController; @@ -44,6 +45,11 @@ Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index'); Route::get('notifications/{notification}', [NotificationController::class, 'show']) ->name('notifications.show'); + + Route::patch('/profile/avatar', [AvatarController::class, 'update']) + ->name('profile.avatar.update'); + Route::delete('/profile/avatar', [AvatarController::class, 'delete']) + ->name('profile.avatar.delete'); }); Route::middleware('auth')->group(function () { diff --git a/tests/Http/Profile/EditTest.php b/tests/Http/Profile/EditTest.php index 425bbd404..7078122f2 100644 --- a/tests/Http/Profile/EditTest.php +++ b/tests/Http/Profile/EditTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Models\User; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; test('guest', function () { @@ -250,3 +251,50 @@ expect($user->refresh()->prefers_anonymous_questions)->toBeFalse(); }); + +test('user can upload an avatar', function () { + Storage::fake('public'); + + $user = User::factory()->create(); + + $this->actingAs($user) + ->patch('/profile/avatar', [ + 'avatar' => UploadedFile::fake()->image('avatar.jpg'), + ]) + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + $user->refresh(); + + expect($user->avatar)->toContain('avatars/') + ->and($user->avatar)->toContain('.png') + ->and($user->avatar)->toContain('storage/') + ->and($user->avatar_updated_at)->not()->toBeNull() + ->and($user->is_uploaded_avatar)->toBeTrue() + ->and(session('flash-message'))->toBe('Avatar updated.'); +}); + +test('user can delete custom avatar', function () { + Storage::fake('public'); + + $user = User::factory()->create([ + 'avatar' => 'storage/avatars/avatar.jpg', + 'is_uploaded_avatar' => true, + ]); + + Storage::disk('public')->put('avatars/avatar.jpg', '...'); + + $this->actingAs($user) + ->delete('/profile/avatar') + ->assertSessionHasNoErrors() + ->assertRedirect('/profile'); + + Storage::disk('public')->assertMissing('avatars/avatar.jpg'); + + $user->refresh(); + + expect($user->avatar)->not->toBeNull() + ->and($user->avatar_updated_at)->not->toBeNull() + ->and($user->is_uploaded_avatar)->toBeFalse() + ->and(session('flash-message'))->toBe('Avatar deleted.'); +}); diff --git a/tests/Unit/Jobs/DownloadUserAvatarTest.php b/tests/Unit/Jobs/DownloadUserAvatarTest.php deleted file mode 100644 index 4109eb6a8..000000000 --- a/tests/Unit/Jobs/DownloadUserAvatarTest.php +++ /dev/null @@ -1,58 +0,0 @@ -assertDirectoryEmpty('avatars'); - - $user = User::factory()->create(); - - $job = new DownloadUserAvatar($user); - - $job->handle(); - - expect($user->avatar)->toBeString(); - expect($user->avatar_updated_at)->not->toBeNull(); - Storage::disk('public')->assertExists(str_replace('storage/', '', $user->avatar)); -}); - -test('ignores deleting avatar file if no longer exists', function () { - $user = User::factory()->create([ - 'avatar' => 'storage/avatars/default.png', - ]); - - $job = new DownloadUserAvatar($user); - - $job->handle(); - - expect($user->avatar)->toBeString(); - Storage::disk('public')->assertExists(str_replace('storage/', '', $user->avatar)); - Storage::disk('public')->assertMissing('avatars/default.png'); -}); - -test('deletes old avatar when downloading new one', function () { - Storage::disk('public')->assertDirectoryEmpty('avatars'); - $lastAvatarUpdated = now()->subDays(2); - $user = User::factory()->create([ - 'avatar' => 'storage/avatars/default.png', - 'avatar_updated_at' => $lastAvatarUpdated, - ]); - - Storage::disk('public')->put('avatars/default.png', '...'); - - Storage::disk('public')->assertExists('avatars/default.png'); - - $job = new DownloadUserAvatar($user); - - $job->handle(); - - expect($user->avatar)->toBeString(); - expect($user->avatar_updated_at)->not->toBe($lastAvatarUpdated); - - Storage::disk('public')->assertExists(str_replace('storage/', '', $user->avatar)); - Storage::disk('public')->assertMissing('avatars/default.png'); -}); diff --git a/tests/Unit/Jobs/UpdateUserAvatarTest.php b/tests/Unit/Jobs/UpdateUserAvatarTest.php new file mode 100644 index 000000000..8cde472e6 --- /dev/null +++ b/tests/Unit/Jobs/UpdateUserAvatarTest.php @@ -0,0 +1,53 @@ +create(); + $file = UploadedFile::fake()->image('avatar.jpg'); + + UpdateUserAvatar::dispatchSync($user, $file->getRealPath()); + + $user = $user->fresh(); + + expect($user->avatar)->toBeString(); + Storage::disk('public')->assertExists(str_replace('storage/', '', $user->avatar)); +}); + +it('stores a url base avatar', function () { + Storage::fake('public'); + + $user = User::factory()->create(); + + UpdateUserAvatar::dispatchSync($user); + + $user = $user->fresh(); + + expect($user->avatar)->toBeString(); + Storage::disk('public')->assertExists(str_replace('storage/', '', $user->avatar)); +}); + +it('deletes the given avatar file', function () { + Storage::fake('public'); + + $contents = file_get_contents(public_path('img/default-avatar.png')); + Storage::disk('public')->put('avatars/1.png', $contents, 'public'); + + $user = User::factory()->create(); + + UpdateUserAvatar::dispatchSync($user, Storage::disk('public')->path('avatars/1.png')); + + $user = $user->fresh(); + + expect($user->avatar)->toBeString(); + Storage::disk('public')->assertExists(str_replace('storage/', '', $user->avatar)); + + Storage::disk('public')->assertMissing('avatars/1.png'); +}); diff --git a/tests/Unit/Livewire/Links/IndexTest.php b/tests/Unit/Livewire/Links/IndexTest.php index 2aac0765c..c0ee90f59 100644 --- a/tests/Unit/Livewire/Links/IndexTest.php +++ b/tests/Unit/Livewire/Links/IndexTest.php @@ -166,62 +166,6 @@ expect($link->refresh()->click_count)->toBe(30); }); -test('reset avatar', function () { - - $user = User::factory()->create([ - 'avatar_updated_at' => now()->subDay(), - ]); - - $component = Livewire::actingAs($user)->test(Index::class, [ - 'userId' => $user->id, - ]); - - $component->call('resetAvatar'); - - $component->assertDispatched('notification.created', message: 'Avatar reset.'); -}); - -test('cannot reset avatar if user is not the owner', function () { - $user = User::factory()->create([ - 'avatar_updated_at' => null, - ]); - - $anotherUser = User::factory()->create([ - 'avatar_updated_at' => null, - ]); - - $component = Livewire::actingAs($anotherUser)->test(Index::class, [ - 'userId' => $user->id, - ]); - - $component->call('resetAvatar'); - - $component->assertDispatched('notification.created', message: 'You have to wait 24 hours before resetting the avatar again.'); - - $this->assertNull($user->avatar_updated_at); - - $this->assertNull($anotherUser->avatar_updated_at); -}); - -test('cannot reset avatar if user avatar updated recently', function () { - - $avatarLastUpdated = now()->subHour()->format('Y-m-d H:i:s'); - - $user = User::factory()->create([ - 'avatar_updated_at' => $avatarLastUpdated, - ]); - - $component = Livewire::actingAs($user)->test(Index::class, [ - 'userId' => $user->id, - ]); - - $component->call('resetAvatar'); - - $component->assertDispatched('notification.created', message: 'You have to wait 24 hours before resetting the avatar again.'); - - $this->assertEquals($avatarLastUpdated, $user->avatar_updated_at->format('Y-m-d H:i:s')); -}); - test('count to be abbreviated', function () { $user = User::factory() diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index 287d6d07b..2cb9ba237 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -26,6 +26,7 @@ 'is_company_verified', 'avatar_updated_at', 'views', + 'is_uploaded_avatar', ]); }); @@ -81,3 +82,19 @@ expect($user->fresh()->views)->toBe(1); }); + +test('default avatar url', function () { + $user = User::factory()->create(); + + expect($user->avatar)->toBeNull() + ->and($user->avatar_url)->toBe(asset('img/default-avatar.png')); +}); + +test('custom avatar url', function () { + $user = User::factory()->create([ + 'avatar' => 'storage/avatars/123.png', + ]); + + expect($user->avatar)->toBe('storage/avatars/123.png') + ->and($user->avatar_url)->toBe(asset('storage/avatars/123.png')); +}); diff --git a/tests/Unit/Services/AvatarTest.php b/tests/Unit/Services/AvatarTest.php index 9abc66bca..feae2fc87 100644 --- a/tests/Unit/Services/AvatarTest.php +++ b/tests/Unit/Services/AvatarTest.php @@ -4,48 +4,10 @@ use App\Services\Avatar; -test('avatar url without links', function () { - $avatar = new Avatar( - email: 'enunomaduro@gmail.com', - links: [], - ); - - expect($avatar->url())->toBe('https://unavatar.io/enunomaduro@gmail.com?fallback=https://gravatar.com/avatar/86cfef5c1f5195df1a9db17a5f8ecb34455e1f0133a725de9acf7f2fb26ac6a1?s=300'); -}); - test('avatar url', function () { $avatar = new Avatar( email: 'enunomaduro@gmail.com', - links: [ - 'https://twitter.com/@enunomaduro', - ], - ); - - expect($avatar->url())->toBe('https://unavatar.io/twitter/enunomaduro?fallback=https://unavatar.io/enunomaduro@gmail.com?fallback=https://gravatar.com/avatar/86cfef5c1f5195df1a9db17a5f8ecb34455e1f0133a725de9acf7f2fb26ac6a1?s=300'); -}); - -test('avatar url with multiple links', function () { - $avatar = new Avatar( - email: 'taylor@laravel.com', - links: [ - 'https://twitter.com/taylorotwell', - 'https://x.com/@laravelphp', - 'https://github.com/taylorotwell', - ], - ); - - expect($avatar->url())->toBe('https://unavatar.io/twitter/laravelphp?fallback=https://unavatar.io/taylor@laravel.com?fallback=https://unavatar.io/twitter/taylorotwell?fallback=https://unavatar.io/github/taylorotwell?fallback=https://gravatar.com/avatar/75cc0771635c9940dd8bf7f884ee259b7502eb4a5797b5265d830e530b05ae7d?s=300'); -}); - -test('avatar url with multiple links with _', function () { - $avatar = new Avatar( - email: 'taylor@laravel.com', - links: [ - 'https://twitter.com/taylorotwell_', - 'https://x.com/@laravelphp_', - 'https://github.com/taylorotwell_', - ], ); - expect($avatar->url())->toBe('https://unavatar.io/twitter/laravelphp_?fallback=https://unavatar.io/taylor@laravel.com?fallback=https://unavatar.io/twitter/taylorotwell_?fallback=https://unavatar.io/github/taylorotwell_?fallback=https://gravatar.com/avatar/75cc0771635c9940dd8bf7f884ee259b7502eb4a5797b5265d830e530b05ae7d?s=300'); + expect($avatar->url())->toBe('https://gravatar.com/avatar/86cfef5c1f5195df1a9db17a5f8ecb34455e1f0133a725de9acf7f2fb26ac6a1?s=300'); });