seedPhotoWithAsset(); $scanner = new class extends PhotoSecurityScanner { public function scan(string $disk, ?string $relativePath): array { return ['status' => 'clean', 'message' => 'ok']; } public function stripExif(string $disk, ?string $relativePath): array { return ['status' => 'stripped', 'message' => 'removed']; } }; (new ProcessPhotoSecurityScan($photo->id))->handle($scanner); $photo->refresh(); $this->assertSame('approved', $photo->status); $this->assertSame('clean', $photo->security_scan_status); $this->assertNotNull($photo->security_scanned_at); } public function test_skipped_scan_still_approves_pending_photo(): void { [$photo] = $this->seedPhotoWithAsset(); $scanner = new class extends PhotoSecurityScanner { public function scan(string $disk, ?string $relativePath): array { return ['status' => 'skipped', 'message' => 'disabled']; } public function stripExif(string $disk, ?string $relativePath): array { return ['status' => 'skipped']; } }; (new ProcessPhotoSecurityScan($photo->id))->handle($scanner); $photo->refresh(); $this->assertSame('approved', $photo->status); $this->assertSame('skipped', $photo->security_scan_status); } public function test_infected_scan_rejects_photo(): void { [$photo, $asset] = $this->seedPhotoWithAsset(); $scanner = new class extends PhotoSecurityScanner { public function scan(string $disk, ?string $relativePath): array { return ['status' => 'infected', 'message' => 'bad']; } public function stripExif(string $disk, ?string $relativePath): array { return ['status' => 'skipped']; } }; (new ProcessPhotoSecurityScan($photo->id))->handle($scanner); $photo->refresh(); $this->assertSame('rejected', $photo->status); $this->assertSame('infected', $photo->security_scan_status); } /** * @return array{Photo, EventMediaAsset} */ private function seedPhotoWithAsset(): array { Storage::fake('public'); $tenant = Tenant::factory()->create(); $eventType = EventType::factory()->create(); $package = Package::factory()->endcustomer()->create([ 'max_photos' => 100, 'gallery_days' => 30, ]); $event = Event::factory()->create([ 'tenant_id' => $tenant->id, 'event_type_id' => $eventType->id, 'status' => 'published', ]); EventPackage::create([ 'event_id' => $event->id, 'package_id' => $package->id, 'purchased_price' => $package->price, 'purchased_at' => now(), 'used_photos' => 0, 'gallery_expires_at' => now()->addDays($package->gallery_days ?? 30), ]); $path = "events/{$event->id}/photos/example.jpg"; Storage::disk('public')->put($path, 'fake-image'); $target = MediaStorageTarget::create([ 'key' => 'public', 'name' => 'Public (test)', 'driver' => 'local', 'config' => [ 'root' => Storage::disk('public')->path(''), 'url' => 'http://localhost/storage', 'visibility' => 'public', ], 'is_hot' => true, 'is_default' => true, 'is_active' => true, 'priority' => 10, ]); $photo = Photo::create([ 'event_id' => $event->id, 'guest_name' => 'tester', 'file_path' => $path, 'thumbnail_path' => $path, 'status' => 'pending', 'ingest_source' => Photo::SOURCE_GUEST_PWA, ]); $asset = EventMediaAsset::create([ 'event_id' => $event->id, 'photo_id' => $photo->id, 'media_storage_target_id' => $target->id, 'disk' => 'public', 'path' => $path, 'variant' => 'original', 'status' => 'hot', 'processed_at' => now(), 'mime_type' => 'image/jpeg', 'size_bytes' => 10, ]); $photo->update(['media_asset_id' => $asset->id]); return [$photo, $asset]; } }