Browse and clean up the media library through the API
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ use App\Models\Application;
|
||||
use App\Models\Call;
|
||||
use App\Models\Category;
|
||||
use App\Models\Client;
|
||||
use App\Models\Media;
|
||||
use App\Models\Member;
|
||||
use App\Models\MissingUrl;
|
||||
use App\Models\Page;
|
||||
@@ -90,6 +91,7 @@ class AdminApiContractTest extends TestCase
|
||||
'redirects' => ['path' => '/v1/redirects'],
|
||||
'missing-urls' => ['path' => '/v1/missing-urls'],
|
||||
'repositories' => ['path' => '/v1/repositories'],
|
||||
'media' => ['path' => '/v1/media'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -111,6 +113,7 @@ class AdminApiContractTest extends TestCase
|
||||
$missingUrl = MissingUrl::factory()->create();
|
||||
$repository = Repository::factory()->create();
|
||||
Setting::set(Maintenance::KEY, ['enabled' => false]);
|
||||
$media = Media::factory()->create();
|
||||
|
||||
// One request, one documented path — the URL a real caller hits and
|
||||
// the path key that describes it, side by side rather than derived
|
||||
@@ -148,6 +151,8 @@ class AdminApiContractTest extends TestCase
|
||||
'settings' => ['/v1/settings', '/v1/settings'],
|
||||
'setting' => ['/v1/settings/'.Maintenance::KEY, '/v1/settings/{setting}'],
|
||||
'maintenance' => ['/v1/maintenance', '/v1/maintenance'],
|
||||
'media' => ['/v1/media', '/v1/media'],
|
||||
'medium' => ["/v1/media/{$media->id}", '/v1/media/{media}'],
|
||||
'users' => ['/v1/users', '/v1/users'],
|
||||
'user' => ["/v1/users/{$this->caller->id}", '/v1/users/{user}'],
|
||||
'me' => ['/v1/me', '/v1/me'],
|
||||
|
||||
133
tests/Feature/Api/MediaLibraryApiTest.php
Normal file
133
tests/Feature/Api/MediaLibraryApiTest.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Jobs\ProcessMedia;
|
||||
use App\Models\Media;
|
||||
use App\Models\Page;
|
||||
use App\Models\Technology;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FakesPireon;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The media library through the admin API: what is on the disk, what uses it,
|
||||
* and deleting what nothing does.
|
||||
*/
|
||||
class MediaLibraryApiTest extends TestCase
|
||||
{
|
||||
use FakesPireon, RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
Storage::fake((string) config('media.disk'));
|
||||
Queue::fake();
|
||||
}
|
||||
|
||||
protected function actingWithRole(string $role): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->syncRoles([$role]);
|
||||
|
||||
$this->withHeaders($this->apiHeadersFor($user));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function stored(string $path): Media
|
||||
{
|
||||
Storage::disk((string) config('media.disk'))->put($path, 'x');
|
||||
|
||||
return Media::for($path);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_library_says_what_is_in_use(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$logo = $this->stored('technologies/angular.svg');
|
||||
$orphan = $this->stored('pages/forgotten.jpg');
|
||||
|
||||
Technology::factory()->create(['logo' => $logo->path]);
|
||||
|
||||
$this->getJson('/api/v1/media')
|
||||
->assertOk()
|
||||
->assertJsonCount(2, 'data')
|
||||
->assertJsonPath('data.0.path', 'pages/forgotten.jpg')
|
||||
->assertJsonPath('data.0.referenced', false)
|
||||
->assertJsonPath('data.1.referenced', true);
|
||||
|
||||
$this->getJson('/api/v1/media?orphans=1')
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $orphan->id);
|
||||
|
||||
$this->getJson('/api/v1/media?for=technologies')
|
||||
->assertJsonCount(1, 'data')
|
||||
->assertJsonPath('data.0.id', $logo->id);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function an_image_nothing_uses_is_deleted_with_its_file(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$orphan = $this->stored('pages/forgotten.jpg');
|
||||
|
||||
$this->deleteJson("/api/v1/media/{$orphan->id}")->assertNoContent();
|
||||
|
||||
$this->assertModelMissing($orphan);
|
||||
Storage::disk((string) config('media.disk'))->assertMissing('pages/forgotten.jpg');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function an_image_in_use_is_not_deleted(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$image = $this->stored('pages/hero.jpg');
|
||||
Page::factory()->create(['featured_image' => $image->path]);
|
||||
|
||||
$this->deleteJson("/api/v1/media/{$image->id}")->assertConflict();
|
||||
|
||||
$this->assertModelExists($image);
|
||||
Storage::disk((string) config('media.disk'))->assertExists('pages/hero.jpg');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_writer_sees_the_library_but_deletes_nothing(): void
|
||||
{
|
||||
$this->actingWithRole('writer');
|
||||
|
||||
$orphan = $this->stored('pages/forgotten.jpg');
|
||||
|
||||
$this->getJson('/api/v1/media')->assertOk();
|
||||
$this->getJson("/api/v1/media/{$orphan->id}")->assertOk()->assertJsonPath('data.referenced', false);
|
||||
|
||||
$this->deleteJson("/api/v1/media/{$orphan->id}")->assertForbidden();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function an_image_is_processed_again_on_request(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$image = $this->stored('pages/hero.jpg');
|
||||
Queue::fake();
|
||||
|
||||
$this->postJson("/api/v1/media/{$image->id}/reprocess")->assertAccepted();
|
||||
|
||||
Queue::assertPushed(ProcessMedia::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user