Track original artwork file availability more accurately.

Resolve local and object-storage originals, persist download status, and audit missing files without guessing from a single path.
This commit is contained in:
test
2026-09-20 14:49:19 +02:00
parent 366dbf1de8
commit 455b514d50
7 changed files with 599 additions and 38 deletions
+79 -5
View File
@@ -6,6 +6,7 @@ use App\Models\Artwork;
use App\Models\User;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
beforeEach(function () {
$root = storage_path('framework/testing/artwork-downloads');
@@ -44,7 +45,7 @@ function makeOriginalFile(string $hash, string $ext, string $content = 'test-ima
}
it('downloads an existing artwork file', function () {
$hash = 'a9f3e6c1b8';
$hash = 'a9f3e6c1b8a9f3e6c1b8a9f3e6c1b8a9f3e6c1b8';
$ext = 'png';
makeOriginalFile($hash, $ext);
@@ -61,7 +62,7 @@ it('downloads an existing artwork file', function () {
});
it('forces the download filename using file_name and extension', function () {
$hash = 'b7c4d1e2f3';
$hash = 'b7c4d1e2f3b7c4d1e2f3b7c4d1e2f3b7c4d1e2f3';
$ext = 'jpg';
makeOriginalFile($hash, $ext);
@@ -77,6 +78,79 @@ it('forces the download filename using file_name and extension', function () {
$response->assertDownload('My Original Name (skinbase.org).jpg');
});
it('downloads an object-only original without restoring it locally', function () {
config(['uploads.object_storage.disk' => 's3']);
$hash = '1234567890abcdef1234567890abcdef12345678';
$key = "artworks/original/12/34/{$hash}.jpg";
$disk = Mockery::mock();
$disk->shouldReceive('exists')->once()->with($key)->andReturnTrue();
$disk->shouldReceive('size')->once()->with($key)->andReturn(15);
$disk->shouldReceive('temporaryUrl')
->once()
->withArgs(fn (string $path, \DateTimeInterface $expires, array $options): bool =>
$path === $key
&& $expires > now()
&& str_contains((string) ($options['ResponseContentDisposition'] ?? ''), 'Object Original')
)
->andReturn('https://object.example.test/signed-object');
$disk->shouldNotReceive('readStream');
$disk->shouldNotReceive('get');
Storage::shouldReceive('disk')->with('s3')->andReturn($disk);
$artwork = Artwork::factory()->create([
'file_name' => 'Object Original',
'file_path' => '',
'hash' => $hash,
'file_ext' => 'jpg',
]);
$response = $this->get("/download/artwork/{$artwork->id}");
$response->assertRedirect('https://object.example.test/signed-object');
expect($artwork->fresh()->download_status)->toBe('available')
->and($artwork->fresh()->download_source)->toBe('object')
->and(DB::table('artwork_downloads')->where('artwork_id', $artwork->id)->count())->toBe(1);
});
it('does not count an object download when signing fails', function () {
config(['uploads.object_storage.disk' => 's3']);
$hash = 'abcdefabcdefabcdefabcdefabcdefabcdefabcd';
$key = "artworks/original/ab/cd/{$hash}.jpg";
$disk = Mockery::mock();
$disk->shouldReceive('exists')->once()->with($key)->andReturnTrue();
$disk->shouldReceive('size')->once()->with($key)->andReturn(15);
$disk->shouldReceive('temporaryUrl')->once()->andThrow(new RuntimeException('signing unavailable'));
Storage::shouldReceive('disk')->with('s3')->andReturn($disk);
$artwork = Artwork::factory()->create([
'file_name' => 'Signing Failure',
'file_path' => '',
'hash' => $hash,
'file_ext' => 'jpg',
]);
$this->mock(\App\Services\ArtworkStatsService::class, function ($mock): void {
$mock->shouldReceive('incrementDownloads')->never();
});
$this->get("/download/artwork/{$artwork->id}")->assertNotFound();
});
it('classifies incomplete original metadata as pending', function () {
$artwork = Artwork::factory()->create([
'file_name' => 'pending',
'file_path' => '',
'hash' => null,
'file_ext' => null,
]);
expect(app(\App\Services\ArtworkOriginalFileLocator::class)->resolve($artwork))->toMatchArray([
'status' => 'pending',
'source' => null,
'exists' => false,
]);
});
it('returns 404 for a missing artwork', function () {
$this->get('/download/artwork/999999')->assertNotFound();
});
@@ -91,7 +165,7 @@ it('returns 404 when the original file is missing', function () {
});
it('logs download metadata with user and request context', function () {
$hash = 'd4e5f6a7b8';
$hash = 'd4e5f6a7b8d4e5f6a7b8d4e5f6a7b8d4e5f6a7b8';
$ext = 'gif';
makeOriginalFile($hash, $ext);
@@ -119,7 +193,7 @@ it('logs download metadata with user and request context', function () {
});
it('logs guest download with null user_id', function () {
$hash = 'e1f2a3b4c5';
$hash = 'e1f2a3b4c5e1f2a3b4c5e1f2a3b4c5e1f2a3b4c5';
$ext = 'png';
makeOriginalFile($hash, $ext);
@@ -137,7 +211,7 @@ it('logs guest download with null user_id', function () {
});
it('increments artwork_stats downloads on the real download route', function () {
$hash = 'f1e2d3c4b5';
$hash = 'f1e2d3c4b5f1e2d3c4b5f1e2d3c4b5f1e2d3c4b5';
$ext = 'png';
makeOriginalFile($hash, $ext);