diff --git a/app/Services/AiMealPostGenerator.php b/app/Services/AiMealPostGenerator.php index b2e8ab3..bacf7fc 100644 --- a/app/Services/AiMealPostGenerator.php +++ b/app/Services/AiMealPostGenerator.php @@ -116,7 +116,7 @@ class AiMealPostGenerator ->timeout($this->imageTimeout()) ->generate(); - $path = $response->storePublicly($this->imageStoragePath(), ); + $path = $response->store($this->imageStoragePath()); if (! is_string($path)) { throw new RuntimeException("L'image du repas IA n'a pas pu etre stockee."); diff --git a/tests/Feature/GenerateAiMealPostTest.php b/tests/Feature/GenerateAiMealPostTest.php index 1b383c3..1233165 100644 --- a/tests/Feature/GenerateAiMealPostTest.php +++ b/tests/Feature/GenerateAiMealPostTest.php @@ -8,10 +8,14 @@ use App\Enums\MealPostType; use App\Enums\MealPostVisibility; use App\Models\AiUsage; use App\Models\MealPosts; +use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Exceptions; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use Laravel\Ai\Image; +use League\Flysystem\Filesystem as Flysystem; +use League\Flysystem\Local\LocalFilesystemAdapter; uses(RefreshDatabase::class); @@ -160,6 +164,71 @@ it('generates an ai meal post with an image', function () { ]); }); +it('stores an ai meal image on s3 without requiring public ACL support', function () { + $root = storage_path('framework/testing/disks/acl-disabled-s3'); + + File::deleteDirectory($root); + File::ensureDirectoryExists($root); + + Storage::extend('acl-disabled-s3', function ($app, array $config): FilesystemAdapter { + $adapter = new LocalFilesystemAdapter($config['root']); + $driver = new Flysystem($adapter, $config); + + return new class($driver, $adapter, $config) extends FilesystemAdapter + { + public function put($path, $contents, $options = []): string|bool + { + $normalizedOptions = is_string($options) + ? ['visibility' => $options] + : (array) $options; + + if (($normalizedOptions['visibility'] ?? null) === 'public') { + return false; + } + + return parent::put($path, $contents, $options); + } + }; + }); + + config()->set('filesystems.default', 'acl-disabled-s3'); + config()->set('filesystems.disks.acl-disabled-s3', [ + 'driver' => 'acl-disabled-s3', + 'root' => $root, + 'throw' => false, + ]); + config()->set('meal_posts.ai_generation.user_email', 'chef-ia@example.test'); + config()->set('meal_posts.ai_generation.image_path', 'meal-posts/ai-generated'); + + MealMaker::fake([ + [ + 'title' => 'Bowl sans ACL publique', + 'caption' => 'Un bowl stocke sur S3 sans ACL objet.', + 'type' => MealPostType::LUNCH->value, + 'calories' => 640, + 'proteins' => 42.4, + 'carbs' => 62.2, + 'fats' => 24.8, + 'ingredients' => [], + 'image_prompt' => 'Photorealistic bowl stored without public ACL', + ], + ]); + + Image::fake([ + base64_encode('fake-image-content'), + ]); + + $this->artisan('meal-posts:generate-ai') + ->assertExitCode(0); + + $mealPost = MealPosts::query() + ->where('ai_generated', true) + ->sole(); + + expect($mealPost->image_url)->toStartWith('meal-posts/ai-generated/'); + Storage::disk('acl-disabled-s3')->assertExists($mealPost->image_url); +}); + it('records a failed ai meal post generation attempt', function () { config()->set('filesystems.default', 's3');