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:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user