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:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

View 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();
}
}