Manage redirects, missing URLs, repositories and settings through the API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:51:00 +02:00
parent 4c65604f04
commit 8d0f2e3639
20 changed files with 1581 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api;
use App\Enums\MissingUrlStatus;
use App\Jobs\SyncRepository;
use App\Models\MissingUrl;
use App\Models\Repository;
use App\Models\Setting;
use App\Models\User;
use App\Support\ContentVersion;
use App\Support\Maintenance;
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;
/**
* Redirects, missing URLs, repositories, settings and maintenance through the
* admin API — the panel's administration screens.
*/
class SiteAdministrationApiTest extends TestCase
{
use FakesPireon, RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
}
protected function actingWithRole(string $role): User
{
$user = User::factory()->create();
$user->syncRoles([$role]);
$this->withHeaders($this->apiHeadersFor($user));
return $user;
}
#[Test]
public function a_redirect_is_normalised_and_closes_the_report_it_answers(): void
{
$this->actingWithRole('editor');
MissingUrl::report('/proyectos/viejo', 'es');
$this->postJson('/api/v1/redirects', [
'from' => 'proyectos/viejo/',
'to' => '/proyectos/nuevo?x=1',
])
->assertCreated()
->assertJsonPath('data.from', '/proyectos/viejo')
->assertJsonPath('data.to', '/proyectos/nuevo')
->assertJsonPath('data.status', 301)
->assertJsonPath('data.automatic', false);
$this->assertSame(MissingUrlStatus::Resolved, MissingUrl::query()->sole()->status);
// The same address in the same language is one rule, not two.
$this->postJson('/api/v1/redirects', ['from' => '/proyectos/viejo', 'to' => '/otra'])
->assertUnprocessable()
->assertJsonValidationErrors('from');
$this->postJson('/api/v1/redirects', ['from' => '/a', 'to' => '/a/'])
->assertUnprocessable()
->assertJsonValidationErrors('to');
}
#[Test]
public function a_missing_url_is_given_a_destination(): void
{
$this->actingWithRole('editor');
MissingUrl::report('/en/old', 'en');
$missing = MissingUrl::query()->sole();
$this->getJson('/api/v1/missing-urls')->assertJsonCount(1, 'data');
$this->postJson("/api/v1/missing-urls/{$missing->id}/redirect", ['to' => '/en/new'])
->assertCreated()
->assertJsonPath('data.from', '/en/old')
->assertJsonPath('data.locale', 'en');
$this->assertSame(MissingUrlStatus::Resolved, $missing->fresh()->status);
$this->getJson('/api/v1/missing-urls')->assertJsonCount(0, 'data');
$this->postJson("/api/v1/missing-urls/{$missing->id}/redirect", ['to' => '/en/other'])
->assertUnprocessable()
->assertJsonValidationErrors('locale');
}
#[Test]
public function a_missing_url_can_be_ignored_but_not_declared_resolved(): void
{
$this->actingWithRole('editor');
$missing = MissingUrl::factory()->create();
$this->patchJson("/api/v1/missing-urls/{$missing->id}", ['status' => 'ignored'])
->assertOk()
->assertJsonPath('data.status', 'ignored');
$this->patchJson("/api/v1/missing-urls/{$missing->id}", ['status' => 'resolved'])
->assertUnprocessable();
}
#[Test]
public function a_repository_is_added_and_synced_on_the_queue(): void
{
Queue::fake();
$this->actingWithRole('admin');
$id = $this->postJson('/api/v1/repositories', [
'provider' => 'github',
'full_name' => 'pireon/landing',
'show_publicly' => true,
// Synced columns are not the caller's to set.
'stars' => 9999,
])
->assertCreated()
->assertJsonPath('data.full_name', 'pireon/landing')
->assertJsonPath('data.stars', 0)
->json('data.id');
$this->postJson('/api/v1/repositories', ['provider' => 'github', 'full_name' => 'pireon/landing'])
->assertUnprocessable()
->assertJsonValidationErrors('full_name');
$this->postJson("/api/v1/repositories/{$id}/sync")->assertAccepted();
Queue::assertPushed(SyncRepository::class);
}
#[Test]
public function an_editor_reads_repositories_but_cannot_sync_them(): void
{
Queue::fake();
$this->actingWithRole('editor');
$repository = Repository::factory()->create();
$this->getJson('/api/v1/repositories')->assertOk();
$this->postJson("/api/v1/repositories/{$repository->id}/sync")->assertForbidden();
Queue::assertNothingPushed();
}
#[Test]
public function a_setting_is_replaced_whole(): void
{
$this->actingWithRole('admin');
Setting::set('site.contact', ['email' => 'hola@example.org']);
$this->getJson('/api/v1/settings')->assertOk()->assertJsonPath('data.0.key', 'site.contact');
$this->patchJson('/api/v1/settings/site.contact', ['value' => ['email' => 'nuevo@example.org']])
->assertOk()
->assertJsonPath('data.value.email', 'nuevo@example.org');
$this->assertSame(['email' => 'nuevo@example.org'], Setting::get('site.contact'));
}
#[Test]
public function the_content_version_is_not_edited_by_hand(): void
{
$this->actingWithRole('admin');
ContentVersion::bump();
$this->patchJson('/api/v1/settings/'.ContentVersion::KEY, ['value' => 1])->assertForbidden();
}
#[Test]
public function an_editor_sees_settings_but_changes_none(): void
{
$this->actingWithRole('editor');
Setting::set(Maintenance::KEY, ['enabled' => false]);
$this->getJson('/api/v1/settings')->assertOk();
$this->getJson('/api/v1/maintenance')->assertOk()->assertJsonPath('data.enabled', false);
$this->patchJson('/api/v1/settings/'.Maintenance::KEY, ['value' => ['enabled' => true]])->assertForbidden();
$this->putJson('/api/v1/maintenance', ['enabled' => true])->assertForbidden();
$this->assertFalse(Maintenance::isEnabled());
}
#[Test]
public function maintenance_is_switched_on_and_off_keeping_its_wording(): void
{
$this->actingWithRole('admin');
$this->putJson('/api/v1/maintenance', [
'enabled' => true,
'message' => ['es' => 'Volvemos enseguida.', 'en' => ''],
'until' => now()->addHour()->toIso8601String(),
])
->assertOk()
->assertJsonPath('data.enabled', true)
->assertJsonPath('data.message', ['es' => 'Volvemos enseguida.']);
$this->assertTrue(Maintenance::isEnabled());
$this->putJson('/api/v1/maintenance', ['enabled' => false])
->assertOk()
->assertJsonPath('data.enabled', false)
->assertJsonPath('data.until', null)
->assertJsonPath('data.message.es', 'Volvemos enseguida.');
}
}