Review, publish, preview and undo changes through the API

Revisions now call publishing only what publishes: a project's status is
whether the work is active, and a service is published by its flag.

The fake identity server answers by token, so a test can sign in a second
person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 08:29:48 +02:00
parent 4c4a2b3299
commit 5b46436641
18 changed files with 1899 additions and 13 deletions

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api;
use App\Enums\PageStatus;
use App\Models\Page;
use App\Models\Project;
use App\Models\Service;
use App\Models\User;
use App\Support\PreviewLink;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use PHPUnit\Framework\Attributes\Test;
use Tests\FakesPireon;
use Tests\TestCase;
/**
* History, review, publication and preview links through the admin API.
*/
class ReviewAndHistoryApiTest extends TestCase
{
use FakesPireon, RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
Notification::fake();
}
protected function actingWithRole(string $role): User
{
$user = User::factory()->create();
$user->syncRoles([$role]);
$this->withHeaders($this->apiHeadersFor($user));
return $user;
}
#[Test]
public function the_history_lists_each_change_with_who_made_it(): void
{
$editor = $this->actingWithRole('editor');
$project = Project::factory()->create(['status' => 'active']);
$this->patchJson("/api/v1/projects/{$project->id}", ['status' => 'paused'])->assertOk();
$this->getJson("/api/v1/projects/{$project->id}/revisions")
->assertOk()
->assertJsonPath('data.0.event', 'updated')
->assertJsonPath('data.0.author.id', $editor->id)
->assertJsonPath('data.0.changes.status.new', 'paused');
}
#[Test]
public function a_change_is_undone_and_the_undoing_is_recorded(): void
{
$this->actingWithRole('editor');
$service = Service::factory()->create(['tagline' => ['es' => 'Antes']]);
$this->patchJson("/api/v1/services/{$service->id}", ['tagline' => ['es' => 'Después']])->assertOk();
$revision = $service->revisions()->first();
$this->postJson("/api/v1/revisions/{$revision->id}/restore")
->assertOk()
->assertJsonPath('data.changes.tagline.new.es', 'Antes');
$this->assertSame('Antes', $service->fresh()->getTranslation('tagline', 'es'));
$this->assertSame(3, $service->revisions()->count());
}
#[Test]
public function undoing_a_publication_takes_the_publish_permission(): void
{
$page = Page::factory()->draft()->create();
$this->actingAs($editor = User::factory()->create()->syncRoles(['editor']));
$page->update(['status' => PageStatus::Published, 'published_at' => now()]);
$this->actingWithRole('writer');
$this->postJson('/api/v1/revisions/'.$page->revisions()->first()->id.'/restore')
->assertUnprocessable()
->assertJsonValidationErrors('revision');
$this->assertSame(PageStatus::Published, $page->fresh()->status);
}
#[Test]
public function a_writer_asks_for_review_and_an_editor_sees_it_waiting(): void
{
$writer = $this->actingWithRole('writer');
$page = Page::factory()->draft()->create();
$this->postJson("/api/v1/pages/{$page->id}/review", ['note' => 'Lista para revisar.'])
->assertOk()
->assertJsonPath('data.awaiting_review', true)
->assertJsonPath('data.requested_by.id', $writer->id);
$this->assertSame(PageStatus::Review, $page->fresh()->status);
$this->postJson("/api/v1/pages/{$page->id}/review")->assertUnprocessable();
$this->actingWithRole('editor');
$this->getJson('/api/v1/reviews')
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.type', 'pages')
->assertJsonPath('data.0.id', $page->id);
$this->getJson("/api/v1/pages/{$page->id}/reviews")
->assertJsonPath('data.0.event', 'requested')
->assertJsonPath('data.0.body', 'Lista para revisar.');
}
#[Test]
public function an_editor_returns_it_with_a_reason(): void
{
$this->actingAs(User::factory()->create()->syncRoles(['writer']));
$page = Page::factory()->draft()->create();
$page->requestReview();
$this->actingWithRole('editor');
$this->postJson("/api/v1/pages/{$page->id}/return", [])->assertUnprocessable();
$this->postJson("/api/v1/pages/{$page->id}/return", ['note' => 'Falta la imagen.'])
->assertOk()
->assertJsonPath('data.awaiting_review', false);
$this->assertSame(PageStatus::Draft, $page->fresh()->status);
}
#[Test]
public function publishing_later_answers_the_review_now(): void
{
$this->actingAs(User::factory()->create()->syncRoles(['writer']));
$project = Project::factory()->draft()->create();
$project->requestReview();
$this->actingWithRole('editor');
$at = now()->addDays(3)->startOfMinute();
$this->postJson("/api/v1/projects/{$project->id}/publish", ['at' => $at->toIso8601String()])
->assertOk()
->assertJsonPath('data.published', false)
->assertJsonPath('data.awaiting_review', false)
->assertJsonPath('data.published_at', $at->toIso8601String());
$this->postJson("/api/v1/projects/{$project->id}/unpublish")
->assertOk()
->assertJsonPath('data.published_at', null);
}
#[Test]
public function a_writer_cannot_publish_or_return(): void
{
$this->actingWithRole('writer');
$service = Service::factory()->create(['is_published' => false]);
$this->postJson("/api/v1/services/{$service->id}/publish")->assertForbidden();
$this->postJson("/api/v1/services/{$service->id}/return", ['note' => 'x'])->assertForbidden();
$this->assertFalse($service->fresh()->is_published);
}
#[Test]
public function preview_links_open_that_draft_only(): void
{
config(['site.url' => 'https://pireon.test']);
$this->actingWithRole('writer');
$page = Page::factory()->draft()->create();
$links = $this->postJson("/api/v1/pages/{$page->id}/preview")
->assertCreated()
->json('data.links');
$this->assertArrayHasKey('es', $links);
$this->assertStringStartsWith('https://pireon.test/', $links['es']);
parse_str((string) parse_url($links['es'], PHP_URL_QUERY), $query);
$this->assertSame(['type' => 'page', 'id' => $page->id, 'locale' => 'es'], PreviewLink::open($query['preview']));
}
#[Test]
public function comments_join_the_thread(): void
{
$this->actingWithRole('editor');
$page = Page::factory()->create();
$this->postJson("/api/v1/pages/{$page->id}/comments", ['body' => '¿Revisamos el título?'])
->assertCreated()
->assertJsonPath('data.event', 'commented');
}
}