insertGetId([ 'name' => 'Photography', 'slug' => 'photography-' . Str::lower(Str::random(6)), 'description' => null, 'created_at' => now(), 'updated_at' => now(), ]); return DB::table('categories')->insertGetId([ 'content_type_id' => $contentTypeId, 'parent_id' => null, 'name' => 'Moderation', 'slug' => 'moderation-' . Str::lower(Str::random(6)), 'description' => null, 'image' => null, 'is_active' => true, 'sort_order' => 0, 'created_at' => now(), 'updated_at' => now(), ]); } function createModerationDraft(int $userId, int $categoryId, array $overrides = []): string { $uploadId = (string) Str::uuid(); DB::table('uploads')->insert(array_merge([ 'id' => $uploadId, 'user_id' => $userId, 'type' => 'image', 'status' => 'draft', 'processing_state' => 'ready', 'moderation_status' => 'pending', 'title' => 'Pending Moderation Upload', 'category_id' => $categoryId, 'is_scanned' => true, 'has_tags' => true, 'preview_path' => "tmp/drafts/{$uploadId}/preview.webp", 'created_at' => now(), 'updated_at' => now(), ], $overrides)); return $uploadId; } function addReadyMainFile(string $uploadId, string $hash = 'aabbccddeeff00112233'): void { Storage::disk('local')->put("tmp/drafts/{$uploadId}/main/main.jpg", 'jpg'); Storage::disk('local')->put("tmp/drafts/{$uploadId}/preview.webp", 'preview'); DB::table('upload_files')->insert([ 'upload_id' => $uploadId, 'path' => "tmp/drafts/{$uploadId}/main/main.jpg", 'type' => 'main', 'hash' => $hash, 'size' => 3, 'mime' => 'image/jpeg', 'created_at' => now(), ]); } it('admin sees pending uploads', function () { $admin = User::factory()->create(['role' => 'admin']); $owner = User::factory()->create(); $categoryId = createModerationCategory(); createModerationDraft($owner->id, $categoryId, ['title' => 'First Pending']); createModerationDraft($owner->id, $categoryId, ['title' => 'Second Pending']); $response = $this->actingAs($admin)->getJson('/api/admin/uploads/pending'); $response->assertOk(); $response->assertJsonCount(2, 'data'); }); it('non-admin is denied moderation API access', function () { $user = User::factory()->create(['role' => 'user']); $response = $this->actingAs($user)->getJson('/api/admin/uploads/pending'); $response->assertStatus(403); }); it('approve works', function () { $admin = User::factory()->create(['role' => 'moderator']); $owner = User::factory()->create(); $categoryId = createModerationCategory(); $uploadId = createModerationDraft($owner->id, $categoryId); $response = $this->actingAs($admin)->postJson("/api/admin/uploads/{$uploadId}/approve", [ 'note' => 'Looks good.', ]); $response->assertOk(); $row = DB::table('uploads')->where('id', $uploadId)->first([ 'moderation_status', 'moderation_note', 'moderated_by', 'moderated_at', ]); expect($row->moderation_status)->toBe('approved'); expect($row->moderation_note)->toBe('Looks good.'); expect((int) $row->moderated_by)->toBe((int) $admin->id); expect($row->moderated_at)->not->toBeNull(); }); it('reject works', function () { $admin = User::factory()->create(['role' => 'admin']); $owner = User::factory()->create(); $categoryId = createModerationCategory(); $uploadId = createModerationDraft($owner->id, $categoryId); $response = $this->actingAs($admin)->postJson("/api/admin/uploads/{$uploadId}/reject", [ 'note' => 'Policy violation.', ]); $response->assertOk(); $row = DB::table('uploads')->where('id', $uploadId)->first([ 'status', 'processing_state', 'moderation_status', 'moderation_note', 'moderated_by', 'moderated_at', ]); expect($row->status)->toBe('rejected'); expect($row->processing_state)->toBe('rejected'); expect($row->moderation_status)->toBe('rejected'); expect($row->moderation_note)->toBe('Policy violation.'); expect((int) $row->moderated_by)->toBe((int) $admin->id); expect($row->moderated_at)->not->toBeNull(); }); it('user cannot publish without approval', function () { Storage::fake('local'); $owner = User::factory()->create(['role' => 'user']); $categoryId = createModerationCategory(); $uploadId = createModerationDraft($owner->id, $categoryId, [ 'moderation_status' => 'pending', 'title' => 'Blocked Publish', ]); addReadyMainFile($uploadId); $response = $this->actingAs($owner)->postJson("/api/uploads/{$uploadId}/publish"); $response->assertStatus(422); $response->assertJsonFragment([ 'message' => 'Upload requires moderation approval before publish.', ]); });