optimizations
This commit is contained in:
182
tests/Feature/Uploads/UploadFinishAiDispatchTest.php
Normal file
182
tests/Feature/Uploads/UploadFinishAiDispatchTest.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Jobs\AutoTagArtworkJob;
|
||||
use App\Jobs\GenerateArtworkEmbeddingJob;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use App\Services\Uploads\UploadSessionStatus;
|
||||
use App\Services\Uploads\UploadTokenService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('dispatches AI processing jobs after upload finish publishes successfully', function () {
|
||||
config()->set('forum_bot_protection.enabled', false);
|
||||
config()->set('uploads.queue_derivatives', false);
|
||||
config()->set('uploads.storage_root', storage_path('framework/testing/uploads'));
|
||||
|
||||
Queue::fake();
|
||||
File::deleteDirectory((string) config('uploads.storage_root'));
|
||||
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'is_public' => false,
|
||||
'is_approved' => false,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$sessionId = (string) Str::uuid();
|
||||
$tmpPath = storage_path('framework/testing/uploads/tmp/' . $sessionId . '.png');
|
||||
$sourceImage = base_path('public/favicon/favicon-96x96.png');
|
||||
File::ensureDirectoryExists(dirname($tmpPath));
|
||||
File::copy($sourceImage, $tmpPath);
|
||||
|
||||
app(UploadSessionRepository::class)->create(
|
||||
$sessionId,
|
||||
$user->id,
|
||||
$tmpPath,
|
||||
UploadSessionStatus::TMP,
|
||||
'127.0.0.1'
|
||||
);
|
||||
|
||||
$token = app(UploadTokenService::class)->generate($sessionId, $user->id);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withHeader('X-Upload-Token', $token)
|
||||
->postJson('/api/uploads/finish', [
|
||||
'session_id' => $sessionId,
|
||||
'artwork_id' => $artwork->id,
|
||||
'file_name' => 'example.png',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('artwork_id', $artwork->id);
|
||||
$response->assertJsonPath('status', UploadSessionStatus::PROCESSED);
|
||||
|
||||
Queue::assertPushed(AutoTagArtworkJob::class, 1);
|
||||
Queue::assertPushed(GenerateArtworkEmbeddingJob::class, 1);
|
||||
|
||||
$artwork->refresh();
|
||||
expect($artwork->hash)->not->toBe('')
|
||||
->and($artwork->thumb_ext)->toBe('webp')
|
||||
->and($artwork->width)->toBeGreaterThan(0)
|
||||
->and($artwork->height)->toBeGreaterThan(0);
|
||||
|
||||
expect(File::exists((string) $tmpPath))->toBeTrue();
|
||||
});
|
||||
|
||||
it('blocks upload finish only when the hash already belongs to a published artwork', function () {
|
||||
config()->set('forum_bot_protection.enabled', false);
|
||||
config()->set('uploads.queue_derivatives', false);
|
||||
config()->set('uploads.storage_root', storage_path('framework/testing/uploads'));
|
||||
|
||||
File::deleteDirectory((string) config('uploads.storage_root'));
|
||||
|
||||
$user = User::factory()->create();
|
||||
$publishedArtwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'hash' => hash_file('sha256', base_path('public/favicon/favicon-96x96.png')),
|
||||
'artwork_status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
'is_public' => true,
|
||||
]);
|
||||
|
||||
$draftArtwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'is_public' => false,
|
||||
'is_approved' => false,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$sessionId = (string) Str::uuid();
|
||||
$tmpPath = storage_path('framework/testing/uploads/tmp/' . $sessionId . '.png');
|
||||
$sourceImage = base_path('public/favicon/favicon-96x96.png');
|
||||
File::ensureDirectoryExists(dirname($tmpPath));
|
||||
File::copy($sourceImage, $tmpPath);
|
||||
|
||||
app(UploadSessionRepository::class)->create(
|
||||
$sessionId,
|
||||
$user->id,
|
||||
$tmpPath,
|
||||
UploadSessionStatus::TMP,
|
||||
'127.0.0.1'
|
||||
);
|
||||
|
||||
$token = app(UploadTokenService::class)->generate($sessionId, $user->id);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withHeader('X-Upload-Token', $token)
|
||||
->postJson('/api/uploads/finish', [
|
||||
'session_id' => $sessionId,
|
||||
'artwork_id' => $draftArtwork->id,
|
||||
'file_name' => 'example.png',
|
||||
]);
|
||||
|
||||
$response->assertStatus(409)
|
||||
->assertJsonPath('reason', 'duplicate_hash');
|
||||
|
||||
expect($publishedArtwork->hash)->not->toBe('');
|
||||
});
|
||||
|
||||
it('allows upload finish when the same hash exists only on an unpublished artwork', function () {
|
||||
config()->set('forum_bot_protection.enabled', false);
|
||||
config()->set('uploads.queue_derivatives', false);
|
||||
config()->set('uploads.storage_root', storage_path('framework/testing/uploads'));
|
||||
|
||||
Queue::fake();
|
||||
File::deleteDirectory((string) config('uploads.storage_root'));
|
||||
|
||||
$user = User::factory()->create();
|
||||
$hash = hash_file('sha256', base_path('public/favicon/favicon-96x96.png'));
|
||||
|
||||
Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'hash' => $hash,
|
||||
'artwork_status' => 'draft',
|
||||
'published_at' => null,
|
||||
'is_public' => false,
|
||||
'is_approved' => false,
|
||||
]);
|
||||
|
||||
$draftArtwork = Artwork::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'is_public' => false,
|
||||
'is_approved' => false,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
||||
$sessionId = (string) Str::uuid();
|
||||
$tmpPath = storage_path('framework/testing/uploads/tmp/' . $sessionId . '.png');
|
||||
$sourceImage = base_path('public/favicon/favicon-96x96.png');
|
||||
File::ensureDirectoryExists(dirname($tmpPath));
|
||||
File::copy($sourceImage, $tmpPath);
|
||||
|
||||
app(UploadSessionRepository::class)->create(
|
||||
$sessionId,
|
||||
$user->id,
|
||||
$tmpPath,
|
||||
UploadSessionStatus::TMP,
|
||||
'127.0.0.1'
|
||||
);
|
||||
|
||||
$token = app(UploadTokenService::class)->generate($sessionId, $user->id);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withHeader('X-Upload-Token', $token)
|
||||
->postJson('/api/uploads/finish', [
|
||||
'session_id' => $sessionId,
|
||||
'artwork_id' => $draftArtwork->id,
|
||||
'file_name' => 'example.png',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonPath('artwork_id', $draftArtwork->id)
|
||||
->assertJsonPath('status', UploadSessionStatus::PROCESSED);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
@@ -107,13 +108,14 @@ it('blocks duplicate hash when policy is block', function () {
|
||||
$main = UploadedFile::fake()->image('dupe.jpg', 400, 400);
|
||||
$hash = hash_file('sha256', $main->getPathname());
|
||||
|
||||
$publishedUploadId = createUploadRowForQuota($owner->id, [
|
||||
'status' => 'published',
|
||||
Artwork::factory()->create([
|
||||
'user_id' => $owner->id,
|
||||
'hash' => $hash,
|
||||
'artwork_status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
'is_public' => true,
|
||||
]);
|
||||
|
||||
attachMainUploadFileForQuota($publishedUploadId, (int) $main->getSize(), $hash);
|
||||
|
||||
$response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [
|
||||
'main' => $main,
|
||||
]);
|
||||
@@ -136,13 +138,14 @@ it('allows duplicate hash and returns warning when policy is warn', function ()
|
||||
$main = UploadedFile::fake()->image('dupe-warn.jpg', 400, 400);
|
||||
$hash = hash_file('sha256', $main->getPathname());
|
||||
|
||||
$publishedUploadId = createUploadRowForQuota($owner->id, [
|
||||
'status' => 'published',
|
||||
Artwork::factory()->create([
|
||||
'user_id' => $owner->id,
|
||||
'hash' => $hash,
|
||||
'artwork_status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
'is_public' => true,
|
||||
]);
|
||||
|
||||
attachMainUploadFileForQuota($publishedUploadId, (int) $main->getSize(), $hash);
|
||||
|
||||
$response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [
|
||||
'main' => $main,
|
||||
]);
|
||||
@@ -175,6 +178,37 @@ it('does not count published uploads as drafts', function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores duplicate hashes that exist only in temporary upload tables', function () {
|
||||
Storage::fake('local');
|
||||
config([
|
||||
'uploads.draft_quota.max_drafts_per_user' => 20,
|
||||
'uploads.draft_quota.duplicate_hash_policy' => 'block',
|
||||
]);
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$uploader = User::factory()->create();
|
||||
|
||||
$main = UploadedFile::fake()->image('temp-only-dupe.jpg', 400, 400);
|
||||
$hash = hash_file('sha256', $main->getPathname());
|
||||
|
||||
$publishedUploadId = createUploadRowForQuota($owner->id, [
|
||||
'status' => 'published',
|
||||
'published_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
attachMainUploadFileForQuota($publishedUploadId, (int) $main->getSize(), $hash);
|
||||
|
||||
$response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [
|
||||
'main' => $main,
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertJsonStructure([
|
||||
'upload_id',
|
||||
'status',
|
||||
'expires_at',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns stable machine codes for quota errors', function () {
|
||||
Storage::fake('local');
|
||||
config(['uploads.draft_quota.max_drafts_per_user' => 1]);
|
||||
|
||||
Reference in New Issue
Block a user