fix(tests): test against real shipped migrations; harden warehouse asset lookup

The suite silently exercised a drifted workbench schema (bigint id()/morphs())
instead of the shipped uuid('id')/uuidMorphs() schema, so every UUID-keyed test
errored with SQLite "datatype mismatch" (39/104 errors). Add a shared
tests/TestCase that loads the package's real database/migrations as the single
source of truth (drift-proof) and centralizes the per-test boilerplate; fix one
ordering test that violated the real filables_unique constraint.

WarehouseService::searchAssetPath now clears the realpath/stat cache and retries
once on a miss, so an asset written by another process (image command, queue
worker) is servable without a restart. Hits are unaffected.

Suite: 105 tests, 235 assertions, green.
This commit is contained in:
Fabian @ Blax Software 2026-06-07 11:37:48 +02:00
parent 5155815043
commit 2ee680cba1
7 changed files with 124 additions and 180 deletions

View File

@ -71,8 +71,31 @@ class WarehouseService
/**
* Search for a static asset, trying preferred extensions.
*
* On a miss the realpath/stat cache is cleared and the lookup retried once:
* an asset can be written by another process (an image-generation command,
* a queue worker) *after* this PHP process last stat()'d the path, so a
* stale negative cache entry would otherwise 404 a file that exists on disk
* until the process restarts. Only misses pay this cost hits return on the
* first attempt and never reach the retry.
*/
protected static function searchAssetPath(string $path): ?File
{
$found = static::resolveAssetPath($path);
if ($found) {
return $found;
}
clearstatcache(true);
return static::resolveAssetPath($path);
}
/**
* Resolve an asset path to a (non-persisted) File: exact path first, then
* the configured preferred extensions when the path carries none.
*/
protected static function resolveAssetPath(string $path): ?File
{
$disk = config('files.disk', 'local');
$extensions = config('files.optimization.preferred_extensions', ['svg', 'webp', 'png', 'jpg', 'jpeg']);

66
tests/TestCase.php Normal file
View File

@ -0,0 +1,66 @@
<?php
namespace Blax\Files\Tests;
use Blax\Files\FilesServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
use Orchestra\Testbench\TestCase as Orchestra;
/**
* Shared base test case for the package suite.
*
* The single most important thing this class does is load the package's
* REAL shipped migrations (database/migrations) rather than a hand-copied
* duplicate. The workbench used to ship its own create_blax_file_tables
* migration which drifted from the shipped schema (bigint `id()`/`morphs()`
* vs the real `uuid('id')`/`uuidMorphs()`), so every UUID-keyed test was
* silently exercising the wrong schema and erroring with SQLite "datatype
* mismatch". Loading the shipped migrations makes the suite a faithful
* mirror of what consumers actually get, and makes that class of drift
* impossible to reintroduce.
*/
abstract class TestCase extends Orchestra
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [FilesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
// Needed by anything exercising encrypt()/decrypt() (e.g. encrypted
// warehouse IDs). Harmless for the rest.
$app['config']->set('app.key', 'base64:' . base64_encode(random_bytes(32)));
// The suite loads the shipped migrations explicitly (see
// defineDatabaseMigrations); disable the provider's auto-load so the
// same migration files aren't registered from two paths.
$app['config']->set('files.run_migrations', false);
}
protected function defineDatabaseMigrations(): void
{
// Host-app tables (users, articles) used as morph targets in tests.
$this->loadMigrationsFrom(__DIR__ . '/../workbench/database/migrations');
// The package's actual shipped schema — the single source of truth.
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
}
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
}
}

View File

@ -2,43 +2,14 @@
namespace Blax\Files\Tests\Unit;
use Blax\Files\FilesServiceProvider;
use Blax\Files\Models\File;
use Blax\Files\Services\ChunkUploadService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Blax\Files\Tests\TestCase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Orchestra\Testbench\TestCase;
class ChunkUploadServiceTest extends TestCase
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [FilesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
}
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
}
// ─── initialize ────────────────────────────────────────────────

View File

