mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-25 14:26:59 +00:00
Users can upload and remove profile pictures on the profile page. Admins choose local or instance S3 storage in advanced settings. Avatars are compressed to JPEG and served via a private cached route.
132 lines
5.4 KiB
PHP
132 lines
5.4 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Profile\Index;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\User;
|
|
use App\Services\AvatarStorageService;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::create([
|
|
'id' => 0,
|
|
'avatar_storage_type' => 'local',
|
|
]));
|
|
});
|
|
|
|
it('compresses and stores an uploaded profile picture on the configured local storage', function () {
|
|
Storage::fake('local');
|
|
$user = User::factory()->create(['name' => 'Test User']);
|
|
$this->actingAs($user);
|
|
|
|
Livewire::test(Index::class)
|
|
->set('avatar', UploadedFile::fake()->image('profile.png', 1200, 900))
|
|
->call('uploadAvatar')
|
|
->assertHasNoErrors();
|
|
|
|
$user->refresh();
|
|
|
|
expect($user->avatar_path)
|
|
->toBe("avatars/{$user->id}/avatar.jpg")
|
|
->and($user->avatar_storage_type)->toBe('local')
|
|
->and($user->avatar_s3_storage_id)->toBeNull();
|
|
|
|
Storage::disk('local')->assertExists($user->avatar_path);
|
|
|
|
$image = getimagesizefromstring(Storage::disk('local')->get($user->avatar_path));
|
|
|
|
expect($image[0])->toBeLessThanOrEqual(256)
|
|
->and($image[1])->toBeLessThanOrEqual(256)
|
|
->and($image['mime'])->toBe('image/jpeg')
|
|
->and(Storage::disk('local')->size($user->avatar_path))->toBeLessThan(100_000);
|
|
});
|
|
|
|
it('stores an already compressed browser JPEG without server image extensions', function () {
|
|
Storage::fake('local');
|
|
$user = User::factory()->create(['name' => 'Test User']);
|
|
$contents = base64_decode('/9j/4AAQSkZJRgABAQEAYABgAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gODAK/9sAQwAGBAUGBQQGBgUGBwcGCAoQCgoJCQoUDg8MEBcUGBgXFBYWGh0lHxobIxwWFiAsICMmJykqKRkfLTAtKDAlKCko/9sAQwEHBwcKCAoTCgoTKBoWGigoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo/8AAEQgAAgACAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A+qaKKKAP/9k=');
|
|
$path = tempnam(sys_get_temp_dir(), 'avatar');
|
|
file_put_contents($path, $contents);
|
|
$upload = new UploadedFile($path, 'avatar.jpg', 'image/jpeg', null, true);
|
|
|
|
app(AvatarStorageService::class)->store($user, $upload);
|
|
|
|
expect(Storage::disk('local')->get("avatars/{$user->id}/avatar.jpg"))->toBe($contents);
|
|
});
|
|
|
|
it('serves the authenticated users profile picture', function () {
|
|
Storage::fake('local');
|
|
$user = User::factory()->create([
|
|
'name' => 'Test User',
|
|
'avatar_path' => 'avatars/1/avatar.jpg',
|
|
'avatar_storage_type' => 'local',
|
|
]);
|
|
Storage::disk('local')->put($user->avatar_path, 'avatar-content');
|
|
|
|
$this->withoutMiddleware()->actingAs($user)
|
|
->get(route('profile.avatar'))
|
|
->assertSuccessful()
|
|
->assertHeader('content-type', 'image/jpeg');
|
|
});
|
|
|
|
it('removes the current profile picture', function () {
|
|
Storage::fake('local');
|
|
$user = User::factory()->create([
|
|
'name' => 'Test User',
|
|
'avatar_path' => 'avatars/1/avatar.jpg',
|
|
'avatar_storage_type' => 'local',
|
|
]);
|
|
Storage::disk('local')->put($user->avatar_path, 'avatar-content');
|
|
$this->actingAs($user);
|
|
|
|
Livewire::test(Index::class)
|
|
->call('removeAvatar')
|
|
->assertHasNoErrors();
|
|
|
|
expect($user->refresh()->avatar_path)->toBeNull();
|
|
Storage::disk('local')->assertMissing('avatars/1/avatar.jpg');
|
|
});
|
|
|
|
it('falls back cleanly when the avatars S3 storage no longer exists', function () {
|
|
$user = User::factory()->create([
|
|
'name' => 'Test User',
|
|
'avatar_path' => 'avatars/1/avatar.webp',
|
|
'avatar_storage_type' => 's3',
|
|
'avatar_s3_storage_id' => null,
|
|
]);
|
|
|
|
$this->withoutMiddleware()->actingAs($user)
|
|
->get(route('profile.avatar'))
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('renders the profile upload and user menu avatar', function () {
|
|
$profile = file_get_contents(resource_path('views/livewire/profile/index.blade.php'));
|
|
$menu = file_get_contents(resource_path('views/components/top-user-menu.blade.php'));
|
|
|
|
expect($profile)
|
|
->toContain("this.\$wire.upload('avatar', compressed")
|
|
->toContain('canvas.toBlob')
|
|
->toContain('wire:click="uploadAvatar"')
|
|
->and($menu)
|
|
->toContain("route('profile.avatar',");
|
|
});
|
|
|
|
it('offers runtime local or existing S3 profile picture storage', function () {
|
|
$component = file_get_contents(app_path('Livewire/Settings/Advanced.php'));
|
|
$view = file_get_contents(resource_path('views/livewire/settings/advanced.blade.php'));
|
|
|
|
expect($component)
|
|
->toContain("['value' => 'local', 'label' => 'Local storage']")
|
|
->toContain("'value' => 's3:'.\$storage->id")
|
|
->toContain('->whereTeamId(0)')
|
|
->toContain("->where('is_usable', true)")
|
|
->and($view)
|
|
->toContain('id="avatar_storage"')
|
|
->toContain('Use S3 for multi-instance or cloud deployments');
|
|
});
|