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,127 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Panel;
use App\Filament\Resources\Clients\ClientResource;
use App\Filament\Resources\Services\ServiceResource;
use App\Filament\Resources\Talks\TalkResource;
use App\Filament\Resources\Testimonials\TestimonialResource;
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 Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use PHPUnit\Framework\Attributes\Test;
use Tests\FakesPireon;
use Tests\TestCase;
/**
* The four content types a landing page is built from.
*/
class ShowcaseResourcesTest extends TestCase
{
use FakesPireon, RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
Queue::fake();
}
#[Test]
public function every_editor_screen_opens(): void
{
$client = Client::factory()->create();
$service = Service::factory()->create();
$testimonial = Testimonial::factory()->create(['client_id' => $client->id]);
$talk = Talk::factory()->create();
$this->actingAs($this->userWithRoles('admin'));
foreach ([
[ServiceResource::class, $service],
[ClientResource::class, $client],
[TestimonialResource::class, $testimonial],
[TalkResource::class, $talk],
] as [$resource, $record]) {
$this->get($resource::getUrl('create'))->assertSuccessful();
$this->get($resource::getUrl('edit', ['record' => $record]))->assertSuccessful();
}
}
#[Test]
public function publishing_a_service_is_a_separate_permission_from_writing_one(): void
{
$editor = $this->userWithRoles('editor');
// Same reasoning as projects and pages: somebody may write the offer
// without deciding it goes on the website.
$this->assertTrue($editor->can('services.create'));
$this->assertTrue($editor->can('services.publish'));
$viewer = $this->userWithRoles('viewer');
$this->assertTrue($viewer->can('services.view_any'));
$this->assertFalse($viewer->can('services.publish'));
$this->assertFalse($viewer->can('services.create'));
}
#[Test]
public function a_viewer_can_look_at_the_showcase_but_not_change_it(): void
{
$this->actingAs($this->userWithRoles('viewer'));
foreach ([ServiceResource::class, ClientResource::class, TestimonialResource::class, TalkResource::class] as $resource) {
$this->assertTrue($resource::canViewAny());
$this->assertFalse($resource::canCreate());
}
}
#[Test]
public function deleting_a_client_leaves_its_projects_standing(): void
{
$client = Client::factory()->create();
$project = Project::factory()->create(['client_id' => $client->id]);
$client->forceDelete();
// The work happened whether or not the organisation is still on file.
$this->assertNotNull($project->fresh());
$this->assertNull($project->fresh()->client_id);
}
#[Test]
public function a_talk_shows_on_the_profile_of_whoever_gave_it(): void
{
$talk = Talk::factory()->create();
$member = Member::factory()->create();
$talk->update(['member_id' => $member->id]);
// Written once, it appears in both places instead of being typed
// twice and drifting.
$this->assertSame(1, $member->talks()->count());
$this->assertSame($talk->id, $member->talks()->sole()->id);
}
#[Test]
public function a_service_knows_how_much_work_backs_it(): void
{
$service = Service::factory()->create();
$this->assertSame(0, $service->projects()->count());
$service->projects()->attach(Project::factory()->published()->create()->id);
// The number the listing shows in orange when it is zero: a service
// with nothing behind it is a promise.
$this->assertSame(1, $service->projects()->count());
}
}