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,269 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api;
use App\Models\Client;
use App\Models\Member;
use App\Models\Project;
use App\Models\Service;
use App\Models\Talk;
use App\Models\Testimonial;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* The pieces a landing page is made of.
*
* Four collections, no "home page" payload and no ordering of sections
* anywhere: what gets composed, and in what order, is the frontend's business.
*/
class ShowcaseApiTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->withHeaders(['Accept-Language' => 'es']);
Queue::fake();
}
#[Test]
public function it_lists_published_services_in_the_order_they_were_arranged(): void
{
Service::factory()->create(['title' => ['es' => 'Segundo'], 'slug' => ['es' => 'segundo'], 'position' => 2]);
Service::factory()->create(['title' => ['es' => 'Primero'], 'slug' => ['es' => 'primero'], 'position' => 1]);
Service::factory()->draft()->create(['title' => ['es' => 'Borrador'], 'slug' => ['es' => 'borrador']]);
$titles = $this->getJson('/api/public/v1/services')
->assertOk()
->json('data.*.title');
$this->assertSame(['Primero', 'Segundo'], $titles);
}
#[Test]
public function a_service_resolves_by_slug_within_its_language(): void
{
Service::factory()->create([
'title' => ['es' => 'Desarrollo', 'ca' => 'Desenvolupament'],
'slug' => ['es' => 'desarrollo', 'ca' => 'desenvolupament'],
]);
$this->getJson('/api/public/v1/services/desarrollo')
->assertOk()
->assertJsonPath('data.title', 'Desarrollo');
$this->getJson('/api/public/v1/services/desenvolupament?lang=ca')
->assertOk()
->assertJsonPath('data.title', 'Desenvolupament');
// A Catalan slug must not answer in Spanish, exactly as everywhere
// else in this API.
$this->getJson('/api/public/v1/services/desenvolupament')->assertNotFound();
}
#[Test]
public function a_service_carries_the_work_that_backs_it(): void
{
$service = Service::factory()->create([
'title' => ['es' => 'Desarrollo'], 'slug' => ['es' => 'desarrollo'],
]);
$published = Project::factory()->published()->create([
'title' => ['es' => 'Web de la asociación'], 'slug' => ['es' => 'web'],
]);
$draft = Project::factory()->draft()->create([
'title' => ['es' => 'A medias'], 'slug' => ['es' => 'a-medias'],
]);
$service->projects()->attach([$published->id, $draft->id]);
$data = $this->getJson('/api/public/v1/services/desarrollo')->assertOk()->json('data');
// Without real work behind it a service is a promise — but an
// unpublished project is not something the public may see.
$this->assertCount(1, $data['projects']);
$this->assertSame('Web de la asociación', $data['projects'][0]['title']);
}
#[Test]
public function a_draft_service_is_reachable_only_with_the_preview_secret(): void
{
config()->set('site.preview_token', 'secreto');
Service::factory()->draft()->create([
'title' => ['es' => 'Sin publicar'], 'slug' => ['es' => 'sin-publicar'],
]);
$this->getJson('/api/public/v1/services/sin-publicar')->assertNotFound();
$this->withHeaders(['X-Preview-Token' => 'secreto'])
->getJson('/api/public/v1/services/sin-publicar')
->assertOk();
}
#[Test]
public function the_wall_of_logos_leaves_out_whoever_asked_to_be_left_out(): void
{
$shy = Client::factory()->private()->create(['name' => 'Discreta']);
$shown = Client::factory()->create(['name' => 'Visible']);
Project::factory()->published()->create(['client_id' => $shy->id]);
Project::factory()->published()->create(['client_id' => $shown->id]);
$names = $this->getJson('/api/public/v1/clients')->assertOk()->json('data.*.name');
// Appearing on somebody else's website is their call, not ours.
$this->assertSame(['Visible'], $names);
}
#[Test]
public function a_client_with_nothing_published_is_not_a_logo_the_site_can_justify(): void
{
$withWork = Client::factory()->create(['name' => 'Con obra']);
Project::factory()->published()->create(['client_id' => $withWork->id]);
$withoutWork = Client::factory()->create(['name' => 'Sin obra']);
Project::factory()->draft()->create(['client_id' => $withoutWork->id]);
$names = $this->getJson('/api/public/v1/clients')->assertOk()->json('data.*.name');
$this->assertSame(['Con obra'], $names);
// Unless the frontend asks for all of them anyway.
$all = $this->getJson('/api/public/v1/clients?with_projects=0')->assertOk()->json('data.*.name');
$this->assertCount(2, $all);
}
#[Test]
public function a_translated_quote_says_that_it_was_translated(): void
{
Testimonial::factory()->create([
'quote' => ['es' => 'Nos cambiaron la forma de trabajar', 'en' => 'They changed how we work'],
'original_locale' => 'es',
'author_name' => 'Marta',
]);
$this->getJson('/api/public/v1/testimonials')
->assertOk()
->assertJsonPath('data.0.is_original', true)
->assertJsonPath('data.0.original_locale', 'es');
// Read in English these are our words, not hers, and the frontend is
// told so it can say "translated from Spanish" rather than pretend.
$this->withHeaders(['Accept-Language' => 'en'])
->getJson('/api/public/v1/testimonials')
->assertOk()
->assertJsonPath('data.0.is_original', false)
->assertJsonPath('data.0.quote', 'They changed how we work');
}
#[Test]
public function testimonials_can_be_asked_for_by_what_they_vouch_for(): void
{
$client = Client::factory()->create(['name' => 'Asociación', 'slug' => 'asociacion']);
$service = Service::factory()->create([
'title' => ['es' => 'Formación'], 'slug' => ['es' => 'formacion'],
]);
Testimonial::factory()->create(['client_id' => $client->id, 'author_name' => 'De cliente']);
Testimonial::factory()->create(['service_id' => $service->id, 'author_name' => 'De servicio']);
Testimonial::factory()->create(['author_name' => 'Suelto']);
// Attached to something rather than to "the home page", so a page asks
// for the ones that belong on it.
$this->getJson('/api/public/v1/testimonials?client=asociacion')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.author.name', 'De cliente');
$this->getJson('/api/public/v1/testimonials?service=formacion')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.author.name', 'De servicio');
}
#[Test]
public function a_private_testimonial_never_leaves_the_panel(): void
{
Testimonial::factory()->private()->create(['author_name' => 'Sin permiso']);
$this->getJson('/api/public/v1/testimonials')
->assertOk()
->assertJsonCount(0, 'data');
}
#[Test]
public function talks_come_back_newest_first_with_their_speaker(): void
{
$member = Member::factory()->create(['name' => 'Ada', 'slug' => 'ada']);
Talk::factory()->create(['title' => 'La vieja', 'held_at' => now()->subYears(2), 'member_id' => $member->id]);
Talk::factory()->create(['title' => 'La reciente', 'held_at' => now()->subMonth(), 'member_id' => $member->id]);
Talk::factory()->create(['title' => 'Oculta', 'is_public' => false]);
$data = $this->getJson('/api/public/v1/talks')->assertOk()->json('data');
$this->assertSame(['La reciente', 'La vieja'], array_column($data, 'title'));
$this->assertSame('Ada', $data[0]['speaker']['name']);
}
#[Test]
public function a_talks_title_is_not_translated(): void
{
Talk::factory()->create([
'title' => 'Taming reactive Node.js',
'language' => 'en',
'summary' => ['es' => 'Sobre streams', 'en' => 'About streams'],
]);
// The title is the name of a talk given once, in one language;
// translating it would invent a talk that never happened.
$this->getJson('/api/public/v1/talks')
->assertOk()
->assertJsonPath('data.0.title', 'Taming reactive Node.js')
->assertJsonPath('data.0.language', 'en')
->assertJsonPath('data.0.summary', 'Sobre streams');
}
#[Test]
public function a_project_now_carries_a_client_rather_than_a_typed_name(): void
{
$client = Client::factory()->create(['name' => 'Cruz Roja', 'slug' => 'cruz-roja']);
Project::factory()->published()->create([
'title' => ['es' => 'Su web'], 'slug' => ['es' => 'su-web'],
'client_id' => $client->id,
]);
$data = $this->getJson('/api/public/v1/projects/su-web')->assertOk()->json('data');
$this->assertSame('Cruz Roja', $data['client']['name']);
$this->assertSame('cruz-roja', $data['client']['slug']);
// The old free-text pair is gone, not renamed.
$this->assertArrayNotHasKey('beneficiary', $data);
}
#[Test]
public function services_appear_in_the_url_list_the_frontend_builds_its_sitemap_from(): void
{
Service::factory()->create([
'title' => ['es' => 'Desarrollo', 'ca' => 'Desenvolupament'],
'slug' => ['es' => 'desarrollo', 'ca' => 'desenvolupament'],
]);
Service::factory()->draft()->create(['slug' => ['es' => 'borrador']]);
$services = $this->getJson('/api/public/v1/slugs')->assertOk()->json('data.services');
$this->assertCount(1, $services);
$this->assertSame('/servicios/desarrollo', $services[0]['alternates']['es']);
$this->assertSame('/ca/serveis/desenvolupament', $services[0]['alternates']['ca']);
}
}