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>
This commit is contained in:
244
tests/Feature/Panel/SiteHealthTest.php
Normal file
244
tests/Feature/Panel/SiteHealthTest.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Panel;
|
||||
|
||||
use App\Enums\CallStatus;
|
||||
use App\Filament\Pages\SiteHealth;
|
||||
use App\Models\Application;
|
||||
use App\Models\Call;
|
||||
use App\Models\Media;
|
||||
use App\Models\Project;
|
||||
use App\Support\Health\Finding;
|
||||
use App\Support\Health\HealthReport;
|
||||
use App\Support\Health\Severity;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FakesPireon;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The checks exist because the public site is somewhere else: none of these
|
||||
* faults is visible where the content is edited.
|
||||
*/
|
||||
class SiteHealthTest extends TestCase
|
||||
{
|
||||
use FakesPireon, RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
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 a_clean_site_reports_nothing(): void
|
||||
{
|
||||
$problems = app(HealthReport::class)->runAll()
|
||||
->filter(fn (Finding $f) => $f->severity->isProblem());
|
||||
|
||||
$this->assertTrue($problems->isEmpty(), 'Expected an empty database to be healthy');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_finds_a_record_translated_without_a_slug(): void
|
||||
{
|
||||
Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Mi proyecto', 'ca' => 'El meu projecte'],
|
||||
'slug' => ['es' => 'mi-proyecto'],
|
||||
]);
|
||||
|
||||
$finding = $this->finding('missing-slugs');
|
||||
|
||||
// Already broken on the public site: the Catalan alternate either
|
||||
// vanishes or points at a 404.
|
||||
$this->assertSame(Severity::Error, $finding->severity);
|
||||
$this->assertSame(1, $finding->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_finds_two_records_answering_at_the_same_url(): void
|
||||
{
|
||||
Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Uno'], 'slug' => ['es' => 'repetido'],
|
||||
]);
|
||||
Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Dos'], 'slug' => ['es' => 'repetido'],
|
||||
]);
|
||||
|
||||
$finding = $this->finding('duplicate-slugs');
|
||||
|
||||
// The write rule prevents this, but a slug lives inside JSON so it can
|
||||
// never be a database constraint — imports and seeders get through.
|
||||
$this->assertSame(Severity::Error, $finding->severity);
|
||||
$this->assertSame(1, $finding->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_same_slug_in_two_languages_is_not_a_duplicate(): void
|
||||
{
|
||||
Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Uno', 'en' => 'One'],
|
||||
'slug' => ['es' => 'igual', 'en' => 'igual'],
|
||||
]);
|
||||
|
||||
$this->assertSame(Severity::Ok, $this->finding('duplicate-slugs')->severity);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_finds_an_image_whose_file_is_gone(): void
|
||||
{
|
||||
Media::factory()->processed()->create(['path' => 'projects/vanished.jpg']);
|
||||
|
||||
$finding = $this->finding('broken-images');
|
||||
|
||||
// The API keeps serving the URL — it has no reason to doubt it — so
|
||||
// the only symptom is a broken image, on the other site.
|
||||
$this->assertSame(Severity::Error, $finding->severity);
|
||||
$this->assertSame(1, $finding->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function an_image_still_in_the_queue_is_not_broken(): void
|
||||
{
|
||||
Media::factory()->create(['path' => 'projects/pending.jpg']);
|
||||
|
||||
// It is early, not broken. That is a different check.
|
||||
$this->assertSame(Severity::Ok, $this->finding('broken-images')->severity);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_notices_when_nothing_is_processing_images(): void
|
||||
{
|
||||
Media::factory()->create([
|
||||
'path' => 'projects/stuck.jpg',
|
||||
'created_at' => now()->subHours(3),
|
||||
]);
|
||||
|
||||
$finding = $this->finding('stalled-media');
|
||||
|
||||
// The site keeps working, images just lose their dimensions and
|
||||
// srcset — which is exactly why nobody would notice.
|
||||
$this->assertSame(1, $finding->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_finds_a_published_image_nobody_described(): void
|
||||
{
|
||||
Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Con portada'],
|
||||
'slug' => ['es' => 'con-portada'],
|
||||
'cover_image' => 'projects/cover.jpg',
|
||||
'cover_image_alt' => null,
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $this->finding('images-without-alt')->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_finds_a_call_left_open_past_its_closing_date(): void
|
||||
{
|
||||
Call::factory()->create([
|
||||
'project_id' => null,
|
||||
'status' => CallStatus::Open,
|
||||
'closes_at' => now()->subWeek(),
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $this->finding('expired-calls')->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_counts_people_who_have_been_waiting_too_long(): void
|
||||
{
|
||||
Application::factory()->create(['created_at' => now()->subDays(10)]);
|
||||
Application::factory()->create(['created_at' => now()->subHours(2)]);
|
||||
|
||||
$finding = $this->finding('unanswered-submissions');
|
||||
|
||||
// Somebody offered to help and heard nothing, which costs more than
|
||||
// any other fault on the page — hence an error, not a warning.
|
||||
$this->assertSame(Severity::Error, $finding->severity);
|
||||
$this->assertSame(1, $finding->count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_check_is_hidden_from_anyone_who_may_not_see_what_it_counts(): void
|
||||
{
|
||||
Application::factory()->create(['created_at' => now()->subDays(10)]);
|
||||
|
||||
$editor = $this->userWithRoles('editor');
|
||||
$viewer = $this->userWithRoles('viewer');
|
||||
|
||||
$report = app(HealthReport::class);
|
||||
|
||||
// Counting submissions is knowing they exist, and who may know that is
|
||||
// the same question as who may read the inbox.
|
||||
$this->assertNotNull($report->runFor($editor)->firstWhere('key', 'unanswered-submissions'));
|
||||
$this->assertNull($report->runFor($viewer)->firstWhere('key', 'unanswered-submissions'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function findings_come_back_worst_first(): void
|
||||
{
|
||||
Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Sin portada'],
|
||||
'slug' => ['es' => 'sin-portada'],
|
||||
]);
|
||||
Media::factory()->processed()->create(['path' => 'projects/vanished.jpg']);
|
||||
|
||||
$severities = app(HealthReport::class)->runAll()
|
||||
->map(fn (Finding $f) => $f->severity->weight())
|
||||
->all();
|
||||
|
||||
$sorted = $severities;
|
||||
sort($sorted);
|
||||
|
||||
$this->assertSame($sorted, $severities);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_page_renders_for_anyone_who_can_reach_the_panel(): void
|
||||
{
|
||||
$this->actingAs($this->userWithRoles('viewer'));
|
||||
|
||||
$this->get(SiteHealth::getUrl())->assertSuccessful();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_command_fails_a_deploy_when_something_is_broken(): void
|
||||
{
|
||||
Media::factory()->processed()->create(['path' => 'projects/vanished.jpg']);
|
||||
|
||||
$this->artisan('site:check')->assertFailed();
|
||||
|
||||
// A warning-only site can still deploy when the threshold says so.
|
||||
$this->artisan('site:check', ['--fail-on' => 'never'])->assertSuccessful();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_command_can_ignore_warnings_and_fail_only_on_faults(): void
|
||||
{
|
||||
// A warning, not a fault: nothing a visitor hits.
|
||||
Call::factory()->create([
|
||||
'project_id' => null,
|
||||
'status' => CallStatus::Open,
|
||||
'closes_at' => now()->subWeek(),
|
||||
]);
|
||||
|
||||
$this->artisan('site:check', ['--fail-on' => 'error'])->assertSuccessful();
|
||||
$this->artisan('site:check')->assertFailed();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user