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:
168
tests/Feature/Site/RedirectsTest.php
Normal file
168
tests/Feature/Site/RedirectsTest.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Site;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\Redirect;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The net under a renamed slug.
|
||||
*
|
||||
* Renaming one breaks every link anybody shared, silently: nothing here
|
||||
* notices and the frontend only finds out when a visitor gets a 404. The
|
||||
* moment the slug changes is the only moment the old address is still known.
|
||||
*/
|
||||
class RedirectsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withHeaders(['Accept-Language' => 'es']);
|
||||
Queue::fake();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function renaming_a_slug_leaves_a_redirect_behind(): void
|
||||
{
|
||||
$project = Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Mi proyecto'],
|
||||
'slug' => ['es' => 'nombre-viejo'],
|
||||
]);
|
||||
|
||||
$project->update(['slug' => ['es' => 'nombre-nuevo']]);
|
||||
|
||||
$redirect = Redirect::query()->sole();
|
||||
|
||||
$this->assertSame('/proyectos/nombre-viejo', $redirect->from);
|
||||
$this->assertSame('/proyectos/nombre-nuevo', $redirect->to);
|
||||
$this->assertSame(301, $redirect->status);
|
||||
$this->assertSame('es', $redirect->locale);
|
||||
$this->assertTrue($redirect->wasAutomatic());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function each_language_gets_its_own_rule(): void
|
||||
{
|
||||
$project = Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Uno', 'ca' => 'U'],
|
||||
'slug' => ['es' => 'viejo-es', 'ca' => 'vell-ca'],
|
||||
]);
|
||||
|
||||
$project->update(['slug' => ['es' => 'nuevo-es', 'ca' => 'nou-ca']]);
|
||||
|
||||
// The Catalan URL carries its own prefix and its own path segment, so
|
||||
// one rule could never cover both.
|
||||
$this->assertSame(
|
||||
['/proyectos/viejo-es' => '/proyectos/nuevo-es', '/ca/projectes/vell-ca' => '/ca/projectes/nou-ca'],
|
||||
Redirect::query()->pluck('to', 'from')->all(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function adding_a_translation_is_not_a_rename(): void
|
||||
{
|
||||
$project = Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Uno'],
|
||||
'slug' => ['es' => 'uno'],
|
||||
]);
|
||||
|
||||
$project->update(['slug' => ['es' => 'uno', 'ca' => 'u']]);
|
||||
|
||||
// Nothing broke: there was no Catalan address before.
|
||||
$this->assertSame(0, Redirect::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_second_rename_does_not_leave_a_chain_behind(): void
|
||||
{
|
||||
$project = Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Uno'], 'slug' => ['es' => 'v1'],
|
||||
]);
|
||||
|
||||
$project->update(['slug' => ['es' => 'v2']]);
|
||||
$project->update(['slug' => ['es' => 'v3']]);
|
||||
|
||||
// Both old addresses point at the current one. A frontend following a
|
||||
// chain would cost an extra round trip per hop.
|
||||
$this->assertSame(
|
||||
['/proyectos/v1' => '/proyectos/v3', '/proyectos/v2' => '/proyectos/v3'],
|
||||
Redirect::query()->orderBy('from')->pluck('to', 'from')->all(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function renaming_back_does_not_create_a_loop(): void
|
||||
{
|
||||
$project = Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Uno'], 'slug' => ['es' => 'original'],
|
||||
]);
|
||||
|
||||
$project->update(['slug' => ['es' => 'cambiado']]);
|
||||
$project->update(['slug' => ['es' => 'original']]);
|
||||
|
||||
// A rule from a path to itself would send a browser round for ever.
|
||||
foreach (Redirect::query()->get() as $redirect) {
|
||||
$this->assertFalse($redirect->isCircular(), "{$redirect->from} → {$redirect->to}");
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_works_for_every_kind_of_record_with_a_public_url(): void
|
||||
{
|
||||
$service = Service::factory()->create([
|
||||
'title' => ['es' => 'Servicio'], 'slug' => ['es' => 'antiguo'],
|
||||
]);
|
||||
|
||||
$service->update(['slug' => ['es' => 'moderno']]);
|
||||
|
||||
$this->assertSame('/servicios/antiguo', Redirect::query()->sole()->from);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_frontend_gets_the_whole_list_in_one_request(): void
|
||||
{
|
||||
Redirect::factory()->create(['from' => '/vieja', 'to' => '/nueva', 'locale' => null]);
|
||||
Redirect::factory()->disabled()->create(['from' => '/apagada', 'to' => '/otra']);
|
||||
|
||||
$data = $this->getJson('/api/public/v1/redirects')->assertOk()->json('data');
|
||||
|
||||
// One request rather than a lookup per 404: there are never many, and
|
||||
// an SSR frontend would otherwise ask on every miss it serves.
|
||||
$this->assertCount(1, $data);
|
||||
$this->assertSame('/vieja', $data[0]['from']);
|
||||
$this->assertNull($data[0]['locale']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function paths_are_normalised_however_they_were_typed(): void
|
||||
{
|
||||
$this->assertSame('/proyectos/uno', Redirect::normalise('proyectos/uno'));
|
||||
$this->assertSame('/proyectos/uno', Redirect::normalise('/proyectos/uno/'));
|
||||
$this->assertSame('/proyectos/uno', Redirect::normalise('/proyectos/uno?utm=x'));
|
||||
|
||||
// So a rule typed by hand matches one written automatically.
|
||||
$this->assertSame('/', Redirect::normalise('/'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function following_one_can_be_counted(): void
|
||||
{
|
||||
$redirect = Redirect::factory()->create(['hits' => 0]);
|
||||
|
||||
$redirect->markUsed();
|
||||
|
||||
// The number that decides whether a rule can ever be deleted.
|
||||
$this->assertSame(1, $redirect->fresh()->hits);
|
||||
$this->assertNotNull($redirect->fresh()->last_used_at);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user