From cff670719868eafff0d5d39dd63e8611916f5751 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:09:19 +0000 Subject: [PATCH 1/3] Align remaining assertions with Laravel Port the remaining applicable assertion changes from Laravel #61093. Use identity and integer assertions directly in HTTP, database, process and string tests, preserving the existing checks and typing touched tests. Upstream: https://github.com/laravel/framework/pull/61093 Source: laravel/framework master cd6e81dff3ba7a4564ac88c3949698c728d20109 Validation: affected test files, full parallel suite, formatting and full static analysis pass. --- tests/Http/HttpRequestTest.php | 2 +- tests/Http/JsonResourceTest.php | 2 +- .../Database/EloquentUniqueStringPrimaryKeysTest.php | 8 ++++---- tests/Integration/Database/SchemaBuilderTest.php | 4 ++-- tests/Process/ProcessTest.php | 4 ++-- tests/Support/SupportStrTest.php | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index f3bf3ff294..6d3933e312 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -2257,7 +2257,7 @@ public function testItDoesNotGenerateJsonErrorsForEmptyContent(): void Request::create('', 'GET')->json(); - $this->assertTrue(json_last_error() === JSON_ERROR_NONE); + $this->assertSame(JSON_ERROR_NONE, json_last_error()); } public function testItClampsValues(): void diff --git a/tests/Http/JsonResourceTest.php b/tests/Http/JsonResourceTest.php index 2591601596..98d4f297cc 100644 --- a/tests/Http/JsonResourceTest.php +++ b/tests/Http/JsonResourceTest.php @@ -44,7 +44,7 @@ public function testJsonResourceToJsonSucceedsWithPriorErrors(): void // Simulate a JSON error json_decode('{'); - $this->assertTrue(json_last_error() !== JSON_ERROR_NONE); + $this->assertNotSame(JSON_ERROR_NONE, json_last_error()); $this->assertSame('{"foo":"bar"}', $resource->toJson(JSON_THROW_ON_ERROR)); } diff --git a/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php b/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php index 634b4f5a8c..a57d5d1cb9 100644 --- a/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php +++ b/tests/Integration/Database/EloquentUniqueStringPrimaryKeysTest.php @@ -67,11 +67,11 @@ public function testModelWithUlidPrimaryKeyCanBeCreated() $this->assertTrue(Str::isUlid($user->bar)); } - public function testModelWithoutUuidPrimaryKeyCanBeCreated() + public function testModelWithoutUuidPrimaryKeyCanBeCreated(): void { $user = ModelWithoutUuidPrimaryKey::create(); - $this->assertTrue(is_int($user->id)); + $this->assertIsInt($user->id); $this->assertTrue(Str::isUuid($user->foo)); $this->assertTrue(Str::isUuid($user->bar)); } @@ -105,13 +105,13 @@ public function testModelWithUlidPrimaryKeyCanBeCreatedQuietly() $this->assertTrue(Str::isUlid($user->bar)); } - public function testModelWithoutUuidPrimaryKeyCanBeCreatedQuietly() + public function testModelWithoutUuidPrimaryKeyCanBeCreatedQuietly(): void { $user = new ModelWithoutUuidPrimaryKey; $user->saveQuietly(); - $this->assertTrue(is_int($user->id)); + $this->assertIsInt($user->id); $this->assertTrue(Str::isUuid($user->foo)); $this->assertTrue(Str::isUuid($user->bar)); } diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index e19ab25cb9..b2a2571d64 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -392,14 +392,14 @@ public function testGetColumns() )); } - public function testGetColumnsOnView() + public function testGetColumnsOnView(): void { DB::statement('create view foo (bar) as select 1'); $columns = Schema::getColumns('foo'); $this->assertCount(1, $columns); - $this->assertTrue($columns[0]['name'] === 'bar'); + $this->assertSame('bar', $columns[0]['name']); } public function testGetIndexes() diff --git a/tests/Process/ProcessTest.php b/tests/Process/ProcessTest.php index 577df8f367..08962c4633 100644 --- a/tests/Process/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -297,8 +297,8 @@ public function testProcessPoolCanReceiveOutputForEachProcessViaStartMethod(): v $poolResults = $pool->wait(); - $this->assertTrue($output[0]['out'] !== []); - $this->assertTrue($output[1]['out'] !== []); + $this->assertNotSame([], $output[0]['out']); + $this->assertNotSame([], $output[1]['out']); $this->assertInstanceOf(ProcessResult::class, $poolResults[0]); $this->assertInstanceOf(ProcessResult::class, $poolResults[1]); $this->assertStringContainsString('ProcessTest.php', $poolResults[0]->output()); diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 8413a866ff..27c10de9a1 100644 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -2283,7 +2283,7 @@ public function testResetFactoryState(): void public function testPasswordCreation(): void { - $this->assertTrue(strlen(Str::password()) === 32); + $this->assertSame(32, strlen(Str::password())); $this->assertStringNotContainsString(' ', Str::password()); $this->assertStringContainsString(' ', Str::password(spaces: true)); From 3a0f4ebe9f081c82d6002c8dacefda72a873fdc9 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:09:30 +0000 Subject: [PATCH 2/3] Validate relative signed URLs for the root route Relative URL generation signs the root path as a single slash, but verification prepended another slash to Request::path() and rejected the generated signature. Normalize getPathInfo() to the same single-slash form. Extend the existing relative-signature test with root-route validation and rejection of an invalid signature. Related upstream: https://github.com/laravel/framework/pull/61009 Source: laravel/framework master cd6e81dff3ba7a4564ac88c3949698c728d20109 Validation: RoutingUrlGeneratorTest, full parallel suite, formatting and full static analysis pass. --- src/routing/src/UrlGenerator.php | 2 +- tests/Routing/RoutingUrlGeneratorTest.php | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/routing/src/UrlGenerator.php b/src/routing/src/UrlGenerator.php index a0dedc6e56..4f2a5d3344 100755 --- a/src/routing/src/UrlGenerator.php +++ b/src/routing/src/UrlGenerator.php @@ -406,7 +406,7 @@ public function hasCorrectSignature(Request $request, bool $absolute = true, Clo $url = $absolute ? rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl() . $request->getPathInfo(), '/') - : '/' . $request->path(); + : '/' . trim($request->getPathInfo(), '/'); // REMOVED: Vapor's VAPOR_RAW_QUERY_STRING override; Swoole supplies the raw QUERY_STRING. // Keep this explode() to avoid an extra Stringable allocation per call. diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index 2064f0742d..918b2d3847 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -1150,17 +1150,17 @@ public function testSignedUrlImplicitModelBinding() $this->assertTrue($url->hasValidSignature($request)); } - public function testSignedRelativeUrl() + public function testSignedRelativeUrl(): void { $url = new UrlGenerator( $routes = new RouteCollection, $request = Request::create('http://www.foo.com/') ); - $url->setKeyResolver(function () { + $url->setKeyResolver(function (): string { return 'secret'; }); - $route = new Route(['GET'], 'foo', ['as' => 'foo', function () { + $route = new Route(['GET'], 'foo', ['as' => 'foo', function (): void { }]); $routes->add($route); @@ -1173,6 +1173,16 @@ public function testSignedRelativeUrl() $request = Request::create($url->signedRoute('foo', [], null, false) . '?tampered=true'); $this->assertFalse($url->hasValidSignature($request, false)); + + $routes->add(new Route(['GET'], '/', ['as' => 'home'])); + + $request = Request::create($url->signedRoute('home', [], null, false)); + + $this->assertTrue($url->hasValidRelativeSignature($request)); + + $request->query->set('signature', 'invalid'); + + $this->assertFalse($url->hasValidRelativeSignature($request)); } public function testSignedUrlParameterCannotBeNamedSignature(): void From 1118f90fd04f0e49baf22feecfae5fc1773337ee Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 27 Sep 2026 04:49:44 +0000 Subject: [PATCH 3/3] Allow faking on-demand filesystem builds Port Laravel #61378 so Storage::fake('ondemand') and persistentFake can replace anonymous Storage::build() calls. Explicit logical names continue to construct their own disks, and forgetting or purging the fake restores normal anonymous construction. Reserve ondemand as a configured disk name so a real cached disk cannot silently replace unrelated builds. Reject that configuration on resolution and document the required rename for Laravel applications. Preserve explicit-name identity and creator callback coverage, and add the upstream fake regression alongside persistent-fake and cleanup assertions. Upstream: https://github.com/laravel/framework/pull/61378 Source: laravel/framework master cd6e81dff3ba7a4564ac88c3949698c728d20109 Validation: filesystem manager and Storage facade tests pass after separating the changes from read-through storage. Full analysis and the affected suite also pass for the reviewed implementation. --- src/docs/filesystem.md | 2 + src/docs/porting-from-laravel.md | 2 + src/filesystem/README.md | 2 + src/filesystem/src/FilesystemManager.php | 16 ++++++-- tests/Filesystem/FilesystemManagerTest.php | 32 ++++++++++------ .../Integration/Support/StorageFacadeTest.php | 37 +++++++++++++++++++ 6 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/docs/filesystem.md b/src/docs/filesystem.md index ae0698148b..ac0dd29cc4 100644 --- a/src/docs/filesystem.md +++ b/src/docs/filesystem.md @@ -414,6 +414,8 @@ $disk = Storage::build($configuration, 'tenant-uploads'); Hypervel uses this name as part of the pool identity for drivers that pool the complete filesystem instance. An on-demand disk does not register a serving route of its own. When a named on-demand disk enables `serve`, Hypervel generates signed URLs through the configured served disk with that name. Use matching storage configuration because the route resolves the configured disk, not the on-demand instance. An anonymous scoped disk may instead generate signed URLs through a named parent disk that has serving enabled. S3 and Google Cloud Storage client pools continue to use the client configuration rather than the logical disk name. +The disk name `ondemand` is reserved and cannot be used in your `filesystems.disks` configuration. In tests, `Storage::fake('ondemand')` or `Storage::persistentFake('ondemand')` replaces disks returned by `Storage::build()` when no logical name is supplied. Builds with an explicit logical name continue to use their own configuration. + ## Retrieving Files diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index 7adeaae1a4..3da5ad74f0 100644 --- a/src/docs/porting-from-laravel.md +++ b/src/docs/porting-from-laravel.md @@ -588,6 +588,8 @@ Hypervel's `Filesystem::hash()` method uses `xxh128` by default. Pass `md5` expl Unlike Laravel, Hypervel honors `read-only` on scoped disk records. Remove that option from any scoped disk that must accept writes. +Rename any configured disk called `ondemand`; Hypervel reserves that name for [on-demand disk fakes](/docs/{{version}}/filesystem#on-demand-disks). + ### Tinker diff --git a/src/filesystem/README.md b/src/filesystem/README.md index e7a4648495..cdabe25954 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -11,6 +11,8 @@ Hypervel omits Laravel's legacy `Storage::cloud()` / `filesystem.cloud` default- Hypervel's `Filesystem::hash()` method uses `xxh128` by default instead of `md5`. Pass `md5` explicitly when Laravel-compatible digests are required. +The configured disk name `ondemand` is reserved. Disks explicitly set under that name, including fakes, intercept anonymous `build()` calls; configured disks with that name are rejected on resolution. + Hypervel pools S3 and Google Cloud Storage SDK clients rather than complete disk adapters. Disks with equivalent client construction config share the expensive client pool while retaining their own bucket, root, visibility, and callback behavior. Pooled disks expose raw internals only through borrow-scoped `withClient()`, `withDriver()`, and `withAdapter()` callbacks. Filesystem construction differs from Laravel in how it carries logical disk identity. `callCustomCreator()` accepts the logical disk name as an optional second parameter, so existing one-argument calls remain valid while overrides must adopt the parameter. The public `build()` method uses a logical-name-aware construction path rather than the protected `resolve()` method because anonymous builds must pass a null name to creators; `resolve()` remains the configured-disk seam. `createScopedDriver()` resolves its prepared descriptor directly rather than through `build()`, so override `createScopedDriver()` for scoped construction customization. Customize on-demand construction through `Storage::extend()` or the public driver creator methods. Creator callbacks may accept the nullable name as a third argument after the application and configuration, while existing two-argument callbacks remain valid. Hypervel carries the name through scoped reconstruction and whole-driver pool fingerprints. A matching explicit fingerprint declares that every construction detail is equivalent, including serving-route ownership. diff --git a/src/filesystem/src/FilesystemManager.php b/src/filesystem/src/FilesystemManager.php index 3ae222df0b..fc962ee95c 100644 --- a/src/filesystem/src/FilesystemManager.php +++ b/src/filesystem/src/FilesystemManager.php @@ -137,6 +137,10 @@ public function disk(UnitEnum|string|null $name = null): Filesystem */ public function build(array|string $config, ?string $name = null): Filesystem { + if ($name === null && isset($this->disks[self::ON_DEMAND_DISK_NAME])) { + return $this->disks[self::ON_DEMAND_DISK_NAME]; + } + $config = is_array($config) ? $config : [ 'driver' => 'local', 'root' => $config, @@ -170,8 +174,8 @@ protected function resolve(string $name, ?array $config = null): Filesystem /** * Resolve the given disk while preserving its logical construction name. * - * The configured disk name "ondemand" is valid, so build() enters this - * method directly to carry anonymous construction as a separate value. + * Anonymous builds carry a null logical name through custom creators + * and pool identity, independently of their internal construction name. */ private function resolveWithLogicalName(string $name, ?array $config, ?string $logicalName): Filesystem { @@ -834,7 +838,13 @@ public function set(string $name, mixed $disk): static */ protected function getConfig(string $name): array { - return $this->app->make('config')->get("filesystems.disks.{$name}") ?: []; + $config = $this->app->make('config')->get("filesystems.disks.{$name}") ?: []; + + if ($name === self::ON_DEMAND_DISK_NAME && $config !== []) { + throw new InvalidArgumentException('The disk name [ondemand] is reserved for on-demand disk fakes. Rename the configured disk.'); + } + + return $config; } /** diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index 98ec308d2f..abae5babef 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -117,6 +117,20 @@ public function testCanBuildOnDemandDisk(): void ])); } + public function testConfiguredOndemandDiskNameIsReserved(): void + { + $filesystem = new FilesystemManager($this->getContainer([ + 'disks' => [ + 'ondemand' => ['driver' => 'local', 'root' => $this->tempDir], + ], + ])); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The disk name [ondemand] is reserved for on-demand disk fakes. Rename the configured disk.'); + + $filesystem->disk('ondemand'); + } + public function testCanBuildReadOnlyDisks(): void { $filesystem = new FilesystemManager($this->getContainer()); @@ -1198,9 +1212,7 @@ public function testCustomCreatorsReceiveTheExactLogicalName(): void 'driver' => 'custom', 'root' => $this->tempDir . '/custom-names', ]; - $container = $this->getContainer([ - 'disks' => ['ondemand' => $config], - ]); + $container = $this->getContainer(); $received = []; $manager = new FilesystemManager($container); $manager->extend('custom', function (Container $app, array $config, ?string $name) use (&$received): FilesystemAdapter { @@ -1212,7 +1224,7 @@ public function testCustomCreatorsReceiveTheExactLogicalName(): void $manager->build($config); $manager->build($config, 'uploads'); - $manager->disk('ondemand'); + $manager->build($config, 'ondemand'); $this->assertSame([null, 'uploads', 'ondemand'], $received); } @@ -1249,7 +1261,6 @@ public function testCustomPoolableDriversIncludeLogicalNamesAndExcludePoolMetada 'disks' => [ 'first' => $config, 'second' => $config, - 'ondemand' => $config, ], ]); Container::setInstance($container); @@ -1264,15 +1275,15 @@ public function testCustomPoolableDriversIncludeLogicalNamesAndExcludePoolMetada $first = $manager->disk('first'); $second = $manager->disk('second'); $anonymous = $manager->build($config); - $configuredOndemand = $manager->disk('ondemand'); + $namedOndemand = $manager->build($config, 'ondemand'); $this->assertInstanceOf(FilesystemPoolProxy::class, $first); $this->assertNotSame($first->getPoolName(), $second->getPoolName()); - $this->assertNotSame($anonymous->getPoolName(), $configuredOndemand->getPoolName()); + $this->assertNotSame($anonymous->getPoolName(), $namedOndemand->getPoolName()); $this->assertFalse($first->exists('missing.txt')); $this->assertFalse($second->exists('missing.txt')); $this->assertFalse($anonymous->exists('missing.txt')); - $this->assertFalse($configuredOndemand->exists('missing.txt')); + $this->assertFalse($namedOndemand->exists('missing.txt')); $this->assertSame(['first', 'second', null, 'ondemand'], array_column($received, 1)); foreach ($received as [$receivedConfig]) { @@ -1292,20 +1303,19 @@ public function testPoolableBuiltInDriversIncludeTheLogicalNameInConstructionIde 'disks' => [ 'first' => $config, 'second' => $config, - 'ondemand' => $config, ], ]); $manager = (new FilesystemManager($container))->addPoolableDriver('local'); $first = $manager->disk('first'); $second = $manager->disk('second'); - $configuredOndemand = $manager->disk('ondemand'); + $namedOndemand = $manager->build($config, 'ondemand'); $anonymous = $manager->build($config); $this->assertInstanceOf(FilesystemPoolProxy::class, $first); $this->assertInstanceOf(FilesystemPoolProxy::class, $second); $this->assertNotSame($first->getPoolName(), $second->getPoolName()); - $this->assertNotSame($configuredOndemand->getPoolName(), $anonymous->getPoolName()); + $this->assertNotSame($namedOndemand->getPoolName(), $anonymous->getPoolName()); } public function testWholeDriverPoolsIncludeRouteOwnershipNotImpliedByEffectiveConfiguration(): void diff --git a/tests/Integration/Support/StorageFacadeTest.php b/tests/Integration/Support/StorageFacadeTest.php index a0d163f860..3828c9df93 100644 --- a/tests/Integration/Support/StorageFacadeTest.php +++ b/tests/Integration/Support/StorageFacadeTest.php @@ -86,6 +86,43 @@ public function testStorageFakeMethodsWithEnums(): void $this->assertNull(Storage::fake(StorageFakeStringDisk::Public)->get('nonExistentFile')); } + public function testCanFakeOnDemandDisk(): void + { + $fake = Storage::fake('ondemand'); + + Storage::build([ + 'driver' => 'ftp', + 'host' => 'example.com', + 'username' => 'foo', + 'password' => 'bar', + ])->put('file.txt', 'contents'); + + $fake->assertExists('file.txt'); + } + + public function testOnDemandFakesPreserveNamedBuildsAndCanBeCleared(): void + { + $root = storage_path('on-demand'); + $config = ['driver' => 'local', 'root' => $root]; + $fake = Storage::persistentFake('ondemand'); + + $this->assertSame($fake, Storage::build($config)); + $this->assertNotSame($fake, Storage::build($config, 'uploads')); + + Storage::forgetDisk('ondemand'); + Storage::build($config)->put('after-forget.txt', 'contents'); + $this->assertFileExists($root . '/after-forget.txt'); + $fake->assertMissing('after-forget.txt'); + + $fake = Storage::fake('ondemand'); + $this->assertSame($fake, Storage::build($root)); + + Storage::purge('ondemand'); + Storage::build($root)->put('after-purge.txt', 'contents'); + $this->assertFileExists($root . '/after-purge.txt'); + $fake->assertMissing('after-purge.txt'); + } + public function testFakePreservesOriginalDiskThrowConfig(): void { config(['filesystems.disks.local.throw' => true]);