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); });