3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/tests/Feature/FileUploads/UploadDeleteStorageFailureTest.php
2026-08-02 12:45:43 +01:00

101 lines
3.6 KiB
PHP

<?php
namespace Tests\Feature\FileUploads;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
/**
* Regression coverage for Christopher Finks (christopherfi-dev) Issue 7:
* both UploadedFilesController and Api\UploadedFilesController called
* Storage::delete() without checking the return value, then created an
* "upload deleted" action log entry and returned success unconditionally.
* HasUploads::uploads excludes rows whose filename matches an "upload
* deleted" log, so a silently failed physical delete produced: file still
* on disk, action log states it's gone, admin sees a success response, and
* the file is invisible through the ordinary UI listing.
*
* The fix checks Storage::delete's return and refuses to create the
* deletion log (or return success) when the physical delete failed.
*/
class UploadDeleteStorageFailureTest extends TestCase
{
private function seedAssetWithUpload(User $actor): array
{
$asset = Asset::factory()->create();
// Upload a real file so a corresponding "uploaded" action log
// exists for the destroy path to find.
$this->actingAs($actor)
->post(route('ui.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create('test.pdf', 10)],
])
->assertRedirect();
$log = Actionlog::where('item_type', Asset::class)
->where('item_id', $asset->id)
->where('action_type', 'uploaded')
->latest()
->first();
return [$asset, $log];
}
private function mockStorageToFailDelete(): void
{
// Storage::exists returns true (file is present), Storage::delete
// returns false (silent-fail). All other calls pass through.
Storage::fake();
$default = Storage::disk();
Storage::shouldReceive('exists')->andReturn(true);
Storage::shouldReceive('delete')->andReturn(false);
Storage::shouldReceive('disk')->andReturn($default);
Storage::shouldReceive('makeDirectory')->andReturnUsing(fn (...$a) => $default->makeDirectory(...$a));
}
public function test_web_delete_does_not_log_deletion_when_physical_delete_fails(): void
{
$user = User::factory()->superuser()->create();
[$asset, $log] = $this->seedAssetWithUpload($user);
$this->mockStorageToFailDelete();
$response = $this->actingAs($user)
->delete(route('ui.files.destroy', ['object_type' => 'assets', 'id' => $asset->id, 'file_id' => $log->id]));
$response->assertSessionHas('error');
$this->assertDatabaseMissing('action_logs', [
'item_type' => Asset::class,
'item_id' => $asset->id,
'action_type' => 'upload deleted',
'filename' => $log->filename,
]);
}
public function test_api_delete_does_not_log_deletion_when_physical_delete_fails(): void
{
$user = User::factory()->superuser()->create();
[$asset, $log] = $this->seedAssetWithUpload($user);
$this->mockStorageToFailDelete();
$response = $this->actingAsForApi($user)
->deleteJson(route('api.files.destroy', ['object_type' => 'assets', 'id' => $asset->id, 'file_id' => $log->id]));
$response->assertStatus(500);
$this->assertDatabaseMissing('action_logs', [
'item_type' => Asset::class,
'item_id' => $asset->id,
'action_type' => 'upload deleted',
'filename' => $log->filename,
]);
}
}