diff --git a/CHANGELOG.md b/CHANGELOG.md index 1748fafce249..2b26793e4ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.23.1...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.24.0...12.x) + +## [v12.24.0](https://github.com/laravel/framework/compare/v12.23.1...v12.24.0) - 2025-08-13 + +* [8.4] Use PHP 8.4 array helpers in Arr utils by [@jnoordsij](https://github.com/jnoordsij) in https://github.com/laravel/framework/pull/56631 +* [12.x] Test Improvements by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56635 +* [12.x] Update `orchestra/testbench-core` deps by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/56637 +* refactor: update cid param name by [@cpenned](https://github.com/cpenned) in https://github.com/laravel/framework/pull/56634 +* [12.x] Cache Singleton/Scoped attribute checks by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/56633 +* [12.x] Add `Arr::push()` by [@inxilpro](https://github.com/inxilpro) in https://github.com/laravel/framework/pull/56632 +* [12.x] Add error message for `doesnt_contain` rule by [@apih](https://github.com/apih) in https://github.com/laravel/framework/pull/56644 ## [v12.23.1](https://github.com/laravel/framework/compare/v12.23.0...v12.23.1) - 2025-08-12 diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index d2c835d59757..64e72c08903c 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -312,7 +312,7 @@ public function group(Closure $events) throw new RuntimeException('Invoke an attribute method such as Schedule::daily() before defining a schedule group.'); } - $this->groupStack[] = $this->attributes; + $this->groupStack[] = clone $this->attributes; $events($this); diff --git a/src/Illuminate/Database/Console/TableCommand.php b/src/Illuminate/Database/Console/TableCommand.php index fde40a78f8a3..94b313f57849 100644 --- a/src/Illuminate/Database/Console/TableCommand.php +++ b/src/Illuminate/Database/Console/TableCommand.php @@ -47,7 +47,17 @@ public function handle(ConnectionResolverInterface $connections) array_keys($tables) ); - $table = $tables[$tableName] ?? Arr::first($tables, fn ($table) => $table['name'] === $tableName); + $table = $tables[$tableName] ?? (new Collection($tables))->when( + Arr::wrap($connection->getSchemaBuilder()->getCurrentSchemaListing() + ?? $connection->getSchemaBuilder()->getCurrentSchemaName()), + fn (Collection $collection, array $currentSchemas) => $collection->sortBy( + function (array $table) use ($currentSchemas) { + $index = array_search($table['schema'], $currentSchemas); + + return $index === false ? PHP_INT_MAX : $index; + } + ) + )->firstWhere('name', $tableName); if (! $table) { $this->components->warn("Table [{$tableName}] doesn't exist."); diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index cce3cac57395..bbe7cce09778 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2385,6 +2385,19 @@ public function setAppends(array $appends) return $this; } + /** + * Merge new appended attributes with existing appended attributes on the model. + * + * @param array $appends + * @return $this + */ + public function mergeAppends(array $appends) + { + $this->appends = array_values(array_unique(array_merge($this->appends, $appends))); + + return $this; + } + /** * Return whether the accessor attribute has been appended. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php index c124fc60324e..dde5ea035297 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HidesAttributes.php @@ -41,6 +41,19 @@ public function setHidden(array $hidden) return $this; } + /** + * Merge new hidden attributes with existing hidden attributes on the model. + * + * @param array $hidden + * @return $this + */ + public function mergeHidden(array $hidden) + { + $this->hidden = array_values(array_unique(array_merge($this->hidden, $hidden))); + + return $this; + } + /** * Get the visible attributes for the model. * @@ -64,6 +77,19 @@ public function setVisible(array $visible) return $this; } + /** + * Merge new visible attributes with existing visible attributes on the model. + * + * @param array $visible + * @return $this + */ + public function mergeVisible(array $visible) + { + $this->visible = array_values(array_unique(array_merge($this->visible, $visible))); + + return $this; + } + /** * Make the given, typically hidden, attributes visible. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php b/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php index 578de7d0a86f..dfd11a86d70d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php +++ b/src/Illuminate/Database/Eloquent/Concerns/TransformsToResource.php @@ -13,8 +13,6 @@ trait TransformsToResource * * @param class-string<\Illuminate\Http\Resources\Json\JsonResource>|null $resourceClass * @return \Illuminate\Http\Resources\Json\JsonResource - * - * @throws \Throwable */ public function toResource(?string $resourceClass = null): JsonResource { @@ -29,8 +27,6 @@ public function toResource(?string $resourceClass = null): JsonResource * Guess the resource class for the model. * * @return \Illuminate\Http\Resources\Json\JsonResource - * - * @throws \Throwable */ protected function guessResource(): JsonResource { diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 9af906dff6d1..436fe9c12674 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '12.24.0'; + const VERSION = '12.25.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php b/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php index 514be710840e..ac8c03476438 100644 --- a/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php +++ b/src/Illuminate/Foundation/Exceptions/Renderer/Renderer.php @@ -89,8 +89,15 @@ public function render(Request $request, Throwable $throwable) $this->htmlErrorRenderer->render($throwable), ); + $exception = new Exception($flattenException, $request, $this->listener, $this->basePath); + + $exceptionAsMarkdown = $this->viewFactory->make('laravel-exceptions-renderer::markdown', [ + 'exception' => $exception, + ])->render(); + return $this->viewFactory->make('laravel-exceptions-renderer::show', [ - 'exception' => new Exception($flattenException, $request, $this->listener, $this->basePath), + 'exception' => $exception, + 'exceptionAsMarkdown' => $exceptionAsMarkdown, ])->render(); } diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index fca2cc61057f..8a39e5892b26 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -210,13 +210,11 @@ protected function registerDeferHandler() $this->app->scoped(DeferredCallbackCollection::class); $this->app['events']->listen(function (CommandFinished $event) { - app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => app()->runningInConsole() && ($event->exitCode === 0 || $callback->always) - ); + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => app()->runningInConsole() && ($event->exitCode === 0 || $callback->always)); }); $this->app['events']->listen(function (JobAttempted $event) { - app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => $event->connectionName !== 'sync' && ($event->successful() || $callback->always) - ); + app(DeferredCallbackCollection::class)->invokeWhen(fn ($callback) => $event->connectionName !== 'sync' && ($event->successful() || $callback->always)); }); } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index e5ac69c289dd..c23ce0de4e37 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -18,7 +18,7 @@ trait InteractsWithDatabase /** * Assert that a given where condition exists in the database. * - * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @return $this @@ -50,7 +50,7 @@ protected function assertDatabaseHas($table, array $data = [], $connection = nul /** * Assert that a given where condition does not exist in the database. * - * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @return $this @@ -117,7 +117,7 @@ protected function assertDatabaseEmpty($table, $connection = null) /** * Assert the given record has been "soft deleted". * - * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @param string|null $deletedAtColumn @@ -157,7 +157,7 @@ protected function assertSoftDeleted($table, array $data = [], $connection = nul /** * Assert the given record has not been "soft deleted". * - * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table + * @param iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $table * @param array $data * @param string|null $connection * @param string|null $deletedAtColumn @@ -197,7 +197,7 @@ protected function assertNotSoftDeleted($table, array $data = [], $connection = /** * Assert the given model exists in the database. * - * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model + * @param iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model * @return $this */ protected function assertModelExists($model) @@ -208,7 +208,7 @@ protected function assertModelExists($model) /** * Assert the given model does not exist in the database. * - * @param \Illuminate\Database\Eloquent\Model[]|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model + * @param iterable<\Illuminate\Database\Eloquent\Model>|\Illuminate\Database\Eloquent\Model|class-string<\Illuminate\Database\Eloquent\Model>|string $model * @return $this */ protected function assertModelMissing($model) diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/copy-button.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/copy-button.blade.php new file mode 100644 index 000000000000..115872f4278e --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/copy-button.blade.php @@ -0,0 +1,25 @@ + +
+ +
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/components/navigation.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/components/navigation.blade.php index 47ad038c7f87..738d03d40b53 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/components/navigation.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/components/navigation.blade.php @@ -21,6 +21,7 @@ class="h-6 w-6 fill-red-500 text-gray-50 dark:text-gray-950"
+
diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php new file mode 100644 index 000000000000..0d5a6a700f3a --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/markdown.blade.php @@ -0,0 +1,48 @@ +# {{ $exception->class() }} - {!! $exception->title() !!} +{!! $exception->message() !!} + +PHP {{ PHP_VERSION }} +Laravel {{ app()->version() }} +{{ $exception->request()->httpHost() }} + +## Stack Trace + +@foreach($exception->frames() as $index => $frame) +{{ $index }} - {{ $frame->file() }}:{{ $frame->line() }} +@endforeach + +## Request + +{{ $exception->request()->method() }} {{ Str::start($exception->request()->path(), '/') }} + +## Headers + +@forelse ($exception->requestHeaders() as $key => $value) +* **{{ $key }}**: {!! $value !!} +@empty +No header data available. +@endforelse + +## Route Context + +@forelse($exception->applicationRouteContext() as $name => $value) +{{ $name }}: {!! $value !!} +@empty +No routing data available. +@endforelse + +## Route Parameters + +@if ($routeParametersContext = $exception->applicationRouteParametersContext()) +{!! $routeParametersContext !!} +@else +No route parameter data available. +@endif + +## Database Queries + +@forelse ($exception->applicationQueries() as ['connectionName' => $connectionName, 'sql' => $sql, 'time' => $time]) +* {{ $connectionName }} - {!! $sql !!} ({{ $time }} ms) +@empty +No database queries detected. +@endforelse diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/show.blade.php b/src/Illuminate/Foundation/resources/exceptions/renderer/show.blade.php index 1aaa57e828f3..aacd7c257c0b 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/show.blade.php +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/show.blade.php @@ -1,6 +1,6 @@
- +
diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 68df34ed973a..c99bace1ce53 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -80,6 +80,13 @@ class Factory */ protected $preventStrayRequests = false; + /** + * A list of URL patterns that are allowed to bypass the stray request guard. + * + * @var array + */ + protected $allowedStrayRequestUrls = []; + /** * Create a new factory instance. * @@ -333,13 +340,22 @@ public function preventingStrayRequests() } /** - * Indicate that an exception should not be thrown if any request is not faked. + * Allow stray, unfaked requests entirely, or optionally allow only specific URLs. * + * @param array|null $only * @return $this */ - public function allowStrayRequests() + public function allowStrayRequests(?array $only = null) { - return $this->preventStrayRequests(false); + if (is_null($only)) { + $this->preventStrayRequests(false); + + $this->allowedStrayRequestUrls = []; + } else { + $this->allowedStrayRequestUrls = array_values($only); + } + + return $this; } /** @@ -486,7 +502,10 @@ public function recorded($callback = null) public function createPendingRequest() { return tap($this->newPendingRequest(), function ($request) { - $request->stub($this->stubCallbacks)->preventStrayRequests($this->preventStrayRequests); + $request + ->stub($this->stubCallbacks) + ->preventStrayRequests($this->preventStrayRequests) + ->allowStrayRequests($this->allowedStrayRequestUrls); }); } diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 66fc30286b29..3a4af82ff379 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -172,6 +172,13 @@ class PendingRequest */ protected $preventStrayRequests = false; + /** + * A list of URL patterns that are allowed to bypass the stray request guard. + * + * @var array + */ + protected $allowedStrayRequestUrls = []; + /** * The middleware callables added by users that will handle requests. * @@ -1376,7 +1383,7 @@ public function buildStubHandler() ->first(); if (is_null($response)) { - if ($this->preventStrayRequests) { + if (! $this->isAllowedRequestUrl((string) $request->getUri())) { throw new StrayRequestException((string) $request->getUri()); } @@ -1501,6 +1508,40 @@ public function preventStrayRequests($prevent = true) return $this; } + /** + * Allow stray, unfaked requests entirely, or optionally allow only specific URLs. + * + * @param array $only + * @return $this + */ + public function allowStrayRequests(array $only) + { + $this->allowedStrayRequestUrls = array_values($only); + + return $this; + } + + /** + * Determine if the given URL is allowed as a stray request. + * + * @param string $url + * @return bool + */ + public function isAllowedRequestUrl($url) + { + if (! $this->preventStrayRequests) { + return true; + } + + foreach ($this->allowedStrayRequestUrls as $pattern) { + if (Str::is($pattern, $url)) { + return true; + } + } + + return false; + } + /** * Toggle asynchronicity in requests. * diff --git a/src/Illuminate/Log/Context/Repository.php b/src/Illuminate/Log/Context/Repository.php index a221409186cb..d0a4b98dec9d 100644 --- a/src/Illuminate/Log/Context/Repository.php +++ b/src/Illuminate/Log/Context/Repository.php @@ -541,6 +541,8 @@ protected function isHiddenStackable($key) * @param array $data * @param array $hidden * @return mixed + * + * @throws \Throwable */ public function scope(callable $callback, array $data = [], array $hidden = []) { diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index 9eeaa7990dc4..f4c423c74f60 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -15,7 +15,7 @@ * @method static \Closure failedConnection(string|null $message = null) * @method static \Illuminate\Http\Client\ResponseSequence sequence(array $responses = []) * @method static bool preventingStrayRequests() - * @method static \Illuminate\Http\Client\Factory allowStrayRequests() + * @method static \Illuminate\Http\Client\Factory allowStrayRequests(array|null $only = null) * @method static \Illuminate\Http\Client\Factory record() * @method static void recordRequestResponsePair(\Illuminate\Http\Client\Request $request, \Illuminate\Http\Client\Response|null $response) * @method static void assertSent(callable|\Closure $callback) @@ -89,6 +89,7 @@ * @method static \GuzzleHttp\Psr7\RequestInterface runBeforeSendingCallbacks(\GuzzleHttp\Psr7\RequestInterface $request, array $options) * @method static array mergeOptions(array ...$options) * @method static \Illuminate\Http\Client\PendingRequest stub(callable $callback) + * @method static bool isAllowedRequestUrl(string $url) * @method static \Illuminate\Http\Client\PendingRequest async(bool $async = true) * @method static \GuzzleHttp\Promise\PromiseInterface|null getPromise() * @method static \Illuminate\Http\Client\PendingRequest truncateExceptionsAt(int $length) diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index c2c440211b5a..89f21a952648 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -1495,6 +1495,18 @@ public function testHidden() $this->assertArrayNotHasKey('age', $array); } + public function testMergeHiddenMergesHidden() + { + $model = new EloquentModelHiddenStub; + + $hiddenCount = count($model->getHidden()); + $this->assertContains('foo', $model->getHidden()); + + $model->mergeHidden(['bar']); + $this->assertCount($hiddenCount + 1, $model->getHidden()); + $this->assertContains('bar', $model->getHidden()); + } + public function testVisible() { $model = new EloquentModelStub(['name' => 'foo', 'age' => 'bar', 'id' => 'baz']); @@ -1504,6 +1516,18 @@ public function testVisible() $this->assertArrayNotHasKey('age', $array); } + public function testMergeVisibleMergesVisible() + { + $model = new EloquentModelVisibleStub; + + $visibleCount = count($model->getVisible()); + $this->assertContains('foo', $model->getVisible()); + + $model->mergeVisible(['bar']); + $this->assertCount($visibleCount + 1, $model->getVisible()); + $this->assertContains('bar', $model->getVisible()); + } + public function testDynamicHidden() { $model = new EloquentModelDynamicHiddenStub(['name' => 'foo', 'age' => 'bar', 'id' => 'baz']); @@ -2421,6 +2445,18 @@ public function testAppendingOfAttributes() $this->assertEquals([], $model->toArray()); } + public function testMergeAppendsMergesAppends() + { + $model = new EloquentModelAppendsStub; + + $appendsCount = count($model->getAppends()); + $this->assertEquals(['is_admin', 'camelCased', 'StudlyCased'], $model->getAppends()); + + $model->mergeAppends(['bar']); + $this->assertCount($appendsCount + 1, $model->getAppends()); + $this->assertContains('bar', $model->getAppends()); + } + public function testGetMutatedAttributes() { $model = new EloquentModelGetMutatorsStub; @@ -3905,6 +3941,18 @@ public function getHidden() } } +class EloquentModelVisibleStub extends Model +{ + protected $table = 'stub'; + protected $visible = ['foo']; +} + +class EloquentModelHiddenStub extends Model +{ + protected $table = 'stub'; + protected $hidden = ['foo']; +} + class EloquentModelDynamicVisibleStub extends Model { protected $table = 'stub'; diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 4c0450db134d..e33020d7b07f 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -3464,6 +3464,21 @@ public function testPreventingStrayRequests() $this->assertTrue($this->factory->preventingStrayRequests()); } + public function testAllowingStrayRequestUrls() + { + $this->assertFalse($this->factory->preventingStrayRequests()); + $this->assertTrue($this->factory->isAllowedRequestUrl('127.0.0.1')); + + $this->factory->preventStrayRequests(); + $this->assertFalse($this->factory->isAllowedRequestUrl('127.0.0.1')); + $this->factory->allowStrayRequests([ + '127.0.0.1', + ]); + + $this->assertTrue($this->factory->preventingStrayRequests()); + $this->assertTrue($this->factory->isAllowedRequestUrl('127.0.0.1')); + } + public function testItCanAddAuthorizationHeaderIntoRequestUsingBeforeSendingCallback() { $this->factory->fake(); diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index f74ee43e9404..eabd8079464e 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -5,6 +5,7 @@ namespace Illuminate\Tests\Integration\Console\Scheduling; use Illuminate\Console\Scheduling\Schedule as ScheduleClass; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Schedule; use Orchestra\Testbench\TestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -117,4 +118,66 @@ public static function groupAttributes(): array 'withoutOverlapping' => ['withoutOverlapping', rand(1000, 1400)], ]; } + + #[DataProvider('scheduleTestCases')] + public function testGroupedScheduleExecution($time, $expected, $description) + { + Carbon::setTestNow($time); + $app = app(); + + Schedule::days([1, 2, 3, 4, 5, 6])->group(function () { + Schedule::between('07:00', '08:00')->group(function () { + Schedule::call(fn () => 'Task 1')->everyMinute(); + Schedule::call(fn () => 'Task 2')->everyFiveMinutes(); + }); + + Schedule::call(fn () => 'Task 3')->at('08:05'); + }); + + $events = Schedule::events(); + + foreach (array_keys($expected) as $index => $task) { + $this->assertTaskExecution( + $events[$index], + $app, + $expected[$task], + "[$description] $task should ".($expected[$task] ? 'run' : 'not run') + ); + } + + Carbon::setTestNow(); + } + + private function assertTaskExecution($event, $app, $expected, $message): void + { + $this->assertSame( + $expected, + $event->filtersPass($app) && $event->isDue($app), + $message + ); + } + + public static function scheduleTestCases() + { + return [ + [ + Carbon::create(2024, 1, 1, 7, 30), + [ + 'Task 1' => true, + 'Task 2' => true, + 'Task 3' => false, + ], + 'Tasks at 07:30', + ], + [ + Carbon::create(2024, 1, 1, 8, 5), + [ + 'Task 1' => false, + 'Task 2' => false, + 'Task 3' => true, + ], + 'Tasks at 08:05', + ], + ]; + } } diff --git a/tests/Support/SupportFacadesHttpTest.php b/tests/Support/SupportFacadesHttpTest.php index e7de568c4904..802f9ef033a8 100644 --- a/tests/Support/SupportFacadesHttpTest.php +++ b/tests/Support/SupportFacadesHttpTest.php @@ -56,6 +56,13 @@ public function testFacadeRootIsSharedWhenEnforcingFaking(): void $this->assertSame($client, $this->app->make(Factory::class)); } + public function testFacadeRootIsSharedWhenEnforcingFakingWithAllowedUrls(): void + { + $client = Http::preventStrayRequests()->allowStrayRequests(['127.0.0.1']); + + $this->assertSame($client, $this->app->make(Factory::class)); + } + public function test_can_set_prevents_to_prevents_stray_requests(): void { Http::preventStrayRequests(true);