@ -3,42 +3,13 @@
namespace Blax\Files\Tests\Unit;
use Blax\Files\Enums\FileLinkType;
use Blax\Files\FilesServiceProvider;
use Blax\Files\Models\Filable;
use Blax\Files\Models\File;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Blax\Files\Tests\TestCase;
use Illuminate\Support\Facades\Storage;
use Orchestra\Testbench\TestCase;
class FilableModelTest extends TestCase
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [FilesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
}
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
}
// ─── scopeAs ───────────────────────────────────────────────────
@ -188,36 +159,26 @@ class FilableModelTest extends TestCase
public function test_default_ordering_by_order_column()
{
$file = File::create([
'name' => 'test',
'extension' => 'png',
'type' => 'image/png',
'disk' => 'local',
]);
// Three DISTINCT files attached to the same host as 'gallery'. They must
// be distinct files so they don't collide on the real
// (file_id, filable_type, filable_id, as) unique constraint; inserted
// out of order to prove the global `ordered` scope sorts ascending.
foreach ([3, 1, 2] as $order) {
$file = File::create([
'name' => 'test',
'extension' => 'png',
'type' => 'image/png',
'disk' => 'local',
]);
Filable::create([
'file_id' => $file->id,
'filable_id' => 1,
'filable_type' => 'App\Models\User',
'as' => 'gallery',
'order' => 3,
]);
Filable::create([
'file_id' => $file->id,
'filable_id' => 1,
'filable_type' => 'App\Models\User',
'as' => 'gallery',
'order' => 1,
]);
Filable::create([
'file_id' => $file->id,
'filable_id' => 1,
'filable_type' => 'App\Models\User',
'as' => 'gallery',
'order' => 2,
]);
Filable::create([
'file_id' => $file->id,
'filable_id' => 1,
'filable_type' => 'App\Models\User',
'as' => 'gallery',
'order' => $order,
]);
}
$filables = Filable::where('filable_type', 'App\Models\User')->get();
$this->assertEquals(1, $filables[0]->order);

View File

@ -3,41 +3,12 @@
namespace Blax\Files\Tests\Unit;
use Blax\Files\Enums\FileLinkType;
use Blax\Files\FilesServiceProvider;
use Blax\Files\Models\File;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Blax\Files\Tests\TestCase;
use Illuminate\Support\Facades\Storage;
use Orchestra\Testbench\TestCase;
class FileModelTest extends TestCase
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [FilesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
}
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
}
// ─── Creation & UUID ───────────────────────────────────────────

View File

@ -3,44 +3,15 @@
namespace Blax\Files\Tests\Unit;
use Blax\Files\Enums\FileLinkType;
use Blax\Files\FilesServiceProvider;
use Blax\Files\Models\Filable;
use Blax\Files\Models\File;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Blax\Files\Tests\TestCase;
use Illuminate\Support\Facades\Storage;
use Orchestra\Testbench\TestCase;
use Workbench\App\Models\Article;
use Workbench\App\Models\User;
class HasFilesTest extends TestCase
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [FilesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
}
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
}
// ─── files() relationship ──────────────────────────────────────

View File

@ -2,43 +2,13 @@
namespace Blax\Files\Tests\Unit;
use Blax\Files\FilesServiceProvider;
use Blax\Files\Models\File;
use Blax\Files\Services\WarehouseService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Blax\Files\Tests\TestCase;
use Illuminate\Support\Facades\Storage;
use Orchestra\Testbench\TestCase;
class WarehouseServiceTest extends TestCase
{
use RefreshDatabase;
protected function getPackageProviders($app): array
{
return [FilesServiceProvider::class];
}
protected function defineEnvironment($app): void
{
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
$app['config']->set('app.key', 'base64:' . base64_encode(random_bytes(32)));
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../../workbench/database/migrations');
}
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
}
// ─── searchFile — UUID lookup ──────────────────────────────────
@ -140,6 +110,17 @@ class WarehouseServiceTest extends TestCase
$this->assertEquals('svg', $result->extension);
}
public function test_search_returns_null_when_asset_genuinely_missing()
{
// Exercises the clearstatcache-and-retry branch in searchAssetPath:
// both resolution attempts miss, so the method must cleanly return null
// (no loop, no error) and let searchFile fall through to storage lookup.
$request = new \Illuminate\Http\Request;
$result = WarehouseService::searchFile($request, 'images/does-not-exist');
$this->assertNull($result);
}
// ─── searchFile — storage path ─────────────────────────────────
public function test_search_finds_by_storage_path()