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:
parent
5155815043
commit
2ee680cba1
|
|
@ -71,8 +71,31 @@ class WarehouseService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for a static asset, trying preferred extensions.
|
* 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
|
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');
|
$disk = config('files.disk', 'local');
|
||||||
$extensions = config('files.optimization.preferred_extensions', ['svg', 'webp', 'png', 'jpg', 'jpeg']);
|
$extensions = config('files.optimization.preferred_extensions', ['svg', 'webp', 'png', 'jpg', 'jpeg']);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,43 +2,14 @@
|
||||||
|
|
||||||
namespace Blax\Files\Tests\Unit;
|
namespace Blax\Files\Tests\Unit;
|
||||||
|
|
||||||
use Blax\Files\FilesServiceProvider;
|
|
||||||
use Blax\Files\Models\File;
|
use Blax\Files\Models\File;
|
||||||
use Blax\Files\Services\ChunkUploadService;
|
use Blax\Files\Services\ChunkUploadService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Blax\Files\Tests\TestCase;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Orchestra\Testbench\TestCase;
|
|
||||||
|
|
||||||
class ChunkUploadServiceTest extends 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 ────────────────────────────────────────────────
|
// ─── initialize ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,42 +3,13 @@
|
||||||
namespace Blax\Files\Tests\Unit;
|
namespace Blax\Files\Tests\Unit;
|
||||||
|
|
||||||
use Blax\Files\Enums\FileLinkType;
|
use Blax\Files\Enums\FileLinkType;
|
||||||
use Blax\Files\FilesServiceProvider;
|
|
||||||
use Blax\Files\Models\Filable;
|
use Blax\Files\Models\Filable;
|
||||||
use Blax\Files\Models\File;
|
use Blax\Files\Models\File;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Blax\Files\Tests\TestCase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Orchestra\Testbench\TestCase;
|
|
||||||
|
|
||||||
class FilableModelTest extends 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 ───────────────────────────────────────────────────
|
// ─── scopeAs ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -188,6 +159,11 @@ class FilableModelTest extends TestCase
|
||||||
|
|
||||||
public function test_default_ordering_by_order_column()
|
public function test_default_ordering_by_order_column()
|
||||||
{
|
{
|
||||||
|
// 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([
|
$file = File::create([
|
||||||
'name' => 'test',
|
'name' => 'test',
|
||||||
'extension' => 'png',
|
'extension' => 'png',
|
||||||
|
|
@ -200,24 +176,9 @@ class FilableModelTest extends TestCase
|
||||||
'filable_id' => 1,
|
'filable_id' => 1,
|
||||||
'filable_type' => 'App\Models\User',
|
'filable_type' => 'App\Models\User',
|
||||||
'as' => 'gallery',
|
'as' => 'gallery',
|
||||||
'order' => 3,
|
'order' => $order,
|
||||||
]);
|
|
||||||
|
|
||||||
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,
|
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$filables = Filable::where('filable_type', 'App\Models\User')->get();
|
$filables = Filable::where('filable_type', 'App\Models\User')->get();
|
||||||
$this->assertEquals(1, $filables[0]->order);
|
$this->assertEquals(1, $filables[0]->order);
|
||||||
|
|
|
||||||
|
|
@ -3,41 +3,12 @@
|
||||||
namespace Blax\Files\Tests\Unit;
|
namespace Blax\Files\Tests\Unit;
|
||||||
|
|
||||||
use Blax\Files\Enums\FileLinkType;
|
use Blax\Files\Enums\FileLinkType;
|
||||||
use Blax\Files\FilesServiceProvider;
|
|
||||||
use Blax\Files\Models\File;
|
use Blax\Files\Models\File;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Blax\Files\Tests\TestCase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Orchestra\Testbench\TestCase;
|
|
||||||
|
|
||||||
class FileModelTest extends 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 ───────────────────────────────────────────
|
// ─── Creation & UUID ───────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,44 +3,15 @@
|
||||||
namespace Blax\Files\Tests\Unit;
|
namespace Blax\Files\Tests\Unit;
|
||||||
|
|
||||||
use Blax\Files\Enums\FileLinkType;
|
use Blax\Files\Enums\FileLinkType;
|
||||||
use Blax\Files\FilesServiceProvider;
|
|
||||||
use Blax\Files\Models\Filable;
|
use Blax\Files\Models\Filable;
|
||||||
use Blax\Files\Models\File;
|
use Blax\Files\Models\File;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Blax\Files\Tests\TestCase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Orchestra\Testbench\TestCase;
|
|
||||||
use Workbench\App\Models\Article;
|
use Workbench\App\Models\Article;
|
||||||
use Workbench\App\Models\User;
|
use Workbench\App\Models\User;
|
||||||
|
|
||||||
class HasFilesTest extends TestCase
|
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 ──────────────────────────────────────
|
// ─── files() relationship ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,13 @@
|
||||||
|
|
||||||
namespace Blax\Files\Tests\Unit;
|
namespace Blax\Files\Tests\Unit;
|
||||||
|
|
||||||
use Blax\Files\FilesServiceProvider;
|
|
||||||
use Blax\Files\Models\File;
|
use Blax\Files\Models\File;
|
||||||
use Blax\Files\Services\WarehouseService;
|
use Blax\Files\Services\WarehouseService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Blax\Files\Tests\TestCase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Orchestra\Testbench\TestCase;
|
|
||||||
|
|
||||||
class WarehouseServiceTest extends 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 ──────────────────────────────────
|
// ─── searchFile — UUID lookup ──────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -140,6 +110,17 @@ class WarehouseServiceTest extends TestCase
|
||||||
$this->assertEquals('svg', $result->extension);
|
$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 ─────────────────────────────────
|
// ─── searchFile — storage path ─────────────────────────────────
|
||||||
|
|
||||||
public function test_search_finds_by_storage_path()
|
public function test_search_finds_by_storage_path()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue