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>
199 lines
6.2 KiB
PHP
199 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Panel;
|
|
|
|
use App\Enums\PageStatus;
|
|
use App\Filament\Resources\Pages\PageResource;
|
|
use App\Models\Page;
|
|
use App\Models\Project;
|
|
use App\Models\Revision;
|
|
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;
|
|
|
|
/**
|
|
* Who changed what.
|
|
*
|
|
* The gap this fills is publishing above all: `pages.publish` is a separate
|
|
* permission precisely because putting something on the public site is a
|
|
* bigger act than writing it, and nothing recorded who did it.
|
|
*/
|
|
class RevisionsTest extends TestCase
|
|
{
|
|
use FakesPireon, RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->seed(RolePermissionSeeder::class);
|
|
Queue::fake();
|
|
}
|
|
|
|
#[Test]
|
|
public function creating_something_is_recorded(): void
|
|
{
|
|
$page = Page::factory()->create();
|
|
|
|
$this->assertSame('created', $page->revisions()->sole()->event);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_records_who_made_the_change_by_name(): void
|
|
{
|
|
$user = $this->userWithRoles('admin', ['name' => 'María']);
|
|
$this->actingAs($user);
|
|
|
|
$page = Page::factory()->create();
|
|
$page->update(['title' => ['es' => 'Nuevo título']]);
|
|
|
|
$revision = $page->revisions()->where('event', 'updated')->sole();
|
|
|
|
$this->assertSame($user->id, $revision->user_id);
|
|
|
|
// The name is kept beside the id because people leave and accounts get
|
|
// deleted, and "somebody published this" is worth less than a name.
|
|
$this->assertSame('María', $revision->user_name);
|
|
$this->assertSame('María', $revision->author());
|
|
}
|
|
|
|
#[Test]
|
|
public function a_change_made_by_no_one_says_so(): void
|
|
{
|
|
$page = Page::factory()->create();
|
|
$page->update(['title' => ['es' => 'Cambiado por un comando']]);
|
|
|
|
// The scheduler and the console have no user, and blaming the last
|
|
// person who logged in would be a lie.
|
|
$this->assertNull($page->revisions()->first()->user_id);
|
|
$this->assertSame(__('panel.revisions.system'), $page->revisions()->first()->author());
|
|
}
|
|
|
|
#[Test]
|
|
public function publishing_reads_differently_from_editing(): void
|
|
{
|
|
$this->actingAs($this->userWithRoles('admin'));
|
|
|
|
$page = Page::factory()->create([
|
|
'status' => PageStatus::Draft,
|
|
'published_at' => null,
|
|
]);
|
|
|
|
$page->update(['status' => PageStatus::Published, 'published_at' => now()->subMinute()]);
|
|
|
|
$this->assertSame('published', $page->revisions()->first()->event);
|
|
|
|
$page->update(['status' => PageStatus::Draft]);
|
|
|
|
// A history where publishing looks like any other save hides exactly
|
|
// the thing somebody came looking for.
|
|
$this->assertSame('unpublished', $page->revisions()->first()->event);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_keeps_the_old_value_beside_the_new_one(): void
|
|
{
|
|
$page = Page::factory()->create(['title' => ['es' => 'Antes']]);
|
|
|
|
$page->update(['title' => ['es' => 'Después']]);
|
|
|
|
$revision = $page->revisions()->where('event', 'updated')->sole();
|
|
$diff = $revision->diff('title');
|
|
|
|
$this->assertSame(['es' => 'Antes'], $diff['old']);
|
|
$this->assertSame(['es' => 'Después'], $diff['new']);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_records_only_what_changed(): void
|
|
{
|
|
$page = Page::factory()->create(['title' => ['es' => 'Uno'], 'is_featured' => false]);
|
|
|
|
$page->update(['is_featured' => true]);
|
|
|
|
$revision = $page->revisions()->where('event', 'updated')->sole();
|
|
|
|
// Storing whole records would make the history larger than the content
|
|
// it describes.
|
|
$this->assertSame(['is_featured'], $revision->fields());
|
|
}
|
|
|
|
#[Test]
|
|
public function saving_without_changing_anything_records_nothing(): void
|
|
{
|
|
$page = Page::factory()->create();
|
|
$before = $page->revisions()->count();
|
|
|
|
$page->update(['title' => $page->getTranslations('title')]);
|
|
|
|
$this->assertSame($before, $page->revisions()->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function a_previous_state_can_be_put_back(): void
|
|
{
|
|
$page = Page::factory()->create(['title' => ['es' => 'Original']]);
|
|
$page->update(['title' => ['es' => 'Equivocado']]);
|
|
|
|
$revision = $page->revisions()->where('event', 'updated')->sole();
|
|
|
|
$page->forceFill($revision->previousValues())->save();
|
|
|
|
$this->assertSame('Original', $page->fresh()->getTranslation('title', 'es'));
|
|
|
|
// Restoring is itself a change, so the way back is never lost.
|
|
$this->assertSame('updated', $page->fresh()->revisions()->first()->event);
|
|
}
|
|
|
|
#[Test]
|
|
public function deleting_and_erasing_are_told_apart(): void
|
|
{
|
|
$page = Page::factory()->create();
|
|
|
|
$page->delete();
|
|
$this->assertSame('deleted', $page->revisions()->first()->event);
|
|
|
|
$page->restore();
|
|
$this->assertSame('restored', $page->revisions()->first()->event);
|
|
|
|
$page->forceDelete();
|
|
|
|
// The record is gone; its history says what happened to it.
|
|
$this->assertSame('erased', Revision::query()
|
|
->where('revisionable_type', $page->getMorphClass())
|
|
->where('revisionable_id', $page->id)
|
|
// By id, not by timestamp: these all land in the same second.
|
|
->orderByDesc('id')
|
|
->first()
|
|
->event);
|
|
}
|
|
|
|
#[Test]
|
|
public function the_history_shows_on_the_record(): void
|
|
{
|
|
$this->actingAs($this->userWithRoles('admin'));
|
|
|
|
$page = Page::factory()->create();
|
|
$page->update(['title' => ['es' => 'Cambiado']]);
|
|
|
|
$this->get(PageResource::getUrl('edit', ['record' => $page]))->assertSuccessful();
|
|
}
|
|
|
|
#[Test]
|
|
public function every_content_type_keeps_one(): void
|
|
{
|
|
// Pinned: the factory randomises this one, and updating a value to
|
|
// what it already was is correctly not a change.
|
|
$project = Project::factory()->create(['is_featured' => false]);
|
|
|
|
$project->update(['is_featured' => true]);
|
|
|
|
$this->assertSame(2, $project->revisions()->count());
|
|
}
|
|
}
|