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