create(); $image = $pressRelease->images()->create([ 'disk' => 'public', 'path' => "press-releases/{$pressRelease->id}/images/cover.jpg", 'variants' => [ 'thumb' => "press-releases/{$pressRelease->id}/images/thumb.jpg", ], ]); $attachment = $pressRelease->attachments()->create([ 'disk' => 'public', 'path' => "press-releases/{$pressRelease->id}/attachments/factsheet.pdf", 'original_name' => 'factsheet.pdf', 'mime' => 'application/pdf', 'size' => 10, ]); foreach ([$image->path, $image->variants['thumb'], $attachment->path] as $path) { Storage::disk('public')->put($path, 'content'); } $this->artisan('press-releases:privatize-media') ->expectsOutput('1 Bilder und 1 Anhänge wurden auf den privaten Datenträger migriert.') ->assertSuccessful(); foreach ([$image->path, $image->variants['thumb'], $attachment->path] as $path) { Storage::disk('public')->assertMissing($path); Storage::disk('local')->assertExists($path); } expect($image->fresh()->disk)->toBe('local') ->and($attachment->fresh()->disk)->toBe('local'); }); test('unmigrated legacy image rows are skipped instead of aborting the run', function () { Storage::fake('public'); Storage::fake('local'); $pressRelease = PressRelease::factory()->create(); $legacyImage = $pressRelease->images()->create([ 'disk' => 'public', 'path' => 'b0cde51ff6799356bfaa8ec4c80e8921.jpg', 'legacy_portal' => 'presseecho', 'legacy_id' => 33683, ]); $migratedImage = $pressRelease->images()->create([ 'disk' => 'public', 'path' => "press-releases/{$pressRelease->id}/images/cover.jpg", ]); Storage::disk('public')->put($migratedImage->path, 'content'); $this->artisan('press-releases:privatize-media') ->expectsOutputToContain('1 unmigrierte Legacy-Bildzeilen übersprungen') ->assertSuccessful(); expect($legacyImage->fresh())->not->toBeNull() ->and($legacyImage->fresh()->disk)->toBe('public') ->and($migratedImage->fresh()->disk)->toBe('local'); }); test('prune-missing deletes unmigrated legacy image rows only when no file exists', function () { Storage::fake('public'); Storage::fake('local'); $pressRelease = PressRelease::factory()->create(); $lostImage = $pressRelease->images()->create([ 'disk' => 'public', 'path' => 'b0cde51ff6799356bfaa8ec4c80e8921.jpg', 'legacy_portal' => 'presseecho', 'legacy_id' => 33683, ]); $recoverableImage = $pressRelease->images()->create([ 'disk' => 'public', 'path' => 'noch-vorhanden.jpg', 'legacy_portal' => 'presseecho', 'legacy_id' => 33684, ]); Storage::disk('public')->put($recoverableImage->path, 'content'); $this->artisan('press-releases:privatize-media', ['--prune-missing' => true]) ->expectsOutputToContain('1 unmigrierte Legacy-Bildzeilen ohne Datei wurden gelöscht.') ->expectsOutputToContain('1 unmigrierte Legacy-Bildzeilen übersprungen') ->assertSuccessful(); expect(PressReleaseImage::withTrashed()->find($lostImage->id))->toBeNull() ->and($recoverableImage->fresh())->not->toBeNull() ->and($recoverableImage->fresh()->disk)->toBe('public'); }); test('a missing source file never flips its database record to the private disk', function () { Storage::fake('public'); Storage::fake('local'); $pressRelease = PressRelease::factory()->create(); $attachment = $pressRelease->attachments()->create([ 'disk' => 'public', 'path' => "press-releases/{$pressRelease->id}/attachments/missing.pdf", 'original_name' => 'missing.pdf', 'mime' => 'application/pdf', 'size' => 10, ]); expect(fn () => app(PressReleaseMediaPrivatizer::class)->privatizeAll()) ->toThrow(RuntimeException::class, 'missing on both disks'); expect($attachment->fresh()->disk)->toBe('public'); });