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:
153
tests/Feature/Media/ProjectGalleryTest.php
Normal file
153
tests/Feature/Media/ProjectGalleryTest.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Media;
|
||||
|
||||
use App\Filament\Resources\Projects\ProjectResource;
|
||||
use App\Models\ImageAttachment;
|
||||
use App\Models\Media;
|
||||
use App\Models\Project;
|
||||
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;
|
||||
|
||||
/**
|
||||
* More than one image on a project: the screenshots and the before-and-after
|
||||
* that make a write-up worth reading.
|
||||
*/
|
||||
class ProjectGalleryTest extends TestCase
|
||||
{
|
||||
use FakesPireon, RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withHeaders(['Accept-Language' => 'es']);
|
||||
Queue::fake();
|
||||
}
|
||||
|
||||
protected function projectWithGallery(): Project
|
||||
{
|
||||
$project = Project::factory()->published()->create([
|
||||
'title' => ['es' => 'Mi proyecto'],
|
||||
'slug' => ['es' => 'mi-proyecto'],
|
||||
]);
|
||||
|
||||
$project->images()->createMany([
|
||||
[
|
||||
'path' => 'projects/gallery/after.jpg',
|
||||
'alt' => ['es' => 'El panel nuevo'],
|
||||
'caption' => ['es' => 'Después'],
|
||||
'position' => 2,
|
||||
],
|
||||
[
|
||||
'path' => 'projects/gallery/before.jpg',
|
||||
'alt' => ['es' => 'La hoja de cálculo que usaban'],
|
||||
'caption' => ['es' => 'Antes'],
|
||||
'position' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_gallery_comes_back_in_the_order_it_was_arranged(): void
|
||||
{
|
||||
$this->projectWithGallery();
|
||||
|
||||
$gallery = $this->getJson('/api/public/v1/projects/mi-proyecto')
|
||||
->assertOk()
|
||||
->json('data.gallery');
|
||||
|
||||
// An unordered gallery is a different gallery every time it is read,
|
||||
// and "before" after "after" tells the wrong story.
|
||||
$this->assertSame(['Antes', 'Después'], array_column($gallery, 'caption'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function each_image_carries_everything_needed_to_render_it(): void
|
||||
{
|
||||
$this->projectWithGallery();
|
||||
|
||||
Media::query()->where('path', 'projects/gallery/before.jpg')->sole()->forceFill([
|
||||
'width' => 1200,
|
||||
'height' => 800,
|
||||
'variants' => [
|
||||
['format' => 'webp', 'width' => 400, 'path' => 'projects/gallery/before-400.webp', 'size' => 8000],
|
||||
],
|
||||
'processed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$first = $this->getJson('/api/public/v1/projects/mi-proyecto')
|
||||
->assertOk()
|
||||
->json('data.gallery.0');
|
||||
|
||||
$this->assertStringStartsWith('http', $first['url']);
|
||||
$this->assertSame(1200, $first['width']);
|
||||
$this->assertSame('La hoja de cálculo que usaban', $first['alt']);
|
||||
$this->assertStringContainsString('400w', $first['srcset']['webp']);
|
||||
|
||||
// A caption is not a description: one is shown to everyone, the other
|
||||
// is read by whoever cannot see the image.
|
||||
$this->assertSame('Antes', $first['caption']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_listing_does_not_carry_every_screenshot_of_every_project(): void
|
||||
{
|
||||
$this->projectWithGallery();
|
||||
|
||||
$project = $this->getJson('/api/public/v1/projects')
|
||||
->assertOk()
|
||||
->json('data.0');
|
||||
|
||||
$this->assertArrayNotHasKey('gallery', $project);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function adding_a_gallery_image_registers_it_like_any_other_upload(): void
|
||||
{
|
||||
$project = Project::factory()->create();
|
||||
|
||||
$project->images()->create(['path' => 'projects/gallery/new.jpg']);
|
||||
|
||||
// Nothing special about a gallery image: same media row, same derived
|
||||
// versions, same queue.
|
||||
$this->assertSame(1, Media::query()->where('path', 'projects/gallery/new.jpg')->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function erasing_a_project_for_good_takes_its_gallery_with_it(): void
|
||||
{
|
||||
$project = $this->projectWithGallery();
|
||||
|
||||
$project->delete();
|
||||
|
||||
// A soft delete keeps it — that is the point of a soft delete.
|
||||
$this->assertSame(2, ImageAttachment::query()->count());
|
||||
|
||||
$project->forceDelete();
|
||||
|
||||
// A morph relation has no foreign key to cascade for it, so this is
|
||||
// handled explicitly and worth pinning down.
|
||||
$this->assertSame(0, ImageAttachment::query()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_gallery_editor_renders(): void
|
||||
{
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
$project = $this->projectWithGallery();
|
||||
|
||||
$this->actingAs($this->userWithRoles('admin'));
|
||||
|
||||
$this->get(ProjectResource::getUrl('edit', ['record' => $project]))
|
||||
->assertSuccessful();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user