diff --git a/app/Enums/MealPostVisibility.php b/app/Enums/MealPostVisibility.php new file mode 100644 index 0000000..4fd9953 --- /dev/null +++ b/app/Enums/MealPostVisibility.php @@ -0,0 +1,10 @@ +hasFile('image')) { $path = $request->file('image')->store('meal-posts', 'public'); - $data['image_url'] = asset(Storage::url($path)); + $data['image_url'] = $path; } unset($data['image']); diff --git a/app/Http/Requests/MealPostsRequest.php b/app/Http/Requests/MealPostsRequest.php index aa9defc..4d5aecc 100644 --- a/app/Http/Requests/MealPostsRequest.php +++ b/app/Http/Requests/MealPostsRequest.php @@ -2,22 +2,26 @@ namespace App\Http\Requests; +use App\Enums\MealPostVisibility; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class MealPostsRequest extends FormRequest { public function rules(): array { + $isCreating = $this->isMethod('post'); + return [ 'image' => ['sometimes', 'nullable', 'image', 'max:4096'], - 'image_url' => ['required_without:image', 'nullable', 'string', 'url', 'max:2048'], 'caption' => ['nullable', 'string', 'max:1000'], 'calories' => ['nullable', 'integer', 'min:0'], 'proteins' => ['nullable', 'numeric', 'min:0'], 'carbs' => ['nullable', 'numeric', 'min:0'], 'fats' => ['nullable', 'numeric', 'min:0'], - 'title' => ['required', 'string', 'max:255'], - 'eaten_at' => ['required', 'date'], + 'title' => [$isCreating ? 'required' : 'sometimes', 'string', 'max:255'], + 'eaten_at' => [$isCreating ? 'required' : 'sometimes', 'date'], + 'visibility' => ['sometimes', Rule::enum(MealPostVisibility::class)], ]; } @@ -31,6 +35,7 @@ class MealPostsRequest extends FormRequest return [ 'image.max' => "L'image ne doit pas dépasser 4 Mo.", 'image_url.required_without' => 'Ajoute une image au repas.', + 'visibility' => 'Choisis une visibilité valide.', ]; } } diff --git a/app/Http/Resources/MealPostsResource.php b/app/Http/Resources/MealPostsResource.php index a83a33c..ecf1b19 100644 --- a/app/Http/Resources/MealPostsResource.php +++ b/app/Http/Resources/MealPostsResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use App\Enums\MealPostVisibility; use App\Models\MealPosts; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; @@ -16,13 +17,14 @@ class MealPostsResource extends JsonResource return [ 'id' => $this->id, - 'imageUrl' => $this->image_url, + 'imageUrl' => $this->image_url ? asset(Storage::url($this->image_url)) : null, 'caption' => $this->caption, 'calories' => $this->calories, 'proteins' => $this->proteins, 'carbs' => $this->carbs, 'fats' => $this->fats, 'title' => $this->title, + 'visibility' => $this->visibility, 'eatenAt' => $this->eaten_at, 'createdAt' => $this->created_at, 'updatedAt' => $this->updated_at, diff --git a/app/Models/MealPosts.php b/app/Models/MealPosts.php index 935fb83..c5769c2 100644 --- a/app/Models/MealPosts.php +++ b/app/Models/MealPosts.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\MealPostVisibility; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -22,6 +23,11 @@ class MealPosts extends Model 'fats', 'title', 'eaten_at', + 'visibility', + ]; + + protected $attributes = [ + 'visibility' => MealPostVisibility::Private->value, ]; public function user(): BelongsTo @@ -33,6 +39,7 @@ class MealPosts extends Model { return [ 'eaten_at' => 'datetime', + 'visibility' => MealPostVisibility::class, ]; } } diff --git a/database/factories/MealPostsFactory.php b/database/factories/MealPostsFactory.php index 8ec55c6..94082b3 100644 --- a/database/factories/MealPostsFactory.php +++ b/database/factories/MealPostsFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Enums\MealPostVisibility; use App\Models\MealPosts; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; @@ -22,6 +23,7 @@ class MealPostsFactory extends Factory 'fats' => $this->faker->randomFloat(), 'title' => $this->faker->word(), 'eaten_at' => Carbon::now(), + 'visibility' => MealPostVisibility::Private, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), diff --git a/database/migrations/2026_05_16_140000_add_visibility_to_meal_posts_table.php b/database/migrations/2026_05_16_140000_add_visibility_to_meal_posts_table.php new file mode 100644 index 0000000..b1bd7f3 --- /dev/null +++ b/database/migrations/2026_05_16_140000_add_visibility_to_meal_posts_table.php @@ -0,0 +1,26 @@ +string('visibility', 20) + ->default('private') + ->index(); + }); + } + + public function down(): void + { + Schema::table('meal_posts', function (Blueprint $table): void { + $table->dropIndex(['visibility']); + $table->dropColumn('visibility'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index f568bff..5304475 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,5 +24,7 @@ Route::middleware('auth:sanctum')->group(function (): void { Route::middleware('auth:sanctum')->group(function (): void { Route::get('meal-posts/stats', [MealPostController::class, 'stats']); - Route::apiResource('meal-posts', MealPostController::class); + Route::apiResource('meal-posts', MealPostController::class)->except(['update']); + Route::put('meal-posts/{mealPost}', [MealPostController::class, 'update'])->name('meal-posts.update'); + Route::patch('meal-posts/{mealPost}', [MealPostController::class, 'update'])->name('meal-posts.patch'); }); diff --git a/tests/Feature/MealPostVisibilityTest.php b/tests/Feature/MealPostVisibilityTest.php new file mode 100644 index 0000000..271d9c6 --- /dev/null +++ b/tests/Feature/MealPostVisibilityTest.php @@ -0,0 +1,85 @@ +create(); + + Sanctum::actingAs($user); + + $response = $this->postJson('/api/meal-posts', [ + 'image_url' => 'https://example.com/meal.jpg', + 'title' => 'Bowl saumon avocat', + 'eaten_at' => '2026-05-16T12:00:00.000Z', + 'visibility' => MealPostVisibility::Followers->value, + ]); + + $response + ->assertCreated() + ->assertJsonPath('data.visibility', MealPostVisibility::Followers->value); + + $this->assertDatabaseHas('meal_posts', [ + 'id' => $response->json('data.id'), + 'user_id' => $user->id, + 'visibility' => MealPostVisibility::Followers->value, + ]); +}); + +it('defaults meal post visibility to private', function () { + $user = User::factory()->create(); + + Sanctum::actingAs($user); + + $response = $this->postJson('/api/meal-posts', [ + 'image_url' => 'https://example.com/meal.jpg', + 'title' => 'Bowl saumon avocat', + 'eaten_at' => '2026-05-16T12:00:00.000Z', + ]); + + $response + ->assertCreated() + ->assertJsonPath('data.visibility', MealPostVisibility::Private->value); +}); + +it('validates meal post visibility', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = $this->postJson('/api/meal-posts', [ + 'image_url' => 'https://example.com/meal.jpg', + 'title' => 'Bowl saumon avocat', + 'eaten_at' => '2026-05-16T12:00:00.000Z', + 'visibility' => 'friends', + ]); + + $response + ->assertUnprocessable() + ->assertJsonValidationErrors(['visibility']); +}); + +it('patches meal post visibility without requiring creation fields', function () { + $user = User::factory()->create(); + $mealPost = MealPosts::factory()->for($user, 'user')->create([ + 'visibility' => MealPostVisibility::Private, + ]); + + Sanctum::actingAs($user); + + $response = $this->patchJson("/api/meal-posts/{$mealPost->id}", [ + 'visibility' => MealPostVisibility::Public->value, + ]); + + $response + ->assertOk() + ->assertJsonPath('data.visibility', MealPostVisibility::Public->value); + + $this->assertDatabaseHas('meal_posts', [ + 'id' => $mealPost->id, + 'visibility' => MealPostVisibility::Public->value, + ]); +});