Files
daily-meal-api/tests/Feature/ContentModerationTest.php
leonm 8b60283eeb
CI / 🧪 Tests Laravel (push) Successful in 2m34s
CI / 🐳 Build & Push Images (push) Successful in 2m26s
feat: translation codes
2026-08-14 12:46:29 +02:00

138 lines
4.7 KiB
PHP

<?php
use App\Enums\MealPostVisibility;
use App\Models\MealPosts;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\Sanctum;
uses(RefreshDatabase::class);
beforeEach(function () {
config()->set('moderation.content.enabled', true);
config()->set('moderation.content.openai_key', 'test-key');
config()->set('moderation.content.openai_url', 'https://api.openai.com/v1');
});
it('rejects a public meal when text moderation flags it', function () {
Storage::fake();
Http::fake([
'https://api.openai.com/v1/moderations' => Http::response([
'results' => [['flagged' => true]],
]),
]);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->postJson('/api/meal-posts', [
'title' => 'Contenu interdit',
'eaten_at' => now()->toISOString(),
'image' => UploadedFile::fake()->image('meal.jpg'),
'type' => 'lunch',
'visibility' => MealPostVisibility::Public->value,
])
->assertUnprocessable()
->assertJsonValidationErrors('content');
$this->assertDatabaseCount('meal_posts', 0);
expect(Storage::allFiles())->toBeEmpty();
});
it('sends public meal text and image to multimodal moderation before storing it', function () {
Storage::fake();
Http::fake([
'https://api.openai.com/v1/moderations' => Http::response([
'results' => [['flagged' => false]],
]),
]);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->postJson('/api/meal-posts', [
'title' => 'Bowl printanier',
'caption' => 'Une assiette fraîche',
'eaten_at' => now()->toISOString(),
'image' => UploadedFile::fake()->image('meal.jpg'),
'type' => 'lunch',
'visibility' => MealPostVisibility::Public->value,
])->assertCreated();
Http::assertSent(function (Request $request): bool {
$input = $request->data()['input'] ?? [];
return $request->url() === 'https://api.openai.com/v1/moderations'
&& ($input[0]['type'] ?? null) === 'text'
&& str_contains((string) ($input[0]['text'] ?? ''), 'Bowl printanier')
&& ($input[1]['type'] ?? null) === 'image_url'
&& str_starts_with((string) ($input[1]['image_url']['url'] ?? ''), 'data:image/jpeg;base64,');
});
});
it('rejects an explicitly forbidden review even when remote moderation is disabled', function () {
config()->set('moderation.content.enabled', false);
$user = User::factory()->create();
$meal = MealPosts::factory()->create();
Sanctum::actingAs($user);
$this->postJson("/api/meal-posts/{$meal->id}/reviews", [
'rating' => 1,
'comment' => 'Kill yourself',
])
->assertUnprocessable()
->assertJsonValidationErrors('content');
$this->assertDatabaseCount('post_reviews', 0);
});
it('moderates existing content when a private meal becomes public', function () {
Http::fake([
'https://api.openai.com/v1/moderations' => Http::response([
'results' => [['flagged' => true]],
]),
]);
$user = User::factory()->create();
$meal = MealPosts::factory()->for($user, 'user')->create([
'title' => 'Ancien contenu privé',
'image_url' => 'https://example.com/private-meal.jpg',
'visibility' => MealPostVisibility::Private,
]);
Sanctum::actingAs($user);
$this->patchJson("/api/meal-posts/{$meal->id}", [
'visibility' => MealPostVisibility::Public->value,
])
->assertUnprocessable()
->assertJsonValidationErrors('content');
expect($meal->fresh()->visibility)->toBe(MealPostVisibility::Private);
Http::assertSent(function (Request $request): bool {
$input = $request->data()['input'] ?? [];
return str_contains((string) ($input[0]['text'] ?? ''), 'Ancien contenu privé')
&& ($input[1]['image_url']['url'] ?? null) === 'https://example.com/private-meal.jpg';
});
});
it('fails closed when production moderation cannot be reached', function () {
Http::fake([
'https://api.openai.com/v1/moderations' => Http::response([], 500),
]);
$user = User::factory()->create();
$meal = MealPosts::factory()->create();
Sanctum::actingAs($user);
$this->postJson("/api/meal-posts/{$meal->id}/reviews", [
'rating' => 5,
'comment' => 'Très bon repas.',
])
->assertServiceUnavailable()
->assertJsonPath('code', 'CONTENT_MODERATION_UNAVAILABLE');
$this->assertDatabaseCount('post_reviews', 0);
});