Keep technology logos and images inside text safe from pruning
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / test-postgres (push) Has been cancelled
Tests / mcp (push) Has been cancelled

Media::referencedPaths() is what `content:prune --media` asks before
deleting a file, and it did not know about two kinds of image a page
shows:

- A technology's logo. Every logo on the radar looked orphaned, and one
  prune would have deleted all of them.
- Images inside rich text — a page's body, a project's description,
  decisions and postmortem — whether attached in the panel's editor or
  uploaded through the API and written in as <img src>. They are now
  found in the HTML, in every language, bin included.

The page body's editor also states where attachments go — the media
disk, pages/content, public — instead of relying on Filament's
defaults: the HTML reaches the frontend as it is, and a private file
would be a broken image there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 23:09:25 +02:00
parent 3daa40570b
commit 96747abdc3
3 changed files with 145 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Media;
use App\Models\Media;
use App\Models\Page;
use App\Models\Project;
use App\Models\Technology;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* What counts as "in use" decides what `content:prune --media` may delete, so
* everything a record shows has to be found here — or pruning breaks it.
*/
class ReferencedPathsTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
Storage::fake((string) config('media.disk'));
}
#[Test]
public function a_technologys_logo_is_in_use(): void
{
Technology::factory()->create(['logo' => 'technologies/angular.svg']);
$this->assertContains('technologies/angular.svg', Media::referencedPaths());
}
#[Test]
public function an_image_inside_a_page_body_is_in_use_in_any_language(): void
{
$url = Storage::disk((string) config('media.disk'))->url('pages/content/diagrama.png');
Page::factory()->create([
'body' => [
'es' => '<p>Así funciona:</p><img src="'.$url.'" alt="Diagrama">',
'en' => '<figure><img src="/storage/pages/content/diagram%20en.png"></figure>',
],
]);
$paths = Media::referencedPaths();
$this->assertContains('pages/content/diagrama.png', $paths);
$this->assertContains('pages/content/diagram en.png', $paths);
}
#[Test]
public function an_image_in_a_projects_prose_is_in_use_even_in_the_bin(): void
{
Project::factory()->create([
'decisions' => ['es' => '<img src="/storage/projects/arquitectura.webp">'],
])->delete();
$this->assertContains('projects/arquitectura.webp', Media::referencedPaths());
}
#[Test]
public function pruning_leaves_an_image_a_body_shows_and_takes_a_true_orphan(): void
{
$disk = Storage::disk((string) config('media.disk'));
foreach (['pages/content/usada.png', 'pages/huerfana.png'] as $path) {
$disk->put($path, 'x');
Media::query()->create(['disk' => (string) config('media.disk'), 'path' => $path]);
}
Page::factory()->create(['body' => ['es' => '<img src="'.$disk->url('pages/content/usada.png').'">']]);
$this->artisan('content:prune', ['--media' => true])->assertSuccessful();
$disk->assertExists('pages/content/usada.png');
$disk->assertMissing('pages/huerfana.png');
}
#[Test]
public function html_without_images_on_the_disk_says_nothing(): void
{
$this->assertSame([], Media::pathsEmbeddedIn('<p>Texto</p><img src="https://otra-web.example/foto.png">'));
}
}