$id, 'user_id' => $userId, 'type' => 'image', 'status' => 'draft', 'title' => null, 'slug' => null, 'category_id' => null, 'description' => null, 'tags' => null, 'license' => null, 'nsfw' => false, 'is_scanned' => false, 'has_tags' => false, 'preview_path' => null, 'published_at' => null, 'final_path' => null, 'expires_at' => null, 'created_at' => now(), 'updated_at' => now(), ]; DB::table('uploads')->insert(array_merge($defaults, $overrides)); return $id; } function attachMainUploadFileForQuota(string $uploadId, int $size, string $hash = 'hash-main'): void { DB::table('upload_files')->insert([ 'upload_id' => $uploadId, 'path' => "tmp/drafts/{$uploadId}/main/file.bin", 'type' => 'main', 'hash' => $hash, 'size' => $size, 'mime' => 'application/octet-stream', 'created_at' => now(), ]); } it('enforces draft count limit', function () { Storage::fake('local'); config(['uploads.draft_quota.max_drafts_per_user' => 1]); $user = User::factory()->create(); createUploadRowForQuota($user->id, ['status' => 'draft']); $main = UploadedFile::fake()->image('wallpaper.jpg', 600, 400); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $main, ]); $response->assertStatus(429) ->assertJsonPath('message', 'draft_limit') ->assertJsonPath('code', 'draft_limit'); }); it('enforces draft storage limit', function () { Storage::fake('local'); config([ 'uploads.draft_quota.max_drafts_per_user' => 20, 'uploads.draft_quota.max_draft_storage_mb_per_user' => 1, ]); $user = User::factory()->create(); $existingDraftId = createUploadRowForQuota($user->id, ['status' => 'draft']); attachMainUploadFileForQuota($existingDraftId, 400 * 1024, 'existing-hash'); $main = UploadedFile::fake()->create('large.jpg', 700, 'image/jpeg'); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $main, ]); $response->assertStatus(413) ->assertJsonPath('message', 'storage_limit') ->assertJsonPath('code', 'storage_limit'); }); it('blocks duplicate hash when policy is block', 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('dupe.jpg', 400, 400); $hash = hash_file('sha256', $main->getPathname()); Artwork::factory()->create([ 'user_id' => $owner->id, 'hash' => $hash, 'artwork_status' => 'published', 'published_at' => now()->subMinute(), 'is_public' => true, ]); $response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [ 'main' => $main, ]); $response->assertStatus(422) ->assertJsonPath('message', 'duplicate_upload') ->assertJsonPath('code', 'duplicate_upload'); }); it('allows duplicate hash and returns warning when policy is warn', function () { Storage::fake('local'); config([ 'uploads.draft_quota.max_drafts_per_user' => 20, 'uploads.draft_quota.duplicate_hash_policy' => 'warn', ]); $owner = User::factory()->create(); $uploader = User::factory()->create(); $main = UploadedFile::fake()->image('dupe-warn.jpg', 400, 400); $hash = hash_file('sha256', $main->getPathname()); Artwork::factory()->create([ 'user_id' => $owner->id, 'hash' => $hash, 'artwork_status' => 'published', 'published_at' => now()->subMinute(), 'is_public' => true, ]); $response = $this->actingAs($uploader)->postJson('/api/uploads/preload', [ 'main' => $main, ]); $response->assertOk() ->assertJsonStructure(['upload_id', 'status', 'expires_at', 'warnings']) ->assertJsonPath('warnings.0', 'duplicate_hash'); }); it('does not count published uploads as drafts', function () { Storage::fake('local'); config(['uploads.draft_quota.max_drafts_per_user' => 1]); $user = User::factory()->create(); createUploadRowForQuota($user->id, [ 'status' => 'published', 'published_at' => now()->subHour(), ]); $main = UploadedFile::fake()->image('new.jpg', 640, 480); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $main, ]); $response->assertOk()->assertJsonStructure([ 'upload_id', 'status', 'expires_at', ]); }); 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]); $user = User::factory()->create(); createUploadRowForQuota($user->id, ['status' => 'draft']); $main = UploadedFile::fake()->image('machine-code.jpg', 600, 400); $response = $this->actingAs($user)->postJson('/api/uploads/preload', [ 'main' => $main, ]); $response->assertStatus(429) ->assertJson([ 'message' => 'draft_limit', 'code' => 'draft_limit', ]); });