Files
landing-api/tests/Feature/Site/PruneAndRedirectHealthTest.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

194 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Site;
use App\Models\Media;
use App\Models\Page;
use App\Models\Project;
use App\Models\Redirect;
use App\Models\Revision;
use App\Support\Health\Finding;
use App\Support\Health\HealthReport;
use App\Support\Health\Severity;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* Keeping the system from rotting: redirects that make things worse, and the
* three kinds of debris that otherwise accumulate for ever.
*/
class PruneAndRedirectHealthTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
Storage::fake(config('media.disk'));
}
protected function finding(string $key): Finding
{
return app(HealthReport::class)->runAll()->firstWhere('key', $key)
?? throw new \RuntimeException("No finding for {$key}");
}
#[Test]
public function it_finds_a_redirect_that_chains_into_another(): void
{
Redirect::factory()->create(['from' => '/a', 'to' => '/b', 'locale' => 'es']);
Redirect::factory()->create(['from' => '/b', 'to' => '/c', 'locale' => 'es']);
$finding = $this->finding('broken-redirects');
// The visitor is sent somewhere that sends them somewhere else: a
// round trip per hop, and eventually a loop.
$this->assertSame(Severity::Error, $finding->severity);
$this->assertSame(1, $finding->count);
}
#[Test]
public function it_finds_a_redirect_that_lands_on_nothing(): void
{
Redirect::factory()->create(['from' => '/viejo', 'to' => '/proyectos/no-existe']);
// Worse than the 404 it was meant to prevent: the visitor is bounced
// and lands on the same nothing.
$this->assertSame(1, $this->finding('redirects-to-nowhere')->count);
}
#[Test]
public function a_redirect_to_a_real_page_is_fine(): void
{
Project::factory()->published()->create([
'title' => ['es' => 'Vivo'], 'slug' => ['es' => 'vivo'],
]);
Redirect::factory()->create(['from' => '/viejo', 'to' => '/proyectos/vivo']);
$this->assertSame(Severity::Ok, $this->finding('redirects-to-nowhere')->severity);
}
#[Test]
public function a_redirect_off_this_site_is_not_ours_to_verify(): void
{
Redirect::factory()->create(['from' => '/viejo', 'to' => 'https://otra-web.example/algo']);
$this->assertSame(Severity::Ok, $this->finding('redirects-to-nowhere')->severity);
}
#[Test]
public function pruning_keeps_the_first_and_last_of_a_history(): void
{
$page = Page::factory()->create();
foreach (range(1, 4) as $n) {
$page->update(['title' => ['es' => "Versión {$n}"]]);
}
Revision::query()->update(['created_at' => now()->subYears(2)]);
$this->artisan('content:prune')->assertSuccessful();
// Two facts survive whatever happens: when it appeared, and who
// touched it last.
$this->assertSame(2, $page->revisions()->count());
// `reorder`, because the relation sorts newest-first by default and
// an added orderBy would only be a tie-breaker behind it.
$this->assertSame('created', $page->revisions()->reorder('id')->first()->event);
}
#[Test]
public function recent_history_is_left_alone(): void
{
$page = Page::factory()->create();
$page->update(['title' => ['es' => 'Reciente']]);
$this->artisan('content:prune')->assertSuccessful();
$this->assertSame(2, $page->revisions()->count());
}
#[Test]
public function it_erases_what_has_been_in_the_bin_long_enough(): void
{
$old = Page::factory()->create();
$old->delete();
$old->forceFill(['deleted_at' => now()->subYear()])->save();
$recent = Page::factory()->create();
$recent->delete();
$this->artisan('content:prune')->assertSuccessful();
$this->assertNull(Page::withTrashed()->find($old->id));
$this->assertNotNull(Page::withTrashed()->find($recent->id));
}
#[Test]
public function a_dry_run_changes_nothing(): void
{
$page = Page::factory()->create();
$page->delete();
$page->forceFill(['deleted_at' => now()->subYear()])->save();
$this->artisan('content:prune', ['--dry-run' => true])->assertSuccessful();
$this->assertNotNull(Page::withTrashed()->find($page->id));
}
#[Test]
public function it_deletes_image_files_nothing_points_at(): void
{
$disk = Storage::disk(config('media.disk'));
$disk->put('projects/en-uso.jpg', 'x');
$disk->put('projects/huerfana.jpg', 'x');
Project::factory()->create(['cover_image' => 'projects/en-uso.jpg']);
Media::factory()->create(['path' => 'projects/huerfana.jpg']);
$this->artisan('content:prune', ['--media' => true])->assertSuccessful();
$this->assertTrue($disk->exists('projects/en-uso.jpg'));
$this->assertFalse($disk->exists('projects/huerfana.jpg'));
}
#[Test]
public function an_image_belonging_to_something_in_the_bin_is_kept(): void
{
$disk = Storage::disk(config('media.disk'));
$disk->put('projects/borrado.jpg', 'x');
$project = Project::factory()->create(['cover_image' => 'projects/borrado.jpg']);
$project->delete();
$this->artisan('content:prune', ['--media' => true])->assertSuccessful();
// It can still be restored, and restoring it to a missing image would
// be worse than keeping a file.
$this->assertTrue($disk->exists('projects/borrado.jpg'));
}
#[Test]
public function files_are_left_alone_unless_asked_for(): void
{
$disk = Storage::disk(config('media.disk'));
$disk->put('projects/huerfana.jpg', 'x');
Media::factory()->create(['path' => 'projects/huerfana.jpg']);
$this->artisan('content:prune')->assertSuccessful();
// Deleting a file is the one thing here that restoring a row cannot
// undo, so it never happens by routine.
$this->assertTrue($disk->exists('projects/huerfana.jpg'));
}
}