seed(RolePermissionSeeder::class); } #[Test] public function the_action_writes_the_redirect_and_closes_the_report(): void { $this->actingAs($this->userWithRoles('super_admin')); $missing = MissingUrl::factory()->create([ 'path' => '/proyectos/nombre-viejo', 'locale' => 'es', ]); Livewire::test(ListMissingUrls::class) ->callTableAction('redirect', $missing, data: [ 'to' => '/proyectos/nombre-nuevo', 'status' => 301, 'locale' => 'es', ]) ->assertHasNoTableActionErrors(); $this->assertDatabaseHas('redirects', [ 'from' => '/proyectos/nombre-viejo', 'to' => '/proyectos/nombre-nuevo', 'status' => 301, 'locale' => 'es', ]); $this->assertSame(MissingUrlStatus::Resolved, $missing->refresh()->status); } /** * The destination goes through the same normalisation as a hand-written * rule, so an address typed with a trailing slash does not become a * redirect that never matches. */ #[Test] public function the_destination_is_normalised(): void { $this->actingAs($this->userWithRoles('super_admin')); $missing = MissingUrl::factory()->create(['path' => '/algo', 'locale' => 'es']); Livewire::test(ListMissingUrls::class) ->callTableAction('redirect', $missing, data: [ 'to' => '/servicios/algo/', 'status' => 301, 'locale' => 'es', ]); $this->assertDatabaseHas('redirects', ['to' => '/servicios/algo']); } #[Test] public function ignoring_hides_an_entry_without_stopping_the_count(): void { $this->actingAs($this->userWithRoles('super_admin')); $missing = MissingUrl::factory()->create([ 'path' => '/ruido', 'locale' => 'es', 'hits' => 1, ]); Livewire::test(ListMissingUrls::class) ->callTableAction('ignore', $missing); $this->assertSame(MissingUrlStatus::Ignored, $missing->refresh()->status); // Still counted: ignoring is a decision about the list, not about // whether the address is being asked for. MissingUrl::report('/ruido', 'es'); $missing->refresh(); $this->assertSame(2, (int) $missing->hits); // And it stays ignored. These addresses are requested constantly — if // the next request revived them, the button would never work twice. $this->assertSame(MissingUrlStatus::Ignored, $missing->status); } /** * Gated by the redirect permissions rather than a set of its own, so a * role that cannot do anything about a broken link does not see the list * of broken links either. */ #[Test] public function a_viewer_cannot_open_the_list(): void { $this->actingAs($this->userWithRoles('viewer')); $this->get(MissingUrlResource::getUrl('index'))->assertForbidden(); } #[Test] public function an_editor_can_open_it(): void { $this->actingAs($this->userWithRoles('editor')); $this->get(MissingUrlResource::getUrl('index'))->assertSuccessful(); } #[Test] public function nobody_creates_one_by_hand(): void { $this->assertFalse(MissingUrlResource::canCreate()); } #[Test] public function the_health_check_counts_what_is_still_broken(): void { MissingUrl::factory()->create(['path' => '/roto', 'locale' => 'es', 'hits' => 12]); // A single request is as likely to be a scanner as a person. MissingUrl::factory()->create(['path' => '/una-vez', 'locale' => 'es', 'hits' => 1]); // Already dealt with. MissingUrl::factory()->resolved()->create(['path' => '/arreglado', 'locale' => 'es', 'hits' => 30]); MissingUrl::factory()->ignored()->create(['path' => '/ruido', 'locale' => 'es', 'hits' => 30]); $finding = app(HealthReport::class)->runAll()->firstWhere('key', 'missing-urls'); $this->assertInstanceOf(Finding::class, $finding); $this->assertSame(1, $finding->count); $this->assertSame(Severity::Warning, $finding->severity); } /** * An address our own pages link to is not somebody else's stale bookmark: * it is a fault in what we publish, and it will keep producing 404s. */ #[Test] public function a_dead_link_on_our_own_site_is_an_error_not_a_warning(): void { MissingUrl::factory()->internal()->create([ 'path' => '/roto', 'locale' => 'es', 'hits' => 4, ]); $finding = app(HealthReport::class)->runAll()->firstWhere('key', 'missing-urls'); $this->assertSame(Severity::Error, $finding->severity); } #[Test] public function pruning_forgets_one_off_scans_and_keeps_the_rest(): void { MissingUrl::factory()->create([ 'path' => '/una-vez', 'locale' => 'es', 'hits' => 1, 'last_seen_at' => now()->subDays(60), ]); MissingUrl::factory()->create([ 'path' => '/repetida', 'locale' => 'es', 'hits' => 9, 'last_seen_at' => now()->subDays(60), ]); MissingUrl::factory()->create([ 'path' => '/reciente', 'locale' => 'es', 'hits' => 1, 'last_seen_at' => now()->subDay(), ]); // A decision already made must not come back next week as though it // had never been made. MissingUrl::factory()->ignored()->create([ 'path' => '/ignorada', 'locale' => 'es', 'hits' => 1, 'last_seen_at' => now()->subDays(60), ]); $this->artisan('content:prune')->assertExitCode(0); $this->assertDatabaseMissing('missing_urls', ['path' => '/una-vez']); $this->assertDatabaseHas('missing_urls', ['path' => '/repetida']); $this->assertDatabaseHas('missing_urls', ['path' => '/reciente']); $this->assertDatabaseHas('missing_urls', ['path' => '/ignorada']); } #[Test] public function a_dry_run_deletes_nothing(): void { MissingUrl::factory()->create([ 'path' => '/una-vez', 'locale' => 'es', 'hits' => 1, 'last_seen_at' => now()->subDays(60), ]); $this->artisan('content:prune --dry-run')->assertExitCode(0); $this->assertDatabaseHas('missing_urls', ['path' => '/una-vez']); } /** * A rule typed straight into the redirects screen closes the report too: * the two lists must never disagree about whether an address is answered. */ #[Test] public function a_hand_written_redirect_also_closes_the_report(): void { $missing = MissingUrl::factory()->create(['path' => '/a-mano', 'locale' => 'es']); Redirect::create([ 'from' => '/a-mano', 'to' => '/destino', 'status' => 301, 'locale' => 'es', 'is_enabled' => true, ]); $this->assertSame(MissingUrlStatus::Resolved, $missing->refresh()->status); } }