diff --git a/composer.json b/composer.json index 957ebb83c1..bf6d04bca1 100644 --- a/composer.json +++ b/composer.json @@ -315,7 +315,7 @@ "mockery/mockery": "^1.6.15", "opis/json-schema": "^2.6.0", "pda/pheanstalk": "^8.0.2", - "phpstan/phpstan": "^2.2.11", + "phpstan/phpstan": "^2.2.14", "phpunit/phpunit": "^13.0.3", "pusher/pusher-php-server": "^7.2", "resend/resend-php": "^1.0", diff --git a/src/cache/src/AnyModeTaggedCache.php b/src/cache/src/AnyModeTaggedCache.php index 4b911f2d25..2c435c03f9 100644 --- a/src/cache/src/AnyModeTaggedCache.php +++ b/src/cache/src/AnyModeTaggedCache.php @@ -139,7 +139,7 @@ public function forget(UnitEnum|string $key): bool * * @throws BadMethodCallException always - tags are for writing and flushing only */ - public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int $ttl): bool { throw new BadMethodCallException( 'Cannot touch items via tags in any mode. Re-put the item through tags() to change ' diff --git a/src/cache/src/Redis/AllTaggedCache.php b/src/cache/src/Redis/AllTaggedCache.php index 2dc7107347..b18bab4457 100644 --- a/src/cache/src/Redis/AllTaggedCache.php +++ b/src/cache/src/Redis/AllTaggedCache.php @@ -206,24 +206,21 @@ public function putMany(array $values, DateInterval|DateTimeInterface|int|null $ } /** - * Set the expiration of a cached item; null TTL will retain the item forever. + * Set the expiration of a cached item. */ - public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int $ttl): bool { $key = $key instanceof UnitEnum ? (string) enum_value($key) : $key; - $value = $this->getRaw($key); - if (is_null($value)) { - return false; - } + $seconds = $this->getSeconds($ttl); - if (is_null($ttl)) { - return $this->forever($key, $value); + if ($seconds <= 0) { + return $this->forget($key); } return $this->store->allTagOps()->touch()->execute( $this->itemKey($key), - $this->getSeconds($ttl), + $seconds, $this->tags->tagIds() ); } diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 7d6e533a44..b0980a0615 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -765,20 +765,19 @@ public function flexibleNullable(UnitEnum|string $key, array $ttl, mixed $callba } /** - * Set the expiration of a cached item; null TTL will retain the item forever. + * Set the expiration of a cached item. */ - public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl = null): bool + public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int $ttl): bool { $key = $key instanceof UnitEnum ? (string) enum_value($key) : $key; - $value = $this->getRaw($key); - if (is_null($value)) { - return false; + $seconds = $this->getSeconds($ttl); + + if ($seconds <= 0) { + return $this->forget($key); } - return is_null($ttl) - ? $this->forever($key, $value) - : $this->store->touch($this->itemKey($key), $this->getSeconds($ttl)); + return $this->store->touch($this->itemKey($key), $seconds); } /** @@ -1372,15 +1371,16 @@ public function manyRaw(array $keys): array $this->events?->hasListeners(CacheMissed::class) || $this->events?->hasListeners(CacheHit::class) ) { + // PHP stores numeric-string keys as integers; event keys must be strings. foreach ($result as $key => $value) { // Keep the per-class checks live: an earlier hit listener may register // a miss listener, or vice versa, before a later result is dispatched. if (is_null($value)) { if ($this->events?->hasListeners(CacheMissed::class)) { - $this->event(new CacheMissed($this->getName(), $key)); + $this->event(new CacheMissed($this->getName(), (string) $key)); } } elseif ($this->events?->hasListeners(CacheHit::class)) { - $this->event(new CacheHit($this->getName(), $key, NullSentinel::unwrap($value))); + $this->event(new CacheHit($this->getName(), (string) $key, NullSentinel::unwrap($value))); } } } diff --git a/src/collections/src/Collection.php b/src/collections/src/Collection.php index 765077b99a..5c04bbba59 100644 --- a/src/collections/src/Collection.php +++ b/src/collections/src/Collection.php @@ -349,6 +349,7 @@ public function duplicates(callable|string|null $callback = null, bool $strict = if ($uniqueItems->isNotEmpty() && $compare($value, $uniqueItems->first())) { $uniqueItems->shift(); } else { + // @phpstan-ignore offsetAssign.dimType (PHPStan 2.2.14 rejects template keys on ArrayAccess) $duplicates[$key] = $value; } } @@ -1468,6 +1469,7 @@ public function chunkWhile(callable $callback): static $chunk = $this->newInstance(); } + // @phpstan-ignore offsetAssign.dimType (PHPStan 2.2.14 rejects template keys on ArrayAccess) $chunk[$key] = $value; } @@ -1795,7 +1797,7 @@ public function zip(Arrayable|iterable ...$items): Collection * @template TPadValue * * @param TPadValue $value - * @return static + * @return static */ public function pad(int $size, mixed $value): Collection { diff --git a/src/collections/src/Enumerable.php b/src/collections/src/Enumerable.php index 5717ba4dde..84be1d30b4 100644 --- a/src/collections/src/Enumerable.php +++ b/src/collections/src/Enumerable.php @@ -1079,7 +1079,7 @@ public function values(): static; * @template TPadValue * * @param TPadValue $value - * @return static + * @return static */ public function pad(int $size, mixed $value): Collection|static; diff --git a/src/collections/src/LazyCollection.php b/src/collections/src/LazyCollection.php index acc9a9ebc1..dff6ded594 100644 --- a/src/collections/src/LazyCollection.php +++ b/src/collections/src/LazyCollection.php @@ -1469,6 +1469,7 @@ public function chunkWhile(callable $callback): static $chunk = new Collection; if ($iterator->valid()) { + // @phpstan-ignore offsetAssign.dimType (PHPStan 2.2.14 rejects template keys on ArrayAccess) $chunk[$iterator->key()] = $iterator->current(); $iterator->next(); @@ -1481,6 +1482,7 @@ public function chunkWhile(callable $callback): static $chunk = new Collection; } + // @phpstan-ignore offsetAssign.dimType (PHPStan 2.2.14 rejects template keys on ArrayAccess) $chunk[$iterator->key()] = $iterator->current(); $iterator->next(); @@ -1807,7 +1809,7 @@ public function zip(Arrayable|iterable ...$items): static * @template TPadValue * * @param TPadValue $value - * @return static + * @return static */ #[Override] public function pad(int $size, mixed $value): static diff --git a/src/concurrency/src/ProcessDriver.php b/src/concurrency/src/ProcessDriver.php index 7399dff06e..7d8c5b4b99 100644 --- a/src/concurrency/src/ProcessDriver.php +++ b/src/concurrency/src/ProcessDriver.php @@ -15,6 +15,7 @@ use Hypervel\Support\Defer\DeferredCallback; use Hypervel\Support\Facades\Context; use Laravel\SerializableClosure\SerializableClosure; +use Throwable; use function Hypervel\Support\defer; @@ -30,6 +31,8 @@ public function __construct( /** * Run the given tasks concurrently and return an array containing the results. + * + * @throws Throwable */ public function run(Closure|array $tasks, CarbonInterval|int|null $timeout = null): array { diff --git a/src/console/README.md b/src/console/README.md index f5b5a71575..8f1f94d195 100644 --- a/src/console/README.md +++ b/src/console/README.md @@ -3,8 +3,12 @@ Console for Hypervel [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/console) -Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Console - ## Differences From Laravel +`schedule:run` is a long-running process by default and replaces `schedule:work`. Use `schedule:run --once` in cron entries. See the [scheduling documentation](https://hypervel.org/docs/scheduling#running-the-scheduler). + +Scheduled tasks do not support `user()`. Run the scheduler as the required OS user, or use `exec()` with an explicit command to run an individual task as another user. + `schedule:list --timezone` converts next-due timestamps but leaves cron expressions in their real evaluation timezone. Laravel's display-only expression converter cannot faithfully handle ranges, special cron syntax, month boundaries, or daylight-saving transitions. JSON output includes `expression_timezone`, and CLI output labels it when it differs from the requested display timezone. + +Ported from: https://github.com/laravel/framework/tree/13.x/src/Illuminate/Console diff --git a/src/console/src/ConsoleServiceProvider.php b/src/console/src/ConsoleServiceProvider.php index 23e76b59c3..75c0c357dd 100644 --- a/src/console/src/ConsoleServiceProvider.php +++ b/src/console/src/ConsoleServiceProvider.php @@ -27,6 +27,7 @@ public function register(): void $this->commands([ ScheduleClearCacheCommand::class, ScheduleListCommand::class, + // REMOVED: ScheduleWorkCommand; schedule:run already runs continuously in coroutines. ScheduleRunCommand::class, ScheduleInterruptCommand::class, SchedulePauseCommand::class, diff --git a/src/console/src/Scheduling/Event.php b/src/console/src/Scheduling/Event.php index 20ba49f3f0..41081f5aec 100644 --- a/src/console/src/Scheduling/Event.php +++ b/src/console/src/Scheduling/Event.php @@ -344,6 +344,8 @@ public function callAfterCallbacks(Container $container): void } } + // REMOVED: buildCommand(); Artisan tasks run in-process, and exec() uses Symfony Process directly. + /** * Determine if the given event should run based on the Cron expression. */ diff --git a/src/console/src/Scheduling/ManagesAttributes.php b/src/console/src/Scheduling/ManagesAttributes.php index 7da4bceba1..3c1e7a1c2a 100644 --- a/src/console/src/Scheduling/ManagesAttributes.php +++ b/src/console/src/Scheduling/ManagesAttributes.php @@ -24,11 +24,6 @@ trait ManagesAttributes */ public DateTimeZone|string|null $timezone = null; - /** - * The user the command should run as. - */ - public ?string $user = null; - /** * The list of environments the command should run under. */ @@ -89,15 +84,7 @@ trait ManagesAttributes */ public array $attributes = []; - /** - * Set which user the command should run as. - */ - public function user(string $user): static - { - $this->user = $user; - - return $this; - } + // REMOVED: Laravel's user() / $user; coroutine tasks share the scheduler's OS user. /** * Limit the environments the command should run in. diff --git a/src/console/src/Scheduling/PendingEventAttributes.php b/src/console/src/Scheduling/PendingEventAttributes.php index 50e43e4bd7..7b440b1bc9 100644 --- a/src/console/src/Scheduling/PendingEventAttributes.php +++ b/src/console/src/Scheduling/PendingEventAttributes.php @@ -92,10 +92,6 @@ public function mergeAttributes(Event $event): void $event->timezone($this->timezone); } - if ($this->user !== null) { - $event->user = $this->user; - } - if (! empty($this->environments)) { $event->environments($this->environments); } diff --git a/src/console/src/Scheduling/Schedule.php b/src/console/src/Scheduling/Schedule.php index 20baf8d86e..dcd85e4c25 100644 --- a/src/console/src/Scheduling/Schedule.php +++ b/src/console/src/Scheduling/Schedule.php @@ -54,7 +54,7 @@ class Schedule /** * All of the events on the schedule. * - * @var array Event[] + * @var list */ protected array $events = []; @@ -111,18 +111,10 @@ class Schedule * Create a new schedule instance. * * @param null|DateTimeZone|string $timezone the timezone the date should be evaluated on - * - * @throws RuntimeException */ public function __construct( protected DateTimeZone|string|null $timezone = null ) { - if (! class_exists(Container::class)) { - throw new RuntimeException( - 'A container implementation is required to use the scheduler. Please install the hypervel/container package.' - ); - } - $container = Container::getInstance(); $this->eventMutex = $container->bound(EventMutex::class) @@ -231,12 +223,6 @@ function () use ($job, $queue, $connection) { protected function dispatchToQueue(object $job, ?string $queue, ?string $connection): void { if ($job instanceof Closure) { - if (! class_exists(CallQueuedClosure::class)) { - throw new RuntimeException( - 'To enable support for closure jobs, please install the illuminate/queue package.' - ); - } - $job = CallQueuedClosure::create($job); } @@ -324,16 +310,19 @@ public function group(Closure $events): void */ protected function mergePendingAttributes(Event $event): void { - if (! empty($this->groupStack)) { - $group = end($this->groupStack); - - $group->mergeAttributes($event); - } - if (isset($this->attributes)) { + // Pending attributes already inherit the current group's callbacks. $this->attributes->mergeAttributes($event); $this->attributes = null; + + return; + } + + if (! empty($this->groupStack)) { + $group = end($this->groupStack); + + $group->mergeAttributes($event); } } @@ -412,7 +401,7 @@ public function dueEventsAt(Application $app, DateTimeInterface $time): Collecti /** * Get all of the events on the schedule. * - * @return array Event[] + * @return list */ public function events(): array { diff --git a/src/contracts/src/Cache/Repository.php b/src/contracts/src/Cache/Repository.php index 3287d0e0d2..211198739a 100644 --- a/src/contracts/src/Cache/Repository.php +++ b/src/contracts/src/Cache/Repository.php @@ -183,9 +183,9 @@ public function searNullable(UnitEnum|string $key, Closure $callback): mixed; public function rememberForeverNullable(UnitEnum|string $key, Closure $callback): mixed; /** - * Set the expiration of a cached item; null TTL will retain the item forever. + * Set the expiration of a cached item. */ - public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int|null $ttl = null): bool; + public function touch(UnitEnum|string $key, DateInterval|DateTimeInterface|int $ttl): bool; /** * Remove an item from the cache. diff --git a/src/database/composer.json b/src/database/composer.json index d34fbeba16..d6b69f5ffe 100644 --- a/src/database/composer.json +++ b/src/database/composer.json @@ -64,7 +64,7 @@ }, "require-dev": { "fakerphp/faker": "^1.24", - "phpstan/phpstan": "^2.2.11" + "phpstan/phpstan": "^2.2.14" }, "suggest": { "fakerphp/faker": "Required to use Eloquent model factories (^1.24)." diff --git a/src/database/src/Eloquent/Collection.php b/src/database/src/Eloquent/Collection.php index 4fdda4fa10..b8ea144650 100644 --- a/src/database/src/Eloquent/Collection.php +++ b/src/database/src/Eloquent/Collection.php @@ -803,9 +803,11 @@ public function keys(): BaseCollection } /** + * Pad collection to the specified length with a value. + * * @template TPadValue * - * @return \Hypervel\Support\Collection + * @return \Hypervel\Support\Collection */ #[Override] public function pad(int $size, mixed $value): BaseCollection diff --git a/src/docs/cache.md b/src/docs/cache.md index 2abca3b0fc..1e01b3ed7e 100644 --- a/src/docs/cache.md +++ b/src/docs/cache.md @@ -538,6 +538,8 @@ You may provide an integer number of seconds, a `DateInterval`, or a `DateTimeIn Cache::touch('key', now()->plus(hours: 2)); ``` +Providing a zero or negative number of seconds, or an expiration time in the past, removes the item from the cache. + #### Storing Items Forever @@ -726,7 +728,7 @@ Because tags are invalidation indexes in `any` mode, flushing any one tag remove Cache::tags(['user:42'])->flush(); ``` -Tag membership is synchronized by tagged writes. Plain `Cache::forget($key)` removes any-mode tag membership for that key, and a finite `Cache::touch($key, $ttl)` keeps the key and tag metadata TTLs in sync. Plain `put`, plain `forever`, and `touch($key, null)` are plain rewrites; they do not add tags or refresh tag metadata for an already-tagged value. To change a tagged value's TTL or tags, write it again through `tags()`. +Tag membership is synchronized by tagged writes. Plain `Cache::forget($key)` removes any-mode tag membership for that key, and `Cache::touch($key, $ttl)` keeps the key and tag metadata TTLs in sync. Plain `put` and `forever` are plain rewrites; they do not add tags or refresh tag metadata for an already-tagged value. To change a tagged value's TTL or tags, write it again through `tags()`. > [!WARNING] > In `any` mode, attempting to retrieve, check, pull, forget, touch, or retrieve many cache items through a tagged cache will throw a `BadMethodCallException`. Use the direct `Cache::get`, `Cache::has`, `Cache::pull`, `Cache::forget`, `Cache::touch`, and `Cache::many` methods with the full cache key instead. diff --git a/src/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index c2dd2e98e2..8d1b4d54df 100644 --- a/src/docs/porting-from-laravel.md +++ b/src/docs/porting-from-laravel.md @@ -22,6 +22,7 @@ - [Configuration](#configuration) - [Other API Differences](#other-api-differences) - [Scheduling](#scheduling) + - [Maintenance Mode](#maintenance-mode) - [HTTP Client and Concurrency](#http-client-and-concurrency) - [CSRF Protection](#csrf-protection) - [Scout](#scout) @@ -474,8 +475,15 @@ Many Laravel APIs have direct Hypervel equivalents under the `Hypervel` namespac Add `--once` to cron entries that invoke `schedule:run`, or run `schedule:run` as a supervised process. For local development, use `schedule:run` in place of Laravel's `schedule:work`. See the [scheduling documentation](/docs/{{version}}/scheduling#running-the-scheduler). +Replace scheduled task `user()` calls by running the scheduler as the required OS user, or by using `exec()` with an explicit command to run that task as another user. See [Scheduling Shell Commands](/docs/{{version}}/scheduling#scheduling-shell-commands). + Scheduled Artisan commands share the scheduler process instead of starting a fresh process for each invocation. Use `exec('php artisan ...')` for commands that rely on process isolation. See [Scheduling Artisan Commands](/docs/{{version}}/scheduling#scheduling-artisan-commands). + +### Maintenance Mode + +Maintenance views prepared with `down --render` are served by running Hypervel workers. To serve a static page while Hypervel is unavailable during deployment, configure your reverse proxy or load balancer. See [Pre-Rendering the Maintenance Mode View](/docs/{{version}}/configuration#pre-rendering-the-maintenance-mode-view). + ### HTTP Client and Concurrency diff --git a/src/docs/scheduling.md b/src/docs/scheduling.md index 4f7f13df9b..f5b81acd29 100644 --- a/src/docs/scheduling.md +++ b/src/docs/scheduling.md @@ -149,6 +149,8 @@ Schedule::exec('node /path/to/script.js')->daily(); If the shell command launches a Hypervel Artisan command, the child command receives the task's visible and hidden [context](/docs/{{version}}/context). +Scheduled tasks run as the scheduler's OS user. To run a task as another user, use `exec` with an explicit command, such as `sudo -u reports -- /usr/local/bin/generate-reports`. + ### Schedule Frequency Options @@ -440,14 +442,14 @@ Schedule::command('analytics:report') Hypervel starts background tasks as coroutines inside the `schedule:run` process. This is well suited to I/O-bound work such as HTTP calls, queries, and file or network I/O because coroutines yield while waiting. For CPU-bound work, coroutines offer limited benefit. In those cases, schedule the task using `exec` so the operating system runs it in a separate process: -Unlike Laravel's detached background processes, Hypervel observes the exit status of background tasks. A non-zero exit dispatches a `ScheduledTaskFailed` event and is reported through the exception handler. - ```php Schedule::exec('php artisan reports:compute') ->daily() ->runInBackground(); ``` +Unlike Laravel's detached background processes, Hypervel observes the exit status of background tasks. A non-zero exit dispatches a `ScheduledTaskFailed` event and is reported through the exception handler. + ### Maintenance Mode @@ -500,7 +502,7 @@ Schedule::daily() }); ``` -Group definitions also replay event lifecycle callbacks and output handlers on every event in the group, so shared hooks may be defined once: +You may also apply event callbacks, output handlers, and event macros to every task in a group. These methods may appear before or after the group's frequency settings: ```php Schedule::daily() diff --git a/src/docs/telescope.md b/src/docs/telescope.md index bacb40d2a6..f921463f5b 100644 --- a/src/docs/telescope.md +++ b/src/docs/telescope.md @@ -546,7 +546,7 @@ You may also use the `ignore_http_methods` and `ignore_status_codes` options to ### Schedule Watcher -The schedule watcher records the task type, description, expression, timezone, user, output, status, and exit code of any [scheduled tasks](/docs/{{version}}/scheduling) run by your application. Opaque command lines are not stored. Describe or name a scheduled command to identify it in Telescope; otherwise, Telescope displays `Scheduled command`. +The schedule watcher records the task type, description, expression, timezone, output, status, and exit code of any [scheduled tasks](/docs/{{version}}/scheduling) run by your application. Opaque command lines are not stored. Describe or name a scheduled command to identify it in Telescope; otherwise, Telescope displays `Scheduled command`. ### View Watcher diff --git a/src/foundation/README.md b/src/foundation/README.md index 34f11728c4..9a6c9888ed 100644 --- a/src/foundation/README.md +++ b/src/foundation/README.md @@ -12,6 +12,8 @@ the same surface, and its mutators are intended for application boot. Laravel's real-time facades are intentionally not supported. Define explicit facade classes or inject services from the container instead. +Maintenance responses are served by the running worker; Laravel's pre-bootstrap `maintenance.php` stub is not generated. Configure your reverse proxy or load balancer to serve a maintenance page when Hypervel is unavailable. + The application locale setters do not change the `app.locale` or `app.fallback_locale` configuration values. `App::setLocale()` applies only to the current request, while `App::setFallbackLocale()` is intended for application boot and changes the fallback shared by the worker. Laravel's deprecated `VerifyCsrfToken` and `ValidateCsrfToken` middleware aliases and `Middleware::validateCsrfTokens()` method are intentionally not ported. Use `PreventRequestForgery` and configure request-forgery protection with `preventRequestForgery()`. diff --git a/src/foundation/src/Console/DownCommand.php b/src/foundation/src/Console/DownCommand.php index 7de099a682..bdb62e9b03 100644 --- a/src/foundation/src/Console/DownCommand.php +++ b/src/foundation/src/Console/DownCommand.php @@ -52,6 +52,8 @@ public function handle(): int $this->hypervel->maintenanceMode()->activate($downFilePayload); $stateCommitted = true; + // REMOVED: The pre-bootstrap maintenance.php stub; running workers serve maintenance responses through middleware. + $exception = null; try { diff --git a/src/rate-limiter/src/Limiter.php b/src/rate-limiter/src/Limiter.php index b08d969cb1..85a4012177 100644 --- a/src/rate-limiter/src/Limiter.php +++ b/src/rate-limiter/src/Limiter.php @@ -32,8 +32,8 @@ public function getStore(): Store } // One-call decisions replace Laravel's split tooManyAttempts(), hit(), - // increment(), attempts(), resetAttempts(), retriesLeft(), availableIn(), - // cleanRateLimiterKey(), and Limit::fallbackKey() APIs. + // increment(), decrement(), attempts(), resetAttempts(), remaining(), + // retriesLeft(), availableIn(), cleanRateLimiterKey(), and Limit::fallbackKey() APIs. /** * Atomically consume capacity from an admission policy. diff --git a/src/support/src/Facades/Cache.php b/src/support/src/Facades/Cache.php index c28cd1f574..1283f95b3f 100644 --- a/src/support/src/Facades/Cache.php +++ b/src/support/src/Facades/Cache.php @@ -72,7 +72,7 @@ * @method static bool supportsFlushingLocks() * @method static bool supportsTags() * @method static \Hypervel\Cache\TaggedCache tags(mixed $names) - * @method static bool touch(\UnitEnum|string $key, \DateInterval|\DateTimeInterface|int|null $ttl = null) + * @method static bool touch(\UnitEnum|string $key, \DateInterval|\DateTimeInterface|int $ttl) * @method static mixed withoutOverlapping(\UnitEnum|string $key, callable $callback, int $lockFor = 0, int $waitFor = 10, string|null $owner = null) * @method static bool flush() * @method static string getPrefix() diff --git a/src/support/src/Facades/Schedule.php b/src/support/src/Facades/Schedule.php index 7b7ad46083..a6ba681f75 100644 --- a/src/support/src/Facades/Schedule.php +++ b/src/support/src/Facades/Schedule.php @@ -12,7 +12,7 @@ * @method static string compileArrayInput(string|int $key, array $value) * @method static \Hypervel\Support\Collection dueEvents(\Hypervel\Contracts\Foundation\Application $app) * @method static \Hypervel\Support\Collection dueEventsAt(\Hypervel\Contracts\Foundation\Application $app, \DateTimeInterface $time) - * @method static array events() + * @method static array events() * @method static array eventsForEnvironments(array $environments) * @method static \Hypervel\Console\Scheduling\Event exec(string $command, array $parameters = [], bool $isSystem = true) * @method static void flushMacros() @@ -80,7 +80,6 @@ * @method static \Hypervel\Console\Scheduling\PendingEventAttributes twiceDailyAt(int $first = 1, int $second = 13, int $offset = 0) * @method static \Hypervel\Console\Scheduling\PendingEventAttributes twiceMonthly(int $first = 1, int $second = 16, string $time = '0:0') * @method static \Hypervel\Console\Scheduling\PendingEventAttributes unlessBetween(string $startTime, string $endTime) - * @method static \Hypervel\Console\Scheduling\PendingEventAttributes user(string $user) * @method static \Hypervel\Console\Scheduling\PendingEventAttributes wednesdays() * @method static \Hypervel\Console\Scheduling\PendingEventAttributes weekdays() * @method static \Hypervel\Console\Scheduling\PendingEventAttributes weekends() diff --git a/src/telescope/dist/app.js b/src/telescope/dist/app.js index a654b11c88..0374a6de07 100644 --- a/src/telescope/dist/app.js +++ b/src/telescope/dist/app.js @@ -86,7 +86,7 @@ If possible, please select a more specific dialect (like sqlite, postgresql, etc `)},a.prototype.displayStateStack=function(u,R){for(var T,v=0,g=0;g0&&R.push(" ^ "+v+" more lines identical to this"),v=0,R.push(" "+P)),T=P}},a.prototype.getSymbolDisplay=function(u){return s(u)},a.prototype.buildFirstStateStack=function(u,R){if(R.indexOf(u)!==-1)return null;if(u.wantedBy.length===0)return[u];var T=u.wantedBy[0],v=[u].concat(R),g=this.buildFirstStateStack(T,v);return g===null?null:[u].concat(g)},a.prototype.save=function(){var u=this.table[this.current];return u.lexerState=this.lexerState,u},a.prototype.restore=function(u){var R=u.index;this.current=R,this.table[R]=u,this.table.splice(R+1),this.lexerState=u.lexerState,this.results=this.finish()},a.prototype.rewind=function(u){if(!this.options.keepHistory)throw new Error("set option `keepHistory` to enable rewinding");this.restore(this.table[u])},a.prototype.finish=function(){var u=[],R=this.grammar.start,T=this.table[this.table.length-1];return T.states.forEach(function(v){v.rule.name===R&&v.dot===v.rule.symbols.length&&v.reference===0&&v.data!==a.fail&&u.push(v)}),u.map(function(v){return v.data})};function s(u){var R=typeof u;if(R==="string")return u;if(R==="object"){if(u.literal)return JSON.stringify(u.literal);if(u instanceof RegExp)return"character matching "+u;if(u.type)return u.type+" token";if(u.test)return"token matching "+String(u.test);throw new Error("Unknown symbol type: "+u)}}function A(u){var R=typeof u;if(R==="string")return u;if(R==="object"){if(u.literal)return JSON.stringify(u.literal);if(u instanceof RegExp)return u.toString();if(u.type)return"%"+u.type;if(u.test)return"<"+String(u.test)+">";throw new Error("Unknown symbol type: "+u)}}return{Parser:a,Grammar:M,Rule:t}})})(KR);var $w=KR.exports;const Kw=Ma($w);function Jw(e){return e.map(jw).map(Qw).map(Zw).map(eH).map(tH)}const jw=(e,t,n)=>{if(FR(e.type)){const r=nH(n,t);if(r&&r.type===_0.PROPERTY_ACCESS_OPERATOR)return Object.assign(Object.assign({},e),{type:_0.IDENTIFIER,text:e.raw});const M=ba(n,t);if(M&&M.type===_0.PROPERTY_ACCESS_OPERATOR)return Object.assign(Object.assign({},e),{type:_0.IDENTIFIER,text:e.raw})}return e},Qw=(e,t,n)=>{if(e.type===_0.RESERVED_FUNCTION_NAME){const r=ba(n,t);if(!r||!JR(r))return Object.assign(Object.assign({},e),{type:_0.IDENTIFIER,text:e.raw})}return e},Zw=(e,t,n)=>{if(e.type===_0.RESERVED_DATA_TYPE){const r=ba(n,t);if(r&&JR(r))return Object.assign(Object.assign({},e),{type:_0.RESERVED_PARAMETERIZED_DATA_TYPE})}return e},eH=(e,t,n)=>{if(e.type===_0.IDENTIFIER){const r=ba(n,t);if(r&&jR(r))return Object.assign(Object.assign({},e),{type:_0.ARRAY_IDENTIFIER})}return e},tH=(e,t,n)=>{if(e.type===_0.RESERVED_DATA_TYPE){const r=ba(n,t);if(r&&jR(r))return Object.assign(Object.assign({},e),{type:_0.ARRAY_KEYWORD})}return e},nH=(e,t)=>ba(e,t,-1),ba=(e,t,n=1)=>{let r=1;for(;e[t+r*n]&&rH(e[t+r*n]);)r++;return e[t+r*n]},JR=e=>e.type===_0.OPEN_PAREN&&e.text==="(",jR=e=>e.type===_0.OPEN_PAREN&&e.text==="[",rH=e=>e.type===_0.BLOCK_COMMENT||e.type===_0.LINE_COMMENT;class QR{constructor(t){this.tokenize=t,this.index=0,this.tokens=[],this.input=""}reset(t,n){this.input=t,this.index=0,this.tokens=this.tokenize(t)}next(){return this.tokens[this.index++]}save(){}formatError(t){const{line:n,col:r}=$R(this.input,t.start);return`Parse error at token: ${t.text} at line ${n} column ${r}`}has(t){return t in _0}}var n1;(function(e){e.statement="statement",e.clause="clause",e.set_operation="set_operation",e.function_call="function_call",e.parameterized_data_type="parameterized_data_type",e.array_subscript="array_subscript",e.property_access="property_access",e.parenthesis="parenthesis",e.between_predicate="between_predicate",e.case_expression="case_expression",e.case_when="case_when",e.case_else="case_else",e.limit_clause="limit_clause",e.all_columns_asterisk="all_columns_asterisk",e.literal="literal",e.identifier="identifier",e.keyword="keyword",e.data_type="data_type",e.parameter="parameter",e.operator="operator",e.comma="comma",e.line_comment="line_comment",e.block_comment="block_comment",e.disable_comment="disable_comment"})(n1=n1||(n1={}));function wb(e){return e[0]}const u1=new QR(e=>[]),To=([[e]])=>e,Xe=e=>({type:n1.keyword,tokenType:e.type,text:e.text,raw:e.raw}),zl=e=>({type:n1.data_type,text:e.text,raw:e.raw}),we=(e,{leading:t,trailing:n})=>(t!=null&&t.length&&(e=Object.assign(Object.assign({},e),{leadingComments:t})),n!=null&&n.length&&(e=Object.assign(Object.assign({},e),{trailingComments:n})),e),oH=(e,{leading:t,trailing:n})=>{if(t!=null&&t.length){const[r,...M]=e;e=[we(r,{leading:t}),...M]}if(n!=null&&n.length){const r=e.slice(0,-1),M=e[e.length-1];e=[...r,we(M,{trailing:n})]}return e},MH={Lexer:u1,ParserRules:[{name:"main$ebnf$1",symbols:[]},{name:"main$ebnf$1",symbols:["main$ebnf$1","statement"],postprocess:e=>e[0].concat([e[1]])},{name:"main",symbols:["main$ebnf$1"],postprocess:([e])=>{const t=e[e.length-1];return t&&!t.hasSemicolon?t.children.length>0?e:e.slice(0,-1):e}},{name:"statement$subexpression$1",symbols:[u1.has("DELIMITER")?{type:"DELIMITER"}:DELIMITER]},{name:"statement$subexpression$1",symbols:[u1.has("EOF")?{type:"EOF"}:EOF]},{name:"statement",symbols:["expressions_or_clauses","statement$subexpression$1"],postprocess:([e,[t]])=>({type:n1.statement,children:e,hasSemicolon:t.type===_0.DELIMITER})},{name:"expressions_or_clauses$ebnf$1",symbols:[]},{name:"expressions_or_clauses$ebnf$1",symbols:["expressions_or_clauses$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"expressions_or_clauses$ebnf$2",symbols:[]},{name:"expressions_or_clauses$ebnf$2",symbols:["expressions_or_clauses$ebnf$2","clause"],postprocess:e=>e[0].concat([e[1]])},{name:"expressions_or_clauses",symbols:["expressions_or_clauses$ebnf$1","expressions_or_clauses$ebnf$2"],postprocess:([e,t])=>[...e,...t]},{name:"clause$subexpression$1",symbols:["limit_clause"]},{name:"clause$subexpression$1",symbols:["select_clause"]},{name:"clause$subexpression$1",symbols:["other_clause"]},{name:"clause$subexpression$1",symbols:["set_operation"]},{name:"clause",symbols:["clause$subexpression$1"],postprocess:To},{name:"limit_clause$ebnf$1$subexpression$1$ebnf$1",symbols:["free_form_sql"]},{name:"limit_clause$ebnf$1$subexpression$1$ebnf$1",symbols:["limit_clause$ebnf$1$subexpression$1$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"limit_clause$ebnf$1$subexpression$1",symbols:[u1.has("COMMA")?{type:"COMMA"}:COMMA,"limit_clause$ebnf$1$subexpression$1$ebnf$1"]},{name:"limit_clause$ebnf$1",symbols:["limit_clause$ebnf$1$subexpression$1"],postprocess:wb},{name:"limit_clause$ebnf$1",symbols:[],postprocess:()=>null},{name:"limit_clause",symbols:[u1.has("LIMIT")?{type:"LIMIT"}:LIMIT,"_","expression_chain_","limit_clause$ebnf$1"],postprocess:([e,t,n,r])=>{if(r){const[M,O]=r;return{type:n1.limit_clause,limitKw:we(Xe(e),{trailing:t}),offset:n,count:O}}else return{type:n1.limit_clause,limitKw:we(Xe(e),{trailing:t}),count:n}}},{name:"select_clause$subexpression$1$ebnf$1",symbols:[]},{name:"select_clause$subexpression$1$ebnf$1",symbols:["select_clause$subexpression$1$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"select_clause$subexpression$1",symbols:["all_columns_asterisk","select_clause$subexpression$1$ebnf$1"]},{name:"select_clause$subexpression$1$ebnf$2",symbols:[]},{name:"select_clause$subexpression$1$ebnf$2",symbols:["select_clause$subexpression$1$ebnf$2","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"select_clause$subexpression$1",symbols:["asteriskless_free_form_sql","select_clause$subexpression$1$ebnf$2"]},{name:"select_clause",symbols:[u1.has("RESERVED_SELECT")?{type:"RESERVED_SELECT"}:RESERVED_SELECT,"select_clause$subexpression$1"],postprocess:([e,[t,n]])=>({type:n1.clause,nameKw:Xe(e),children:[t,...n]})},{name:"select_clause",symbols:[u1.has("RESERVED_SELECT")?{type:"RESERVED_SELECT"}:RESERVED_SELECT],postprocess:([e])=>({type:n1.clause,nameKw:Xe(e),children:[]})},{name:"all_columns_asterisk",symbols:[u1.has("ASTERISK")?{type:"ASTERISK"}:ASTERISK],postprocess:()=>({type:n1.all_columns_asterisk})},{name:"other_clause$ebnf$1",symbols:[]},{name:"other_clause$ebnf$1",symbols:["other_clause$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"other_clause",symbols:[u1.has("RESERVED_CLAUSE")?{type:"RESERVED_CLAUSE"}:RESERVED_CLAUSE,"other_clause$ebnf$1"],postprocess:([e,t])=>({type:n1.clause,nameKw:Xe(e),children:t})},{name:"set_operation$ebnf$1",symbols:[]},{name:"set_operation$ebnf$1",symbols:["set_operation$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"set_operation",symbols:[u1.has("RESERVED_SET_OPERATION")?{type:"RESERVED_SET_OPERATION"}:RESERVED_SET_OPERATION,"set_operation$ebnf$1"],postprocess:([e,t])=>({type:n1.set_operation,nameKw:Xe(e),children:t})},{name:"expression_chain_$ebnf$1",symbols:["expression_with_comments_"]},{name:"expression_chain_$ebnf$1",symbols:["expression_chain_$ebnf$1","expression_with_comments_"],postprocess:e=>e[0].concat([e[1]])},{name:"expression_chain_",symbols:["expression_chain_$ebnf$1"],postprocess:wb},{name:"expression_chain$ebnf$1",symbols:[]},{name:"expression_chain$ebnf$1",symbols:["expression_chain$ebnf$1","_expression_with_comments"],postprocess:e=>e[0].concat([e[1]])},{name:"expression_chain",symbols:["expression","expression_chain$ebnf$1"],postprocess:([e,t])=>[e,...t]},{name:"andless_expression_chain$ebnf$1",symbols:[]},{name:"andless_expression_chain$ebnf$1",symbols:["andless_expression_chain$ebnf$1","_andless_expression_with_comments"],postprocess:e=>e[0].concat([e[1]])},{name:"andless_expression_chain",symbols:["andless_expression","andless_expression_chain$ebnf$1"],postprocess:([e,t])=>[e,...t]},{name:"expression_with_comments_",symbols:["expression","_"],postprocess:([e,t])=>we(e,{trailing:t})},{name:"_expression_with_comments",symbols:["_","expression"],postprocess:([e,t])=>we(t,{leading:e})},{name:"_andless_expression_with_comments",symbols:["_","andless_expression"],postprocess:([e,t])=>we(t,{leading:e})},{name:"free_form_sql$subexpression$1",symbols:["asteriskless_free_form_sql"]},{name:"free_form_sql$subexpression$1",symbols:["asterisk"]},{name:"free_form_sql",symbols:["free_form_sql$subexpression$1"],postprocess:To},{name:"asteriskless_free_form_sql$subexpression$1",symbols:["asteriskless_andless_expression"]},{name:"asteriskless_free_form_sql$subexpression$1",symbols:["logic_operator"]},{name:"asteriskless_free_form_sql$subexpression$1",symbols:["comma"]},{name:"asteriskless_free_form_sql$subexpression$1",symbols:["comment"]},{name:"asteriskless_free_form_sql$subexpression$1",symbols:["other_keyword"]},{name:"asteriskless_free_form_sql",symbols:["asteriskless_free_form_sql$subexpression$1"],postprocess:To},{name:"expression$subexpression$1",symbols:["andless_expression"]},{name:"expression$subexpression$1",symbols:["logic_operator"]},{name:"expression",symbols:["expression$subexpression$1"],postprocess:To},{name:"andless_expression$subexpression$1",symbols:["asteriskless_andless_expression"]},{name:"andless_expression$subexpression$1",symbols:["asterisk"]},{name:"andless_expression",symbols:["andless_expression$subexpression$1"],postprocess:To},{name:"asteriskless_andless_expression$subexpression$1",symbols:["atomic_expression"]},{name:"asteriskless_andless_expression$subexpression$1",symbols:["between_predicate"]},{name:"asteriskless_andless_expression$subexpression$1",symbols:["case_expression"]},{name:"asteriskless_andless_expression",symbols:["asteriskless_andless_expression$subexpression$1"],postprocess:To},{name:"atomic_expression$subexpression$1",symbols:["array_subscript"]},{name:"atomic_expression$subexpression$1",symbols:["function_call"]},{name:"atomic_expression$subexpression$1",symbols:["property_access"]},{name:"atomic_expression$subexpression$1",symbols:["parenthesis"]},{name:"atomic_expression$subexpression$1",symbols:["curly_braces"]},{name:"atomic_expression$subexpression$1",symbols:["square_brackets"]},{name:"atomic_expression$subexpression$1",symbols:["operator"]},{name:"atomic_expression$subexpression$1",symbols:["identifier"]},{name:"atomic_expression$subexpression$1",symbols:["parameter"]},{name:"atomic_expression$subexpression$1",symbols:["literal"]},{name:"atomic_expression$subexpression$1",symbols:["data_type"]},{name:"atomic_expression$subexpression$1",symbols:["keyword"]},{name:"atomic_expression",symbols:["atomic_expression$subexpression$1"],postprocess:To},{name:"array_subscript",symbols:[u1.has("ARRAY_IDENTIFIER")?{type:"ARRAY_IDENTIFIER"}:ARRAY_IDENTIFIER,"_","square_brackets"],postprocess:([e,t,n])=>({type:n1.array_subscript,array:we({type:n1.identifier,quoted:!1,text:e.text},{trailing:t}),parenthesis:n})},{name:"array_subscript",symbols:[u1.has("ARRAY_KEYWORD")?{type:"ARRAY_KEYWORD"}:ARRAY_KEYWORD,"_","square_brackets"],postprocess:([e,t,n])=>({type:n1.array_subscript,array:we(Xe(e),{trailing:t}),parenthesis:n})},{name:"function_call",symbols:[u1.has("RESERVED_FUNCTION_NAME")?{type:"RESERVED_FUNCTION_NAME"}:RESERVED_FUNCTION_NAME,"_","parenthesis"],postprocess:([e,t,n])=>({type:n1.function_call,nameKw:we(Xe(e),{trailing:t}),parenthesis:n})},{name:"parenthesis",symbols:[{literal:"("},"expressions_or_clauses",{literal:")"}],postprocess:([e,t,n])=>({type:n1.parenthesis,children:t,openParen:"(",closeParen:")"})},{name:"curly_braces$ebnf$1",symbols:[]},{name:"curly_braces$ebnf$1",symbols:["curly_braces$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"curly_braces",symbols:[{literal:"{"},"curly_braces$ebnf$1",{literal:"}"}],postprocess:([e,t,n])=>({type:n1.parenthesis,children:t,openParen:"{",closeParen:"}"})},{name:"square_brackets$ebnf$1",symbols:[]},{name:"square_brackets$ebnf$1",symbols:["square_brackets$ebnf$1","free_form_sql"],postprocess:e=>e[0].concat([e[1]])},{name:"square_brackets",symbols:[{literal:"["},"square_brackets$ebnf$1",{literal:"]"}],postprocess:([e,t,n])=>({type:n1.parenthesis,children:t,openParen:"[",closeParen:"]"})},{name:"property_access$subexpression$1",symbols:["identifier"]},{name:"property_access$subexpression$1",symbols:["array_subscript"]},{name:"property_access$subexpression$1",symbols:["all_columns_asterisk"]},{name:"property_access$subexpression$1",symbols:["parameter"]},{name:"property_access",symbols:["atomic_expression","_",u1.has("PROPERTY_ACCESS_OPERATOR")?{type:"PROPERTY_ACCESS_OPERATOR"}:PROPERTY_ACCESS_OPERATOR,"_","property_access$subexpression$1"],postprocess:([e,t,n,r,[M]])=>({type:n1.property_access,object:we(e,{trailing:t}),operator:n.text,property:we(M,{leading:r})})},{name:"between_predicate",symbols:[u1.has("BETWEEN")?{type:"BETWEEN"}:BETWEEN,"_","andless_expression_chain","_",u1.has("AND")?{type:"AND"}:AND,"_","andless_expression"],postprocess:([e,t,n,r,M,O,a])=>({type:n1.between_predicate,betweenKw:Xe(e),expr1:oH(n,{leading:t,trailing:r}),andKw:Xe(M),expr2:[we(a,{leading:O})]})},{name:"case_expression$ebnf$1",symbols:["expression_chain_"],postprocess:wb},{name:"case_expression$ebnf$1",symbols:[],postprocess:()=>null},{name:"case_expression$ebnf$2",symbols:[]},{name:"case_expression$ebnf$2",symbols:["case_expression$ebnf$2","case_clause"],postprocess:e=>e[0].concat([e[1]])},{name:"case_expression",symbols:[u1.has("CASE")?{type:"CASE"}:CASE,"_","case_expression$ebnf$1","case_expression$ebnf$2",u1.has("END")?{type:"END"}:END],postprocess:([e,t,n,r,M])=>({type:n1.case_expression,caseKw:we(Xe(e),{trailing:t}),endKw:Xe(M),expr:n||[],clauses:r})},{name:"case_clause",symbols:[u1.has("WHEN")?{type:"WHEN"}:WHEN,"_","expression_chain_",u1.has("THEN")?{type:"THEN"}:THEN,"_","expression_chain_"],postprocess:([e,t,n,r,M,O])=>({type:n1.case_when,whenKw:we(Xe(e),{trailing:t}),thenKw:we(Xe(r),{trailing:M}),condition:n,result:O})},{name:"case_clause",symbols:[u1.has("ELSE")?{type:"ELSE"}:ELSE,"_","expression_chain_"],postprocess:([e,t,n])=>({type:n1.case_else,elseKw:we(Xe(e),{trailing:t}),result:n})},{name:"comma$subexpression$1",symbols:[u1.has("COMMA")?{type:"COMMA"}:COMMA]},{name:"comma",symbols:["comma$subexpression$1"],postprocess:([[e]])=>({type:n1.comma})},{name:"asterisk$subexpression$1",symbols:[u1.has("ASTERISK")?{type:"ASTERISK"}:ASTERISK]},{name:"asterisk",symbols:["asterisk$subexpression$1"],postprocess:([[e]])=>({type:n1.operator,text:e.text})},{name:"operator$subexpression$1",symbols:[u1.has("OPERATOR")?{type:"OPERATOR"}:OPERATOR]},{name:"operator",symbols:["operator$subexpression$1"],postprocess:([[e]])=>({type:n1.operator,text:e.text})},{name:"identifier$subexpression$1",symbols:[u1.has("IDENTIFIER")?{type:"IDENTIFIER"}:IDENTIFIER]},{name:"identifier$subexpression$1",symbols:[u1.has("QUOTED_IDENTIFIER")?{type:"QUOTED_IDENTIFIER"}:QUOTED_IDENTIFIER]},{name:"identifier$subexpression$1",symbols:[u1.has("VARIABLE")?{type:"VARIABLE"}:VARIABLE]},{name:"identifier",symbols:["identifier$subexpression$1"],postprocess:([[e]])=>({type:n1.identifier,quoted:e.type!=="IDENTIFIER",text:e.text})},{name:"parameter$subexpression$1",symbols:[u1.has("NAMED_PARAMETER")?{type:"NAMED_PARAMETER"}:NAMED_PARAMETER]},{name:"parameter$subexpression$1",symbols:[u1.has("QUOTED_PARAMETER")?{type:"QUOTED_PARAMETER"}:QUOTED_PARAMETER]},{name:"parameter$subexpression$1",symbols:[u1.has("NUMBERED_PARAMETER")?{type:"NUMBERED_PARAMETER"}:NUMBERED_PARAMETER]},{name:"parameter$subexpression$1",symbols:[u1.has("POSITIONAL_PARAMETER")?{type:"POSITIONAL_PARAMETER"}:POSITIONAL_PARAMETER]},{name:"parameter$subexpression$1",symbols:[u1.has("CUSTOM_PARAMETER")?{type:"CUSTOM_PARAMETER"}:CUSTOM_PARAMETER]},{name:"parameter",symbols:["parameter$subexpression$1"],postprocess:([[e]])=>({type:n1.parameter,key:e.key,text:e.text})},{name:"literal$subexpression$1",symbols:[u1.has("NUMBER")?{type:"NUMBER"}:NUMBER]},{name:"literal$subexpression$1",symbols:[u1.has("STRING")?{type:"STRING"}:STRING]},{name:"literal",symbols:["literal$subexpression$1"],postprocess:([[e]])=>({type:n1.literal,text:e.text})},{name:"keyword$subexpression$1",symbols:[u1.has("RESERVED_KEYWORD")?{type:"RESERVED_KEYWORD"}:RESERVED_KEYWORD]},{name:"keyword$subexpression$1",symbols:[u1.has("RESERVED_KEYWORD_PHRASE")?{type:"RESERVED_KEYWORD_PHRASE"}:RESERVED_KEYWORD_PHRASE]},{name:"keyword$subexpression$1",symbols:[u1.has("RESERVED_JOIN")?{type:"RESERVED_JOIN"}:RESERVED_JOIN]},{name:"keyword",symbols:["keyword$subexpression$1"],postprocess:([[e]])=>Xe(e)},{name:"data_type$subexpression$1",symbols:[u1.has("RESERVED_DATA_TYPE")?{type:"RESERVED_DATA_TYPE"}:RESERVED_DATA_TYPE]},{name:"data_type$subexpression$1",symbols:[u1.has("RESERVED_DATA_TYPE_PHRASE")?{type:"RESERVED_DATA_TYPE_PHRASE"}:RESERVED_DATA_TYPE_PHRASE]},{name:"data_type",symbols:["data_type$subexpression$1"],postprocess:([[e]])=>zl(e)},{name:"data_type",symbols:[u1.has("RESERVED_PARAMETERIZED_DATA_TYPE")?{type:"RESERVED_PARAMETERIZED_DATA_TYPE"}:RESERVED_PARAMETERIZED_DATA_TYPE,"_","parenthesis"],postprocess:([e,t,n])=>({type:n1.parameterized_data_type,dataType:we(zl(e),{trailing:t}),parenthesis:n})},{name:"logic_operator$subexpression$1",symbols:[u1.has("AND")?{type:"AND"}:AND]},{name:"logic_operator$subexpression$1",symbols:[u1.has("OR")?{type:"OR"}:OR]},{name:"logic_operator$subexpression$1",symbols:[u1.has("XOR")?{type:"XOR"}:XOR]},{name:"logic_operator",symbols:["logic_operator$subexpression$1"],postprocess:([[e]])=>Xe(e)},{name:"other_keyword$subexpression$1",symbols:[u1.has("WHEN")?{type:"WHEN"}:WHEN]},{name:"other_keyword$subexpression$1",symbols:[u1.has("THEN")?{type:"THEN"}:THEN]},{name:"other_keyword$subexpression$1",symbols:[u1.has("ELSE")?{type:"ELSE"}:ELSE]},{name:"other_keyword$subexpression$1",symbols:[u1.has("END")?{type:"END"}:END]},{name:"other_keyword",symbols:["other_keyword$subexpression$1"],postprocess:([[e]])=>Xe(e)},{name:"_$ebnf$1",symbols:[]},{name:"_$ebnf$1",symbols:["_$ebnf$1","comment"],postprocess:e=>e[0].concat([e[1]])},{name:"_",symbols:["_$ebnf$1"],postprocess:([e])=>e},{name:"comment",symbols:[u1.has("LINE_COMMENT")?{type:"LINE_COMMENT"}:LINE_COMMENT],postprocess:([e])=>({type:n1.line_comment,text:e.text,precedingWhitespace:e.precedingWhitespace})},{name:"comment",symbols:[u1.has("BLOCK_COMMENT")?{type:"BLOCK_COMMENT"}:BLOCK_COMMENT],postprocess:([e])=>({type:n1.block_comment,text:e.text,precedingWhitespace:e.precedingWhitespace})},{name:"comment",symbols:[u1.has("DISABLE_COMMENT")?{type:"DISABLE_COMMENT"}:DISABLE_COMMENT],postprocess:([e])=>({type:n1.disable_comment,text:e.text,precedingWhitespace:e.precedingWhitespace})}],ParserStart:"main"},{Parser:aH,Grammar:iH}=Kw;function OH(e){let t={};const n=new QR(M=>[...Jw(e.tokenize(M,t)),GR(M.length)]),r=new aH(iH.fromCompiled(MH),{lexer:n});return{parse:(M,O)=>{t=O;const{results:a}=r.feed(M);if(a.length===1)return a[0];throw a.length===0?new Error("Parse error: Invalid SQL"):new Error(`Parse error: Ambiguous grammar ${JSON.stringify(a,void 0,2)}`)}}}var N0;(function(e){e[e.SPACE=0]="SPACE",e[e.NO_SPACE=1]="NO_SPACE",e[e.NO_NEWLINE=2]="NO_NEWLINE",e[e.NEWLINE=3]="NEWLINE",e[e.MANDATORY_NEWLINE=4]="MANDATORY_NEWLINE",e[e.INDENT=5]="INDENT",e[e.SINGLE_INDENT=6]="SINGLE_INDENT"})(N0=N0||(N0={}));class ZR{constructor(t){this.indentation=t,this.items=[]}add(...t){for(const n of t)switch(n){case N0.SPACE:this.items.push(N0.SPACE);break;case N0.NO_SPACE:this.trimHorizontalWhitespace();break;case N0.NO_NEWLINE:this.trimWhitespace();break;case N0.NEWLINE:this.trimHorizontalWhitespace(),this.addNewline(N0.NEWLINE);break;case N0.MANDATORY_NEWLINE:this.trimHorizontalWhitespace(),this.addNewline(N0.MANDATORY_NEWLINE);break;case N0.INDENT:this.addIndentation();break;case N0.SINGLE_INDENT:this.items.push(N0.SINGLE_INDENT);break;default:this.items.push(n)}}trimHorizontalWhitespace(){for(;pH(ja(this.items));)this.items.pop()}trimWhitespace(){for(;cH(ja(this.items));)this.items.pop()}addNewline(t){if(this.items.length>0)switch(ja(this.items)){case N0.NEWLINE:this.items.pop(),this.items.push(t);break;case N0.MANDATORY_NEWLINE:break;default:this.items.push(t);break}}addIndentation(){for(let t=0;tthis.itemToString(t)).join("")}getLayoutItems(){return this.items}isAtStartOfLine(){for(let t=this.items.length-1;t>=0;t--){const n=this.items[t];if(n!==N0.SINGLE_INDENT)return n===N0.NEWLINE||n===N0.MANDATORY_NEWLINE}return!1}itemToString(t){switch(t){case N0.SPACE:return" ";case N0.NEWLINE:case N0.MANDATORY_NEWLINE:return` `;case N0.SINGLE_INDENT:return this.indentation.getSingleIndent();default:return t}}}const pH=e=>e===N0.SPACE||e===N0.SINGLE_INDENT,cH=e=>e===N0.SPACE||e===N0.SINGLE_INDENT||e===N0.NEWLINE;function El(e,t){if(t==="standard")return e;let n=[];return e.length>=10&&e.includes(" ")&&([e,...n]=e.split(" ")),t==="tabularLeft"?e=e.padEnd(9," "):e=e.padStart(9," "),e+["",...n].join(" ")}function ul(e){return U8(e)||e===_0.RESERVED_CLAUSE||e===_0.RESERVED_SELECT||e===_0.RESERVED_SET_OPERATION||e===_0.RESERVED_JOIN||e===_0.LIMIT}const Hb="top-level",bH="block-level";class eT{constructor(t){this.indent=t,this.indentTypes=[]}getSingleIndent(){return this.indent}getLevel(){return this.indentTypes.length}increaseTopLevel(){this.indentTypes.push(Hb)}increaseBlockLevel(){this.indentTypes.push(bH)}decreaseTopLevel(){this.indentTypes.length>0&&ja(this.indentTypes)===Hb&&this.indentTypes.pop()}decreaseBlockLevel(){for(;this.indentTypes.length>0&&this.indentTypes.pop()===Hb;);}}class sH extends ZR{constructor(t){super(new eT("")),this.expressionWidth=t,this.length=0,this.trailingSpace=!1}add(...t){if(t.forEach(n=>this.addToLength(n)),this.length>this.expressionWidth)throw new ms;super.add(...t)}addToLength(t){if(typeof t=="string")this.length+=t.length,this.trailingSpace=!1;else{if(t===N0.MANDATORY_NEWLINE||t===N0.NEWLINE)throw new ms;t===N0.INDENT||t===N0.SINGLE_INDENT||t===N0.SPACE?this.trailingSpace||(this.length++,this.trailingSpace=!0):(t===N0.NO_NEWLINE||t===N0.NO_SPACE)&&this.trailingSpace&&(this.trailingSpace=!1,this.length--)}}}class ms extends Error{}class zp{constructor({cfg:t,dialectCfg:n,params:r,layout:M,inline:O=!1}){this.inline=!1,this.nodes=[],this.index=-1,this.cfg=t,this.dialectCfg=n,this.inline=O,this.params=r,this.layout=M}format(t){for(this.nodes=t,this.index=0;this.index{this.layout.add(this.showFunctionKw(t.nameKw))}),this.formatNode(t.parenthesis)}formatParameterizedDataType(t){this.withComments(t.dataType,()=>{this.layout.add(this.showDataType(t.dataType))}),this.formatNode(t.parenthesis)}formatArraySubscript(t){let n;switch(t.array.type){case n1.data_type:n=this.showDataType(t.array);break;case n1.keyword:n=this.showKw(t.array);break;default:n=this.showIdentifier(t.array);break}this.withComments(t.array,()=>{this.layout.add(n)}),this.formatNode(t.parenthesis)}formatPropertyAccess(t){this.formatNode(t.object),this.layout.add(N0.NO_SPACE,t.operator),this.formatNode(t.property)}formatParenthesis(t){const n=this.formatInlineExpression(t.children);n?(this.layout.add(t.openParen),this.layout.add(...n.getLayoutItems()),this.layout.add(N0.NO_SPACE,t.closeParen,N0.SPACE)):(this.layout.add(t.openParen,N0.NEWLINE),vM(this.cfg)?(this.layout.add(N0.INDENT),this.layout=this.formatSubExpression(t.children)):(this.layout.indentation.increaseBlockLevel(),this.layout.add(N0.INDENT),this.layout=this.formatSubExpression(t.children),this.layout.indentation.decreaseBlockLevel()),this.layout.add(N0.NEWLINE,N0.INDENT,t.closeParen,N0.SPACE))}formatBetweenPredicate(t){this.layout.add(this.showKw(t.betweenKw),N0.SPACE),this.layout=this.formatSubExpression(t.expr1),this.layout.add(N0.NO_SPACE,N0.SPACE,this.showNonTabularKw(t.andKw),N0.SPACE),this.layout=this.formatSubExpression(t.expr2),this.layout.add(N0.SPACE)}formatCaseExpression(t){this.formatNode(t.caseKw),this.layout.indentation.increaseBlockLevel(),this.layout=this.formatSubExpression(t.expr),this.layout=this.formatSubExpression(t.clauses),this.layout.indentation.decreaseBlockLevel(),this.layout.add(N0.NEWLINE,N0.INDENT),this.formatNode(t.endKw)}formatCaseWhen(t){this.layout.add(N0.NEWLINE,N0.INDENT),this.formatNode(t.whenKw),this.layout=this.formatSubExpression(t.condition),this.formatNode(t.thenKw),this.layout=this.formatSubExpression(t.result)}formatCaseElse(t){this.layout.add(N0.NEWLINE,N0.INDENT),this.formatNode(t.elseKw),this.layout=this.formatSubExpression(t.result)}formatClause(t){this.isOnelineClause(t)?this.formatClauseInOnelineStyle(t):vM(this.cfg)?this.formatClauseInTabularStyle(t):this.formatClauseInIndentedStyle(t)}isOnelineClause(t){return vM(this.cfg)?this.dialectCfg.tabularOnelineClauses[t.nameKw.text]:this.dialectCfg.onelineClauses[t.nameKw.text]}formatClauseInIndentedStyle(t){this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t.nameKw),N0.NEWLINE),this.layout.indentation.increaseTopLevel(),this.layout.add(N0.INDENT),this.layout=this.formatSubExpression(t.children),this.layout.indentation.decreaseTopLevel()}formatClauseInOnelineStyle(t){this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t.nameKw),N0.SPACE),this.layout=this.formatSubExpression(t.children)}formatClauseInTabularStyle(t){this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t.nameKw),N0.SPACE),this.layout.indentation.increaseTopLevel(),this.layout=this.formatSubExpression(t.children),this.layout.indentation.decreaseTopLevel()}formatSetOperation(t){this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t.nameKw),N0.NEWLINE),this.layout.add(N0.INDENT),this.layout=this.formatSubExpression(t.children)}formatLimitClause(t){this.withComments(t.limitKw,()=>{this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t.limitKw))}),this.layout.indentation.increaseTopLevel(),vM(this.cfg)?this.layout.add(N0.SPACE):this.layout.add(N0.NEWLINE,N0.INDENT),t.offset?(this.layout=this.formatSubExpression(t.offset),this.layout.add(N0.NO_SPACE,",",N0.SPACE),this.layout=this.formatSubExpression(t.count)):this.layout=this.formatSubExpression(t.count),this.layout.indentation.decreaseTopLevel()}formatAllColumnsAsterisk(t){this.layout.add("*",N0.SPACE)}formatLiteral(t){this.layout.add(t.text,N0.SPACE)}formatIdentifier(t){this.layout.add(this.showIdentifier(t),N0.SPACE)}formatParameter(t){this.layout.add(this.params.get(t),N0.SPACE)}formatOperator({text:t}){t==="-"&&this.dialectCfg.identifierDashes?this.layout.add(t,N0.SPACE):this.cfg.denseOperators||this.dialectCfg.alwaysDenseOperators.includes(t)?this.layout.add(N0.NO_SPACE,t):t===":"?this.layout.add(N0.NO_SPACE,t,N0.SPACE):this.layout.add(t,N0.SPACE)}formatComma(t){this.inline?this.layout.add(N0.NO_SPACE,",",N0.SPACE):this.layout.add(N0.NO_SPACE,",",N0.NEWLINE,N0.INDENT)}withComments(t,n){this.formatComments(t.leadingComments),n(),this.formatComments(t.trailingComments)}formatComments(t){t&&t.forEach(n=>{n.type===n1.line_comment?this.formatLineComment(n):this.formatBlockComment(n)})}formatLineComment(t){Ub(t.precedingWhitespace||"")?this.layout.add(N0.NEWLINE,N0.INDENT,t.text,N0.MANDATORY_NEWLINE,N0.INDENT):this.layout.getLayoutItems().length>0?this.layout.add(N0.NO_NEWLINE,N0.SPACE,t.text,N0.MANDATORY_NEWLINE,N0.INDENT):this.layout.add(t.text,N0.MANDATORY_NEWLINE,N0.INDENT)}formatBlockComment(t){t.type===n1.block_comment&&this.isStandaloneBlockComment(t)?(this.splitBlockComment(t.text).forEach(n=>{this.layout.add(N0.NEWLINE,N0.INDENT,n)}),this.layout.add(N0.NEWLINE,N0.INDENT)):this.layout.add(t.text,N0.SPACE)}isStandaloneBlockComment(t){return Ub(t.text)||Ub(t.precedingWhitespace||"")||this.layout.isAtStartOfLine()}isDocComment(t){const n=t.split(/\n/);return/^\/\*\*?$/.test(n[0])&&n.slice(1,n.length-1).every(r=>/^\s*\*/.test(r))&&/^\s*\*\/$/.test(ja(n))}splitBlockComment(t){return this.isDocComment(t)?t.split(/\n/).map(n=>/^\s*\*/.test(n)?" "+n.replace(/^\s*/,""):n):t.split(/\n/).map(n=>n.replace(/^\s*/,""))}formatSubExpression(t){return new zp({cfg:this.cfg,dialectCfg:this.dialectCfg,params:this.params,layout:this.layout,inline:this.inline}).format(t)}formatInlineExpression(t){const n=this.params.getPositionalParameterIndex();try{return new zp({cfg:this.cfg,dialectCfg:this.dialectCfg,params:this.params,layout:new sH(this.cfg.expressionWidth),inline:!0}).format(t)}catch(r){if(r instanceof ms){this.params.setPositionalParameterIndex(n);return}else throw r}}formatKeywordNode(t){switch(t.tokenType){case _0.RESERVED_JOIN:return this.formatJoin(t);case _0.AND:case _0.OR:case _0.XOR:return this.formatLogicalOperator(t);default:return this.formatKeyword(t)}}formatJoin(t){vM(this.cfg)?(this.layout.indentation.decreaseTopLevel(),this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t),N0.SPACE),this.layout.indentation.increaseTopLevel()):this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t),N0.SPACE)}formatKeyword(t){this.layout.add(this.showKw(t),N0.SPACE)}formatLogicalOperator(t){this.cfg.logicalOperatorNewline==="before"?vM(this.cfg)?(this.layout.indentation.decreaseTopLevel(),this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t),N0.SPACE),this.layout.indentation.increaseTopLevel()):this.layout.add(N0.NEWLINE,N0.INDENT,this.showKw(t),N0.SPACE):this.layout.add(this.showKw(t),N0.NEWLINE,N0.INDENT)}formatDataType(t){this.layout.add(this.showDataType(t),N0.SPACE)}showKw(t){return ul(t.tokenType)?El(this.showNonTabularKw(t),this.cfg.indentStyle):this.showNonTabularKw(t)}showNonTabularKw(t){switch(this.cfg.keywordCase){case"preserve":return BO(t.raw);case"upper":return t.text;case"lower":return t.text.toLowerCase()}}showFunctionKw(t){return ul(t.tokenType)?El(this.showNonTabularFunctionKw(t),this.cfg.indentStyle):this.showNonTabularFunctionKw(t)}showNonTabularFunctionKw(t){switch(this.cfg.functionCase){case"preserve":return BO(t.raw);case"upper":return t.text;case"lower":return t.text.toLowerCase()}}showIdentifier(t){if(t.quoted)return t.text;switch(this.cfg.identifierCase){case"preserve":return t.text;case"upper":return t.text.toUpperCase();case"lower":return t.text.toLowerCase()}}showDataType(t){switch(this.cfg.dataTypeCase){case"preserve":return BO(t.raw);case"upper":return t.text;case"lower":return t.text.toLowerCase()}}}class AH{constructor(t,n){this.dialect=t,this.cfg=n,this.params=new kw(this.cfg.params)}format(t){const n=this.parse(t);return this.formatAst(n).trimEnd()}parse(t){return OH(this.dialect.tokenizer).parse(t,this.cfg.paramTypes||{})}formatAst(t){return t.map(n=>this.formatStatement(n)).join(` -`.repeat(this.cfg.linesBetweenQueries+1))}formatStatement(t){const n=new zp({cfg:this.cfg,dialectCfg:this.dialect.formatOptions,params:this.params,layout:new ZR(new eT(Vw(this.cfg)))}).format(t.children);return t.hasSemicolon&&(this.cfg.newlineBeforeSemicolon?n.add(N0.NEWLINE,";"):n.add(N0.NO_NEWLINE,";")),n.toString()}}class yO extends Error{}function zH(e){const t=["multilineLists","newlineBeforeOpenParen","newlineBeforeCloseParen","aliasAs","commaPosition","tabulateAlias"];for(const n of t)if(n in e)throw new yO(`${n} config is no more supported.`);if(e.expressionWidth<=0)throw new yO(`expressionWidth config must be positive number. Received ${e.expressionWidth} instead.`);if(e.params&&!EH(e.params)&&console.warn('WARNING: All "params" option values should be strings.'),e.paramTypes&&!uH(e.paramTypes))throw new yO("Empty regex given in custom paramTypes. That would result in matching infinite amount of parameters.");return e}function EH(e){return(e instanceof Array?e:Object.values(e)).every(n=>typeof n=="string")}function uH(e){return e.custom&&Array.isArray(e.custom)?e.custom.every(t=>t.regex!==""):!0}var lH=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var M=0,r=Object.getOwnPropertySymbols(e);M{if(typeof t.language=="string"&&!nT.includes(t.language))throw new yO(`Unsupported SQL dialect: ${t.language}`);const n=tT[t.language||"sql"];return RH(e,Object.assign(Object.assign({},t),{dialect:Ww[n]}))},RH=(e,t)=>{var{dialect:n}=t,r=lH(t,["dialect"]);if(typeof e!="string")throw new Error("Invalid query argument. Expected string, instead got "+typeof e);const M=zH(Object.assign(Object.assign({},fH),r));return new AH(Fw(n),M).format(e)};wp.registerLanguage("sql",W8);const TH={methods:{highlightSQL(){this.$nextTick(()=>{wp.highlightElement(this.$refs.sqlcode)})},formatSql(e,t){let n={};return t&&(t==="pgsql"&&(t="postgresql"),t==="sqlsrv"&&(t="transactsql"),nT.includes(t)&&(n={language:t})),dH(e,n)}}};var NH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Query Details",resource:"queries",id:t.$route.params.id},on:{ready:function(r){return t.highlightSQL()}},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Connection")]),n("td",[t._v(" "+t._s(r.entry.content.connection)+" ")])]),r.entry.content.file?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Location")]),n("td",[t._v(t._s(r.entry.content.file)+":"+t._s(r.entry.content.line))])]):t._e(),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Duration")]),n("td",[r.entry.content.slow?n("span",{staticClass:"badge badge-danger"},[t._v(" "+t._s(r.entry.content.time)+"ms ")]):n("span",[t._v(" "+t._s(r.entry.content.time)+"ms ")])])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Query")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:t.formatSql(r.entry.content.sql,r.entry.content.driver)}},[n("pre",{ref:"sqlcode",staticClass:"code-bg text-white"},[t._v(t._s(t.formatSql(r.entry.content.sql,r.entry.content.driver)))])])],1)])])}}])})},qH=[],SH=N1(TH,NH,qH,!1,null,null);const _H=SH.exports,LH={mixins:[De]};var hH=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Models",resource:"models"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",[t._v(t._s(t.truncate(r.entry.content.model,70)))]),n("td",{staticClass:"table-fit"},[n("span",{staticClass:"badge",class:"badge-"+t.modelActionClass(r.entry.content.action)},[t._v(" "+t._s(r.entry.content.action)+" ")])]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"model-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Model")]),n("th",{attrs:{scope:"col"}},[t._v("Action")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},IH=[],WH=N1(LH,hH,IH,!1,null,null);const CH=WH.exports,mH={mixins:[De],data(){return{entry:null,batch:[]}}};var vH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Model Action",resource:"models",id:t.$route.params.id},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Model")]),n("td",[t._v(" "+t._s(r.entry.content.model)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Action")]),n("td",[n("span",{staticClass:"badge",class:"badge-"+t.modelActionClass(r.entry.content.action)},[t._v(" "+t._s(r.entry.content.action)+" ")])])]),r.entry.content.count?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Hydrated")]),n("td",[t._v(" "+t._s(r.entry.content.count)+" ")])]):t._e()]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[r.entry.content.action!="deleted"&&r.entry.content.changes?n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Changes")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content.changes}},[n("vue-json-pretty",{attrs:{data:r.entry.content.changes}})],1)],1)]):t._e()])}}])})},gH=[],DH=N1(mH,vH,gH,!1,null,null);const PH=DH.exports,BH={mixins:[De]};var yH=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Requests",resource:"requests"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",{staticClass:"table-fit pr-0"},[n("span",{staticClass:"badge",class:"badge-"+t.requestMethodClass(r.entry.content.method)},[t._v(" "+t._s(r.entry.content.method)+" ")])]),n("td",{attrs:{title:r.entry.content.uri}},[t._v(" "+t._s(t.truncate(r.entry.content.uri,50))+" ")]),n("td",{staticClass:"table-fit text-center"},[n("span",{staticClass:"badge",class:"badge-"+t.requestStatusClass(r.entry.content.response_status)},[t._v(" "+t._s(r.entry.content.response_status)+" ")])]),n("td",{staticClass:"table-fit text-right text-muted"},[r.entry.content.duration?n("span",[t._v(t._s(r.entry.content.duration)+"ms")]):n("span",[t._v("-")])]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"request-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Verb")]),n("th",{attrs:{scope:"col"}},[t._v("Path")]),n("th",{staticClass:"text-center",attrs:{scope:"col"}},[t._v("Status")]),n("th",{staticClass:"text-right",attrs:{scope:"col"}},[t._v("Duration")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},UH=[],XH=N1(BH,yH,UH,!1,null,null);const wH=XH.exports,HH={mixins:[De],data(){return{entry:null,batch:[],currentRequestTab:"payload",currentResponseTab:"response"}}};var GH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Request Details",resource:"requests",id:t.$route.params.id,"entry-point":"true"},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Method")]),n("td",[n("span",{staticClass:"badge",class:"badge-"+t.requestMethodClass(r.entry.content.method)},[t._v(" "+t._s(r.entry.content.method)+" ")])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Controller Action")]),n("td",[t._v(" "+t._s(r.entry.content.controller_action)+" ")])]),r.entry.content.middleware?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Middleware")]),n("td",[t._v(" "+t._s(r.entry.content.middleware.join(", "))+" ")])]):t._e(),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Path")]),n("td",[t._v(" "+t._s(r.entry.content.uri)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Status")]),n("td",[n("span",{staticClass:"badge",class:"badge-"+t.requestStatusClass(r.entry.content.response_status)},[t._v(" "+t._s(r.entry.content.response_status)+" ")])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Duration")]),n("td",[t._v(t._s(r.entry.content.duration||"-")+" ms")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("IP Address")]),n("td",[t._v(" "+t._s(r.entry.content.ip_address||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Worker memory peak")]),n("td",[t._v(t._s(r.entry.content.memory||"-")+" MB")])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentRequestTab=="payload"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentRequestTab="payload"}}},[t._v("Payload")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentRequestTab=="headers"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentRequestTab="headers"}}},[t._v("Headers")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content[t.currentRequestTab]}},[n("vue-json-pretty",{attrs:{data:r.entry.content[t.currentRequestTab]}})],1)],1)]),n("div",{staticClass:"card mt-5"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="response"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="response"}}},[t._v("Response")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="response_headers"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="response_headers"}}},[t._v("Headers")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="session"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="session"}}},[t._v("Session")])]),r.entry.content.context?n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="context"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="context"}}},[t._v("Context")])]):t._e(),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="coroutine_context"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="coroutine_context"}}},[t._v("Coroutine Context")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content[t.currentResponseTab]}},[n("vue-json-pretty",{attrs:{data:r.entry.content[t.currentResponseTab]}})],1)],1)]),n("related-entries",{attrs:{entry:t.entry,batch:t.batch}})],1)}}])})},FH=[],xH=N1(HH,GH,FH,!1,null,null);const YH=xH.exports,VH={};var kH=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Commands",resource:"commands"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",{attrs:{title:r.entry.content.command}},[n("code",[t._v(t._s(t.truncate(r.entry.content.command,90)))])]),n("td",{staticClass:"table-fit text-center text-muted"},[t._v(" "+t._s(r.entry.content.exit_code)+" ")]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"command-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Command")]),n("th",{staticClass:"table-fit",attrs:{scope:"col"}},[t._v("Exit Code")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},$H=[],KH=N1(VH,kH,$H,!1,null,null);const JH=KH.exports,jH={data(){return{entry:null,batch:[],currentTab:"arguments"}}};var QH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Command Details",resource:"commands",id:t.$route.params.id,"entry-point":"true"},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Command")]),n("td",[n("code",[t._v(t._s(r.entry.content.command))])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exit Code")]),n("td",[t._v(" "+t._s(r.entry.content.exit_code)+" ")])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentTab=="arguments"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentTab="arguments"}}},[t._v("Arguments")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentTab=="options"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentTab="options"}}},[t._v("Options")])])]),n("div",[n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content[t.currentTab]}},[n("vue-json-pretty",{attrs:{data:r.entry.content[t.currentTab]}})],1)],1)])]),n("related-entries",{attrs:{entry:t.entry,batch:t.batch}})],1)}}])})},ZH=[],eG=N1(jH,QH,ZH,!1,null,null);const tG=eG.exports,nG={};var rG=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Schedule",resource:"schedule"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",[n("code",[t._v(t._s(t.truncate(r.entry.content.description,85)||t.truncate(r.entry.content.command,85)))])]),n("td",{staticClass:"table-fit text-muted"},[t._v(" "+t._s(r.entry.content.expression)+" ")]),n("td",{staticClass:"table-fit"},[n("span",{staticClass:"badge",class:r.entry.content.status==="finished"?"badge-success":"badge-danger"},[t._v(" "+t._s(r.entry.content.status)+" ")])]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"schedule-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Command")]),n("th",{attrs:{scope:"col"}},[t._v("Expression")]),n("th",{attrs:{scope:"col"}},[t._v("Status")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},oG=[],MG=N1(nG,rG,oG,!1,null,null);const aG=MG.exports,iG={data(){return{entry:null,batch:[]}}};var OG=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Scheduled Command Details",resource:"requests",id:t.$route.params.id},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Description")]),n("td",[t._v(" "+t._s(r.entry.content.description||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Task")]),n("td",[n("code",[t._v(t._s(r.entry.content.command||"-"))])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Expression")]),n("td",[t._v(" "+t._s(r.entry.content.expression)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("User")]),n("td",[t._v(" "+t._s(r.entry.content.user||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Timezone")]),n("td",[t._v(" "+t._s(r.entry.content.timezone||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Status")]),n("td",[n("span",{staticClass:"badge",class:r.entry.content.status==="finished"?"badge-success":"badge-danger"},[t._v(" "+t._s(r.entry.content.status)+" ")])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exit Code")]),n("td",[t._v(" "+t._s(r.entry.content.exit_code===void 0||r.entry.content.exit_code===null?"-":r.entry.content.exit_code)+" ")])]),r.entry.content.exception?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exception")]),n("td",[n("code",[t._v(t._s(r.entry.content.exception.class))])])]):t._e(),r.entry.content.exception?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exception Message")]),n("td",[t._v(" "+t._s(r.entry.content.exception.message)+" ")])]):t._e()]}},{key:"after-attributes-card",fn:function(r){return r.entry.content.output?n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Output")])])]),n("copy-clipboard",{attrs:{data:r.entry.content.output}},[n("pre",{staticClass:"code-bg p-4 mb-0 text-white"},[t._v(t._s(r.entry.content.output))])])],1)]):t._e()}}],null,!0)})},pG=[],cG=N1(iG,OG,pG,!1,null,null);const bG=cG.exports,sG={};var AG=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Redis",resource:"redis"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",[n("code",[t._v(t._s(t.truncate(r.entry.content.command,80)))])]),n("td",{staticClass:"table-fit text-right text-muted"},[t._v(t._s(r.entry.content.time)+"ms")]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"redis-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Command")]),n("th",{staticClass:"text-right",attrs:{scope:"col"}},[t._v("Duration")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},zG=[],EG=N1(sG,AG,zG,!1,null,null);const uG=EG.exports,lG={data(){return{entry:null,batch:[]}}};var fG=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Redis Command Details",resource:"redis",id:t.$route.params.id},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Connection")]),n("td",[t._v(" "+t._s(r.entry.content.connection)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Duration")]),n("td",[t._v(t._s(r.entry.content.time)+"ms")])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Command")])])]),n("pre",{staticClass:"code-bg p-4 mb-0 text-white"},[t._v(t._s(r.entry.content.command))])])])}}])})},dG=[],RG=N1(lG,fG,dG,!1,null,null);const TG=RG.exports;var Gb={exports:{}};/*! +`.repeat(this.cfg.linesBetweenQueries+1))}formatStatement(t){const n=new zp({cfg:this.cfg,dialectCfg:this.dialect.formatOptions,params:this.params,layout:new ZR(new eT(Vw(this.cfg)))}).format(t.children);return t.hasSemicolon&&(this.cfg.newlineBeforeSemicolon?n.add(N0.NEWLINE,";"):n.add(N0.NO_NEWLINE,";")),n.toString()}}class yO extends Error{}function zH(e){const t=["multilineLists","newlineBeforeOpenParen","newlineBeforeCloseParen","aliasAs","commaPosition","tabulateAlias"];for(const n of t)if(n in e)throw new yO(`${n} config is no more supported.`);if(e.expressionWidth<=0)throw new yO(`expressionWidth config must be positive number. Received ${e.expressionWidth} instead.`);if(e.params&&!EH(e.params)&&console.warn('WARNING: All "params" option values should be strings.'),e.paramTypes&&!uH(e.paramTypes))throw new yO("Empty regex given in custom paramTypes. That would result in matching infinite amount of parameters.");return e}function EH(e){return(e instanceof Array?e:Object.values(e)).every(n=>typeof n=="string")}function uH(e){return e.custom&&Array.isArray(e.custom)?e.custom.every(t=>t.regex!==""):!0}var lH=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var M=0,r=Object.getOwnPropertySymbols(e);M{if(typeof t.language=="string"&&!nT.includes(t.language))throw new yO(`Unsupported SQL dialect: ${t.language}`);const n=tT[t.language||"sql"];return RH(e,Object.assign(Object.assign({},t),{dialect:Ww[n]}))},RH=(e,t)=>{var{dialect:n}=t,r=lH(t,["dialect"]);if(typeof e!="string")throw new Error("Invalid query argument. Expected string, instead got "+typeof e);const M=zH(Object.assign(Object.assign({},fH),r));return new AH(Fw(n),M).format(e)};wp.registerLanguage("sql",W8);const TH={methods:{highlightSQL(){this.$nextTick(()=>{wp.highlightElement(this.$refs.sqlcode)})},formatSql(e,t){let n={};return t&&(t==="pgsql"&&(t="postgresql"),t==="sqlsrv"&&(t="transactsql"),nT.includes(t)&&(n={language:t})),dH(e,n)}}};var NH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Query Details",resource:"queries",id:t.$route.params.id},on:{ready:function(r){return t.highlightSQL()}},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Connection")]),n("td",[t._v(" "+t._s(r.entry.content.connection)+" ")])]),r.entry.content.file?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Location")]),n("td",[t._v(t._s(r.entry.content.file)+":"+t._s(r.entry.content.line))])]):t._e(),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Duration")]),n("td",[r.entry.content.slow?n("span",{staticClass:"badge badge-danger"},[t._v(" "+t._s(r.entry.content.time)+"ms ")]):n("span",[t._v(" "+t._s(r.entry.content.time)+"ms ")])])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Query")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:t.formatSql(r.entry.content.sql,r.entry.content.driver)}},[n("pre",{ref:"sqlcode",staticClass:"code-bg text-white"},[t._v(t._s(t.formatSql(r.entry.content.sql,r.entry.content.driver)))])])],1)])])}}])})},qH=[],SH=N1(TH,NH,qH,!1,null,null);const _H=SH.exports,LH={mixins:[De]};var hH=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Models",resource:"models"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",[t._v(t._s(t.truncate(r.entry.content.model,70)))]),n("td",{staticClass:"table-fit"},[n("span",{staticClass:"badge",class:"badge-"+t.modelActionClass(r.entry.content.action)},[t._v(" "+t._s(r.entry.content.action)+" ")])]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"model-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Model")]),n("th",{attrs:{scope:"col"}},[t._v("Action")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},IH=[],WH=N1(LH,hH,IH,!1,null,null);const CH=WH.exports,mH={mixins:[De],data(){return{entry:null,batch:[]}}};var vH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Model Action",resource:"models",id:t.$route.params.id},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Model")]),n("td",[t._v(" "+t._s(r.entry.content.model)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Action")]),n("td",[n("span",{staticClass:"badge",class:"badge-"+t.modelActionClass(r.entry.content.action)},[t._v(" "+t._s(r.entry.content.action)+" ")])])]),r.entry.content.count?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Hydrated")]),n("td",[t._v(" "+t._s(r.entry.content.count)+" ")])]):t._e()]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[r.entry.content.action!="deleted"&&r.entry.content.changes?n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Changes")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content.changes}},[n("vue-json-pretty",{attrs:{data:r.entry.content.changes}})],1)],1)]):t._e()])}}])})},gH=[],DH=N1(mH,vH,gH,!1,null,null);const PH=DH.exports,BH={mixins:[De]};var yH=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Requests",resource:"requests"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",{staticClass:"table-fit pr-0"},[n("span",{staticClass:"badge",class:"badge-"+t.requestMethodClass(r.entry.content.method)},[t._v(" "+t._s(r.entry.content.method)+" ")])]),n("td",{attrs:{title:r.entry.content.uri}},[t._v(" "+t._s(t.truncate(r.entry.content.uri,50))+" ")]),n("td",{staticClass:"table-fit text-center"},[n("span",{staticClass:"badge",class:"badge-"+t.requestStatusClass(r.entry.content.response_status)},[t._v(" "+t._s(r.entry.content.response_status)+" ")])]),n("td",{staticClass:"table-fit text-right text-muted"},[r.entry.content.duration?n("span",[t._v(t._s(r.entry.content.duration)+"ms")]):n("span",[t._v("-")])]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"request-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Verb")]),n("th",{attrs:{scope:"col"}},[t._v("Path")]),n("th",{staticClass:"text-center",attrs:{scope:"col"}},[t._v("Status")]),n("th",{staticClass:"text-right",attrs:{scope:"col"}},[t._v("Duration")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},UH=[],XH=N1(BH,yH,UH,!1,null,null);const wH=XH.exports,HH={mixins:[De],data(){return{entry:null,batch:[],currentRequestTab:"payload",currentResponseTab:"response"}}};var GH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Request Details",resource:"requests",id:t.$route.params.id,"entry-point":"true"},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Method")]),n("td",[n("span",{staticClass:"badge",class:"badge-"+t.requestMethodClass(r.entry.content.method)},[t._v(" "+t._s(r.entry.content.method)+" ")])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Controller Action")]),n("td",[t._v(" "+t._s(r.entry.content.controller_action)+" ")])]),r.entry.content.middleware?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Middleware")]),n("td",[t._v(" "+t._s(r.entry.content.middleware.join(", "))+" ")])]):t._e(),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Path")]),n("td",[t._v(" "+t._s(r.entry.content.uri)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Status")]),n("td",[n("span",{staticClass:"badge",class:"badge-"+t.requestStatusClass(r.entry.content.response_status)},[t._v(" "+t._s(r.entry.content.response_status)+" ")])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Duration")]),n("td",[t._v(t._s(r.entry.content.duration||"-")+" ms")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("IP Address")]),n("td",[t._v(" "+t._s(r.entry.content.ip_address||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Worker memory peak")]),n("td",[t._v(t._s(r.entry.content.memory||"-")+" MB")])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentRequestTab=="payload"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentRequestTab="payload"}}},[t._v("Payload")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentRequestTab=="headers"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentRequestTab="headers"}}},[t._v("Headers")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content[t.currentRequestTab]}},[n("vue-json-pretty",{attrs:{data:r.entry.content[t.currentRequestTab]}})],1)],1)]),n("div",{staticClass:"card mt-5"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="response"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="response"}}},[t._v("Response")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="response_headers"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="response_headers"}}},[t._v("Headers")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="session"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="session"}}},[t._v("Session")])]),r.entry.content.context?n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="context"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="context"}}},[t._v("Context")])]):t._e(),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentResponseTab=="coroutine_context"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentResponseTab="coroutine_context"}}},[t._v("Coroutine Context")])])]),n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content[t.currentResponseTab]}},[n("vue-json-pretty",{attrs:{data:r.entry.content[t.currentResponseTab]}})],1)],1)]),n("related-entries",{attrs:{entry:t.entry,batch:t.batch}})],1)}}])})},FH=[],xH=N1(HH,GH,FH,!1,null,null);const YH=xH.exports,VH={};var kH=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Commands",resource:"commands"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",{attrs:{title:r.entry.content.command}},[n("code",[t._v(t._s(t.truncate(r.entry.content.command,90)))])]),n("td",{staticClass:"table-fit text-center text-muted"},[t._v(" "+t._s(r.entry.content.exit_code)+" ")]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"command-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Command")]),n("th",{staticClass:"table-fit",attrs:{scope:"col"}},[t._v("Exit Code")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},$H=[],KH=N1(VH,kH,$H,!1,null,null);const JH=KH.exports,jH={data(){return{entry:null,batch:[],currentTab:"arguments"}}};var QH=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Command Details",resource:"commands",id:t.$route.params.id,"entry-point":"true"},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Command")]),n("td",[n("code",[t._v(t._s(r.entry.content.command))])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exit Code")]),n("td",[t._v(" "+t._s(r.entry.content.exit_code)+" ")])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentTab=="arguments"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentTab="arguments"}}},[t._v("Arguments")])]),n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link",class:{active:t.currentTab=="options"},attrs:{href:"#"},on:{click:function(M){M.preventDefault(),t.currentTab="options"}}},[t._v("Options")])])]),n("div",[n("div",{staticClass:"code-bg p-4 mb-0 text-white"},[n("copy-clipboard",{attrs:{data:r.entry.content[t.currentTab]}},[n("vue-json-pretty",{attrs:{data:r.entry.content[t.currentTab]}})],1)],1)])]),n("related-entries",{attrs:{entry:t.entry,batch:t.batch}})],1)}}])})},ZH=[],eG=N1(jH,QH,ZH,!1,null,null);const tG=eG.exports,nG={};var rG=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Schedule",resource:"schedule"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",[n("code",[t._v(t._s(t.truncate(r.entry.content.description,85)||t.truncate(r.entry.content.command,85)))])]),n("td",{staticClass:"table-fit text-muted"},[t._v(" "+t._s(r.entry.content.expression)+" ")]),n("td",{staticClass:"table-fit"},[n("span",{staticClass:"badge",class:r.entry.content.status==="finished"?"badge-success":"badge-danger"},[t._v(" "+t._s(r.entry.content.status)+" ")])]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"schedule-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Command")]),n("th",{attrs:{scope:"col"}},[t._v("Expression")]),n("th",{attrs:{scope:"col"}},[t._v("Status")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},oG=[],MG=N1(nG,rG,oG,!1,null,null);const aG=MG.exports,iG={data(){return{entry:null,batch:[]}}};var OG=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Scheduled Command Details",resource:"requests",id:t.$route.params.id},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Description")]),n("td",[t._v(" "+t._s(r.entry.content.description||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Task")]),n("td",[n("code",[t._v(t._s(r.entry.content.command||"-"))])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Expression")]),n("td",[t._v(" "+t._s(r.entry.content.expression)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Timezone")]),n("td",[t._v(" "+t._s(r.entry.content.timezone||"-")+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Status")]),n("td",[n("span",{staticClass:"badge",class:r.entry.content.status==="finished"?"badge-success":"badge-danger"},[t._v(" "+t._s(r.entry.content.status)+" ")])])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exit Code")]),n("td",[t._v(" "+t._s(r.entry.content.exit_code===void 0||r.entry.content.exit_code===null?"-":r.entry.content.exit_code)+" ")])]),r.entry.content.exception?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exception")]),n("td",[n("code",[t._v(t._s(r.entry.content.exception.class))])])]):t._e(),r.entry.content.exception?n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Exception Message")]),n("td",[t._v(" "+t._s(r.entry.content.exception.message)+" ")])]):t._e()]}},{key:"after-attributes-card",fn:function(r){return r.entry.content.output?n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Output")])])]),n("copy-clipboard",{attrs:{data:r.entry.content.output}},[n("pre",{staticClass:"code-bg p-4 mb-0 text-white"},[t._v(t._s(r.entry.content.output))])])],1)]):t._e()}}],null,!0)})},pG=[],cG=N1(iG,OG,pG,!1,null,null);const bG=cG.exports,sG={};var AG=function(){var t=this,n=t._self._c;return n("index-screen",{attrs:{title:"Redis",resource:"redis"},scopedSlots:t._u([{key:"row",fn:function(r){return[n("td",[n("code",[t._v(t._s(t.truncate(r.entry.content.command,80)))])]),n("td",{staticClass:"table-fit text-right text-muted"},[t._v(t._s(r.entry.content.time)+"ms")]),n("td",{staticClass:"table-fit text-muted",attrs:{"data-timeago":r.entry.created_at,title:r.entry.created_at}},[t._v(" "+t._s(t.timeAgo(r.entry.created_at))+" ")]),n("td",{staticClass:"table-fit"},[n("router-link",{staticClass:"control-action",attrs:{to:{name:"redis-preview",params:{id:r.entry.id}}}},[n("svg",{attrs:{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20"}},[n("path",{attrs:{"fill-rule":"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM6.75 9.25a.75.75 0 000 1.5h4.59l-2.1 1.95a.75.75 0 001.02 1.1l3.5-3.25a.75.75 0 000-1.1l-3.5-3.25a.75.75 0 10-1.02 1.1l2.1 1.95H6.75z","clip-rule":"evenodd"}})])])],1)]}}])},[n("tr",{attrs:{slot:"table-header"},slot:"table-header"},[n("th",{attrs:{scope:"col"}},[t._v("Command")]),n("th",{staticClass:"text-right",attrs:{scope:"col"}},[t._v("Duration")]),n("th",{attrs:{scope:"col"}},[t._v("Happened")]),n("th",{attrs:{scope:"col"}})])])},zG=[],EG=N1(sG,AG,zG,!1,null,null);const uG=EG.exports,lG={data(){return{entry:null,batch:[]}}};var fG=function(){var t=this,n=t._self._c;return n("preview-screen",{attrs:{title:"Redis Command Details",resource:"redis",id:t.$route.params.id},scopedSlots:t._u([{key:"table-parameters",fn:function(r){return[n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Connection")]),n("td",[t._v(" "+t._s(r.entry.content.connection)+" ")])]),n("tr",[n("td",{staticClass:"table-fit text-muted"},[t._v("Duration")]),n("td",[t._v(t._s(r.entry.content.time)+"ms")])])]}},{key:"after-attributes-card",fn:function(r){return n("div",{},[n("div",{staticClass:"card mt-5 overflow-hidden"},[n("ul",{staticClass:"nav nav-pills"},[n("li",{staticClass:"nav-item"},[n("a",{staticClass:"nav-link active"},[t._v("Command")])])]),n("pre",{staticClass:"code-bg p-4 mb-0 text-white"},[t._v(t._s(r.entry.content.command))])])])}}])})},dG=[],RG=N1(lG,fG,dG,!1,null,null);const TG=RG.exports;var Gb={exports:{}};/*! * jQuery JavaScript Library v3.7.1 * https://jquery.com/ * diff --git a/src/telescope/resources/js/screens/schedule/preview.vue b/src/telescope/resources/js/screens/schedule/preview.vue index 79486e0db2..387bc9c128 100644 --- a/src/telescope/resources/js/screens/schedule/preview.vue +++ b/src/telescope/resources/js/screens/schedule/preview.vue @@ -33,13 +33,6 @@ export default { - - User - - {{ slotProps.entry.content.user || '-' }} - - - Timezone diff --git a/src/telescope/src/Watchers/ScheduleWatcher.php b/src/telescope/src/Watchers/ScheduleWatcher.php index c6b65f7229..c2705922f2 100644 --- a/src/telescope/src/Watchers/ScheduleWatcher.php +++ b/src/telescope/src/Watchers/ScheduleWatcher.php @@ -127,7 +127,6 @@ protected function makeEntry(Event $task, array $outcome): IncomingEntry 'description' => $task->description, 'expression' => $task->expression, 'timezone' => $task->timezone, - 'user' => $task->user, 'output' => $task->getOutput($this->app), ], $outcome)); } diff --git a/src/testing/src/TestResponse.php b/src/testing/src/TestResponse.php index 9047960ff0..3334d07c86 100644 --- a/src/testing/src/TestResponse.php +++ b/src/testing/src/TestResponse.php @@ -485,7 +485,7 @@ public function assertCookieExpired(string $cookieName): static $expiresAt = CarbonImmutable::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); PHPUnit::withResponse($this)->assertTrue( - $cookie->getExpiresTime() !== 0 && $expiresAt->lessThan(CarbonImmutable::now()), + $cookie->getExpiresTime() !== 0 && $expiresAt->isPast(), "Cookie [{$cookieName}] is not expired, it expires at [{$expiresAt}]." ); @@ -505,7 +505,7 @@ public function assertCookieNotExpired(string $cookieName): static $expiresAt = CarbonImmutable::createFromTimestamp($cookie->getExpiresTime(), date_default_timezone_get()); PHPUnit::withResponse($this)->assertTrue( - $cookie->getExpiresTime() === 0 || $expiresAt->greaterThan(CarbonImmutable::now()), + $cookie->getExpiresTime() === 0 || $expiresAt->isFuture(), "Cookie [{$cookieName}] is expired, it expired at [{$expiresAt}]." ); diff --git a/tests/Auth/AuthDatabaseTokenRepositoryTest.php b/tests/Auth/AuthDatabaseTokenRepositoryTest.php index 81a6865b23..83e33d4348 100755 --- a/tests/Auth/AuthDatabaseTokenRepositoryTest.php +++ b/tests/Auth/AuthDatabaseTokenRepositoryTest.php @@ -19,13 +19,14 @@ class AuthDatabaseTokenRepositoryTest extends TestCase public function testCreateInsertsNewRecordIntoTable(): void { $repo = $this->getRepo(); - $repo->getHasher()->shouldReceive('make')->once()->andReturn('hashed-token'); - $repo->getConnection()->shouldReceive('table')->times(2)->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $query->shouldReceive('delete')->once(); - $query->shouldReceive('insert')->once(); + $repo->getHasher()->expects('make')->andReturn('hashed-token'); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->times(2)->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); + $query->expects('delete'); + $query->expects('insert'); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->times(2)->andReturn('email'); + $user->expects('getEmailForPasswordReset')->times(2)->andReturn('email'); $results = $repo->create($user); @@ -36,11 +37,12 @@ public function testCreateInsertsNewRecordIntoTable(): void public function testExistReturnsFalseIfNoRowFoundForUser(): void { $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $query->shouldReceive('first')->once()->andReturn(null); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); + $query->expects('first')->andReturn(null); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertFalse($repo->exists($user, 'token')); } @@ -48,12 +50,13 @@ public function testExistReturnsFalseIfNoRowFoundForUser(): void public function testExistReturnsFalseIfRecordIsExpired(): void { $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); $date = CarbonImmutable::now()->subSeconds(300000)->toDateTimeString(); - $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); + $query->expects('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertFalse($repo->exists($user, 'token')); } @@ -61,13 +64,14 @@ public function testExistReturnsFalseIfRecordIsExpired(): void public function testExistReturnsTrueIfValidRecordExists(): void { $repo = $this->getRepo(); - $repo->getHasher()->shouldReceive('check')->once()->with('token', 'hashed-token')->andReturn(true); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); + $repo->getHasher()->expects('check')->with('token', 'hashed-token')->andReturn(true); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); $date = CarbonImmutable::now()->subMinutes(10)->toDateTimeString(); - $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); + $query->expects('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertTrue($repo->exists($user, 'token')); } @@ -75,13 +79,14 @@ public function testExistReturnsTrueIfValidRecordExists(): void public function testExistReturnsFalseIfInvalidToken(): void { $repo = $this->getRepo(); - $repo->getHasher()->shouldReceive('check')->once()->with('wrong-token', 'hashed-token')->andReturn(false); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); + $repo->getHasher()->expects('check')->with('wrong-token', 'hashed-token')->andReturn(false); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); $date = CarbonImmutable::now()->subMinutes(10)->toDateTimeString(); - $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); + $query->expects('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertFalse($repo->exists($user, 'wrong-token')); } @@ -89,41 +94,44 @@ public function testExistReturnsFalseIfInvalidToken(): void public function testRecentlyCreatedReturnsFalseIfNoRowFoundForUser(): void { $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $query->shouldReceive('first')->once()->andReturn(null); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); + $query->expects('first')->andReturn(null); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertFalse($repo->recentlyCreatedToken($user)); } public function testRecentlyCreatedReturnsTrueIfRecordIsRecentlyCreated(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = CarbonImmutable::now()->subSeconds(59)->toDateTimeString(); - $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); + $date = $now->subSeconds(59)->toDateTimeString(); + $query->expects('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertTrue($repo->recentlyCreatedToken($user)); } public function testRecentlyCreatedReturnsFalseIfValidRecordExists(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $date = CarbonImmutable::now()->subSeconds(61)->toDateTimeString(); - $query->shouldReceive('first')->once()->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); + $date = $now->subSeconds(61)->toDateTimeString(); + $query->expects('first')->andReturn((object) ['created_at' => $date, 'token' => 'hashed-token']); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $this->assertFalse($repo->recentlyCreatedToken($user)); } @@ -131,11 +139,12 @@ public function testRecentlyCreatedReturnsFalseIfValidRecordExists(): void public function testDeleteMethodDeletesByToken(): void { $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('email', 'email')->andReturn($query); - $query->shouldReceive('delete')->once(); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('email', 'email')->andReturn($query); + $query->expects('delete'); $user = m::mock(CanResetPassword::class); - $user->shouldReceive('getEmailForPasswordReset')->once()->andReturn('email'); + $user->expects('getEmailForPasswordReset')->andReturn('email'); $repo->delete($user); } @@ -143,9 +152,10 @@ public function testDeleteMethodDeletesByToken(): void public function testDeleteExpiredMethodDeletesExpiredTokens(): void { $repo = $this->getRepo(); - $repo->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($query = m::mock(Builder::class)); - $query->shouldReceive('where')->once()->with('created_at', '<', m::any())->andReturn($query); - $query->shouldReceive('delete')->once(); + $query = m::mock(Builder::class); + $repo->getConnection()->expects('table')->with('table')->andReturn($query); + $query->expects('where')->with('created_at', '<', m::any())->andReturn($query); + $query->expects('delete'); $repo->deleteExpired(); } @@ -207,6 +217,9 @@ public function testResolverContractTakesPrecedenceForObjectsImplementingBothCon $this->assertSame($resolvedConnection, $repository->getConnection()); } + /** + * Create a token repository with mocked dependencies. + */ protected function getRepo(): DatabaseTokenRepository { return new DatabaseTokenRepository( diff --git a/tests/Auth/AuthDatabaseUserProviderTest.php b/tests/Auth/AuthDatabaseUserProviderTest.php index a33698083a..b96071ab42 100755 --- a/tests/Auth/AuthDatabaseUserProviderTest.php +++ b/tests/Auth/AuthDatabaseUserProviderTest.php @@ -19,12 +19,12 @@ class AuthDatabaseUserProviderTest extends TestCase { - public function testRetrieveByIDReturnsUserWhenUserIsFound() + public function testRetrieveByIDReturnsUserWhenUserIsFound(): void { $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('find')->once()->with(1)->andReturn(['id' => 1, 'name' => 'Dayle']); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('find')->with(1)->andReturn(['id' => 1, 'name' => 'Dayle']); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveById(1); @@ -34,12 +34,12 @@ public function testRetrieveByIDReturnsUserWhenUserIsFound() $this->assertSame('Dayle', $user->name); } - public function testRetrieveByIDReturnsNullWhenUserIsNotFound() + public function testRetrieveByIDReturnsNullWhenUserIsNotFound(): void { $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('find')->once()->with(1)->andReturn(null); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('find')->with(1)->andReturn(null); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveById(1); @@ -124,15 +124,15 @@ public function testResolverContractTakesPrecedenceForObjectsImplementingBothCon $this->assertNull($provider->retrieveById(1)); } - public function testRetrieveByTokenReturnsUser() + public function testRetrieveByTokenReturnsUser(): void { $mockUser = new stdClass; $mockUser->remember_token = 'a'; $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('find')->once()->with(1)->andReturn($mockUser); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('find')->with(1)->andReturn($mockUser); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveByToken(1, 'a'); @@ -140,12 +140,12 @@ public function testRetrieveByTokenReturnsUser() $this->assertEquals(new GenericUser((array) $mockUser), $user); } - public function testRetrieveTokenWithBadIdentifierReturnsNull() + public function testRetrieveTokenWithBadIdentifierReturnsNull(): void { $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('find')->once()->with(1)->andReturn(null); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('find')->with(1)->andReturn(null); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveByToken(1, 'a'); @@ -153,15 +153,15 @@ public function testRetrieveTokenWithBadIdentifierReturnsNull() $this->assertNull($user); } - public function testRetrieveByBadTokenReturnsNull() + public function testRetrieveByBadTokenReturnsNull(): void { $mockUser = new stdClass; $mockUser->remember_token = null; $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('find')->once()->with(1)->andReturn($mockUser); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('find')->with(1)->andReturn($mockUser); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveByToken(1, 'a'); @@ -169,14 +169,14 @@ public function testRetrieveByBadTokenReturnsNull() $this->assertNull($user); } - public function testRetrieveByCredentialsReturnsUserWhenUserIsFound() + public function testRetrieveByCredentialsReturnsUserWhenUserIsFound(): void { $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('where')->once()->with('username', 'dayle'); - $query->shouldReceive('whereIn')->once()->with('group', ['one', 'two']); - $query->shouldReceive('first')->once()->andReturn(['id' => 1, 'name' => 'taylor']); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('where')->with('username', 'dayle'); + $query->expects('whereIn')->with('group', ['one', 'two']); + $query->expects('first')->andReturn(['id' => 1, 'name' => 'taylor']); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveByCredentials(['username' => 'dayle', 'password' => 'foo', 'group' => ['one', 'two']]); @@ -186,18 +186,18 @@ public function testRetrieveByCredentialsReturnsUserWhenUserIsFound() $this->assertSame('taylor', $user->name); } - public function testRetrieveByCredentialsAcceptsCallback() + public function testRetrieveByCredentialsAcceptsCallback(): void { $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('where')->once()->with('username', 'dayle'); - $query->shouldReceive('whereIn')->once()->with('group', ['one', 'two']); - $query->shouldReceive('first')->once()->andReturn(['id' => 1, 'name' => 'taylor']); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('where')->with('username', 'dayle'); + $query->expects('whereIn')->with('group', ['one', 'two']); + $query->expects('first')->andReturn(['id' => 1, 'name' => 'taylor']); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); - $user = $provider->retrieveByCredentials([function ($builder) { + $user = $provider->retrieveByCredentials([function (Builder $builder): void { $builder->where('username', 'dayle'); $builder->whereIn('group', ['one', 'two']); }]); @@ -207,13 +207,13 @@ public function testRetrieveByCredentialsAcceptsCallback() $this->assertSame('taylor', $user->name); } - public function testRetrieveByCredentialsReturnsNullWhenUserIsFound() + public function testRetrieveByCredentialsReturnsNullWhenUserIsFound(): void { $conn = m::mock(ConnectionInterface::class); $query = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($query); - $query->shouldReceive('where')->once()->with('username', 'dayle'); - $query->shouldReceive('first')->once()->andReturn(null); + $conn->expects('table')->with('foo')->andReturn($query); + $query->expects('where')->with('username', 'dayle'); + $query->expects('first')->andReturn(null); $hasher = m::mock(Hasher::class); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveByCredentials(['username' => 'dayle']); @@ -221,7 +221,7 @@ public function testRetrieveByCredentialsReturnsNullWhenUserIsFound() $this->assertNull($user); } - public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull() + public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull(): void { $conn = m::mock(ConnectionInterface::class); $hasher = m::mock(Hasher::class); @@ -234,71 +234,71 @@ public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull() $this->assertNull($user); } - public function testCredentialValidation() + public function testCredentialValidation(): void { $conn = m::mock(ConnectionInterface::class); $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('check')->once()->with('plain', 'hash')->andReturn(true); + $hasher->expects('check')->with('plain', 'hash')->andReturn(true); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->expects('getAuthPassword')->andReturn('hash'); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertTrue($result); } - public function testCredentialValidationFails() + public function testCredentialValidationFails(): void { $conn = m::mock(ConnectionInterface::class); $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('check')->once()->with('plain', 'hash')->andReturn(false); + $hasher->expects('check')->with('plain', 'hash')->andReturn(false); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->expects('getAuthPassword')->andReturn('hash'); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertFalse($result); } - public function testCredentialValidationFailsGracefullyWithNullPassword() + public function testCredentialValidationFailsGracefullyWithNullPassword(): void { $conn = m::mock(ConnectionInterface::class); $hasher = m::mock(Hasher::class); $hasher->shouldReceive('check')->never(); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn(null); + $user->expects('getAuthPassword')->andReturn(null); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertFalse($result); } - public function testRehashPasswordIfRequired() + public function testRehashPasswordIfRequired(): void { $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(true); - $hasher->shouldReceive('make')->once()->with('plain')->andReturn('rehashed'); + $hasher->expects('needsRehash')->with('hash')->andReturn(true); + $hasher->expects('make')->with('plain')->andReturn('rehashed'); $conn = m::mock(ConnectionInterface::class); $table = m::mock(Builder::class); - $conn->shouldReceive('table')->once()->with('foo')->andReturn($table); - $table->shouldReceive('where')->once()->with('id', 1)->andReturnSelf(); - $table->shouldReceive('update')->once()->with(['password_attribute' => 'rehashed']); + $conn->expects('table')->with('foo')->andReturn($table); + $table->expects('where')->with('id', 1)->andReturnSelf(); + $table->expects('update')->with(['password_attribute' => 'rehashed']); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthIdentifierName')->once()->andReturn('id'); - $user->shouldReceive('getAuthIdentifier')->once()->andReturn(1); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); - $user->shouldReceive('getAuthPasswordName')->once()->andReturn('password_attribute'); + $user->expects('getAuthIdentifierName')->andReturn('id'); + $user->expects('getAuthIdentifier')->andReturn(1); + $user->expects('getAuthPassword')->andReturn('hash'); + $user->expects('getAuthPasswordName')->andReturn('password_attribute'); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); } - public function testDontRehashPasswordIfNotRequired() + public function testDontRehashPasswordIfNotRequired(): void { $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(false); + $hasher->expects('needsRehash')->with('hash')->andReturn(false); $hasher->shouldNotReceive('make'); $conn = m::mock(ConnectionInterface::class); @@ -308,7 +308,7 @@ public function testDontRehashPasswordIfNotRequired() $table->shouldNotReceive('update'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->expects('getAuthPassword')->andReturn('hash'); $user->shouldNotReceive('getAuthIdentifierName'); $user->shouldNotReceive('getAuthIdentifier'); $user->shouldNotReceive('getAuthPasswordName'); diff --git a/tests/Auth/AuthEloquentUserProviderTest.php b/tests/Auth/AuthEloquentUserProviderTest.php index 7656d9a58e..e88c1b15db 100755 --- a/tests/Auth/AuthEloquentUserProviderTest.php +++ b/tests/Auth/AuthEloquentUserProviderTest.php @@ -12,60 +12,61 @@ use Hypervel\Foundation\Auth\User; use Hypervel\Tests\TestCase; use Mockery as m; +use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; class AuthEloquentUserProviderTest extends TestCase { - public function testRetrieveByIDReturnsUser() + public function testRetrieveByIDReturnsUser(): void { $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); $expectedUser = m::mock(Authenticatable::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $model->shouldReceive('getAuthIdentifierName')->once()->andReturn('id'); - $builder->shouldReceive('where')->once()->with('id', 1)->andReturn($builder); - $builder->shouldReceive('first')->once()->andReturn($expectedUser); + $model->expects('newQuery')->andReturn($builder); + $model->expects('getAuthIdentifierName')->andReturn('id'); + $builder->expects('where')->with('id', 1)->andReturn($builder); + $builder->expects('first')->andReturn($expectedUser); $provider->expects($this->once())->method('createModel')->willReturn($model); $user = $provider->retrieveById(1); $this->assertSame($expectedUser, $user); } - public function testRetrieveByTokenReturnsUser() + public function testRetrieveByTokenReturnsUser(): void { $mockUser = m::mock(Authenticatable::class); - $mockUser->shouldReceive('getRememberToken')->once()->andReturn('a'); + $mockUser->expects('getRememberToken')->andReturn('a'); $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $model->shouldReceive('getAuthIdentifierName')->once()->andReturn('id'); - $builder->shouldReceive('where')->once()->with('id', 1)->andReturn($builder); - $builder->shouldReceive('first')->once()->andReturn($mockUser); + $model->expects('newQuery')->andReturn($builder); + $model->expects('getAuthIdentifierName')->andReturn('id'); + $builder->expects('where')->with('id', 1)->andReturn($builder); + $builder->expects('first')->andReturn($mockUser); $provider->expects($this->once())->method('createModel')->willReturn($model); $user = $provider->retrieveByToken(1, 'a'); $this->assertEquals($mockUser, $user); } - public function testRetrieveTokenWithBadIdentifierReturnsNull() + public function testRetrieveTokenWithBadIdentifierReturnsNull(): void { $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $model->shouldReceive('getAuthIdentifierName')->once()->andReturn('id'); - $builder->shouldReceive('where')->once()->with('id', 1)->andReturn($builder); - $builder->shouldReceive('first')->once()->andReturn(null); + $model->expects('newQuery')->andReturn($builder); + $model->expects('getAuthIdentifierName')->andReturn('id'); + $builder->expects('where')->with('id', 1)->andReturn($builder); + $builder->expects('first')->andReturn(null); $provider->expects($this->once())->method('createModel')->willReturn($model); $user = $provider->retrieveByToken(1, 'a'); $this->assertNull($user); } - public function testRetrievingWithOnlyPasswordCredentialReturnsNull() + public function testRetrievingWithOnlyPasswordCredentialReturnsNull(): void { $provider = $this->getProviderMock(); $provider->expects($this->never())->method('createModel'); @@ -74,18 +75,18 @@ public function testRetrievingWithOnlyPasswordCredentialReturnsNull() $this->assertNull($user); } - public function testRetrieveByBadTokenReturnsNull() + public function testRetrieveByBadTokenReturnsNull(): void { $mockUser = m::mock(Authenticatable::class); - $mockUser->shouldReceive('getRememberToken')->once()->andReturn(null); + $mockUser->expects('getRememberToken')->andReturn(null); $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $model->shouldReceive('getAuthIdentifierName')->once()->andReturn('id'); - $builder->shouldReceive('where')->once()->with('id', 1)->andReturn($builder); - $builder->shouldReceive('first')->once()->andReturn($mockUser); + $model->expects('newQuery')->andReturn($builder); + $model->expects('getAuthIdentifierName')->andReturn('id'); + $builder->expects('where')->with('id', 1)->andReturn($builder); + $builder->expects('first')->andReturn($mockUser); $provider->expects($this->once())->method('createModel')->willReturn($model); $user = $provider->retrieveByToken(1, 'a'); @@ -135,34 +136,34 @@ public function testUpdateRememberTokenRestoresTimestampsAfterSaveFailure(): voi $this->assertSame('remember-token', $user->getRememberToken()); } - public function testRetrieveByCredentialsReturnsUser() + public function testRetrieveByCredentialsReturnsUser(): void { $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); $expectedUser = m::mock(Authenticatable::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $builder->shouldReceive('where')->once()->with('username', 'dayle'); - $builder->shouldReceive('whereIn')->once()->with('group', ['one', 'two']); - $builder->shouldReceive('first')->once()->andReturn($expectedUser); + $model->expects('newQuery')->andReturn($builder); + $builder->expects('where')->with('username', 'dayle'); + $builder->expects('whereIn')->with('group', ['one', 'two']); + $builder->expects('first')->andReturn($expectedUser); $provider->expects($this->once())->method('createModel')->willReturn($model); $user = $provider->retrieveByCredentials(['username' => 'dayle', 'password' => 'foo', 'group' => ['one', 'two']]); $this->assertSame($expectedUser, $user); } - public function testRetrieveByCredentialsAcceptsCallback() + public function testRetrieveByCredentialsAcceptsCallback(): void { $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); $expectedUser = m::mock(Authenticatable::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $builder->shouldReceive('where')->once()->with('username', 'dayle'); - $builder->shouldReceive('whereIn')->once()->with('group', ['one', 'two']); - $builder->shouldReceive('first')->once()->andReturn($expectedUser); + $model->expects('newQuery')->andReturn($builder); + $builder->expects('where')->with('username', 'dayle'); + $builder->expects('whereIn')->with('group', ['one', 'two']); + $builder->expects('first')->andReturn($expectedUser); $provider->expects($this->once())->method('createModel')->willReturn($model); - $user = $provider->retrieveByCredentials([function ($builder) { + $user = $provider->retrieveByCredentials([function (Builder $builder): void { $builder->where('username', 'dayle'); $builder->whereIn('group', ['one', 'two']); }]); @@ -170,7 +171,7 @@ public function testRetrieveByCredentialsAcceptsCallback() $this->assertSame($expectedUser, $user); } - public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull() + public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull(): void { $provider = $this->getProviderMock(); $provider->expects($this->never())->method('createModel'); @@ -182,66 +183,66 @@ public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull() $this->assertNull($user); } - public function testCredentialValidation() + public function testCredentialValidation(): void { $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('check')->once()->with('plain', 'hash')->andReturn(true); + $hasher->expects('check')->with('plain', 'hash')->andReturn(true); $provider = new EloquentUserProvider($hasher, 'foo'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->expects('getAuthPassword')->andReturn('hash'); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertTrue($result); } - public function testCredentialValidationFailed() + public function testCredentialValidationFailed(): void { $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('check')->once()->with('plain', 'hash')->andReturn(false); + $hasher->expects('check')->with('plain', 'hash')->andReturn(false); $provider = new EloquentUserProvider($hasher, 'foo'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->expects('getAuthPassword')->andReturn('hash'); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertFalse($result); } - public function testCredentialValidationFailsGracefullyWithNullPassword() + public function testCredentialValidationFailsGracefullyWithNullPassword(): void { $hasher = m::mock(Hasher::class); $hasher->shouldReceive('check')->never(); $provider = new EloquentUserProvider($hasher, 'foo'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn(null); + $user->expects('getAuthPassword')->andReturn(null); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertFalse($result); } - public function testRehashPasswordIfRequired() + public function testRehashPasswordIfRequired(): void { $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(true); - $hasher->shouldReceive('make')->once()->with('plain')->andReturn('rehashed'); + $hasher->expects('needsRehash')->with('hash')->andReturn(true); + $hasher->expects('make')->with('plain')->andReturn('rehashed'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); - $user->shouldReceive('getAuthPasswordName')->once()->andReturn('password_attribute'); - $user->shouldReceive('forceFill')->once()->with(['password_attribute' => 'rehashed'])->andReturnSelf(); - $user->shouldReceive('save')->once(); + $user->expects('getAuthPassword')->andReturn('hash'); + $user->expects('getAuthPasswordName')->andReturn('password_attribute'); + $user->expects('forceFill')->with(['password_attribute' => 'rehashed'])->andReturnSelf(); + $user->expects('save'); $provider = new EloquentUserProvider($hasher, 'foo'); $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); } - public function testDontRehashPasswordIfNotRequired() + public function testDontRehashPasswordIfNotRequired(): void { $hasher = m::mock(Hasher::class); - $hasher->shouldReceive('needsRehash')->once()->with('hash')->andReturn(false); + $hasher->expects('needsRehash')->with('hash')->andReturn(false); $hasher->shouldNotReceive('make'); $user = m::mock(Authenticatable::class); - $user->shouldReceive('getAuthPassword')->once()->andReturn('hash'); + $user->expects('getAuthPassword')->andReturn('hash'); $user->shouldNotReceive('getAuthPasswordName'); $user->shouldNotReceive('forceFill'); $user->shouldNotReceive('save'); @@ -250,7 +251,7 @@ public function testDontRehashPasswordIfNotRequired() $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); } - public function testModelsCanBeCreated() + public function testModelsCanBeCreated(): void { $hasher = m::mock(Hasher::class); $provider = new EloquentUserProvider($hasher, EloquentProviderUserStub::class); @@ -259,23 +260,23 @@ public function testModelsCanBeCreated() $this->assertInstanceOf(EloquentProviderUserStub::class, $model); } - public function testRegistersQueryHandler() + public function testRegistersQueryHandler(): void { - $callback = function ($builder) { + $callback = function (Builder $builder): void { $builder->whereIn('group', ['one', 'two']); }; $provider = $this->getProviderMock(); $model = m::mock(Model::class); $builder = m::mock(Builder::class); - $model->shouldReceive('newQuery')->once()->andReturn($builder); - $builder->shouldReceive('where')->once()->with('username', 'dayle'); - $builder->shouldReceive('whereIn')->once()->with('group', ['one', 'two']); + $model->expects('newQuery')->andReturn($builder); + $builder->expects('where')->with('username', 'dayle'); + $builder->expects('whereIn')->with('group', ['one', 'two']); $expectedUser = m::mock(Authenticatable::class); - $builder->shouldReceive('first')->once()->andReturn($expectedUser); + $builder->expects('first')->andReturn($expectedUser); $provider->expects($this->once())->method('createModel')->willReturn($model); $provider->withQuery($callback); - $user = $provider->retrieveByCredentials([function ($builder) { + $user = $provider->retrieveByCredentials([function (Builder $builder): void { $builder->where('username', 'dayle'); }]); @@ -283,7 +284,10 @@ public function testRegistersQueryHandler() $this->assertSame($callback, $provider->getQueryCallback()); } - protected function getProviderMock() + /** + * Create a user provider with a mocked model factory. + */ + protected function getProviderMock(): EloquentUserProvider&MockObject { $hasher = m::mock(Hasher::class); diff --git a/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php b/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php index 5e1d64c1f8..4c6bd9dd06 100644 --- a/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php +++ b/tests/Auth/AuthListenersSendEmailVerificationNotificationHandleFunctionTest.php @@ -14,7 +14,7 @@ class AuthListenersSendEmailVerificationNotificationHandleFunctionTest extends TestCase { - public function testWillExecuted() + public function testWillExecuted(): void { $user = m::mock(Authenticatable::class, MustVerifyEmail::class); $user->shouldReceive('hasVerifiedEmail')->andReturn(false); @@ -25,7 +25,7 @@ public function testWillExecuted() $listener->handle(new Registered($user)); } - public function testUserIsNotInstanceOfMustVerifyEmail() + public function testUserIsNotInstanceOfMustVerifyEmail(): void { $user = m::mock(User::class); $user->shouldNotReceive('sendEmailVerificationNotification'); @@ -35,7 +35,7 @@ public function testUserIsNotInstanceOfMustVerifyEmail() $listener->handle(new Registered($user)); } - public function testHasVerifiedEmailAsTrue() + public function testHasVerifiedEmailAsTrue(): void { $user = m::mock(Authenticatable::class, MustVerifyEmail::class); $user->shouldReceive('hasVerifiedEmail')->andReturn(true); diff --git a/tests/Auth/AuthPasswordBrokerManagerTest.php b/tests/Auth/AuthPasswordBrokerManagerTest.php index 0e0d29b7e6..cf1d35a1e8 100644 --- a/tests/Auth/AuthPasswordBrokerManagerTest.php +++ b/tests/Auth/AuthPasswordBrokerManagerTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Auth; +use BackedEnum; use Hypervel\Auth\Passwords\DatabaseTokenRepository; use Hypervel\Auth\Passwords\PasswordBroker as PasswordBrokerImplementation; use Hypervel\Auth\Passwords\PasswordBrokerManager; @@ -215,13 +216,14 @@ public function testSetDefaultDriverOverridesGuardDeclaration(): void $this->assertSame('other', $manager->getDefaultDriver()); } - public function testSetDefaultDriverAcceptsAnIntBackedZeroEnum(): void + #[DataProvider('backedBrokerNames')] + public function testSetDefaultDriverAcceptsBackedEnum(BackedEnum $name, string $expected): void { $manager = new PasswordBrokerManager(new Container); - $manager->setDefaultDriver(AuthPasswordBrokerIntEnum::Zero); + $manager->setDefaultDriver($name); - $this->assertSame('0', $manager->getDefaultDriver()); + $this->assertSame($expected, $manager->getDefaultDriver()); } public function testSetDefaultDriverIsCoroutineIsolated(): void @@ -475,15 +477,27 @@ public function testBrokerRejectsUnknownDriver(): void (new PasswordBrokerManager($container))->broker('users'); } - public function testBrokerNormalizesEnumsBeforeCaching(): void + #[DataProvider('backedBrokerNames')] + public function testBrokerNormalizesEnumsBeforeCaching(BackedEnum $name, string $expected): void { $broker = m::mock(PasswordBrokerContract::class); $manager = new AuthPasswordBrokerManagerStub(new Container); $manager->resolvedBroker = $broker; - $this->assertSame($broker, $manager->broker(AuthPasswordBrokerIntEnum::Zero)); - $this->assertSame($broker, $manager->broker('0')); - $this->assertSame(['0'], $manager->resolvedNames); + $this->assertSame($broker, $manager->broker($name)); + $this->assertSame($broker, $manager->broker($expected)); + $this->assertSame([$expected], $manager->resolvedNames); + } + + /** + * Provide backed enum broker names. + */ + public static function backedBrokerNames(): array + { + return [ + 'string backed' => [AuthPasswordBrokerStringEnum::Users, 'users'], + 'integer backed zero' => [AuthPasswordBrokerIntEnum::Zero, '0'], + ]; } public function testRefreshingDispatcherUpdatesOnlyConcreteResolvedBrokers(): void @@ -576,6 +590,7 @@ protected function resolve(string $name): PasswordBrokerContract enum AuthPasswordBrokerStringEnum: string { + case Users = 'users'; case Staff = 'staff'; } diff --git a/tests/Auth/AuthPasswordBrokerTest.php b/tests/Auth/AuthPasswordBrokerTest.php index 388f82ddfd..e7db53b8ce 100755 --- a/tests/Auth/AuthPasswordBrokerTest.php +++ b/tests/Auth/AuthPasswordBrokerTest.php @@ -24,7 +24,7 @@ public function testIfUserIsNotFoundErrorRedirectIsReturned(): void { $mocks = $this->getMocks(); $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); - $broker->shouldReceive('getUser')->once()->andReturnNull(); + $broker->expects('getUser')->andReturnNull(); $this->assertSame(PasswordBrokerContract::INVALID_USER, $broker->sendResetLink(['credentials'])); } @@ -33,9 +33,9 @@ public function testIfTokenIsRecentlyCreated(): void { $mocks = $this->getMocks(); $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(Authenticatable::class . ',' . CanResetPassword::class)); - $mocks['tokens']->shouldReceive('recentlyCreatedToken')->once()->with($user)->andReturn(true); - $user->shouldReceive('sendPasswordResetNotification')->with('token'); + $user = m::mock(Authenticatable::class, CanResetPassword::class); + $mocks['users']->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); + $mocks['tokens']->expects('recentlyCreatedToken')->with($user)->andReturn(true); $this->assertSame(PasswordBrokerContract::RESET_THROTTLED, $broker->sendResetLink(['foo'])); } @@ -46,7 +46,7 @@ public function testGetUserThrowsExceptionIfUserDoesntImplementCanResetPassword( $this->expectExceptionMessage('User must implement CanResetPassword interface.'); $broker = $this->getBroker($mocks = $this->getMocks()); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn(m::mock(Authenticatable::class)); + $mocks['users']->expects('retrieveByCredentials')->with(['foo'])->andReturn(m::mock(Authenticatable::class)); $broker->getUser(['foo']); } @@ -54,7 +54,8 @@ public function testGetUserThrowsExceptionIfUserDoesntImplementCanResetPassword( public function testUserIsRetrievedByCredentials(): void { $broker = $this->getBroker($mocks = $this->getMocks()); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(Authenticatable::class . ',' . CanResetPassword::class)); + $user = m::mock(Authenticatable::class, CanResetPassword::class); + $mocks['users']->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); $this->assertEquals($user, $broker->getUser(['foo'])); } @@ -63,10 +64,11 @@ public function testBrokerCreatesTokenAndRedirectsWithoutError(): void { $mocks = $this->getMocks(); $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(Authenticatable::class . ',' . CanResetPassword::class)); - $mocks['tokens']->shouldReceive('recentlyCreatedToken')->once()->with($user)->andReturn(false); - $mocks['tokens']->shouldReceive('create')->once()->with($user)->andReturn('token'); - $user->shouldReceive('sendPasswordResetNotification')->with('token'); + $user = m::mock(Authenticatable::class, CanResetPassword::class); + $mocks['users']->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); + $mocks['tokens']->expects('recentlyCreatedToken')->with($user)->andReturn(false); + $mocks['tokens']->expects('create')->with($user)->andReturn('token'); + $user->expects('sendPasswordResetNotification')->with('token'); $this->assertSame(PasswordBrokerContract::RESET_LINK_SENT, $broker->sendResetLink(['foo'])); } @@ -127,53 +129,56 @@ public function testEventDispatcherCanBeReplacedOnAnExistingBroker(): void public function testRedirectIsReturnedByResetWhenUserCredentialsInvalid(): void { $broker = $this->getBroker($mocks = $this->getMocks()); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['creds'])->andReturn(null); + $mocks['users']->expects('retrieveByCredentials')->with(['creds'])->andReturn(null); - $this->assertSame(PasswordBrokerContract::INVALID_USER, $broker->reset(['creds'], function () { + $this->assertSame(PasswordBrokerContract::INVALID_USER, $broker->reset(['creds'], function (): void { })); } public function testRedirectReturnedByRemindWhenRecordDoesntExistInTable(): void { - $creds = ['token' => 'token']; + $credentials = ['token' => 'token']; $broker = $this->getBroker($mocks = $this->getMocks()); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(Arr::except($creds, ['token']))->andReturn($user = m::mock(Authenticatable::class . ',' . CanResetPassword::class)); - $mocks['tokens']->shouldReceive('exists')->with($user, 'token')->andReturn(false); + $user = m::mock(Authenticatable::class, CanResetPassword::class); + $mocks['users']->expects('retrieveByCredentials')->with(Arr::except($credentials, ['token']))->andReturn($user); + $mocks['tokens']->expects('exists')->with($user, 'token')->andReturn(false); - $this->assertSame(PasswordBrokerContract::INVALID_TOKEN, $broker->reset($creds, function () { + $this->assertSame(PasswordBrokerContract::INVALID_TOKEN, $broker->reset($credentials, function (): void { })); } public function testResetRemovesRecordOnReminderTableAndCallsCallback(): void { - unset($_SERVER['__password.reset.test']); + $resetArguments = null; $mocks = $this->getMocks(); $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial()->shouldAllowMockingProtectedMethods(); - $broker->shouldReceive('validateReset')->once()->andReturn($user = m::mock(Authenticatable::class . ',' . CanResetPassword::class)); - $mocks['tokens']->shouldReceive('delete')->once()->with($user); - $callback = function ($user, $password) { - $_SERVER['__password.reset.test'] = compact('user', 'password'); + $user = m::mock(Authenticatable::class, CanResetPassword::class); + $broker->expects('validateReset')->andReturn($user); + $mocks['tokens']->expects('delete')->with($user); + $callback = function (CanResetPassword $user, string $password) use (&$resetArguments): string { + $resetArguments = ['user' => $user, 'password' => $password]; return 'foo'; }; $this->assertSame(PasswordBrokerContract::PASSWORD_RESET, $broker->reset(['password' => 'password', 'token' => 'token'], $callback)); - $this->assertEquals(['user' => $user, 'password' => 'password'], $_SERVER['__password.reset.test']); + $this->assertEquals(['user' => $user, 'password' => 'password'], $resetArguments); } public function testExecutesCallbackInsteadOfSendingNotification(): void { $executed = false; - $closure = function () use (&$executed) { + $closure = function () use (&$executed): void { $executed = true; }; $mocks = $this->getMocks(); $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); - $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(Authenticatable::class . ',' . CanResetPassword::class)); - $mocks['tokens']->shouldReceive('recentlyCreatedToken')->once()->with($user)->andReturn(false); - $mocks['tokens']->shouldReceive('create')->once()->with($user)->andReturn('token'); + $user = m::mock(Authenticatable::class, CanResetPassword::class); + $mocks['users']->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); + $mocks['tokens']->expects('recentlyCreatedToken')->with($user)->andReturn(false); + $mocks['tokens']->expects('create')->with($user)->andReturn('token'); $user->shouldNotReceive('sendPasswordResetNotification'); $this->assertEquals(PasswordBrokerContract::RESET_LINK_SENT, $broker->sendResetLink(['foo'], $closure)); @@ -233,16 +238,22 @@ public function testSendResetLinkStampsContextForCallback(): void $mocks['tokens']->shouldReceive('create')->once()->with($user)->andReturn('token'); $user->shouldNotReceive('sendPasswordResetNotification'); - $this->assertSame(PasswordBrokerContract::RESET_LINK_SENT, $broker->sendResetLink(['foo'], function () { + $this->assertSame(PasswordBrokerContract::RESET_LINK_SENT, $broker->sendResetLink(['foo'], function (): void { $this->assertSame('users', CoroutineContext::get(PasswordBroker::SENDING_BROKER_CONTEXT_KEY)); })); } + /** + * Create a broker with the given dependencies. + */ protected function getBroker(array $mocks): PasswordBroker { return new PasswordBroker($mocks['tokens'], $mocks['users'], $mocks['name']); } + /** + * Create the broker's dependencies. + */ protected function getMocks(): array { return [ diff --git a/tests/Auth/AuthTokenGuardTest.php b/tests/Auth/AuthTokenGuardTest.php index 59693027c8..222cf7c360 100644 --- a/tests/Auth/AuthTokenGuardTest.php +++ b/tests/Auth/AuthTokenGuardTest.php @@ -32,12 +32,12 @@ protected function createGuard( return new TokenGuard('token', $provider, $this->app, $inputKey, $storageKey, $hash); } - public function testUserCanBeRetrievedByQueryStringVariable() + public function testUserCanBeRetrievedByQueryStringVariable(): void { $provider = m::mock(UserProvider::class); $user = new AuthTokenGuardTestUser; $user->id = 1; - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => 'foo'])->andReturn($user); + $provider->expects('retrieveByCredentials')->with(['api_token' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', ['api_token' => 'foo']); $guard = $this->createGuard($provider, $request); @@ -50,12 +50,12 @@ public function testUserCanBeRetrievedByQueryStringVariable() $this->assertSame(1, $guard->id()); } - public function testTokenCanBeHashed() + public function testTokenCanBeHashed(): void { $provider = m::mock(UserProvider::class); $user = new AuthTokenGuardTestUser; $user->id = 1; - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => hash('sha256', 'foo')])->andReturn($user); + $provider->expects('retrieveByCredentials')->with(['api_token' => hash('sha256', 'foo')])->andReturn($user); $request = Request::create('/', 'GET', ['api_token' => 'foo']); $guard = $this->createGuard($provider, $request, 'api_token', 'api_token', hash: true); @@ -68,13 +68,23 @@ public function testTokenCanBeHashed() $this->assertSame(1, $guard->id()); } - public function testUserCanBeRetrievedByAuthHeaders() + public function testUserCannotBeRetrievedWithNonStringToken(): void { $provider = m::mock(UserProvider::class); - $mockUser = m::mock(Authenticatable::class); - $mockUser->id = 1; - $mockUser->shouldReceive('getAuthIdentifier')->andReturn(1); - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => 'foo'])->andReturn($mockUser); + $provider->shouldNotReceive('retrieveByCredentials'); + $request = Request::create('/', 'GET', ['api_token' => [0]]); + + $guard = $this->createGuard($provider, $request); + + $this->assertNull($guard->user()); + } + + public function testUserCanBeRetrievedByAuthHeaders(): void + { + $provider = m::mock(UserProvider::class); + $user = new AuthTokenGuardTestUser; + $user->id = 1; + $provider->expects('retrieveByCredentials')->with(['api_token' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'foo']); $guard = $this->createGuard($provider, $request); @@ -84,13 +94,12 @@ public function testUserCanBeRetrievedByAuthHeaders() $this->assertSame(1, $user->id); } - public function testUserCanBeRetrievedByBearerToken() + public function testUserCanBeRetrievedByBearerToken(): void { $provider = m::mock(UserProvider::class); - $mockUser = m::mock(Authenticatable::class); - $mockUser->id = 1; - $mockUser->shouldReceive('getAuthIdentifier')->andReturn(1); - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => 'foo'])->andReturn($mockUser); + $user = new AuthTokenGuardTestUser; + $user->id = 1; + $provider->expects('retrieveByCredentials')->with(['api_token' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', [], [], [], ['HTTP_AUTHORIZATION' => 'Bearer foo']); $guard = $this->createGuard($provider, $request); @@ -100,12 +109,12 @@ public function testUserCanBeRetrievedByBearerToken() $this->assertSame(1, $user->id); } - public function testValidateCanDetermineIfCredentialsAreValid() + public function testValidateCanDetermineIfCredentialsAreValid(): void { $provider = m::mock(UserProvider::class); $user = new AuthTokenGuardTestUser; $user->id = 1; - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => 'foo'])->andReturn($user); + $provider->expects('retrieveByCredentials')->with(['api_token' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', ['api_token' => 'foo']); $guard = $this->createGuard($provider, $request); @@ -113,10 +122,10 @@ public function testValidateCanDetermineIfCredentialsAreValid() $this->assertTrue($guard->validate(['api_token' => 'foo'])); } - public function testValidateCanDetermineIfCredentialsAreInvalid() + public function testValidateCanDetermineIfCredentialsAreInvalid(): void { $provider = m::mock(UserProvider::class); - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => 'foo'])->andReturn(null); + $provider->expects('retrieveByCredentials')->with(['api_token' => 'foo'])->andReturn(null); $request = Request::create('/', 'GET', ['api_token' => 'foo']); $guard = $this->createGuard($provider, $request); @@ -170,7 +179,7 @@ public function testValidatePreservesStringZero(): void $this->assertTrue($guard->validate(['api_token' => '0'])); } - public function testValidateIfApiTokenIsEmpty() + public function testValidateIfApiTokenIsEmpty(): void { $provider = m::mock(UserProvider::class); $request = Request::create('/', 'GET', ['api_token' => '']); @@ -180,12 +189,23 @@ public function testValidateIfApiTokenIsEmpty() $this->assertFalse($guard->validate(['api_token' => ''])); } - public function testItAllowsToPassCustomRequestViaContainerAndUseItForValidation() + public function testValidateRejectsNonStringToken(): void + { + $provider = m::mock(UserProvider::class); + $provider->shouldNotReceive('retrieveByCredentials'); + $request = Request::create('/'); + + $guard = $this->createGuard($provider, $request); + + $this->assertFalse($guard->validate(['api_token' => [0]])); + } + + public function testItAllowsToPassCustomRequestViaContainerAndUseItForValidation(): void { $provider = m::mock(UserProvider::class); $user = new AuthTokenGuardTestUser; $user->id = 1; - $provider->shouldReceive('retrieveByCredentials')->once()->with(['api_token' => 'custom'])->andReturn($user); + $provider->expects('retrieveByCredentials')->with(['api_token' => 'custom'])->andReturn($user); $request = Request::create('/', 'GET', ['api_token' => 'foo']); $guard = $this->createGuard($provider, $request); @@ -198,13 +218,12 @@ public function testItAllowsToPassCustomRequestViaContainerAndUseItForValidation $this->assertSame(1, $user->id); } - public function testUserCanBeRetrievedByBearerTokenWithCustomKey() + public function testUserCanBeRetrievedByBearerTokenWithCustomKey(): void { $provider = m::mock(UserProvider::class); - $mockUser = m::mock(Authenticatable::class); - $mockUser->id = 1; - $mockUser->shouldReceive('getAuthIdentifier')->andReturn(1); - $provider->shouldReceive('retrieveByCredentials')->once()->with(['custom_token_field' => 'foo'])->andReturn($mockUser); + $user = new AuthTokenGuardTestUser; + $user->id = 1; + $provider->expects('retrieveByCredentials')->with(['custom_token_field' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', [], [], [], ['HTTP_AUTHORIZATION' => 'Bearer foo']); $guard = $this->createGuard($provider, $request, 'custom_token_field', 'custom_token_field'); @@ -214,12 +233,12 @@ public function testUserCanBeRetrievedByBearerTokenWithCustomKey() $this->assertSame(1, $user->id); } - public function testUserCanBeRetrievedByQueryStringVariableWithCustomKey() + public function testUserCanBeRetrievedByQueryStringVariableWithCustomKey(): void { $provider = m::mock(UserProvider::class); $user = new AuthTokenGuardTestUser; $user->id = 1; - $provider->shouldReceive('retrieveByCredentials')->once()->with(['custom_token_field' => 'foo'])->andReturn($user); + $provider->expects('retrieveByCredentials')->with(['custom_token_field' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', ['custom_token_field' => 'foo']); $guard = $this->createGuard($provider, $request, 'custom_token_field', 'custom_token_field'); @@ -232,13 +251,12 @@ public function testUserCanBeRetrievedByQueryStringVariableWithCustomKey() $this->assertSame(1, $guard->id()); } - public function testUserCanBeRetrievedByAuthHeadersWithCustomField() + public function testUserCanBeRetrievedByAuthHeadersWithCustomField(): void { $provider = m::mock(UserProvider::class); - $mockUser = m::mock(Authenticatable::class); - $mockUser->id = 1; - $mockUser->shouldReceive('getAuthIdentifier')->andReturn(1); - $provider->shouldReceive('retrieveByCredentials')->once()->with(['custom_token_field' => 'foo'])->andReturn($mockUser); + $user = new AuthTokenGuardTestUser; + $user->id = 1; + $provider->expects('retrieveByCredentials')->with(['custom_token_field' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo', 'PHP_AUTH_PW' => 'foo']); $guard = $this->createGuard($provider, $request, 'custom_token_field', 'custom_token_field'); @@ -248,12 +266,12 @@ public function testUserCanBeRetrievedByAuthHeadersWithCustomField() $this->assertSame(1, $user->id); } - public function testValidateCanDetermineIfCredentialsAreValidWithCustomKey() + public function testValidateCanDetermineIfCredentialsAreValidWithCustomKey(): void { $provider = m::mock(UserProvider::class); $user = new AuthTokenGuardTestUser; $user->id = 1; - $provider->shouldReceive('retrieveByCredentials')->once()->with(['custom_token_field' => 'foo'])->andReturn($user); + $provider->expects('retrieveByCredentials')->with(['custom_token_field' => 'foo'])->andReturn($user); $request = Request::create('/', 'GET', ['custom_token_field' => 'foo']); $guard = $this->createGuard($provider, $request, 'custom_token_field', 'custom_token_field'); @@ -261,10 +279,10 @@ public function testValidateCanDetermineIfCredentialsAreValidWithCustomKey() $this->assertTrue($guard->validate(['custom_token_field' => 'foo'])); } - public function testValidateCanDetermineIfCredentialsAreInvalidWithCustomKey() + public function testValidateCanDetermineIfCredentialsAreInvalidWithCustomKey(): void { $provider = m::mock(UserProvider::class); - $provider->shouldReceive('retrieveByCredentials')->once()->with(['custom_token_field' => 'foo'])->andReturn(null); + $provider->expects('retrieveByCredentials')->with(['custom_token_field' => 'foo'])->andReturn(null); $request = Request::create('/', 'GET', ['custom_token_field' => 'foo']); $guard = $this->createGuard($provider, $request, 'custom_token_field', 'custom_token_field'); @@ -272,7 +290,7 @@ public function testValidateCanDetermineIfCredentialsAreInvalidWithCustomKey() $this->assertFalse($guard->validate(['custom_token_field' => 'foo'])); } - public function testValidateIfApiTokenIsEmptyWithCustomKey() + public function testValidateIfApiTokenIsEmptyWithCustomKey(): void { $provider = m::mock(UserProvider::class); $request = Request::create('/', 'GET', ['custom_token_field' => '']); @@ -324,7 +342,7 @@ public function testTokenLookupPreservesStringZero(): void // Context Isolation Tests (Hypervel-specific) // ========================================================================= - public function testDifferentTokensGetDifferentCachedUsers() + public function testDifferentTokensGetDifferentCachedUsers(): void { $user1 = new AuthTokenGuardTestUser; $user1->id = 1; @@ -355,7 +373,7 @@ public function testDifferentTokensGetDifferentCachedUsers() $this->assertSame($user2, $guard->user()); } - public function testEmptyTokenUsesDefaultKey() + public function testEmptyTokenUsesDefaultKey(): void { $provider = m::mock(UserProvider::class); $request = Request::create('/', 'GET'); @@ -476,7 +494,7 @@ public function testAuthContextKeysIncludeOnlyDurableExplicitState(): void ], $guard->getAuthContextKeys()); } - public function testChangingRequestTokenChangesWhichCachedUserIsSeen() + public function testChangingRequestTokenChangesWhichCachedUserIsSeen(): void { $user1 = new AuthTokenGuardTestUser; $user1->id = 1; @@ -505,35 +523,56 @@ class AuthTokenGuardTestUser implements Authenticatable { public int $id; + /** + * Get the identifier name. + */ public function getAuthIdentifierName(): string { return 'id'; } - public function getAuthIdentifier(): mixed + /** + * Get the identifier. + */ + public function getAuthIdentifier(): int { return $this->id; } + /** + * Get the password attribute name. + */ public function getAuthPasswordName(): string { return 'password'; } + /** + * Get the password. + */ public function getAuthPassword(): ?string { return null; } + /** + * Get the remember token. + */ public function getRememberToken(): ?string { return null; } + /** + * Set the remember token. + */ public function setRememberToken(string $value): void { } + /** + * Get the remember token attribute name. + */ public function getRememberTokenName(): string { return 'remember_token'; diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index 50dc28841e..8993594ed3 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -38,7 +38,7 @@ protected function setUp(): void return $this->createConfig(); }); - $container->singleton('request', fn (): Request => m::mock(Request::class)); + $container->singleton('request', fn (): Request => new Request); } public function testItCanGenerateDefinitionViaStaticMethod(): void diff --git a/tests/Auth/AuthorizeMiddlewareTest.php b/tests/Auth/AuthorizeMiddlewareTest.php index 054c7e3c6b..a4879e3fa5 100644 --- a/tests/Auth/AuthorizeMiddlewareTest.php +++ b/tests/Auth/AuthorizeMiddlewareTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Auth; +use App\Models\Comment; use Hypervel\Auth\Access\AuthorizationException; use Hypervel\Auth\Access\Gate; use Hypervel\Auth\Middleware\Authorize; @@ -21,15 +22,19 @@ use Hypervel\Tests\TestCase; use Mockery as m; use stdClass; +use Symfony\Component\HttpFoundation\Response; class AuthorizeMiddlewareTest extends TestCase { - protected $container; + protected Container $container; - protected $user; + protected stdClass $user; - protected $router; + protected Router $router; + /** + * Set up the authorization services. + */ protected function setUp(): void { parent::setUp(); @@ -38,27 +43,20 @@ protected function setUp(): void Container::setInstance($this->container = new Container); - $this->container->singleton(GateContract::class, function () { - return new Gate($this->container, function () { + $this->container->singleton(GateContract::class, function (): Gate { + return new Gate($this->container, function (): stdClass { return $this->user; }); }); $this->router = new Router(new Dispatcher, $this->container); - $this->container->bind(CallableDispatcherContract::class, fn ($app) => new CallableDispatcher($app)); + $this->container->bind(CallableDispatcherContract::class, fn (Container $app): CallableDispatcher => new CallableDispatcher($app)); $this->container->instance(Registrar::class, $this->router); } - protected function tearDown(): void - { - Container::setInstance(null); - - parent::tearDown(); - } - - public function testItCanGenerateDefinitionViaStaticMethod() + public function testItCanGenerateDefinitionViaStaticMethod(): void { $signature = Authorize::using('ability'); $this->assertSame('Hypervel\Auth\Middleware\Authorize:ability', $signature); @@ -66,58 +64,58 @@ public function testItCanGenerateDefinitionViaStaticMethod() $signature = Authorize::using('ability', 'model'); $this->assertSame('Hypervel\Auth\Middleware\Authorize:ability,model', $signature); - $signature = Authorize::using('ability', 'model', \App\Models\Comment::class); + $signature = Authorize::using('ability', 'model', Comment::class); $this->assertSame('Hypervel\Auth\Middleware\Authorize:ability,model,App\Models\Comment', $signature); } - public function testUsingWithBackedEnum() + public function testUsingWithBackedEnum(): void { $result = Authorize::using(AbilitiesEnum::ViewDashboard); $this->assertSame(Authorize::class . ':view-dashboard', $result); } - public function testUsingWithBackedEnumAndModels() + public function testUsingWithBackedEnumAndModels(): void { $result = Authorize::using(AbilitiesEnum::ViewDashboard, 'App\Models\User'); $this->assertSame(Authorize::class . ':view-dashboard,App\Models\User', $result); } - public function testUsingWithUnitEnum() + public function testUsingWithUnitEnum(): void { $result = Authorize::using(AuthorizeMiddlewareTestUnitEnum::ManageUsers); $this->assertSame(Authorize::class . ':ManageUsers', $result); } - public function testUsingWithUnitEnumAndModels() + public function testUsingWithUnitEnumAndModels(): void { $result = Authorize::using(AuthorizeMiddlewareTestUnitEnum::ViewReports, 'App\Models\Report'); $this->assertSame(Authorize::class . ':ViewReports,App\Models\Report', $result); } - public function testUsingWithIntBackedEnum() + public function testUsingWithIntBackedEnum(): void { $result = Authorize::using(AuthorizeMiddlewareTestIntBackedEnum::CreatePost); $this->assertSame(Authorize::class . ':1', $result); } - public function testUsingWithStringAbilityAndMultipleModels() + public function testUsingWithStringAbilityAndMultipleModels(): void { $result = Authorize::using('transfer', 'App\Models\Account', 'App\Models\User'); $this->assertSame(Authorize::class . ':transfer,App\Models\Account,App\Models\User', $result); } - public function testSimpleAbilityUnauthorized() + public function testSimpleAbilityUnauthorized(): void { $this->expectException(AuthorizationException::class); $this->expectExceptionMessage('This action is unauthorized.'); - $this->gate()->define('view-dashboard', function ($user, $additional = null) { + $this->gate()->define('view-dashboard', function (stdClass $user, mixed $additional = null): bool { $this->assertNull($additional); return false; @@ -125,7 +123,7 @@ public function testSimpleAbilityUnauthorized() $this->router->get('dashboard', [ 'middleware' => Authorize::class . ':view-dashboard', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -133,15 +131,15 @@ public function testSimpleAbilityUnauthorized() $this->router->dispatch(Request::create('dashboard', 'GET')); } - public function testSimpleAbilityAuthorized() + public function testSimpleAbilityAuthorized(): void { - $this->gate()->define('view-dashboard', function ($user) { + $this->gate()->define('view-dashboard', function (stdClass $user): bool { return true; }); $this->router->get('dashboard', [ 'middleware' => Authorize::class . ':view-dashboard', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -151,15 +149,15 @@ public function testSimpleAbilityAuthorized() $this->assertSame('success', $response->content()); } - public function testSimpleAbilityWithStringParameter() + public function testSimpleAbilityWithStringParameter(): void { - $this->gate()->define('view-dashboard', function ($user, $param) { + $this->gate()->define('view-dashboard', function (stdClass $user, string $param): bool { return $param === 'some string'; }); $this->router->get('dashboard', [ 'middleware' => Authorize::class . ':view-dashboard,"some string"', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -169,14 +167,14 @@ public function testSimpleAbilityWithStringParameter() $this->assertSame('success', $response->content()); } - public function testSimpleAbilityWithBackedEnumParameter() + public function testSimpleAbilityWithBackedEnumParameter(): void { - $this->gate()->define('view-dashboard', function ($user) { + $this->gate()->define('view-dashboard', function (stdClass $user): bool { return true; }); $this->router->middleware(Authorize::using(AbilitiesEnum::ViewDashboard))->get('dashboard', [ - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -186,9 +184,9 @@ public function testSimpleAbilityWithBackedEnumParameter() $this->assertSame('success', $response->content()); } - public function testSimpleAbilityWithNullParameter() + public function testSimpleAbilityWithNullParameter(): void { - $this->gate()->define('view-dashboard', function ($user, $param = null) { + $this->gate()->define('view-dashboard', function (stdClass $user, mixed $param = null): bool { $this->assertNull($param); return true; @@ -196,7 +194,7 @@ public function testSimpleAbilityWithNullParameter() $this->router->get('dashboard', [ 'middleware' => Authorize::class . ':view-dashboard,null', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -204,15 +202,15 @@ public function testSimpleAbilityWithNullParameter() $this->router->dispatch(Request::create('dashboard', 'GET')); } - public function testSimpleAbilityWithOptionalParameter() + public function testSimpleAbilityWithOptionalParameter(): void { $post = new stdClass; - $this->router->bind('post', function () use ($post) { + $this->router->bind('post', function () use ($post): stdClass { return $post; }); - $this->gate()->define('view-comments', function ($user, $model = null) { + $this->gate()->define('view-comments', function (stdClass $user, ?stdClass $model = null): bool { return true; }); @@ -220,13 +218,13 @@ public function testSimpleAbilityWithOptionalParameter() $this->router->get('comments', [ 'middleware' => $middleware, - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); $this->router->get('posts/{post}/comments', [ 'middleware' => $middleware, - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -238,15 +236,15 @@ public function testSimpleAbilityWithOptionalParameter() $this->assertSame('success', $response->content()); } - public function testSimpleAbilityWithStringParameterFromRouteParameter() + public function testSimpleAbilityWithStringParameterFromRouteParameter(): void { - $this->gate()->define('view-dashboard', function ($user, $param) { + $this->gate()->define('view-dashboard', function (stdClass $user, string $param): bool { return $param === 'true'; }); $this->router->get('dashboard/{route_parameter}', [ 'middleware' => Authorize::class . ':view-dashboard,route_parameter', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -256,15 +254,15 @@ public function testSimpleAbilityWithStringParameterFromRouteParameter() $this->assertSame('success', $response->content()); } - public function testSimpleAbilityWithStringParameter0FromRouteParameter() + public function testSimpleAbilityWithStringParameter0FromRouteParameter(): void { - $this->gate()->define('view-dashboard', function ($user, $param) { + $this->gate()->define('view-dashboard', function (stdClass $user, string $param): bool { return $param === '0'; }); $this->router->get('dashboard/{route_parameter}', [ 'middleware' => Authorize::class . ':view-dashboard,route_parameter', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -274,12 +272,12 @@ public function testSimpleAbilityWithStringParameter0FromRouteParameter() $this->assertSame('success', $response->content()); } - public function testModelTypeUnauthorized() + public function testModelTypeUnauthorized(): void { $this->expectException(AuthorizationException::class); $this->expectExceptionMessage('This action is unauthorized.'); - $this->gate()->define('create', function ($user, $model) { + $this->gate()->define('create', function (stdClass $user, string $model): bool { $this->assertSame('App\User', $model); return false; @@ -287,7 +285,7 @@ public function testModelTypeUnauthorized() $this->router->get('users/create', [ 'middleware' => [SubstituteBindings::class, Authorize::class . ':create,App\User'], - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -295,9 +293,9 @@ public function testModelTypeUnauthorized() $this->router->dispatch(Request::create('users/create', 'GET')); } - public function testModelTypeAuthorized() + public function testModelTypeAuthorized(): void { - $this->gate()->define('create', function ($user, $model) { + $this->gate()->define('create', function (stdClass $user, string $model): bool { $this->assertSame('App\User', $model); return true; @@ -305,7 +303,7 @@ public function testModelTypeAuthorized() $this->router->get('users/create', [ 'middleware' => Authorize::class . ':create,App\User', - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -315,18 +313,18 @@ public function testModelTypeAuthorized() $this->assertSame('success', $response->content()); } - public function testModelUnauthorized() + public function testModelUnauthorized(): void { $this->expectException(AuthorizationException::class); $this->expectExceptionMessage('This action is unauthorized.'); $post = new stdClass; - $this->router->bind('post', function () use ($post) { + $this->router->bind('post', function () use ($post): stdClass { return $post; }); - $this->gate()->define('edit', function ($user, $model) use ($post) { + $this->gate()->define('edit', function (stdClass $user, stdClass $model) use ($post): bool { $this->assertSame($model, $post); return false; @@ -334,7 +332,7 @@ public function testModelUnauthorized() $this->router->get('posts/{post}/edit', [ 'middleware' => [SubstituteBindings::class, Authorize::class . ':edit,post'], - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -342,15 +340,15 @@ public function testModelUnauthorized() $this->router->dispatch(Request::create('posts/1/edit', 'GET')); } - public function testModelAuthorized() + public function testModelAuthorized(): void { $post = new stdClass; - $this->router->bind('post', function () use ($post) { + $this->router->bind('post', function () use ($post): stdClass { return $post; }); - $this->gate()->define('edit', function ($user, $model) use ($post) { + $this->gate()->define('edit', function (stdClass $user, stdClass $model) use ($post): bool { $this->assertSame($model, $post); return true; @@ -358,7 +356,7 @@ public function testModelAuthorized() $this->router->get('posts/{post}/edit', [ 'middleware' => [SubstituteBindings::class, Authorize::class . ':edit,post'], - 'uses' => function () { + 'uses' => function (): string { return 'success'; }, ]); @@ -368,11 +366,11 @@ public function testModelAuthorized() $this->assertSame('success', $response->content()); } - public function testModelInstanceAsParameter() + public function testModelInstanceAsParameter(): void { $instance = m::mock(Model::class); - $this->gate()->define('success', function ($user, $model) use ($instance) { + $this->gate()->define('success', function (stdClass $user, Model $model) use ($instance): bool { $this->assertSame($model, $instance); return true; @@ -380,8 +378,8 @@ public function testModelInstanceAsParameter() $request = m::mock(Request::class); - $next = function () { - return new \Symfony\Component\HttpFoundation\Response; + $next = function (): Response { + return new Response; }; (new Authorize($this->gate())) @@ -391,7 +389,7 @@ public function testModelInstanceAsParameter() /** * Get the Gate instance from the container. */ - protected function gate() + protected function gate(): GateContract { return $this->container->make(GateContract::class); } diff --git a/tests/Broadcasting/AblyBroadcasterTest.php b/tests/Broadcasting/AblyBroadcasterTest.php index db445f0a08..95c1c527f4 100644 --- a/tests/Broadcasting/AblyBroadcasterTest.php +++ b/tests/Broadcasting/AblyBroadcasterTest.php @@ -27,6 +27,9 @@ class AblyBroadcasterTest extends TestCase protected Container $container; + /** + * Set up the broadcaster and its dependencies. + */ protected function setUp(): void { parent::setUp(); @@ -39,12 +42,11 @@ protected function setUp(): void public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue(): void { - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return true; }); - $this->broadcaster->shouldReceive('generateAblySignature') - ->once() + $this->broadcaster->expects('generateAblySignature') ->with('private-test', 'abcd.1234') ->andReturn('signature'); @@ -60,7 +62,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenCall { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return false; }); @@ -73,7 +75,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequ { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return true; }); @@ -85,12 +87,11 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequ public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCallbackReturnAnArray(): void { $returnData = [1, 2, 3, 4]; - $this->broadcaster->channel('test', function () use ($returnData) { + $this->broadcaster->channel('test', function () use ($returnData): array { return $returnData; }); - $this->broadcaster->shouldReceive('generateAblySignature') - ->once() + $this->broadcaster->expects('generateAblySignature') ->with( 'presence-test', 'abcd.1234', @@ -116,7 +117,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCal { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): void { }); $this->broadcaster->auth( @@ -128,7 +129,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenReq { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): array { return [1, 2, 3, 4]; }); @@ -160,7 +161,7 @@ public function testAuthUsesRewrittenChannelForConfiguredGuardAndOriginalNameFor $this->broadcaster->channel( 'application.orders.{order}', - static fn ($authenticatedUser, string $order): array|false => $authenticatedUser === $user && $order === '5' + static fn (object $authenticatedUser, string $order): array|false => $authenticatedUser === $user && $order === '5' ? ['role' => 'viewer'] : false, ['guards' => ['members']], @@ -296,6 +297,9 @@ protected function createAbly(): AblyRest ]); } + /** + * Create an authenticated channel request. + */ protected function getMockRequestWithUserForChannel(string $channel): Request { $request = m::mock(Request::class); @@ -311,12 +315,15 @@ protected function getMockRequestWithUserForChannel(string $channel): Request return $request; } + /** + * Create a channel request without an authenticated user. + */ protected function getMockRequestWithoutUserForChannel(string $channel): Request { $request = m::mock(Request::class); $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - $request->shouldReceive('user')->andReturn(null); + $request->expects('user')->andReturn(null); return $request; } @@ -324,6 +331,9 @@ protected function getMockRequestWithoutUserForChannel(string $channel): Request class InspectableAblyBroadcaster extends AblyBroadcaster { + /** + * Format outgoing channel names for inspection. + */ public function formatOutgoingChannels(array $channels): array { return parent::formatChannels($channels); @@ -336,7 +346,10 @@ class BroadcastingAblyHttpFake extends Http public int $requestCount = 0; - public function request($method, $url, $headers = [], $params = []): array + /** + * Record a publication without sending an HTTP request. + */ + public function request(mixed $method, mixed $url, mixed $headers = [], mixed $params = []): array { ++$this->requestCount; diff --git a/tests/Broadcasting/BroadcastEventTest.php b/tests/Broadcasting/BroadcastEventTest.php index 9692d19a03..606e69e9e7 100644 --- a/tests/Broadcasting/BroadcastEventTest.php +++ b/tests/Broadcasting/BroadcastEventTest.php @@ -11,6 +11,7 @@ use Hypervel\Contracts\Broadcasting\Factory as BroadcastingFactory; use Hypervel\Contracts\Broadcasting\ShouldBroadcast; use Hypervel\Queue\Attributes\Backoff; +use Hypervel\Support\Collection; use Hypervel\Tests\TestCase; use Mockery as m; use Throwable; @@ -21,7 +22,7 @@ public function testBasicEventBroadcastParameterFormatting(): void { $broadcaster = m::mock(Broadcaster::class); - $broadcaster->shouldReceive('broadcast')->once()->with( + $broadcaster->expects('broadcast')->with( ['test-channel'], TestBroadcastEvent::class, ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] @@ -29,7 +30,7 @@ public function testBasicEventBroadcastParameterFormatting(): void $manager = m::mock(BroadcastingFactory::class); - $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + $manager->expects('connection')->with(null)->andReturn($broadcaster); $event = new TestBroadcastEvent; @@ -40,7 +41,7 @@ public function testManualParameterSpecification(): void { $broadcaster = m::mock(Broadcaster::class); - $broadcaster->shouldReceive('broadcast')->once()->with( + $broadcaster->expects('broadcast')->with( ['test-channel'], TestBroadcastEventWithManualData::class, ['name' => 'Taylor', 'socket' => null] @@ -48,7 +49,7 @@ public function testManualParameterSpecification(): void $manager = m::mock(BroadcastingFactory::class); - $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + $manager->expects('connection')->with(null)->andReturn($broadcaster); $event = new TestBroadcastEventWithManualData; @@ -59,11 +60,11 @@ public function testSpecificBroadcasterGiven(): void { $broadcaster = m::mock(Broadcaster::class); - $broadcaster->shouldReceive('broadcast')->once(); + $broadcaster->expects('broadcast'); $manager = m::mock(BroadcastingFactory::class); - $manager->shouldReceive('connection')->once()->with('log')->andReturn($broadcaster); + $manager->expects('connection')->with('log')->andReturn($broadcaster); $event = new TestBroadcastEventWithSpecificBroadcaster; @@ -74,13 +75,13 @@ public function testSpecificChannelsPerConnection(): void { $broadcaster = m::mock(Broadcaster::class); - $broadcaster->shouldReceive('broadcast')->once()->with( + $broadcaster->expects('broadcast')->with( ['first-channel'], TestBroadcastEventWithChannelsPerConnection::class, ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] ); - $broadcaster->shouldReceive('broadcast')->once()->with( + $broadcaster->expects('broadcast')->with( ['second-channel'], TestBroadcastEventWithChannelsPerConnection::class, ['firstName' => 'Taylor'] @@ -88,8 +89,8 @@ public function testSpecificChannelsPerConnection(): void $manager = m::mock(BroadcastingFactory::class); - $manager->shouldReceive('connection')->once()->with('first_connection')->andReturn($broadcaster); - $manager->shouldReceive('connection')->once()->with('second_connection')->andReturn($broadcaster); + $manager->expects('connection')->with('first_connection')->andReturn($broadcaster); + $manager->expects('connection')->with('second_connection')->andReturn($broadcaster); $event = new TestBroadcastEventWithChannelsPerConnection; @@ -131,12 +132,11 @@ public function testBroadcastAsUnitEnumResolvesToName(): void public function testSingleStringChannelIsBroadcast(): void { $broadcaster = m::mock(Broadcaster::class); - $broadcaster->shouldReceive('broadcast') - ->once() + $broadcaster->expects('broadcast') ->with(['test-channel'], TestBroadcastEventWithStringChannel::class, m::type('array')); $manager = m::mock(BroadcastingFactory::class); - $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + $manager->expects('connection')->with(null)->andReturn($broadcaster); (new BroadcastEvent(new TestBroadcastEventWithStringChannel))->handle($manager); } @@ -162,6 +162,9 @@ public function testCloningIsolatesOrdinaryEventObjects(): void public function testMiddlewareProxiesMiddlewareFromUnderlyingEvent(): void { $event = new class { + /** + * Get the middleware for the event. + */ public function middleware(): array { return ['foo', 'bar']; @@ -176,6 +179,9 @@ public function middleware(): array public function testMiddlewareProxiesFailedHandlerFromUnderlyingEvent(): void { $event = new class { + /** + * Handle a job failure. + */ public function failed(?Throwable $e = null): void { $e->validateCall(); @@ -199,6 +205,17 @@ public function testDeleteWhenMissingModelsDefaultsToTrue(): void $this->assertTrue($job->deleteWhenMissingModels); } + public function testDeletingWhenMissingModelsCanBeDisabled(): void + { + $event = new class { + public bool $deleteWhenMissingModels = false; + }; + + $job = new BroadcastEvent($event); + + $this->assertFalse($job->deleteWhenMissingModels); + } + public function testArrayBackoffIsReadFromTheUnderlyingEvent(): void { $job = new BroadcastEvent(new TestBroadcastEventWithArrayBackoff); @@ -219,12 +236,14 @@ public function testVariadicBackoffIsReadFromTheUnderlyingEvent(): void protected function assertEventBroadcastsAs(object $event, string $name): void { $broadcaster = m::mock(Broadcaster::class); - $broadcaster->shouldReceive('broadcast') - ->once() - ->with(['test-channel'], $name, m::type('array')); + $broadcaster->expects('broadcast')->with( + ['test-channel'], + $name, + ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] + ); $manager = m::mock(BroadcastingFactory::class); - $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + $manager->expects('connection')->with(null)->andReturn($broadcaster); (new BroadcastEvent($event))->handle($manager); } @@ -232,20 +251,26 @@ protected function assertEventBroadcastsAs(object $event, string $name): void class TestBroadcastEvent { - public $firstName = 'Taylor'; + public string $firstName = 'Taylor'; - public $lastName = 'Otwell'; + public string $lastName = 'Otwell'; - public $collection; + public ?Collection $collection = null; - private $title = 'Developer'; + private string $title = 'Developer'; + /** + * Create a new event instance. + */ public function __construct() { $this->collection = collect(['foo' => 'bar']); } - public function broadcastOn() + /** + * Get the channels the event should broadcast on. + */ + public function broadcastOn(): array|string { return ['test-channel']; } @@ -253,6 +278,9 @@ public function broadcastOn() class TestBroadcastEventWithStringName extends TestBroadcastEvent { + /** + * Get the broadcast event name. + */ public function broadcastAs(): string { return 'custom-name'; @@ -261,6 +289,9 @@ public function broadcastAs(): string class TestBroadcastEventWithEnumName extends TestBroadcastEvent { + /** + * Get the broadcast event name. + */ public function broadcastAs(): TestBroadcastEventName { return TestBroadcastEventName::Custom; @@ -269,6 +300,9 @@ public function broadcastAs(): TestBroadcastEventName class TestBroadcastEventWithIntegerEnumName extends TestBroadcastEvent { + /** + * Get the broadcast event name. + */ public function broadcastAs(): TestBroadcastIntegerEventName { return TestBroadcastIntegerEventName::Zero; @@ -277,6 +311,9 @@ public function broadcastAs(): TestBroadcastIntegerEventName class TestBroadcastEventWithUnitEnumName extends TestBroadcastEvent { + /** + * Get the broadcast event name. + */ public function broadcastAs(): TestBroadcastUnitEventName { return TestBroadcastUnitEventName::Custom; @@ -285,6 +322,9 @@ public function broadcastAs(): TestBroadcastUnitEventName class TestBroadcastEventWithStringChannel extends TestBroadcastEvent implements ShouldBroadcast { + /** + * Get the channel the event should broadcast on. + */ public function broadcastOn(): string { return 'test-channel'; @@ -308,7 +348,10 @@ enum TestBroadcastUnitEventName class TestBroadcastEventWithManualData extends TestBroadcastEvent { - public function broadcastWith() + /** + * Get the data to broadcast. + */ + public function broadcastWith(): array { return ['name' => 'Taylor']; } @@ -318,6 +361,9 @@ class TestBroadcastEventWithSpecificBroadcaster extends TestBroadcastEvent { use InteractsWithBroadcasting; + /** + * Create a new event instance. + */ public function __construct() { $this->broadcastVia('log'); @@ -326,7 +372,10 @@ public function __construct() class TestBroadcastEventWithChannelsPerConnection extends TestBroadcastEvent { - public function broadcastConnections() + /** + * Get the connections to broadcast on. + */ + public function broadcastConnections(): array { return [ 'first_connection', @@ -334,7 +383,10 @@ public function broadcastConnections() ]; } - public function broadcastWith() + /** + * Get the data to broadcast. + */ + public function broadcastWith(): array { return [ 'first_connection' => [ @@ -348,7 +400,10 @@ public function broadcastWith() ]; } - public function broadcastOn() + /** + * Get the channels the event should broadcast on. + */ + public function broadcastOn(): array { return [ 'first_connection' => ['first-channel'], diff --git a/tests/Broadcasting/PusherBroadcasterTest.php b/tests/Broadcasting/PusherBroadcasterTest.php index 1b38cf0129..e5ba0a675e 100644 --- a/tests/Broadcasting/PusherBroadcasterTest.php +++ b/tests/Broadcasting/PusherBroadcasterTest.php @@ -29,6 +29,9 @@ class PusherBroadcasterTest extends TestCase protected Pusher $pusher; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); @@ -41,12 +44,11 @@ protected function setUp(): void public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue(): void { - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return true; }); - $this->pusher->shouldReceive('authorizeChannel') - ->once() + $this->pusher->expects('authorizeChannel') ->andReturn(json_encode(['auth' => 'signed'])); $this->assertSame( @@ -61,7 +63,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenCall { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return false; }); @@ -74,7 +76,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequ { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return true; }); @@ -86,12 +88,11 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequ public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCallbackReturnAnArray(): void { $returnData = [1, 2, 3, 4]; - $this->broadcaster->channel('test', function () use ($returnData) { + $this->broadcaster->channel('test', function () use ($returnData): array { return $returnData; }); - $this->pusher->shouldReceive('authorizePresenceChannel') - ->once() + $this->pusher->expects('authorizePresenceChannel') ->andReturn(json_encode(['auth' => 'signed'])); $this->assertSame( @@ -106,7 +107,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCal { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): void { }); $this->broadcaster->auth( @@ -118,7 +119,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenReq { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): array { return [1, 2, 3, 4]; }); @@ -133,13 +134,13 @@ public function testAuthUsesRewrittenChannelForConfiguredGuardAndOriginalNameFor $calls = 0; $boundOrder = null; $user = m::mock('User'); - $user->shouldReceive('getAuthIdentifier')->once()->andReturn(42); + $user->expects('getAuthIdentifier')->andReturn(42); $request = m::mock(Request::class); $request->shouldReceive('input')->with('channel_name')->andReturn($wireChannel); $request->shouldReceive('input')->with('socket_id')->andReturn('abcd.1234'); $request->shouldReceive('input')->with('callback', false)->andReturn(false); - $request->shouldReceive('user')->times(3)->with('members')->andReturn($user); + $request->expects('user')->times(3)->with('members')->andReturn($user); $request->shouldNotReceive('user')->withNoArgs(); Broadcaster::authorizeChannelsUsing(function (Request $request, string $channel) use (&$calls): ?string { @@ -152,7 +153,7 @@ public function testAuthUsesRewrittenChannelForConfiguredGuardAndOriginalNameFor $this->broadcaster->channel( 'application.orders.{order}', - function ($authenticatedUser, PusherBroadcasterTestEloquentModelStub $order) use ($user, &$boundOrder): array|false { + function (object $authenticatedUser, PusherBroadcasterTestEloquentModelStub $order) use ($user, &$boundOrder): array|false { $boundOrder = $order; return $authenticatedUser === $user ? ['role' => 'viewer'] : false; @@ -160,8 +161,7 @@ function ($authenticatedUser, PusherBroadcasterTestEloquentModelStub $order) use ['guards' => ['members']], ); - $this->pusher->shouldReceive('authorizePresenceChannel') - ->once() + $this->pusher->expects('authorizePresenceChannel') ->with($wireChannel, 'abcd.1234', '42', ['role' => 'viewer']) ->andReturn(json_encode(['auth' => 'signed'])); @@ -182,8 +182,7 @@ public function testValidAuthenticationResponseCallPusherSocketAuthMethodWithPri 'auth' => 'abcd:efgh', ]; - $this->pusher->shouldReceive('authorizeChannel') - ->once() + $this->pusher->expects('authorizeChannel') ->andReturn(json_encode($data)); $this->assertEquals( @@ -204,8 +203,7 @@ public function testValidAuthenticationResponseCallPusherPresenceAuthMethodWithP ], ]; - $this->pusher->shouldReceive('authorizePresenceChannel') - ->once() + $this->pusher->expects('authorizePresenceChannel') ->andReturn(json_encode($data)); $this->assertEquals( @@ -216,24 +214,26 @@ public function testValidAuthenticationResponseCallPusherPresenceAuthMethodWithP public function testUserAuthenticationForPusher(): void { - $authenticateUser = [ - 'auth' => '278d425bdf160c739803:4708d583dada6a56435fb8bc611c77c359a31eebde13337c16ab43aa6de336ba', - 'user_data' => json_encode(['id' => '12345']), - ]; - - $this->pusher - ->shouldReceive('authenticateUser') - ->andReturn(json_encode($authenticateUser)); + $this->broadcaster = new PusherBroadcaster($this->container, new Pusher( + '278d425bdf160c739803', + '7ad3773142a6692b25b8', + '12345', + )); - $this->broadcaster->resolveAuthenticatedUserUsing(function () { + $this->broadcaster->resolveAuthenticatedUserUsing(function (): array { return ['id' => '12345']; }); $response = $this->broadcaster->resolveAuthenticatedUser( - $this->getMockRequestWithUserForChannel('presence-test') + Request::create('/?socket_id=1234.1234') ); - $this->assertSame($authenticateUser, $response); + // The result is hard-coded from the Pusher docs + // See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication + $this->assertSame([ + 'auth' => '278d425bdf160c739803:4708d583dada6a56435fb8bc611c77c359a31eebde13337c16ab43aa6de336ba', + 'user_data' => json_encode(['id' => '12345']), + ], $response); } public function testBroadcastUsesFormattedChannelNames(): void @@ -245,8 +245,7 @@ public function testBroadcastUsesFormattedChannelNames(): void ), ); - $this->pusher->shouldReceive('trigger') - ->once() + $this->pusher->expects('trigger') ->with(['application.orders'], 'OrderCreated', ['id' => 1], []); $this->broadcaster->broadcast(['orders'], 'OrderCreated', ['id' => 1]); @@ -262,8 +261,7 @@ public function testJsonpCallbackReturnsJsonWithoutExplicitOptIn(): void $data = ['auth' => 'abcd:efgh']; - $this->pusher->shouldReceive('authorizeChannel') - ->once() + $this->pusher->expects('authorizeChannel') ->andReturn(json_encode($data)); $response = $this->broadcaster->validAuthenticationResponse($request, true); @@ -276,7 +274,7 @@ public function testJsonpCallbackReturnsJsonpWhenExplicitlyEnabled(): void $container = ApplicationContainer::getInstance(); $container->singleton( ResponseFactoryContract::class, - fn () => new ResponseFactory( + fn (): ResponseFactory => new ResponseFactory( m::mock(ViewFactory::class), m::mock(Redirector::class), ) @@ -291,8 +289,7 @@ public function testJsonpCallbackReturnsJsonpWhenExplicitlyEnabled(): void $data = ['auth' => 'abcd:efgh']; - $this->pusher->shouldReceive('authorizeChannel') - ->once() + $this->pusher->expects('authorizeChannel') ->andReturn(json_encode($data)); $broadcaster = m::mock( @@ -314,8 +311,7 @@ public function testExplicitJsonpOptInWithoutCallbackReturnsJson(): void $request = $this->getMockRequestWithUserForChannel('private-test'); $data = ['auth' => 'abcd:efgh']; - $this->pusher->shouldReceive('authorizeChannel') - ->once() + $this->pusher->expects('authorizeChannel') ->andReturn(json_encode($data)); $broadcaster = m::mock( @@ -329,6 +325,9 @@ public function testExplicitJsonpOptInWithoutCallbackReturnsJson(): void ); } + /** + * Create a channel request with an authenticated user. + */ protected function getMockRequestWithUserForChannel(string $channel): Request { $request = m::mock(Request::class); @@ -345,6 +344,9 @@ protected function getMockRequestWithUserForChannel(string $channel): Request return $request; } + /** + * Create a channel request without an authenticated user. + */ protected function getMockRequestWithoutUserForChannel(string $channel): Request { $request = m::mock(Request::class); @@ -360,6 +362,9 @@ class PusherBroadcasterTestEloquentModelStub extends Model { public string $boundValue = ''; + /** + * Retrieve the model for a bound value. + */ public function resolveRouteBinding(mixed $value, ?string $field = null): ?self { $instance = new static; diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php index 8283b7ae9f..96a9e22758 100644 --- a/tests/Broadcasting/RedisBroadcasterTest.php +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -24,8 +24,11 @@ class RedisBroadcasterTest extends TestCase protected Container $container; - protected Redis|m\MockInterface $redis; + protected Redis&m\MockInterface $redis; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); @@ -38,7 +41,7 @@ protected function setUp(): void public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue(): void { - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return true; }); @@ -64,7 +67,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenCall { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return false; }); @@ -77,7 +80,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequ { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): bool { return true; }); @@ -89,7 +92,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequ public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCallbackReturnAnArray(): void { $returnData = [1, 2, 3, 4]; - $this->broadcaster->channel('test', function () use ($returnData) { + $this->broadcaster->channel('test', function () use ($returnData): array { return $returnData; }); @@ -110,7 +113,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCal { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): void { }); $this->broadcaster->auth( @@ -122,7 +125,7 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenReq { $this->expectException(AccessDeniedHttpException::class); - $this->broadcaster->channel('test', function () { + $this->broadcaster->channel('test', function (): array { return [1, 2, 3, 4]; }); @@ -138,13 +141,13 @@ public function testAuthUsesRewrittenChannelForConfiguredGuardAndPresenceUser(): [$this->container, $this->redis, 'default', 'redis.'], )->makePartial(); $user = m::mock('User'); - $user->shouldReceive('getAuthIdentifier')->once()->andReturn(42); + $user->expects('getAuthIdentifier')->andReturn(42); $request = m::mock(Request::class); $request->shouldReceive('input') ->with('channel_name') ->andReturn('redis.presence-application.tenant.orders.5'); - $request->shouldReceive('user')->times(3)->with('members')->andReturn($user); + $request->expects('user')->times(3)->with('members')->andReturn($user); $request->shouldNotReceive('user')->withNoArgs(); $calls = 0; @@ -158,7 +161,7 @@ public function testAuthUsesRewrittenChannelForConfiguredGuardAndPresenceUser(): $broadcaster->channel( 'application.orders.{order}', - static fn ($authenticatedUser, string $order): array|false => $authenticatedUser === $user && $order === '5' + static fn (object $authenticatedUser, string $order): array|false => $authenticatedUser === $user && $order === '5' ? ['role' => 'viewer'] : false, ['guards' => ['members']], @@ -252,12 +255,12 @@ public function testPresenceAuthenticationThrowsWhenUserDataCannotBeEncoded(): v public function testBroadcastUsesPublishPerChannelOnCluster(): void { $connection = m::mock(RedisProxy::class); - $connection->shouldReceive('isCluster')->once()->andReturnTrue(); - $connection->shouldReceive('publish')->once()->with('test-channel-1', m::type('string')); - $connection->shouldReceive('publish')->once()->with('test-channel-2', m::type('string')); + $connection->expects('isCluster')->andReturnTrue(); + $connection->expects('publish')->with('test-channel-1', m::type('string')); + $connection->expects('publish')->with('test-channel-2', m::type('string')); $connection->shouldNotReceive('eval'); - $this->redis->shouldReceive('connection')->once()->andReturn($connection); + $this->redis->expects('connection')->andReturn($connection); $broadcaster = new RedisBroadcaster($this->container, $this->redis); $broadcaster->broadcast(['test-channel-1', 'test-channel-2'], 'test-event', ['data' => 'value']); @@ -266,13 +269,12 @@ public function testBroadcastUsesPublishPerChannelOnCluster(): void public function testClusterBroadcastWrapsRedisClusterException(): void { $connection = m::mock(RedisProxy::class); - $connection->shouldReceive('isCluster')->once()->andReturnTrue(); - $connection->shouldReceive('publish') - ->once() + $connection->expects('isCluster')->andReturnTrue(); + $connection->expects('publish') ->with('test-channel', m::type('string')) ->andThrow(new RedisClusterException('Cluster unavailable')); - $this->redis->shouldReceive('connection')->once()->andReturn($connection); + $this->redis->expects('connection')->andReturn($connection); $this->expectException(BroadcastException::class); $this->expectExceptionMessage('Redis error: Cluster unavailable.'); @@ -284,11 +286,11 @@ public function testClusterBroadcastWrapsRedisClusterException(): void public function testBroadcastUsesEvalOnNonCluster(): void { $connection = m::mock(RedisProxy::class); - $connection->shouldReceive('isCluster')->once()->andReturnFalse(); - $connection->shouldReceive('eval')->once(); + $connection->expects('isCluster')->andReturnFalse(); + $connection->expects('eval'); $connection->shouldNotReceive('publish'); - $this->redis->shouldReceive('connection')->once()->andReturn($connection); + $this->redis->expects('connection')->andReturn($connection); $broadcaster = new RedisBroadcaster($this->container, $this->redis); $broadcaster->broadcast(['test-channel'], 'test-event', ['data' => 'value']); @@ -304,12 +306,11 @@ public function testClusterBroadcastLeavesRedisPrefixToNativePublishAfterFormatt ); $connection = m::mock(RedisProxy::class); - $connection->shouldReceive('isCluster')->once()->andReturnTrue(); - $connection->shouldReceive('publish') - ->once() + $connection->expects('isCluster')->andReturnTrue(); + $connection->expects('publish') ->with('application.orders', m::type('string')); - $this->redis->shouldReceive('connection')->once()->andReturn($connection); + $this->redis->expects('connection')->andReturn($connection); (new RedisBroadcaster( $this->container, @@ -328,9 +329,8 @@ public function testLuaBroadcastAddsRedisPrefixAfterFormattingChannels(): void ); $connection = m::mock(RedisProxy::class); - $connection->shouldReceive('isCluster')->once()->andReturnFalse(); - $connection->shouldReceive('eval') - ->once() + $connection->expects('isCluster')->andReturnFalse(); + $connection->expects('eval') ->with( m::type('string'), 0, @@ -338,7 +338,7 @@ public function testLuaBroadcastAddsRedisPrefixAfterFormattingChannels(): void 'redis.application.orders', ); - $this->redis->shouldReceive('connection')->once()->andReturn($connection); + $this->redis->expects('connection')->andReturn($connection); (new RedisBroadcaster( $this->container, @@ -351,7 +351,7 @@ public function testBroadcastThrowsWhenPayloadCannotBeEncoded(): void { $this->expectException(JsonException::class); - $this->redis->shouldReceive('connection')->once()->andReturn( + $this->redis->expects('connection')->andReturn( m::mock(RedisProxy::class) ); @@ -366,7 +366,7 @@ public function testBroadcastPayloadDoesNotDuplicateSocketInData(): void { $connection = m::mock(RedisProxy::class); $connection->shouldReceive('isCluster')->andReturnFalse(); - $connection->shouldReceive('eval')->once()->withArgs(function ($script, $numKeys, $payload) { + $connection->expects('eval')->withArgs(function (string $script, int $numberOfKeys, string $payload): bool { $decoded = json_decode($payload, true); // socket should be at top level only, not inside data @@ -380,6 +380,9 @@ public function testBroadcastPayloadDoesNotDuplicateSocketInData(): void $broadcaster->broadcast(['test-channel'], 'test-event', ['message' => 'hello', 'socket' => 'test-socket']); } + /** + * Create a channel request with an authenticated user. + */ protected function getMockRequestWithUserForChannel(string $channel): Request { $request = m::mock(Request::class); @@ -394,6 +397,9 @@ protected function getMockRequestWithUserForChannel(string $channel): Request return $request; } + /** + * Create a channel request without an authenticated user. + */ protected function getMockRequestWithoutUserForChannel(string $channel): Request { $request = m::mock(Request::class); diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 5aecb6ed39..ae4e62fee7 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Bus; +use Generator; use Hypervel\Bus\Batch; use Hypervel\Bus\Batchable; use Hypervel\Bus\BatchFactory; @@ -31,6 +32,7 @@ use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; +use Throwable; class BusBatchTest extends TestCase { @@ -38,6 +40,9 @@ class BusBatchTest extends TestCase protected bool $migrateRefresh = true; + /** + * Get the migration options for the batch repository. + */ protected function migrateFreshUsing(): array { return [ @@ -48,6 +53,9 @@ protected function migrateFreshUsing(): array ]; } + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); @@ -58,6 +66,9 @@ protected function setUp(): void $_SERVER['__catch.count'] = 0; } + /** + * Clean up the callback state. + */ protected function tearDown(): void { unset( @@ -126,18 +137,19 @@ public function testJobsCanBeAddedToTheBatch(): void $thirdJob = function (): void { }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once()->with(m::on(function (array $args) use ($job, $secondJob, $thirdJob): bool { + $connection->expects('bulk')->with(m::on(function (array $jobs) use ($job, $secondJob, $thirdJob): bool { return - count($args) === 3 - && $args[0] === $job - && $args[1] === $secondJob - && $args[2] instanceof CallQueuedClosure - && $args[2]->closure->getClosure() === $thirdJob - && is_string($args[2]->batchId); + count($jobs) === 3 + && $jobs[0] === $job + && $jobs[1] === $secondJob + && $jobs[2] instanceof CallQueuedClosure + && $jobs[2]->closure->getClosure() === $thirdJob + && is_string($jobs[2]->batchId); }), '', 'test-queue'); $batch = $batch->add([$job, $secondJob, $thirdJob]); @@ -148,7 +160,7 @@ public function testJobsCanBeAddedToTheBatch(): void $this->assertSame(CarbonImmutable::class, $batch->createdAt::class); } - public function testJobsCanBeAddedToPendingBatch() + public function testJobsCanBeAddedToPendingBatch(): void { $batch = new PendingBatch($this->app, collect()); $this->assertCount(0, $batch->jobs); @@ -168,13 +180,13 @@ public function testJobsCanBeAddedToPendingBatch() $this->assertCount(2, $batch->jobs); } - public function testJobsCanBeAddedToThePendingBatchFromIterable() + public function testJobsCanBeAddedToThePendingBatchFromIterable(): void { $batch = new PendingBatch($this->app, collect()); $this->assertCount(0, $batch->jobs); $count = 3; - $generator = function (int $jobsCount) { + $generator = function (int $jobsCount): Generator { for ($i = 0; $i < $jobsCount; ++$i) { yield new class { use Batchable; @@ -186,7 +198,7 @@ public function testJobsCanBeAddedToThePendingBatchFromIterable() $this->assertCount($count, $batch->jobs); } - public function testProcessedJobsCanBeCalculated() + public function testProcessedJobsCanBeCalculated(): void { $queue = m::mock(Factory::class); @@ -199,7 +211,7 @@ public function testProcessedJobsCanBeCalculated() $this->assertEquals(60, $batch->progress()); } - public function testSuccessfulJobsCanBeRecorded() + public function testSuccessfulJobsCanBeRecorded(): void { $queue = m::mock(Factory::class); @@ -213,11 +225,12 @@ public function testSuccessfulJobsCanBeRecorded() use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$job, $secondJob]); $this->assertEquals(2, $batch->pendingJobs); @@ -237,9 +250,10 @@ public function testSuccessfulJobsCanBeRecorded() $this->assertEquals(1, $_SERVER['__then.count']); } - public function testBatchFinishedEventIsDispatched() + public function testBatchFinishedEventIsDispatched(): void { - $this->app->instance(EventDispatcher::class, $events = m::mock(EventDispatcher::class)); + $events = m::mock(EventDispatcher::class); + $this->app->instance(EventDispatcher::class, $events); $queue = m::mock(Factory::class); $batch = $this->createTestBatch($queue); @@ -248,23 +262,24 @@ public function testBatchFinishedEventIsDispatched() use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$job]); - $events->shouldReceive('hasListeners')->once()->with(BatchStarted::class)->andReturnTrue(); + $events->expects('hasListeners')->with(BatchStarted::class)->andReturnTrue(); - $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + $events->expects('dispatch')->with(m::on(function (object $event) use ($batch): bool { return $event instanceof BatchStarted && $event->batch === $batch; })); - $events->shouldReceive('hasListeners')->once()->with(BatchFinished::class)->andReturnTrue(); + $events->expects('hasListeners')->with(BatchFinished::class)->andReturnTrue(); - $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + $events->expects('dispatch')->with(m::on(function (object $event) use ($batch): bool { return $event instanceof BatchFinished && $event->batch === $batch; })); @@ -273,7 +288,8 @@ public function testBatchFinishedEventIsDispatched() public function testBatchStartedEventIsDispatchedOnceWhenTheFirstJobSucceeds(): void { - $this->app->instance(EventDispatcher::class, $events = m::mock(EventDispatcher::class)); + $events = m::mock(EventDispatcher::class); + $this->app->instance(EventDispatcher::class, $events); $queue = m::mock(Factory::class); $batch = $this->createTestBatch($queue); @@ -286,20 +302,21 @@ public function testBatchStartedEventIsDispatchedOnceWhenTheFirstJobSucceeds(): use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$firstJob, $secondJob]); - $events->shouldReceive('hasListeners')->once()->with(BatchStarted::class)->andReturnTrue(); - $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + $events->expects('hasListeners')->with(BatchStarted::class)->andReturnTrue(); + $events->expects('dispatch')->with(m::on(function (object $event) use ($batch): bool { return $event instanceof BatchStarted && $event->batch === $batch; })); - $events->shouldReceive('hasListeners')->once()->with(BatchFinished::class)->andReturnTrue(); - $events->shouldReceive('dispatch')->once()->with(m::type(BatchFinished::class)); + $events->expects('hasListeners')->with(BatchFinished::class)->andReturnTrue(); + $events->expects('dispatch')->with(m::type(BatchFinished::class)); $batch->recordSuccessfulJob('test-id-1'); $batch->recordSuccessfulJob('test-id-2'); @@ -307,7 +324,8 @@ public function testBatchStartedEventIsDispatchedOnceWhenTheFirstJobSucceeds(): public function testBatchStartedEventIsDispatchedOnceWhenTheFirstJobFails(): void { - $this->app->instance(EventDispatcher::class, $events = m::mock(EventDispatcher::class)); + $events = m::mock(EventDispatcher::class); + $this->app->instance(EventDispatcher::class, $events); $queue = m::mock(Factory::class); $batch = $this->createTestBatch($queue, $allowFailures = true); @@ -320,16 +338,17 @@ public function testBatchStartedEventIsDispatchedOnceWhenTheFirstJobFails(): voi use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$firstJob, $secondJob]); - $events->shouldReceive('hasListeners')->once()->with(BatchStarted::class)->andReturnTrue(); - $events->shouldReceive('dispatch')->once()->with(m::on(function ($event) use ($batch) { + $events->expects('hasListeners')->with(BatchStarted::class)->andReturnTrue(); + $events->expects('dispatch')->with(m::on(function (object $event) use ($batch): bool { return $event instanceof BatchStarted && $event->batch === $batch; })); @@ -337,7 +356,7 @@ public function testBatchStartedEventIsDispatchedOnceWhenTheFirstJobFails(): voi $batch->recordFailedJob('test-id-2', new RuntimeException('Something else went wrong.')); } - public function testFailedJobsCanBeRecordedWhileNotAllowingFailures() + public function testFailedJobsCanBeRecordedWhileNotAllowingFailures(): void { $queue = m::mock(Factory::class); @@ -351,11 +370,12 @@ public function testFailedJobsCanBeRecordedWhileNotAllowingFailures() use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$job, $secondJob]); $this->assertEquals(2, $batch->pendingJobs); @@ -377,7 +397,7 @@ public function testFailedJobsCanBeRecordedWhileNotAllowingFailures() $this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage()); } - public function testFailedJobsCanBeRecordedWhileAllowingFailures() + public function testFailedJobsCanBeRecordedWhileAllowingFailures(): void { $queue = m::mock(Factory::class); @@ -391,11 +411,12 @@ public function testFailedJobsCanBeRecordedWhileAllowingFailures() use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$job, $secondJob]); $this->assertEquals(2, $batch->pendingJobs); @@ -416,7 +437,7 @@ public function testFailedJobsCanBeRecordedWhileAllowingFailures() $this->assertSame('Something went wrong.', $_SERVER['__catch.exception']->getMessage()); } - public function testPendingBatchFiltersOutFalsyJobs() + public function testPendingBatchFiltersOutFalsyJobs(): void { $job = new class { use Batchable; @@ -435,7 +456,7 @@ public function testPendingBatchFiltersOutFalsyJobs() $this->assertTrue($batch->jobs->contains($secondJob)); } - public function testFailureCallbacksExecuteCorrectly() + public function testFailureCallbacksExecuteCorrectly(): void { $queue = m::mock(Factory::class); @@ -447,11 +468,11 @@ public function testFailureCallbacksExecuteCorrectly() $pendingBatch = (new PendingBatch($this->app, collect())) ->allowFailures([ - static fn (Batch $batch, $e): true => $_SERVER['__failure1.invoked'] = true, - function (Batch $batch, $e) { + static fn (Batch $batch, ?Throwable $e): true => $_SERVER['__failure1.invoked'] = true, + function (Batch $batch, ?Throwable $e): void { $_SERVER['__failure2.invoked'] = true; }, - function (Batch $batch, $e) { + function (Batch $batch, ?Throwable $e): void { $_SERVER['__failure3.batch'] = $batch; $_SERVER['__failure3.exception'] = $e; $_SERVER['__failure3.batch_id'] = $batch->id; @@ -470,11 +491,12 @@ function (Batch $batch, $e) { use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$job]); @@ -495,7 +517,7 @@ function (Batch $batch, $e) { $this->assertEquals(2, $_SERVER['__failure3.param_count']); } - public function testBatchCanBeCancelled() + public function testBatchCanBeCancelled(): void { $queue = m::mock(Factory::class); @@ -510,15 +532,16 @@ public function testBatchCanBeCancelled() public function testBatchCancelledEventIsDispatched(): void { - $this->app->instance(EventDispatcher::class, $events = m::mock(EventDispatcher::class)); + $events = m::mock(EventDispatcher::class); + $this->app->instance(EventDispatcher::class, $events); $queue = m::mock(Factory::class); $batch = $this->createTestBatch($queue); $exception = new RuntimeException('Something went wrong.'); - $events->shouldReceive('hasListeners')->once()->with(BatchCanceled::class)->andReturnTrue(); - $events->shouldReceive('dispatch')->once()->with(m::on(function (object $event) use ($batch, $exception): bool { + $events->expects('hasListeners')->with(BatchCanceled::class)->andReturnTrue(); + $events->expects('dispatch')->with(m::on(function (object $event) use ($batch, $exception): bool { return $event instanceof BatchCanceled && $event->batch->id === $batch->id && $event->exception === $exception; @@ -527,7 +550,7 @@ public function testBatchCancelledEventIsDispatched(): void $batch->cancel($exception); } - public function testBatchCanBeDeleted() + public function testBatchCanBeDeleted(): void { $queue = m::mock(Factory::class); @@ -549,11 +572,12 @@ public function testDeletedBatchIgnoresLateJobResultsAndCallbacks(): void use Batchable; }; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once(); + $connection->expects('bulk'); $batch = $batch->add([$job]); $batch->delete(); @@ -620,15 +644,16 @@ public function testChainCanBeAddedToBatch(): void $thirdJob = new ThirdTestJob; - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with('test-connection') - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once()->with(m::on(function ($args) use ($chainHeadJob, $secondJob, $thirdJob) { + $connection->expects('bulk')->with(m::on(function (array $jobs) use ($chainHeadJob, $secondJob, $thirdJob): bool { return - $args[0] == $chainHeadJob - && serialize($secondJob) == $args[0]->chained[0] - && serialize($thirdJob) == $args[0]->chained[1]; + $jobs[0] === $chainHeadJob + && serialize($secondJob) === $jobs[0]->chained[0] + && serialize($thirdJob) === $jobs[0]->chained[1]; }), '', 'test-queue'); $batch = $batch->add([ @@ -663,11 +688,12 @@ public function testChainedJobsPreserveTheirRoutesWhenTheBatchHasNone(): void ->onConnection('custom-connection') ->onQueue('custom-queue'); - $queue->shouldReceive('connection')->once() + $connection = m::mock(QueueContract::class); + $queue->expects('connection') ->with(null) - ->andReturn($connection = m::mock(QueueContract::class)); + ->andReturn($connection); - $connection->shouldReceive('bulk')->once()->with(m::type('array'), '', null); + $connection->expects('bulk')->with(m::type('array'), '', null); $batch->add([ [$firstJob, $secondJob], @@ -679,21 +705,21 @@ public function testChainedJobsPreserveTheirRoutesWhenTheBatchHasNone(): void $this->assertSame('custom-queue', $secondJob->queue); } - public function testChainedClosureAfterMultipleBatchesIsProperlyDispatched() + public function testChainedClosureAfterMultipleBatchesIsProperlyDispatched(): void { Queue::fake(); Bus::chain([ Bus::batch([new TestBatchJob])->name('Batch 1'), Bus::batch([new TestBatchJob])->name('Batch 2'), - function () { + function (): void { }, ])->dispatch(); $this->assertTrue(true); } - public function testOptionsSerializationOnPostgres() + public function testOptionsSerializationOnPostgres(): void { $pendingBatch = (new PendingBatch($this->app, Collection::make())) ->onQueue('test-queue'); @@ -703,10 +729,10 @@ public function testOptionsSerializationOnPostgres() $resolver->shouldReceive('connection')->andReturn($connection); $builder = m::spy(Builder::class); - $connection->shouldReceive('table')->andReturn($builder); - $builder->shouldReceive('useWritePdo')->andReturnSelf(); - $builder->shouldReceive('where')->andReturnSelf(); - $builder->shouldReceive('first')->andReturn((object) [ + $connection->expects('table')->times(2)->andReturn($builder); + $builder->expects('useWritePdo')->andReturnSelf(); + $builder->expects('where')->andReturnSelf(); + $builder->expects('first')->andReturn((object) [ 'id' => 'test-id', 'name' => '', 'total_jobs' => 0, @@ -728,13 +754,13 @@ public function testOptionsSerializationOnPostgres() $repository->store($pendingBatch); $builder->shouldHaveReceived('insert') - ->withArgs(function ($argument) use ($pendingBatch) { + ->withArgs(function (array $argument) use ($pendingBatch): bool { return unserialize(base64_decode($argument['options'])) === $pendingBatch->options; }); } #[DataProvider('serializedOptions')] - public function testOptionsUnserializeOnPostgres($serialize, $options): void + public function testOptionsUnserializeOnPostgres(string $serialize, array $options): void { $factory = m::mock(BatchFactory::class); @@ -742,8 +768,8 @@ public function testOptionsUnserializeOnPostgres($serialize, $options): void $resolver = m::mock(ConnectionResolverInterface::class); $resolver->shouldReceive('connection')->andReturn($connection); - $connection->shouldReceive('table->useWritePdo->where->first') - ->andReturn($m = (object) [ + $connection->expects('table->useWritePdo->where->first') + ->andReturn((object) [ 'id' => '', 'name' => '', 'total_jobs' => '', @@ -758,13 +784,16 @@ public function testOptionsUnserializeOnPostgres($serialize, $options): void $batch = new DatabaseBatchRepository($factory, $resolver, 'job_batches'); - $factory->shouldReceive('make') - ->withSomeOfArgs($batch, '', '', '', '', '', '', $options) + $factory->expects('make') + ->withSomeOfArgs($batch, '', '', 0, 0, 0, [], $options) ->andReturn(m::mock(Batch::class)); $batch->find('1'); } + /** + * Provide supported serialized batch options. + */ public static function serializedOptions(): array { $options = [1, 2]; @@ -775,7 +804,10 @@ public static function serializedOptions(): array ]; } - protected function createTestBatch($queue, $allowFailures = false) + /** + * Store a batch with callbacks that record their invocation. + */ + protected function createTestBatch(Factory $queue, bool $allowFailures = false): Batch { $repository = new DatabaseBatchRepository( new BatchFactory($queue), @@ -784,20 +816,20 @@ protected function createTestBatch($queue, $allowFailures = false) ); $pendingBatch = (new PendingBatch($this->app, Collection::make())) - ->progress(function (Batch $batch) { + ->progress(function (Batch $batch): void { $_SERVER['__progress.batch'] = $batch; ++$_SERVER['__progress.count']; }) - ->then(function (Batch $batch) { + ->then(function (Batch $batch): void { $_SERVER['__then.batch'] = $batch; ++$_SERVER['__then.count']; }) - ->catch(function (Batch $batch, $e) { + ->catch(function (Batch $batch, ?Throwable $e): void { $_SERVER['__catch.batch'] = $batch; $_SERVER['__catch.exception'] = $e; ++$_SERVER['__catch.count']; }) - ->finally(function (Batch $batch) { + ->finally(function (Batch $batch): void { $_SERVER['__finally.batch'] = $batch; ++$_SERVER['__finally.count']; }) @@ -815,7 +847,10 @@ class TestBatchJob implements ShouldQueue use Dispatchable; use Queueable; - public function handle() + /** + * Handle the job. + */ + public function handle(): void { } } diff --git a/tests/Bus/BusBatchableTest.php b/tests/Bus/BusBatchableTest.php index 85216b02dd..06fa2c8927 100644 --- a/tests/Bus/BusBatchableTest.php +++ b/tests/Bus/BusBatchableTest.php @@ -15,7 +15,7 @@ class BusBatchableTest extends TestCase { - public function testBatchMayBeRetrieved() + public function testBatchMayBeRetrieved(): void { $class = new class { use Batchable; @@ -28,12 +28,10 @@ public function testBatchMayBeRetrieved() $repository = m::mock(BatchRepository::class); $batch = m::mock(Batch::class); - $repository->shouldReceive('find')->once()->with('test-batch-id')->andReturn($batch); + $repository->expects('find')->with('test-batch-id')->andReturn($batch); $container->instance(BatchRepository::class, $repository); $this->assertSame($batch, $class->batch()); - - Container::setInstance(null); } public function testWithFakeBatchSetsAndReturnsFake(): void @@ -63,7 +61,7 @@ public function testZeroBatchIdMayBeRetrievedAndFaked(): void $repository = m::mock(BatchRepository::class); $batch = m::mock(Batch::class); - $repository->shouldReceive('find')->once()->with('0')->andReturn($batch); + $repository->expects('find')->with('0')->andReturn($batch); $container->instance(BatchRepository::class, $repository); $job->withBatchId('0'); @@ -79,7 +77,7 @@ public function testZeroBatchIdMayBeRetrievedAndFaked(): void $this->assertSame('0', $fakeBatch->id); } - public function testBatchingReflectsCancelledState() + public function testBatchingReflectsCancelledState(): void { $job = new class { use Batchable; diff --git a/tests/Bus/BusDispatcherTest.php b/tests/Bus/BusDispatcherTest.php index a60605300d..30a490f1c2 100644 --- a/tests/Bus/BusDispatcherTest.php +++ b/tests/Bus/BusDispatcherTest.php @@ -20,118 +20,108 @@ class BusDispatcherTest extends TestCase { - public function testCommandsThatShouldQueueIsQueued() + public function testCommandsThatShouldQueueIsQueued(): void { $container = new Container; - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getQueue')->andReturn(null); + $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function () { + $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); - $mock->shouldReceive('push')->once(); + $mock->expects('push'); return $mock; }); $dispatcher->dispatch(m::mock(ShouldQueue::class)); - - Container::setInstance(null); } - public function testCommandsThatShouldQueueIsQueuedUsingCustomHandler() + public function testCommandsThatShouldQueueIsQueuedUsingCustomHandler(): void { $container = new Container; - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function () { + $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); - $mock->shouldReceive('push')->once(); + $mock->expects('push'); return $mock; }); $dispatcher->dispatch(new BusDispatcherTestCustomQueueCommand); - - Container::setInstance(null); } - public function testCommandsThatShouldQueueIsQueuedUsingCustomQueueAndDelay() + public function testCommandsThatShouldQueueIsQueuedUsingCustomQueueAndDelay(): void { $container = new Container; - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function () { + $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); - $mock->shouldReceive('later')->once()->with(10, m::type(BusDispatcherTestSpecificQueueAndDelayCommand::class), '', 'foo'); + $mock->expects('later')->with(10, m::type(BusDispatcherTestSpecificQueueAndDelayCommand::class), '', 'foo'); return $mock; }); $dispatcher->dispatch(new BusDispatcherTestSpecificQueueAndDelayCommand); - - Container::setInstance(null); } public function testCommandsThatShouldQueueIsQueuedUsingQueueAndDelayAttributes(): void { $container = new Container; - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function () { + $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); - $mock->shouldReceive('later')->once()->with(10, m::type(BusDispatcherTestSpecificQueueAndDelayAttributesCommand::class), '', 'foo'); + $mock->expects('later')->with(10, m::type(BusDispatcherTestSpecificQueueAndDelayAttributesCommand::class), '', 'foo'); return $mock; }); $dispatcher->dispatch(new BusDispatcherTestSpecificQueueAndDelayAttributesCommand); - - Container::setInstance(null); } public function testCommandDelayPropertyOverridesDelayAttribute(): void { $container = new Container; - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function () { + $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); - $mock->shouldReceive('later')->once()->with(60, m::type(BusDispatcherTestSpecificQueueAndDelayAttributeWithPropertyCommand::class), '', 'foo'); + $mock->expects('later')->with(60, m::type(BusDispatcherTestSpecificQueueAndDelayAttributeWithPropertyCommand::class), '', 'foo'); return $mock; }); $dispatcher->dispatch((new BusDispatcherTestSpecificQueueAndDelayAttributeWithPropertyCommand)->delay(60)); - - Container::setInstance(null); } - public function testCommandsAreDispatchedWithQueueRoute() + public function testCommandsAreDispatchedWithQueueRoute(): void { Container::setInstance($container = new Container); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn('high-priority'); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getQueue')->andReturn('high-priority'); + $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); $mock = m::mock(Queue::class); - $mock->shouldReceive('push')->once()->with(BusDispatcherQueueable::class, '', 'high-priority'); + $mock->expects('push')->with(BusDispatcherQueueable::class, '', 'high-priority'); - $dispatcher = new Dispatcher($container, function () use ($mock) { + $dispatcher = new Dispatcher($container, function () use ($mock): Queue { return $mock; }); $dispatcher->dispatch(new BusDispatcherQueueable); - - Container::setInstance(null); } public function testCommandsAreForwardedToConnectionByQueueName(): void @@ -180,23 +170,23 @@ public function testExplicitConnectionWinsOverForwardedQueue(): void $this->assertSame('redis', $usedConnection); } - public function testDispatchNowShouldNeverQueue() + public function testDispatchNowShouldNeverQueue(): void { $container = new Container; $mock = m::mock(Queue::class); $mock->shouldReceive('push')->never(); - $dispatcher = new Dispatcher($container, function () use ($mock) { + $dispatcher = new Dispatcher($container, function () use ($mock): Queue { return $mock; }); $dispatcher->dispatch(new BusDispatcherBasicCommand); } - public function testDispatcherCanDispatchStandAloneHandler() + public function testDispatcherCanDispatchStandAloneHandler(): void { $container = new Container; $mock = m::mock(Queue::class); - $dispatcher = new Dispatcher($container, function () use ($mock) { + $dispatcher = new Dispatcher($container, function () use ($mock): Queue { return $mock; }); @@ -223,10 +213,10 @@ public function testDisabledDispatchAfterResponseUsesExplicitHandler(): void $this->assertFalse($command->handled); } - public function testOnConnectionOnJobWhenDispatching() + public function testOnConnectionOnJobWhenDispatching(): void { Container::setInstance($container = new Container); - $container->singleton('config', function () { + $container->singleton('config', function (): Config { return new Config([ 'queue' => [ 'default' => 'null', @@ -236,14 +226,13 @@ public function testOnConnectionOnJobWhenDispatching() ], ]); }); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); - Container::setInstance($container); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getQueue')->andReturn(null); + $container->instance('queue.routes', $queueRoutes); - $dispatcher = new Dispatcher($container, function () { + $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); - $mock->shouldReceive('push')->once(); + $mock->expects('push'); return $mock; }); @@ -251,28 +240,27 @@ public function testOnConnectionOnJobWhenDispatching() $job = (new ShouldNotBeDispatched)->onConnection('null'); $dispatcher->dispatch($job); - - Container::setInstance(null); } public function testDispatchBulk(): void { $container = new Container; - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->shouldReceive('getQueue')->andReturn(null); - $queueRoutes->shouldReceive('getConnection')->andReturn(null); + $queueRoutes = m::mock(QueueRoutes::class); + $queueRoutes->expects('getQueue')->times(2)->andReturn(null); + $queueRoutes->expects('getConnection')->times(3)->andReturn(null); + $container->instance('queue.routes', $queueRoutes); Container::setInstance($container); $defaultQueue = m::mock(Queue::class); - $defaultQueue->shouldReceive('bulk')->once()->with(m::on(fn ($jobs) => count($jobs) === 2), '', null); - $defaultQueue->shouldReceive('bulk')->once()->with(m::on(fn ($jobs) => count($jobs) === 1), '', 'high'); + $defaultQueue->expects('bulk')->with(m::on(fn (array $jobs): bool => count($jobs) === 2), '', null); + $defaultQueue->expects('bulk')->with(m::on(fn (array $jobs): bool => count($jobs) === 1), '', 'high'); $priorityQueue = m::mock(Queue::class); - $priorityQueue->shouldReceive('bulk')->once()->with(m::on(fn ($jobs) => count($jobs) === 1), '', 'high'); + $priorityQueue->expects('bulk')->with(m::on(fn (array $jobs): bool => count($jobs) === 1), '', 'high'); $dispatcher = new Dispatcher( $container, - fn (?string $connection) => $connection === 'priority' ? $priorityQueue : $defaultQueue + fn (?string $connection): Queue => $connection === 'priority' ? $priorityQueue : $defaultQueue ); $immediate = new BusDispatcherImmediateCommand; @@ -286,8 +274,6 @@ public function testDispatchBulk(): void ]); $this->assertTrue($immediate->handled); - - Container::setInstance(null); } public function testDispatchBulkKeepsColonBearingRoutesSeparate(): void @@ -296,10 +282,10 @@ public function testDispatchBulkKeepsColonBearingRoutesSeparate(): void $secondJob = (new BusDispatcherQueueable)->onConnection('a')->onQueue('b:c'); $firstQueue = m::mock(Queue::class); - $firstQueue->shouldReceive('bulk')->once()->with([$firstJob], '', 'c'); + $firstQueue->expects('bulk')->with([$firstJob], '', 'c'); $secondQueue = m::mock(Queue::class); - $secondQueue->shouldReceive('bulk')->once()->with([$secondJob], '', 'b:c'); + $secondQueue->expects('bulk')->with([$secondJob], '', 'b:c'); $dispatcher = new Dispatcher( new Container, @@ -320,21 +306,30 @@ class BusInjectionStub class BusDispatcherBasicCommand { - public $name; + public mixed $name; - public function __construct($name = null) + /** + * Create a command with the given name. + */ + public function __construct(mixed $name = null) { $this->name = $name; } - public function handle(BusInjectionStub $stub) + /** + * Handle the command. + */ + public function handle(BusInjectionStub $stub): void { } } class BusDispatcherTestCustomQueueCommand implements ShouldQueue { - public function queue($queue, $command) + /** + * Queue the command using a custom handler. + */ + public function queue(Queue $queue, object $command): void { $queue->push($command); } @@ -342,9 +337,9 @@ public function queue($queue, $command) class BusDispatcherTestSpecificQueueAndDelayCommand implements ShouldQueue { - public $queue = 'foo'; + public string $queue = 'foo'; - public $delay = 10; + public int $delay = 10; } class BusDispatcherTestSpecificQueueCommand implements ShouldQueue @@ -363,6 +358,9 @@ class BusDispatcherImmediateCommand { public bool $handled = false; + /** + * Handle the command. + */ public function handle(): void { $this->handled = true; @@ -393,7 +391,10 @@ class StandAloneCommand class StandAloneHandler { - public function handle(StandAloneCommand $command) + /** + * Handle the standalone command. + */ + public function handle(StandAloneCommand $command): StandAloneCommand { return $command; } @@ -404,7 +405,12 @@ class ShouldNotBeDispatched implements ShouldQueue use InteractsWithQueue; use Queueable; - public function handle() + /** + * Reject unexpected inline dispatch. + * + * @throws RuntimeException + */ + public function handle(): never { throw new RuntimeException('This should not be run'); } diff --git a/tests/Bus/BusPendingBatchTest.php b/tests/Bus/BusPendingBatchTest.php index 621d9ee911..eb00664312 100644 --- a/tests/Bus/BusPendingBatchTest.php +++ b/tests/Bus/BusPendingBatchTest.php @@ -52,9 +52,8 @@ public function testChainedBatchPreservesZeroConnectionAndQueueIdentifiers(): vo ->onQueue(PendingBatchIntegerIdentifier::Zero); $dispatcher = m::mock(BusDispatcher::class); - $dispatcher->shouldReceive('batch') - ->once() - ->andReturnUsing(fn ($jobs) => new PendingBatch($container, $jobs)); + $dispatcher->expects('batch') + ->andReturnUsing(fn (Collection $jobs): PendingBatch => new PendingBatch($container, $jobs)); $container->instance(BusDispatcher::class, $dispatcher); $pendingBatch = (new ChainedBatch($source))->toPendingBatch(); @@ -78,9 +77,8 @@ public function testDirectlyRoutedChainedBatchPreservesZeroConnectionAndQueueIde ->onQueue('0'); $dispatcher = m::mock(BusDispatcher::class); - $dispatcher->shouldReceive('batch') - ->once() - ->andReturnUsing(fn ($jobs) => new PendingBatch($container, $jobs)); + $dispatcher->expects('batch') + ->andReturnUsing(fn (Collection $jobs): PendingBatch => new PendingBatch($container, $jobs)); $container->instance(BusDispatcher::class, $dispatcher); $pendingBatch = $chainedBatch->toPendingBatch(); @@ -103,9 +101,8 @@ public function testChainedBatchPreservesSourceEmptyConnectionAndQueueOptions(): ->onQueue(''); $dispatcher = m::mock(BusDispatcher::class); - $dispatcher->shouldReceive('batch') - ->once() - ->andReturnUsing(fn ($jobs) => new PendingBatch($container, $jobs)); + $dispatcher->expects('batch') + ->andReturnUsing(fn (Collection $jobs): PendingBatch => new PendingBatch($container, $jobs)); $container->instance(BusDispatcher::class, $dispatcher); $pendingBatch = (new ChainedBatch($source))->toPendingBatch(); @@ -131,7 +128,7 @@ public function testChainedBatchRemainderInheritsEmptyIdentifiersAndPreservesZer $chainedBatch->chainQueue = 'chain-queue'; $dispatcher = m::mock(BusDispatcher::class); - $dispatcher->shouldReceive('dispatch')->once()->with(m::on(function (ChainedBatchQueueableJob $job) use ($expectedConnection, $expectedQueue): bool { + $dispatcher->expects('dispatch')->with(m::on(function (ChainedBatchQueueableJob $job) use ($expectedConnection, $expectedQueue): bool { $this->assertSame($expectedConnection, $job->connection); $this->assertSame($expectedQueue, $job->queue); @@ -147,18 +144,18 @@ public function testChainedBatchRemainderInheritsEmptyIdentifiersAndPreservesZer $this->assertCount(1, $callbacks); $batch = m::mock(Batch::class); - $batch->shouldReceive('cancelled')->once()->andReturnFalse(); + $batch->expects('cancelled')->andReturnFalse(); $callbacks[0]($batch); } } - public function testPendingBatchMayBeConfiguredAndDispatched() + public function testPendingBatchMayBeConfiguredAndDispatched(): void { $container = new Container; $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldReceive('hasListeners')->once()->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->shouldReceive('dispatch')->once()->with(m::type(BatchDispatched::class)); + $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); + $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); $container->instance(Dispatcher::class, $eventDispatcher); @@ -168,10 +165,10 @@ public function testPendingBatchMayBeConfiguredAndDispatched() $pendingBatch = new PendingBatch($container, new Collection([$job])); - $pendingBatch = $pendingBatch->before(function () { - })->progress(function () { - })->then(function () { - })->catch(function () { + $pendingBatch = $pendingBatch->before(function (): void { + })->progress(function (): void { + })->then(function (): void { + })->catch(function (): void { })->allowFailures()->onConnection('test-connection')->onQueue('test-queue')->withOption('extra-option', 123); $this->assertSame('test-connection', $pendingBatch->connection()); @@ -184,8 +181,10 @@ public function testPendingBatchMayBeConfiguredAndDispatched() $this->assertSame(123, $pendingBatch->options['extra-option']); $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->with($pendingBatch)->andReturn($batch = m::mock(Batch::class)); - $batch->shouldReceive('add')->once()->with(m::type(Collection::class))->andReturn($batch = m::mock(Batch::class)); + $storedBatch = m::mock(Batch::class); + $repository->expects('store')->with($pendingBatch)->andReturn($storedBatch); + $batch = m::mock(Batch::class); + $storedBatch->expects('add')->with(m::type(Collection::class))->andReturn($batch); $container->instance(BatchRepository::class, $repository); @@ -197,7 +196,7 @@ public function testBatchDispatchedEventIsSkippedWithoutListeners(): void $container = new Container; $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldReceive('hasListeners')->once()->with(BatchDispatched::class)->andReturnFalse(); + $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnFalse(); $eventDispatcher->shouldNotReceive('dispatch'); $container->instance(Dispatcher::class, $eventDispatcher); @@ -208,8 +207,9 @@ public function testBatchDispatchedEventIsSkippedWithoutListeners(): void $pendingBatch = new PendingBatch($container, new Collection([$job])); $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->with($pendingBatch)->andReturn($batch = m::mock(Batch::class)); - $batch->shouldReceive('add')->once()->with(m::type(Collection::class))->andReturnSelf(); + $batch = m::mock(Batch::class); + $repository->expects('store')->with($pendingBatch)->andReturn($batch); + $batch->expects('add')->with(m::type(Collection::class))->andReturnSelf(); $container->instance(BatchRepository::class, $repository); $this->assertSame($batch, $pendingBatch->dispatch()); @@ -220,8 +220,8 @@ public function testBatchDispatchedEventIsDispatchedAfterResponse(): void $container = new Container; $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldReceive('hasListeners')->once()->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->shouldReceive('dispatch')->once()->with(m::type(BatchDispatched::class)); + $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); + $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -231,14 +231,15 @@ public function testBatchDispatchedEventIsDispatchedAfterResponse(): void $pendingBatch = new PendingBatch($container, new Collection([$job])); $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->with($pendingBatch)->andReturn($batch = m::mock(Batch::class)); - $batch->shouldReceive('add')->once()->with(m::type(Collection::class))->andReturnSelf(); + $batch = m::mock(Batch::class); + $repository->expects('store')->with($pendingBatch)->andReturn($batch); + $batch->expects('add')->with(m::type(Collection::class))->andReturnSelf(); $container->instance(BatchRepository::class, $repository); $this->assertSame($batch, $pendingBatch->dispatchAfterResponse()); } - public function testBatchIsDeletedFromStorageIfExceptionThrownDuringBatching() + public function testBatchIsDeletedFromStorageIfExceptionThrownDuringBatching(): void { $this->expectException(RuntimeException::class); @@ -252,28 +253,29 @@ public function testBatchIsDeletedFromStorageIfExceptionThrownDuringBatching() $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->with($pendingBatch)->andReturn($batch = m::mock(Batch::class)); + $batch = m::mock(Batch::class); + $repository->expects('store')->with($pendingBatch)->andReturn($batch); $batch->id = 'test-id'; - $batch->shouldReceive('add')->once()->andReturnUsing(function () { + $batch->expects('add')->andReturnUsing(function (): never { throw new RuntimeException('Failed to add jobs...'); }); - $repository->shouldReceive('delete')->once()->with('test-id'); + $repository->expects('delete')->with('test-id'); $container->instance(BatchRepository::class, $repository); $pendingBatch->dispatch(); } - public function testBatchIsDispatchedWhenDispatchifIsTrue() + public function testBatchIsDispatchedWhenDispatchifIsTrue(): void { $container = new Container; $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldReceive('hasListeners')->once()->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->shouldReceive('dispatch')->once()->with(m::type(BatchDispatched::class)); + $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); + $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -283,8 +285,10 @@ public function testBatchIsDispatchedWhenDispatchifIsTrue() $pendingBatch = new PendingBatch($container, new Collection([$job])); $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->andReturn($batch = m::mock(Batch::class)); - $batch->shouldReceive('add')->once()->andReturn($batch = m::mock(Batch::class)); + $storedBatch = m::mock(Batch::class); + $repository->expects('store')->andReturn($storedBatch); + $batch = m::mock(Batch::class); + $storedBatch->expects('add')->andReturn($batch); $container->instance(BatchRepository::class, $repository); @@ -293,7 +297,7 @@ public function testBatchIsDispatchedWhenDispatchifIsTrue() $this->assertInstanceOf(Batch::class, $result); } - public function testBatchIsNotDispatchedWhenDispatchifIsFalse() + public function testBatchIsNotDispatchedWhenDispatchifIsFalse(): void { $container = new Container; @@ -315,13 +319,13 @@ public function testBatchIsNotDispatchedWhenDispatchifIsFalse() $this->assertNull($result); } - public function testBatchIsDispatchedWhenDispatchunlessIsFalse() + public function testBatchIsDispatchedWhenDispatchunlessIsFalse(): void { $container = new Container; $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldReceive('hasListeners')->once()->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->shouldReceive('dispatch')->once()->with(m::type(BatchDispatched::class)); + $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); + $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -331,8 +335,10 @@ public function testBatchIsDispatchedWhenDispatchunlessIsFalse() $pendingBatch = new PendingBatch($container, new Collection([$job])); $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->andReturn($batch = m::mock(Batch::class)); - $batch->shouldReceive('add')->once()->andReturn($batch = m::mock(Batch::class)); + $storedBatch = m::mock(Batch::class); + $repository->expects('store')->andReturn($storedBatch); + $batch = m::mock(Batch::class); + $storedBatch->expects('add')->andReturn($batch); $container->instance(BatchRepository::class, $repository); @@ -341,7 +347,7 @@ public function testBatchIsDispatchedWhenDispatchunlessIsFalse() $this->assertInstanceOf(Batch::class, $result); } - public function testBatchIsNotDispatchedWhenDispatchunlessIsTrue() + public function testBatchIsNotDispatchedWhenDispatchunlessIsTrue(): void { $container = new Container; @@ -363,13 +369,13 @@ public function testBatchIsNotDispatchedWhenDispatchunlessIsTrue() $this->assertNull($result); } - public function testBatchBeforeEventIsCalled() + public function testBatchBeforeEventIsCalled(): void { $container = new Container; $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldReceive('hasListeners')->once()->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->shouldReceive('dispatch')->once()->with(m::type(BatchDispatched::class)); + $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); + $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); $container->instance(Dispatcher::class, $eventDispatcher); @@ -381,13 +387,15 @@ public function testBatchBeforeEventIsCalled() $pendingBatch = new PendingBatch($container, new Collection([$job])); - $pendingBatch = $pendingBatch->before(function () use (&$beforeCalled) { + $pendingBatch = $pendingBatch->before(function () use (&$beforeCalled): void { $beforeCalled = true; })->onConnection('test-connection')->onQueue('test-queue'); $repository = m::mock(BatchRepository::class); - $repository->shouldReceive('store')->once()->with($pendingBatch)->andReturn($batch = m::mock(Batch::class)); - $batch->shouldReceive('add')->once()->with(m::type(Collection::class))->andReturn($batch = m::mock(Batch::class)); + $storedBatch = m::mock(Batch::class); + $repository->expects('store')->with($pendingBatch)->andReturn($storedBatch); + $batch = m::mock(Batch::class); + $storedBatch->expects('add')->with(m::type(Collection::class))->andReturn($batch); $container->instance(BatchRepository::class, $repository); @@ -396,7 +404,7 @@ public function testBatchBeforeEventIsCalled() $this->assertTrue($beforeCalled); } - public function testItThrowsExceptionIfBatchedJobIsNotBatchable() + public function testItThrowsExceptionIfBatchedJobIsNotBatchable(): void { $nonBatchableJob = new class { }; @@ -406,7 +414,7 @@ public function testItThrowsExceptionIfBatchedJobIsNotBatchable() new PendingBatch(new Container, new Collection([$nonBatchableJob])); } - public function testItThrowsAnExceptionIfBatchedJobContainsBatchWithNonbatchableJob() + public function testItThrowsAnExceptionIfBatchedJobContainsBatchWithNonbatchableJob(): void { $this->expectException(RuntimeException::class); @@ -420,19 +428,19 @@ public function testItThrowsAnExceptionIfBatchedJobContainsBatchWithNonbatchable ); } - public function testItCanBatchAClosure() + public function testItCanBatchAClosure(): void { new PendingBatch( new Container, new Collection([ - function () { + function (): void { }, ]) ); $this->expectNotToPerformAssertions(); } - public function testAllowFailuresWithBooleanTrueEnablesFailureTolerance() + public function testAllowFailuresWithBooleanTrueEnablesFailureTolerance(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -443,7 +451,7 @@ public function testAllowFailuresWithBooleanTrueEnablesFailureTolerance() $this->assertEmpty($batch->failureCallbacks()); } - public function testAllowFailuresWithBooleanFalseDisablesFailureTolerance() + public function testAllowFailuresWithBooleanFalseDisablesFailureTolerance(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -454,7 +462,7 @@ public function testAllowFailuresWithBooleanFalseDisablesFailureTolerance() $this->assertEmpty($batch->failureCallbacks()); } - public function testAllowFailuresWithSingleClosureRegistersCallback() + public function testAllowFailuresWithSingleClosureRegistersCallback(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -465,7 +473,7 @@ public function testAllowFailuresWithSingleClosureRegistersCallback() $this->assertCount(1, $batch->failureCallbacks()); } - public function testAllowFailuresWithSingleCallableRegistersCallback() + public function testAllowFailuresWithSingleCallableRegistersCallback(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -476,7 +484,7 @@ public function testAllowFailuresWithSingleCallableRegistersCallback() $this->assertCount(1, $batch->failureCallbacks()); } - public function testAllowFailuresWithArrayOfCallablesRegistersMultipleCallbacks() + public function testAllowFailuresWithArrayOfCallablesRegistersMultipleCallbacks(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -492,7 +500,7 @@ public function testAllowFailuresWithArrayOfCallablesRegistersMultipleCallbacks( $this->assertCount(4, $batch->failureCallbacks()); } - public function testAllowFailuresRegistersOnlyValidCallbacks() + public function testAllowFailuresRegistersOnlyValidCallbacks(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -514,7 +522,7 @@ public function testAllowFailuresRegistersOnlyValidCallbacks() $this->assertCount(3, $batch->failureCallbacks()); } - public function testAllowFailuresWithEmptyArrayEnablesToleranceWithoutCallbacks() + public function testAllowFailuresWithEmptyArrayEnablesToleranceWithoutCallbacks(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -525,7 +533,7 @@ public function testAllowFailuresWithEmptyArrayEnablesToleranceWithoutCallbacks( $this->assertEmpty($batch->failureCallbacks()); } - public function testAllowFailuresIsChainable() + public function testAllowFailuresIsChainable(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -537,7 +545,7 @@ public function testAllowFailuresIsChainable() $this->assertSame($batch, $batch->allowFailures([])); } - public function testFailureCallbacksAccessorReturnsRegisteredCallbacks() + public function testFailureCallbacksAccessorReturnsRegisteredCallbacks(): void { $batch = new PendingBatch(new Container, new Collection([new BatchableJob])); @@ -593,6 +601,9 @@ class ChainedBatchQueueableJob class TestableChainedBatch extends ChainedBatch { + /** + * Attach the remaining chain to the batch's final callback. + */ public function attachRemainder(PendingBatch $batch): PendingBatch { return $this->attachRemainderOfChainToEndOfBatch($batch); diff --git a/tests/Bus/BusPendingDispatchTest.php b/tests/Bus/BusPendingDispatchTest.php index a8993eb564..09982d569a 100644 --- a/tests/Bus/BusPendingDispatchTest.php +++ b/tests/Bus/BusPendingDispatchTest.php @@ -22,6 +22,9 @@ class PendingDispatchWithoutDestructor extends PendingDispatch { + /** + * Prevent dispatch while testing the configuration methods. + */ public function __destruct() { // Prevent the job from being dispatched @@ -30,13 +33,13 @@ public function __destruct() class BusPendingDispatchTest extends TestCase { - protected $job; + protected stdClass&m\MockInterface $job; + + protected PendingDispatchWithoutDestructor $pendingDispatch; /** - * @var PendingDispatchWithoutDestructor + * Set up the pending dispatch. */ - protected $pendingDispatch; - protected function setUp(): void { parent::setUp(); @@ -47,47 +50,54 @@ protected function setUp(): void public function testOnConnection(): void { - $this->job->shouldReceive('onConnection')->once()->with('test-connection'); + $this->job->expects('onConnection')->with('test-connection'); $this->pendingDispatch->onConnection('test-connection'); } public function testOnQueue(): void { - $this->job->shouldReceive('onQueue')->once()->with('test-queue'); + $this->job->expects('onQueue')->with('test-queue'); $this->pendingDispatch->onQueue('test-queue'); } public function testConditionableCanConfigurePendingDispatch(): void { - $this->job->shouldReceive('onQueue')->once()->with('conditional-queue'); + $this->job->expects('onQueue')->with('conditional-queue'); + + $this->pendingDispatch->when(true, fn (PendingDispatch $pendingDispatch): PendingDispatch => $pendingDispatch->onQueue('conditional-queue')); + } + + public function testWhenMethodOfConditionableTraitWithTrue(): void + { + $this->job->expects('delay')->with(300); - $this->pendingDispatch->when(true, fn ($pendingDispatch) => $pendingDispatch->onQueue('conditional-queue')); + $this->pendingDispatch->when(true, fn (PendingDispatch $pendingDispatch): PendingDispatch => $pendingDispatch->delay(300)); } public function testWhenMethodOfConditionableTraitWithFalse(): void { $this->job->shouldReceive('delay')->never(); - $this->pendingDispatch->when(false, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + $this->pendingDispatch->when(false, fn (PendingDispatch $pendingDispatch): PendingDispatch => $pendingDispatch->delay(300)); } public function testUnlessMethodOfConditionableTraitWithTrue(): void { $this->job->shouldReceive('delay')->never(); - $this->pendingDispatch->unless(true, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + $this->pendingDispatch->unless(true, fn (PendingDispatch $pendingDispatch): PendingDispatch => $pendingDispatch->delay(300)); } public function testUnlessMethodOfConditionableTraitWithFalse(): void { - $this->job->shouldReceive('delay')->once()->with(300); + $this->job->expects('delay')->with(300); - $this->pendingDispatch->unless(false, fn ($pendingDispatch) => $pendingDispatch->delay(300)); + $this->pendingDispatch->unless(false, fn (PendingDispatch $pendingDispatch): PendingDispatch => $pendingDispatch->delay(300)); } public function testOnGroup(): void { - $this->job->shouldReceive('onGroup')->once()->with('test-group'); + $this->job->expects('onGroup')->with('test-group'); $this->pendingDispatch->onGroup('test-group'); } @@ -95,14 +105,14 @@ public function testOnGroupForwardsAnArray(): void { $groups = ['first', 'second']; - $this->job->shouldReceive('onGroup')->once()->with($groups); + $this->job->expects('onGroup')->with($groups); $this->pendingDispatch->onGroup($groups); } public function testWithDeduplicator(): void { - $deduplicator = fn () => 'id'; - $this->job->shouldReceive('withDeduplicator')->once()->with($deduplicator); + $deduplicator = fn (): string => 'id'; + $this->job->expects('withDeduplicator')->with($deduplicator); $this->pendingDispatch->withDeduplicator($deduplicator); } @@ -110,10 +120,13 @@ public function testWithDeduplicatorForwardsAnArrayCallable(): void { $deduplicator = [$this, 'resolveDeduplicationId']; - $this->job->shouldReceive('withDeduplicator')->once()->with($deduplicator); + $this->job->expects('withDeduplicator')->with($deduplicator); $this->pendingDispatch->withDeduplicator($deduplicator); } + /** + * Resolve the message deduplication ID. + */ public function resolveDeduplicationId(): string { return 'id'; @@ -121,44 +134,44 @@ public function resolveDeduplicationId(): string public function testAllOnConnection(): void { - $this->job->shouldReceive('allOnConnection')->once()->with('test-connection'); + $this->job->expects('allOnConnection')->with('test-connection'); $this->pendingDispatch->allOnConnection('test-connection'); } public function testAllOnQueue(): void { - $this->job->shouldReceive('allOnQueue')->once()->with('test-queue'); + $this->job->expects('allOnQueue')->with('test-queue'); $this->pendingDispatch->allOnQueue('test-queue'); } public function testDelay(): void { - $this->job->shouldReceive('delay')->once()->with(60); + $this->job->expects('delay')->with(60); $this->pendingDispatch->delay(60); } public function testWithoutDelay(): void { - $this->job->shouldReceive('withoutDelay')->once(); + $this->job->expects('withoutDelay'); $this->pendingDispatch->withoutDelay(); } public function testAfterCommit(): void { - $this->job->shouldReceive('afterCommit')->once(); + $this->job->expects('afterCommit'); $this->pendingDispatch->afterCommit(); } public function testBeforeCommit(): void { - $this->job->shouldReceive('beforeCommit')->once(); + $this->job->expects('beforeCommit'); $this->pendingDispatch->beforeCommit(); } public function testChain(): void { $chain = [new stdClass]; - $this->job->shouldReceive('chain')->once()->with($chain); + $this->job->expects('chain')->with($chain); $this->pendingDispatch->chain($chain); } @@ -188,97 +201,90 @@ public function testPrepareForDispatchCanAbortDispatchBeforeDebounceCacheIsResol { Container::setInstance($container = new Container); - try { - $dispatcher = m::mock(Dispatcher::class); - $dispatcher->shouldReceive('dispatch')->never(); - $dispatcher->shouldReceive('dispatchAfterResponse')->never(); - $container->instance(Dispatcher::class, $dispatcher); + $dispatcher = m::mock(Dispatcher::class); + $dispatcher->shouldReceive('dispatch')->never(); + $dispatcher->shouldReceive('dispatchAfterResponse')->never(); + $container->instance(Dispatcher::class, $dispatcher); - $job = new PreparingDebouncedPendingDispatchJob(false); - $pendingDispatch = new PendingDispatch($job); - unset($pendingDispatch); + $job = new PreparingDebouncedPendingDispatchJob(false); + $pendingDispatch = new PendingDispatch($job); + unset($pendingDispatch); - $this->assertSame('', $job->debounceOwner); - } finally { - Container::setInstance(null); - } + $this->assertSame('', $job->debounceOwner); } public function testPrepareForDispatchAllowsDispatch(): void { Container::setInstance($container = new Container); - try { - $dispatcher = m::mock(Dispatcher::class); - $dispatcher->shouldReceive('dispatch')->once()->with(m::type(PreparingPendingDispatchJob::class)); - $dispatcher->shouldReceive('dispatchAfterResponse')->never(); - $container->instance(Dispatcher::class, $dispatcher); + $dispatcher = m::mock(Dispatcher::class); + $dispatcher->expects('dispatch')->with(m::type(PreparingPendingDispatchJob::class)); + $dispatcher->shouldReceive('dispatchAfterResponse')->never(); + $container->instance(Dispatcher::class, $dispatcher); - $pendingDispatch = new PendingDispatch(new PreparingPendingDispatchJob(true)); - unset($pendingDispatch); - } finally { - Container::setInstance(null); - } + $pendingDispatch = new PendingDispatch(new PreparingPendingDispatchJob(true)); + unset($pendingDispatch); } public function testUniqueMetadataReachesAfterResponseDispatcherBeforeUnclaimedOwnershipIsReleased(): void { Container::setInstance($container = new Container); - try { - $cache = new CacheRepository(new WorkerArrayStore, ['store' => 'unique']); - $container->instance(CacheContract::class, $cache); - - $job = new UniquePendingDispatchJob($cache); - $deferredJob = null; - $metadata = null; - - $dispatcher = m::mock(Dispatcher::class); - $dispatcher->shouldReceive('dispatch')->never(); - $dispatcher->shouldReceive('dispatchAfterResponse') - ->once() - ->with($job) - ->andReturnUsing(function (object $job) use (&$deferredJob, &$metadata): void { - $deferredJob = $job; - $metadata = DispatchLockContext::peekPayloadMetadata($job); - }); - $container->instance(Dispatcher::class, $dispatcher); - - $pendingDispatch = (new PendingDispatch($job))->afterResponse(); - unset($pendingDispatch); - - $this->assertSame($job, $deferredJob); - $this->assertNotNull($metadata); - $this->assertSame('unique', $metadata['laravel_unique_job_cache_store']); - $this->assertSame( - 'laravel_unique_job:' . UniquePendingDispatchJob::class . ':after-response', - $metadata['laravel_unique_job_key'], - ); - $this->assertNotSame('', $metadata['laravel_unique_job_lock_owner']); - $this->assertNull(DispatchLockContext::peekPayloadMetadata($job)); - $this->assertFalse( - $cache->restoreLock($metadata['laravel_unique_job_key'], $metadata['laravel_unique_job_lock_owner'])->isLocked() - ); - } finally { - Container::setInstance(null); - } + $cache = new CacheRepository(new WorkerArrayStore, ['store' => 'unique']); + $container->instance(CacheContract::class, $cache); + + $job = new UniquePendingDispatchJob($cache); + $deferredJob = null; + $metadata = null; + + $dispatcher = m::mock(Dispatcher::class); + $dispatcher->shouldReceive('dispatch')->never(); + $dispatcher->expects('dispatchAfterResponse') + ->with($job) + ->andReturnUsing(function (object $job) use (&$deferredJob, &$metadata): void { + $deferredJob = $job; + $metadata = DispatchLockContext::peekPayloadMetadata($job); + }); + $container->instance(Dispatcher::class, $dispatcher); + + $pendingDispatch = (new PendingDispatch($job))->afterResponse(); + unset($pendingDispatch); + + $this->assertSame($job, $deferredJob); + $this->assertNotNull($metadata); + $this->assertSame('unique', $metadata['laravel_unique_job_cache_store']); + $this->assertSame( + 'laravel_unique_job:' . UniquePendingDispatchJob::class . ':after-response', + $metadata['laravel_unique_job_key'], + ); + $this->assertNotSame('', $metadata['laravel_unique_job_lock_owner']); + $this->assertNull(DispatchLockContext::peekPayloadMetadata($job)); + $this->assertFalse( + $cache->restoreLock($metadata['laravel_unique_job_key'], $metadata['laravel_unique_job_lock_owner'])->isLocked() + ); } public function testDynamicallyProxyMethods(): void { $newJob = m::mock(stdClass::class); - $this->job->shouldReceive('appendToChain')->once()->with($newJob); + $this->job->expects('appendToChain')->with($newJob); $this->pendingDispatch->appendToChain($newJob); } } class PreparingPendingDispatchJob implements PreparesForDispatch { + /** + * Create a job with the given dispatch decision. + */ public function __construct( protected bool $shouldDispatch ) { } + /** + * Determine whether the job should be dispatched. + */ public function prepareForDispatch(): bool { return $this->shouldDispatch; @@ -293,16 +299,25 @@ class PreparingDebouncedPendingDispatchJob extends PreparingPendingDispatchJob class UniquePendingDispatchJob implements ShouldBeUnique { + /** + * Create a job with its unique-lock cache. + */ public function __construct( protected CacheRepository $cache ) { } + /** + * Get the job's unique ID. + */ public function uniqueId(): string { return 'after-response'; } + /** + * Get the cache for the job's unique lock. + */ public function uniqueVia(): CacheRepository { return $this->cache; diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 366d362e5b..1db55858f5 100644 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -57,12 +57,12 @@ public function testMultipleItemsCanBeSetAndRetrieved(): void public function testItemsCanExpire(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $store->put('foo', 'bar', 10); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow($now->addSeconds(10)->addSecond()); $result = $store->get('foo'); $this->assertNull($result); @@ -82,17 +82,18 @@ public function testTouchExtendsTtl(): void $this->assertSame('value', $store->get('key')); } - public function testTouchDoesNotReviveAnExpiredItem(): void + public function testTouchDoesNotRestoreExpiredItem(): void { CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; - $store->put('key', 'value', 10); + $store->put('key', 'value', 30); - CarbonImmutable::setTestNow($now->addSeconds(10)); + CarbonImmutable::setTestNow($now->addSeconds(30)); $this->assertFalse($store->touch('key', 60)); $this->assertArrayNotHasKey('key', $store->all(false)); + $this->assertNull($store->get('key')); } public function testStoreItemForeverProperlyStoresInArray(): void @@ -157,12 +158,12 @@ public function testNonExistingKeysCanBeIncremented(): void public function testExpiredKeysAreIncrementedLikeNonExistingKeys(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $store->put('foo', 999, 10); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow($now->addSeconds(10)->addSecond()); $result = $store->increment('foo'); $this->assertEquals(1, $result); @@ -233,12 +234,12 @@ public function testCannotAcquireLockTwice(): void public function testCanAcquireLockAgainAfterExpiry(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)); + CarbonImmutable::setTestNow($now->addSeconds(10)); $this->assertTrue($lock->acquire()); } @@ -264,12 +265,12 @@ public function testExpiredLockIsNotLockedOrOwned(): void public function testLockExpirationLowerBoundary(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore; $lock = $store->lock('foo', 10); $lock->acquire(); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->subMicrosecond()); + CarbonImmutable::setTestNow($now->addSeconds(10)->subMicrosecond()); $this->assertFalse($lock->acquire()); } @@ -279,7 +280,7 @@ public function testLockWithNoExpirationNeverExpires(): void $store = new ArrayStore; $lock = $store->lock('foo'); $lock->acquire(); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addYears(100)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addCentury()); $this->assertFalse($lock->acquire()); } @@ -446,6 +447,19 @@ public function testOtherOwnerDoesNotOwnLockAfterRestore(): void $this->assertFalse($secondLock->isOwnedByCurrentProcess()); } + public function testExpiredLockCannotBeRefreshedByPreviousOwner(): void + { + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + + $store = new ArrayStore; + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + + CarbonImmutable::setTestNow($now->addSeconds(10)->addSecond()); + + $this->assertFalse($lock->refresh(20)); + } + public function testRestoringNonExistingLockDoesNotOwnAnything(): void { $store = new ArrayStore; @@ -456,13 +470,13 @@ public function testRestoringNonExistingLockDoesNotOwnAnything(): void public function testCanGetAll(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore(false); $store->put('foo', 'bar', 10); $this->assertEquals([ - 'foo' => ['value' => 'bar', 'expiresAt' => CarbonImmutable::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000], + 'foo' => ['value' => 'bar', 'expiresAt' => $now->addSeconds(10)->getPreciseTimestamp(3) / 1000], ], $store->all()); } @@ -500,27 +514,27 @@ public function testAllOnlyReturnsCurrentStoreContextData(): void public function testCanGetAllWhenSerialized(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new ArrayStore(true); $store->put('foo', 'bar', 10); $this->assertEquals([ - 'foo' => ['value' => 'bar', 'expiresAt' => $expiresAt = (CarbonImmutable::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000)], + 'foo' => ['value' => 'bar', 'expiresAt' => $expiresAt = ($now->addSeconds(10)->getPreciseTimestamp(3) / 1000)], ], $store->all()); // Now let's put a serializable value in there $store->forget('foo'); - $store->put('foo', CarbonImmutable::now(), 10); + $store->put('foo', $now, 10); $this->assertEquals([ 'foo' => [ - 'value' => CarbonImmutable::now(), + 'value' => $now, 'expiresAt' => $expiresAt, ], ], $store->all()); $this->assertEquals( - serialize(CarbonImmutable::now()), + serialize($now), $store->all(false)['foo']['value'] ); } @@ -706,6 +720,9 @@ public function testGetRemainingLifetimeReturnsNullWhenExpired(): void class InspectableArrayStore extends ArrayStore { + /** + * Get the current lock records. + */ public function lockRecords(): array { return $this->getLockRecords(); diff --git a/tests/Cache/CacheEventsTest.php b/tests/Cache/CacheEventsTest.php index 5984596dd1..29d637635a 100644 --- a/tests/Cache/CacheEventsTest.php +++ b/tests/Cache/CacheEventsTest.php @@ -32,6 +32,8 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Tests\TestCase; use Mockery as m; +use Mockery\Matcher\Closure as ClosureMatcher; +use Mockery\MockInterface; use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; use Swoole\Coroutine\CanceledException; @@ -43,6 +45,9 @@ public function testRepositorySkipsEventDispatchSeamWithoutListenersAndEntersItW $repository = new class(new ArrayStore, ['store' => 'array']) extends Repository { public int $eventCalls = 0; + /** + * Count event dispatches through the repository. + */ protected function event(object $event): void { ++$this->eventCalls; @@ -75,6 +80,9 @@ public function testNamespacedTaggedCacheSkipsEventDispatchSeamWithoutListenersA $repository = new class($store, new VersionedTagSet($store, ['tag'])) extends NamespacedTaggedCache { public int $eventCalls = 0; + /** + * Count event dispatches through the tagged repository. + */ protected function event(object $event): void { ++$this->eventCalls; @@ -101,47 +109,52 @@ protected function event(object $event): void $this->assertSame(2, $repository->eventCalls); } - public function testHasTriggersEvents() + public function testHasTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo'])); $this->assertFalse($repository->has('foo')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'baz'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheHit::class, ['key' => 'baz', 'value' => 'qux'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheHit::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux'])); $this->assertTrue($repository->has('baz')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); $this->assertFalse($repository->tags('taylor')->has('foo')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'baz', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheHit::class, ['key' => 'baz', 'value' => 'qux', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheHit::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux', 'tags' => ['taylor']])); $this->assertTrue($repository->tags('taylor')->has('baz')); } - public function testGetTriggersEvents() + public function testGetTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo'])); $this->assertNull($repository->get('foo')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'baz'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheHit::class, ['key' => 'baz', 'value' => 'qux'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingManyKeys::class, ['storeName' => 'array', 'keys' => ['foo', 'bar']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'bar'])); + $this->assertSame(['foo' => null, 'bar' => null], $repository->get(['foo', 'bar'])); + + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheHit::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux'])); $this->assertSame('qux', $repository->get('baz')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); $this->assertNull($repository->tags('taylor')->get('foo')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'baz', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheHit::class, ['key' => 'baz', 'value' => 'qux', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheHit::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux', 'tags' => ['taylor']])); $this->assertSame('qux', $repository->tags('taylor')->get('baz')); } @@ -366,6 +379,9 @@ public function testCancellationDoesNotDispatchFailureEvents( $this->assertSame([$startedEvent], array_map(get_class(...), $events)); } + /** + * Provide cache operations that propagate coroutine cancellation. + */ public static function repositoryCancellationOperations(): array { return [ @@ -380,143 +396,144 @@ public static function repositoryCancellationOperations(): array ]; } - public function testPullTriggersEvents() + public function testPullTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'baz'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheHit::class, ['key' => 'baz', 'value' => 'qux'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(ForgettingKey::class, ['key' => 'baz'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyForgotten::class, ['key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheHit::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(ForgettingKey::class, ['storeName' => 'array', 'key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyForgotten::class, ['storeName' => 'array', 'key' => 'baz'])); $this->assertSame('qux', $repository->pull('baz')); } - public function testPullTriggersEventsUsingTags() + public function testPullTriggersEventsUsingTags(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'baz', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheHit::class, ['key' => 'baz', 'value' => 'qux', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(ForgettingKey::class, ['key' => 'baz', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyForgotten::class, ['key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheHit::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(ForgettingKey::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyForgotten::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); $this->assertSame('qux', $repository->tags('taylor')->pull('baz')); } - public function testPutTriggersEvents() + public function testPutTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); $repository->put('foo', 'bar', 99); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); - $repository->tags('taylor')->put('foo', 'bar', 99); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingManyKeys::class, ['storeName' => 'array', 'keys' => ['foo', 'baz'], 'values' => ['bar', 'qux'], 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'baz', 'value' => 'qux', 'seconds' => 99])); + $repository->putMany(['foo' => 'bar', 'baz' => 'qux'], 99); - $this->assertTrue(true); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); + $repository->tags('taylor')->put('foo', 'bar', 99); } - public function testAddTriggersEvents() + public function testAddTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); $this->assertTrue($repository->add('foo', 'bar', 99)); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); $this->assertTrue($repository->tags('taylor')->add('foo', 'bar', 99)); } - public function testForeverTriggersEvents() + public function testForeverTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null])); $repository->forever('foo', 'bar'); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); $repository->tags('taylor')->forever('foo', 'bar'); - - $this->assertTrue(true); } - public function testRememberTriggersEvents() + public function testRememberTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99])); - $this->assertSame('bar', $repository->remember('foo', 99, function () { + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99])); + $this->assertSame('bar', $repository->remember('foo', 99, function (): string { return 'bar'; })); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); - $this->assertSame('bar', $repository->tags('taylor')->remember('foo', 99, function () { + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => 99, 'tags' => ['taylor']])); + $this->assertSame('bar', $repository->tags('taylor')->remember('foo', 99, function (): string { return 'bar'; })); } - public function testRememberForeverTriggersEvents() + public function testRememberForeverTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null])); - $this->assertSame('bar', $repository->rememberForever('foo', function () { + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null])); + $this->assertSame('bar', $repository->rememberForever('foo', function (): string { return 'bar'; })); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(RetrievingKey::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(CacheMissed::class, ['key' => 'foo', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(WritingKey::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyWritten::class, ['key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); - $this->assertSame('bar', $repository->tags('taylor')->rememberForever('foo', function () { + $dispatcher->expects('dispatch')->with($this->assertEventMatches(RetrievingKey::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(CacheMissed::class, ['storeName' => 'array', 'key' => 'foo', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(WritingKey::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyWritten::class, ['storeName' => 'array', 'key' => 'foo', 'value' => 'bar', 'seconds' => null, 'tags' => ['taylor']])); + $this->assertSame('bar', $repository->tags('taylor')->rememberForever('foo', function (): string { return 'bar'; })); } - public function testForgetTriggersEvents() + public function testForgetTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(ForgettingKey::class, ['key' => 'baz'])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyForgotten::class, ['key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(ForgettingKey::class, ['storeName' => 'array', 'key' => 'baz'])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyForgotten::class, ['storeName' => 'array', 'key' => 'baz'])); $this->assertTrue($repository->forget('baz')); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(ForgettingKey::class, ['key' => 'baz', 'tags' => ['taylor']])); - $dispatcher->shouldReceive('dispatch')->once()->with($this->assertEventMatches(KeyForgotten::class, ['key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(ForgettingKey::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); + $dispatcher->expects('dispatch')->with($this->assertEventMatches(KeyForgotten::class, ['storeName' => 'array', 'key' => 'baz', 'tags' => ['taylor']])); $this->assertTrue($repository->tags('taylor')->forget('baz')); } - public function testForgetDoesTriggerFailedEventOnFailure() + public function testForgetDoesTriggerFailedEventOnFailure(): void { $dispatcher = $this->getDispatcher(); $store = m::mock(Store::class); - $store->shouldReceive('forget')->andReturn(false); + $store->expects('forget')->andReturn(false); $repository = new Repository($store); $repository->setEventDispatcher($dispatcher); @@ -529,7 +546,7 @@ public function testForgetDoesTriggerFailedEventOnFailure() $this->assertFalse($repository->forget('baz')); } - public function testFlushTriggersEvents() + public function testFlushTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); @@ -584,7 +601,7 @@ public function testFlushDispatchesFailureEventWithExactException(): void $this->assertSame($exception, $events[1]->exception); } - public function testFlushLocksTriggersEvents() + public function testFlushLocksTriggersEvents(): void { $dispatcher = $this->getDispatcher(); $repository = $this->getRepository($dispatcher); @@ -627,13 +644,13 @@ public function testFlushLocksDispatchesFailureEventWithExactException(): void $this->assertSame($exception, $events[1]->exception); } - public function testFlushFailureDoesDispatchEvent() + public function testFlushFailureDoesDispatchEvent(): void { $dispatcher = $this->getDispatcher(); // Create a store that fails to flush $failingStore = m::mock(Store::class); - $failingStore->shouldReceive('flush')->andReturn(false); + $failingStore->expects('flush')->andReturn(false); $repository = new Repository($failingStore, ['store' => 'array']); $repository->setEventDispatcher($dispatcher); @@ -653,14 +670,14 @@ public function testFlushFailureDoesDispatchEvent() $this->assertFalse($repository->clear()); } - public function testFlushLocksFailureDoesDispatchEvent() + public function testFlushLocksFailureDoesDispatchEvent(): void { $dispatcher = $this->getDispatcher(); // Create a store that fails to flush locks $failingStore = m::mock(ArrayStore::class); $failingStore->shouldReceive('supportsFlushingLocks')->andReturn(true); - $failingStore->shouldReceive('flushLocks')->andReturn(false); + $failingStore->expects('flushLocks')->andReturn(false); $repository = new Repository($failingStore, ['store' => 'array']); $repository->setEventDispatcher($dispatcher); @@ -680,9 +697,12 @@ public function testFlushLocksFailureDoesDispatchEvent() $this->assertFalse($repository->flushLocks()); } - protected function assertEventMatches($eventClass, $properties = []) + /** + * Match an event by its class and property values. + */ + protected function assertEventMatches(string $eventClass, array $properties = []): ClosureMatcher { - return m::on(function ($event) use ($eventClass, $properties) { + return m::on(function (mixed $event) use ($eventClass, $properties): bool { if (! $event instanceof $eventClass) { return false; } @@ -697,7 +717,10 @@ protected function assertEventMatches($eventClass, $properties = []) }); } - protected function getDispatcher() + /** + * Create a dispatcher with event listeners enabled. + */ + protected function getDispatcher(): Dispatcher&MockInterface { $dispatcher = m::mock(Dispatcher::class); $dispatcher->shouldReceive('hasListeners')->withAnyArgs()->andReturn(true); @@ -705,6 +728,9 @@ protected function getDispatcher() return $dispatcher; } + /** + * Create a dispatcher that records each emitted event. + */ protected function getCapturingDispatcher(array &$events): Dispatcher { $dispatcher = $this->getDispatcher(); @@ -715,7 +741,10 @@ protected function getCapturingDispatcher(array &$events): Dispatcher return $dispatcher; } - protected function getRepository($dispatcher) + /** + * Create a repository with ordinary and tagged cache entries. + */ + protected function getRepository(Dispatcher $dispatcher): Repository { $repository = new Repository(new ArrayStore, ['store' => 'array']); $repository->put('baz', 'qux', 99); diff --git a/tests/Cache/CacheFileStoreTest.php b/tests/Cache/CacheFileStoreTest.php index e2b5802493..bc653eace4 100644 --- a/tests/Cache/CacheFileStoreTest.php +++ b/tests/Cache/CacheFileStoreTest.php @@ -549,12 +549,11 @@ public function testForeversAreNotRemovedOnIncrement(): void public function testIncrementExpiredKeys(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $filePath = $this->getCachePath('foo'); $files = $this->mockFilesystem(); - $now = CarbonImmutable::now()->getTimestamp(); - $initialValue = ($now - 10) . serialize(77); + $initialValue = $now->subSeconds(10)->getTimestamp() . serialize(77); $valueAfterIncrement = '9999999999' . serialize(3); $store = new FileStore($files, __DIR__); @@ -628,10 +627,10 @@ public function testIncrementNonExistentKeys(): void public function testIncrementDoesNotExtendCacheLife(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $files = $this->mockFilesystem(); - $expiration = CarbonImmutable::now()->addSeconds(50)->getTimestamp(); + $expiration = $now->addSeconds(50)->getTimestamp(); $initialValue = $expiration . serialize(1); $valueAfterIncrement = $expiration . serialize(2); $store = new FileStore($files, __DIR__); diff --git a/tests/Cache/CacheManagerTest.php b/tests/Cache/CacheManagerTest.php index 12d36c3a5b..2145db0716 100644 --- a/tests/Cache/CacheManagerTest.php +++ b/tests/Cache/CacheManagerTest.php @@ -12,6 +12,7 @@ use Hypervel\Cache\NullStore; use Hypervel\Cache\RedisStore; use Hypervel\Cache\Repository; +use Hypervel\Cache\SessionStore; use Hypervel\Cache\StorageStore; use Hypervel\Cache\SwooleStore; use Hypervel\Cache\SwooleTableManager; @@ -24,6 +25,7 @@ use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Filesystem\Factory as FilesystemFactory; use Hypervel\Contracts\Redis\Factory as RedisFactory; +use Hypervel\Contracts\Session\Session; use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Events\Dispatcher as Event; use Hypervel\Filesystem\Filesystem; @@ -102,7 +104,7 @@ public function testInvokableObjectDriverClosure(): void $this->assertSame($driver, $manager->store(__CLASS__)); } - public function testCustomDriverOverridesInternalDrivers() + public function testCustomDriverOverridesInternalDrivers(): void { $userConfig = [ 'cache' => [ @@ -117,18 +119,18 @@ public function testCustomDriverOverridesInternalDrivers() $app = $this->getApp($userConfig); $cacheManager = new CacheManager($app); - /** @var CacheRepository|MockInterface */ + /** @var CacheRepository&MockInterface */ $repository = m::mock(CacheRepository::class); $repository->shouldReceive('get')->with('foo')->andReturn('bar'); - $cacheManager->extend('array', fn () => $repository); + $cacheManager->extend('array', fn (): CacheRepository => $repository); $driver = $cacheManager->store('my_store'); $this->assertSame('bar', $driver->get('foo')); } - public function testItCanBuildRepositories() + public function testItCanBuildRepositories(): void { $app = $this->getApp([]); $cacheManager = new CacheManager($app); @@ -282,7 +284,7 @@ public function testItCanCreateStorageDriver(): void $disk = new ArrayFilesystem; $filesystem = m::mock(FilesystemFactory::class); - $filesystem->shouldReceive('disk')->with('s3')->once()->andReturn($disk); + $filesystem->expects('disk')->with('s3')->andReturn($disk); $app = $this->getApp([ 'cache' => [ @@ -380,12 +382,12 @@ public function testCustomCreatorsStillOverrideMultiWordInternalDrivers(): void $cacheManager = new CacheManager($this->getApp($userConfig)); $repository = m::mock(CacheRepository::class); - $cacheManager->extend('worker-array', fn () => $repository); + $cacheManager->extend('worker-array', fn (): CacheRepository => $repository); $this->assertSame($repository, $cacheManager->store('worker')); } - public function testItMakesRepositoryWhenContainerHasNoDispatcher() + public function testItMakesRepositoryWhenContainerHasNoDispatcher(): void { $userConfig = [ 'cache' => [ @@ -399,6 +401,7 @@ public function testItMakesRepositoryWhenContainerHasNoDispatcher() ]; $app = $this->getApp($userConfig); + $this->assertFalse($app->bound(Dispatcher::class)); $cacheManager = new CacheManager($app); $repo = $cacheManager->repository($theStore = new NullStore, ['events' => true]); @@ -472,7 +475,7 @@ public function testRefreshEventDispatcherSkipsCustomRepositoryImplementations() $cacheManager = new CacheManager($app); $repository = m::mock(CacheRepository::class); $repository->shouldNotReceive('setEventDispatcher'); - $cacheManager->extend('custom', fn () => $repository); + $cacheManager->extend('custom', fn (): CacheRepository => $repository); $this->assertSame($repository, $cacheManager->store('custom')); @@ -481,7 +484,7 @@ public function testRefreshEventDispatcherSkipsCustomRepositoryImplementations() $this->assertSame($repository, $cacheManager->store('custom')); } - public function testItSetsDefaultDriverChangesGlobalConfig() + public function testItSetsDefaultDriverChangesGlobalConfig(): void { $userConfig = [ 'cache' => [ @@ -502,10 +505,10 @@ public function testItSetsDefaultDriverChangesGlobalConfig() $cacheManager->setDefaultDriver('><((((@>'); - $this->assertEquals('><((((@>', $app->make('config')->get('cache.default')); + $this->assertSame('><((((@>', $app->make('config')->get('cache.default')); } - public function testItPurgesMemoizedStoreObjects() + public function testItPurgesMemoizedStoreObjects(): void { $userConfig = [ 'cache' => [ @@ -538,7 +541,7 @@ public function testItPurgesMemoizedStoreObjects() $cacheManager->purge('store_1'); - // Make sure a now object is built this time. + // Make sure a new object is built this time. $repo6 = $cacheManager->store('store_1'); $this->assertNotSame($repo1, $repo6); @@ -547,19 +550,18 @@ public function testItPurgesMemoizedStoreObjects() $this->assertSame($repo3, $repo7); } - public function testForgetDriver() + public function testForgetDriver(): void { $cacheManager = m::mock(CacheManager::class) ->shouldAllowMockingProtectedMethods() ->makePartial(); - $cacheManager->shouldReceive('resolve') + $cacheManager->expects('resolve') ->withArgs(['array']) ->times(4) ->andReturn(m::mock(CacheRepository::class)); - $cacheManager->shouldReceive('getDefaultDriver') - ->once() + $cacheManager->expects('getDefaultDriver') ->andReturn('array'); foreach (['array', ['array'], null] as $option) { @@ -571,7 +573,7 @@ public function testForgetDriver() } } - public function testForgetDriverForgets() + public function testForgetDriverForgets(): void { $userConfig = [ 'cache' => [ @@ -588,8 +590,8 @@ public function testForgetDriverForgets() $count = 0; $cacheManager = new CacheManager($app); - $cacheManager->extend('forget', function () use (&$count) { - /** @var CacheRepository|MockInterface */ + $cacheManager->extend('forget', function () use (&$count): CacheRepository { + /** @var CacheRepository&MockInterface */ $repository = m::mock(CacheRepository::class); if ($count++ === 0) { @@ -638,7 +640,7 @@ public function testForgetDriverForgetsTheCallingCoroutinesMemoizedRepository(): // REMOVED: CacheApcStoreTest, CacheDynamoDbStoreTest, CacheMemcachedConnectorTest, // CacheMemcachedStoreTest and their integration tests; these drivers are unsupported. - public function testThrowExceptionWhenUnknownDriverIsUsed() + public function testThrowExceptionWhenUnknownDriverIsUsed(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Driver [unknown_taxi_driver] is not supported.'); @@ -660,7 +662,7 @@ public function testThrowExceptionWhenUnknownDriverIsUsed() $cacheManager->store('my_store'); } - public function testThrowExceptionWhenUnknownStoreIsUsed() + public function testThrowExceptionWhenUnknownStoreIsUsed(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cache store [alien_store] is not defined.'); @@ -757,7 +759,7 @@ public function testRedisDriverRejectsInvalidTagMode(): void $cacheManager->store('redis'); } - public function testSessionDriverResolvesSessionStore() + public function testSessionDriverResolvesSessionStore(): void { $userConfig = [ 'cache' => [ @@ -772,7 +774,7 @@ public function testSessionDriverResolvesSessionStore() $app = $this->getApp($userConfig); - $session = m::mock(\Hypervel\Contracts\Session\Session::class); + $session = m::mock(Session::class); $app->instance('session.store', $session); $cacheManager = new CacheManager($app); @@ -780,10 +782,10 @@ public function testSessionDriverResolvesSessionStore() $repository = $cacheManager->store('session'); $store = $repository->getStore(); - $this->assertInstanceOf(\Hypervel\Cache\SessionStore::class, $store); + $this->assertInstanceOf(SessionStore::class, $store); } - public function testSessionDriverThrowsWhenSessionNotAvailable() + public function testSessionDriverThrowsWhenSessionNotAvailable(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Session store requires session manager to be available in container.'); @@ -980,6 +982,9 @@ public function testSetDefaultDriverAcceptsEnum(): void $this->assertSame('array', $app->get('config')->get('cache.default')); } + /** + * Create a container with the given cache configuration. + */ protected function getApp(array $userConfig): Container { $app = new Container; @@ -989,6 +994,9 @@ protected function getApp(array $userConfig): Container return $app; } + /** + * Create a container with Redis collaborators. + */ protected function getAppWithRedis(array $userConfig): Container { $app = $this->getApp($userConfig); diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 7deef807e2..2bfc7e53e2 100644 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -148,11 +148,11 @@ public function testGetReturnsMultipleValuesFromCacheWhenGivenAnArrayWithDefault $this->assertEquals(['foo' => 'default', 'bar' => 'baz'], $repo->get(['foo' => 'default', 'bar'])); } - public function testGetReturnsMultipleValuesFromCacheWhenGivenAnArrayOfOneTwoThree() + public function testGetReturnsMultipleValuesFromCacheWhenGivenAnArrayOfOneTwoThree(): void { $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('many')->once()->with(['one', 'two', 'three'])->andReturn(['one' => null, 'two' => null, 'three' => null]); - $this->assertEquals(['one' => null, 'two' => null, 'three' => null], $repo->get(['one', 'two', 'three'])); + $repo->getStore()->expects('many')->with(['1', '2', '3'])->andReturn([1 => null, 2 => null, 3 => null]); + $this->assertEquals([1 => null, 2 => null, 3 => null], $repo->get([1, 2, 3])); } public function testDefaultValueIsReturned() @@ -1193,65 +1193,55 @@ public function testItThrowsExceptionWhenStoreDoesNotSupportFlushingLocks() $nonFlushableRepo->flushLocks(); } - public function testTouchWithNullTTLRemembersItemForever() + public function testTouchWithSecondsTtlCorrectlyProxiesToStore(): void { + $key = 'key'; + $ttl = 60; + $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('key')->andReturn('bar'); - $repo->getStore()->shouldReceive('forever')->once()->with('key', 'bar')->andReturn(true); - $this->assertTrue($repo->touch('key', null)); + $repo->getStore()->expects('touch')->with($key, $ttl)->andReturn(true); + $this->assertTrue($repo->touch($key, $ttl)); } - public function testTouchWithNullTtlPreservesCachedNullSentinel() + public function testTouchWithDatetimeTtlCorrectlyProxiesToStore(): void { - $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('key')->andReturn(NullSentinel::VALUE); - $repo->getStore()->shouldReceive('forever')->once()->with('key', NullSentinel::VALUE)->andReturn(true); + $key = 'key'; + $ttl = 60; - $this->assertTrue($repo->touch('key', null)); - } + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); - public function testTouchWithSecondsTtlCorrectlyProxiesToStore() - { $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('key')->andReturn('bar'); - $repo->getStore()->shouldReceive('touch')->once()->with('key', 60)->andReturn(true); - $this->assertTrue($repo->touch('key', 60)); + $repo->getStore()->expects('touch')->with($key, $ttl)->andReturn(true); + $this->assertTrue($repo->touch($key, $now->addSeconds($ttl))); } - public function testTouchWithSecondsTtlTreatsCachedNullSentinelAsHit() + public function testTouchWithDateIntervalTtlCorrectlyProxiesToStore(): void { - $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('key')->andReturn(NullSentinel::VALUE); - $repo->getStore()->shouldReceive('touch')->once()->with('key', 60)->andReturn(true); + $key = 'key'; + $ttl = 60; - $this->assertTrue($repo->touch('key', 60)); + $repo = $this->getRepository(); + $repo->getStore()->expects('touch')->with($key, $ttl)->andReturn(true); + $this->assertTrue($repo->touch($key, DateInterval::createFromDateString("{$ttl} seconds"))); } - public function testTouchWithEnumKeyProxiesResolvedKeyToStore() + public function testTouchWithDatetimeInPastOrZeroSecondsRemovesOldItem(): void { $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('foo')->andReturn('bar'); - $repo->getStore()->shouldReceive('touch')->once()->with('foo', 60)->andReturn(true); + $repo->getStore()->shouldReceive('touch')->never(); + $repo->getStore()->expects('forget')->times(2)->with('key')->andReturn(true); - $this->assertTrue($repo->touch(TestCacheKey::Foo, 60)); + $this->assertTrue($repo->touch('key', CarbonImmutable::now()->subMinute())); + $this->assertTrue($repo->touch('key', 0)); } - public function testTouchWithDatetimeTtlCorrectlyProxiesToStore() + public function testTouchWorksWithEnumKey(): void { - CarbonImmutable::setTestNow($now = CarbonImmutable::now()); - - $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('key')->andReturn('bar'); - $repo->getStore()->shouldReceive('touch')->once()->with('key', 60)->andReturn(true); - $this->assertTrue($repo->touch('key', $now->addSeconds(60))); - } + $ttl = 60; - public function testTouchWithDateIntervalTtlCorrectlyProxiesToStore() - { $repo = $this->getRepository(); - $repo->getStore()->shouldReceive('get')->with('key')->andReturn('bar'); - $repo->getStore()->shouldReceive('touch')->once()->with('key', 60)->andReturn(true); - $this->assertTrue($repo->touch('key', DateInterval::createFromDateString('60 seconds'))); + $repo->getStore()->expects('touch')->with('foo', $ttl)->andReturn(true); + $this->assertTrue($repo->touch(TestCacheKey::Foo, $ttl)); } public function testAtomicExecutesCallbackAndReturnsResult() @@ -1356,6 +1346,30 @@ public function testTaggedPutManyHandlesIntegerArrayKeys() $this->assertSame('string-value', $repo->get('a')); } + public function testManyDispatchesEventsForIntegerArrayKeys(): void + { + $repo = new Repository(new ArrayStore); + $repo->put('1', 'cached', 60); + + $captured = []; + $dispatcher = m::mock(Dispatcher::class); + $dispatcher->shouldReceive('hasListeners')->andReturnUsing( + static fn (string $event): bool => in_array($event, [CacheHit::class, CacheMissed::class], true) + ); + $dispatcher->shouldReceive('dispatch')->andReturnUsing(function (CacheHit|CacheMissed $event) use (&$captured): void { + $captured[] = $event; + }); + $repo->setEventDispatcher($dispatcher); + + $this->assertSame([1 => 'cached', 2 => null], $repo->many(['1', '2'])); + $this->assertCount(2, $captured); + $this->assertInstanceOf(CacheHit::class, $captured[0]); + $this->assertSame('1', $captured[0]->key); + $this->assertSame('cached', $captured[0]->value); + $this->assertInstanceOf(CacheMissed::class, $captured[1]); + $this->assertSame('2', $captured[1]->key); + } + public function testStringTypedGetter(): void { $repo = $this->getRepository(); diff --git a/tests/Cache/CacheSessionStoreTest.php b/tests/Cache/CacheSessionStoreTest.php index 93921b7a4b..0f9de97fdf 100755 --- a/tests/Cache/CacheSessionStoreTest.php +++ b/tests/Cache/CacheSessionStoreTest.php @@ -13,7 +13,7 @@ class CacheSessionStoreTest extends TestCase { - public function testItemsCanBeSetAndRetrieved() + public function testItemsCanBeSetAndRetrieved(): void { $store = new SessionStore(self::getSession()); $result = $store->put('foo', 'bar', 10); @@ -37,7 +37,7 @@ public function testDottedKeysAreStoredLiterallyAndIndependently(): void $this->assertSame('second', $store->get('form.value')); } - public function testCacheTtl() + public function testCacheTtl(): void { $store = new SessionStore(self::getSession()); @@ -51,7 +51,7 @@ public function testCacheTtl() $this->assertNull($store->get('hello')); } - public function testMultipleItemsCanBeSetAndRetrieved() + public function testMultipleItemsCanBeSetAndRetrieved(): void { $store = new SessionStore(self::getSession()); $result = $store->put('foo', 'bar', 10); @@ -69,36 +69,36 @@ public function testMultipleItemsCanBeSetAndRetrieved() ], $store->many(['foo', 'fizz', 'quz', 'norf'])); } - public function testItemsCanExpire() + public function testItemsCanExpire(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow($now->addSeconds(10)->addSecond()); $result = $store->get('foo'); $this->assertNull($result); } - public function testTouchExtendsTtl() + public function testTouchExtendsTtl(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); // Move time forward and touch to extend TTL - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(5)); + CarbonImmutable::setTestNow($now = $now->addSeconds(5)); $this->assertTrue($store->touch('foo', 60)); // Value should still exist past the original expiry - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)); + CarbonImmutable::setTestNow($now = $now->addSeconds(10)); $this->assertSame('bar', $store->get('foo')); // Value should expire after the new TTL - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(50)); + CarbonImmutable::setTestNow($now->addSeconds(50)); $this->assertNull($store->get('foo')); } @@ -115,7 +115,7 @@ public function testStoreItemForeverProperlyStoresInArray(): void $this->assertTrue($result); } - public function testValuesCanBeIncremented() + public function testValuesCanBeIncremented(): void { $store = new SessionStore(self::getSession()); $store->put('foo', 1, 10); @@ -143,7 +143,7 @@ public function testDottedKeysCanBeIncrementedWithoutChangingTheirExpiration(): $this->assertSame(['counter.value'], array_keys($store->all())); } - public function testValuesGetCastedByIncrementOrDecrement() + public function testValuesGetCastedByIncrementOrDecrement(): void { $store = new SessionStore(self::getSession()); $store->put('foo', '1', 10); @@ -157,7 +157,7 @@ public function testValuesGetCastedByIncrementOrDecrement() $this->assertEquals(0, $store->get('bar')); } - public function testIncrementNonNumericValues() + public function testIncrementNonNumericValues(): void { $store = new SessionStore(self::getSession()); $store->put('foo', 'I am string', 10); @@ -166,7 +166,7 @@ public function testIncrementNonNumericValues() $this->assertEquals(1, $store->get('foo')); } - public function testNonExistingKeysCanBeIncremented() + public function testNonExistingKeysCanBeIncremented(): void { $store = new SessionStore(self::getSession()); $result = $store->increment('foo'); @@ -174,24 +174,24 @@ public function testNonExistingKeysCanBeIncremented() $this->assertEquals(1, $store->get('foo')); // Will be there forever - CarbonImmutable::setTestNow(CarbonImmutable::now()->addYears(10)); + CarbonImmutable::setTestNow(CarbonImmutable::now()->addCentury()); $this->assertEquals(1, $store->get('foo')); } - public function testExpiredKeysAreIncrementedLikeNonExistingKeys() + public function testExpiredKeysAreIncrementedLikeNonExistingKeys(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 999, 10); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(10)->addSecond()); + CarbonImmutable::setTestNow($now->addSeconds(10)->addSecond()); $result = $store->increment('foo'); $this->assertEquals(1, $result); } - public function testValuesCanBeDecremented() + public function testValuesCanBeDecremented(): void { $store = new SessionStore(self::getSession()); $store->put('foo', 1, 10); @@ -204,7 +204,7 @@ public function testValuesCanBeDecremented() $this->assertEquals(-2, $store->get('foo')); } - public function testItemsCanBeRemoved() + public function testItemsCanBeRemoved(): void { $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); @@ -213,7 +213,7 @@ public function testItemsCanBeRemoved() $this->assertFalse($store->forget('foo')); } - public function testItemsCanBeFlushed() + public function testItemsCanBeFlushed(): void { $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); @@ -224,19 +224,19 @@ public function testItemsCanBeFlushed() $this->assertNull($store->get('baz')); } - public function testCacheKey() + public function testCacheKey(): void { $store = new SessionStore(self::getSession()); $this->assertEmpty($store->getPrefix()); } - public function testItemKey() + public function testItemKey(): void { $store = new SessionStore(self::getSession(), 'custom_prefix'); - $this->assertEquals('custom_prefix.foo', $store->itemKey('foo')); + $this->assertSame('custom_prefix.foo', $store->itemKey('foo')); } - public function testValuesAreStoredByReference() + public function testValuesAreStoredByReference(): void { $store = new SessionStore(self::getSession()); $object = new stdClass; @@ -251,19 +251,22 @@ public function testValuesAreStoredByReference() $this->assertTrue($retrievedObject->bar); } - public function testCanGetAll() + public function testCanGetAll(): void { - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $store = new SessionStore(self::getSession()); $store->put('foo', 'bar', 10); $this->assertEquals([ - 'foo' => ['value' => 'bar', 'expiresAt' => CarbonImmutable::now()->addSeconds(10)->getPreciseTimestamp(3) / 1000], + 'foo' => ['value' => 'bar', 'expiresAt' => $now->addSeconds(10)->getPreciseTimestamp(3) / 1000], ], $store->all()); } - protected static function getSession() + /** + * Create the session store. + */ + protected static function getSession(): Store { return new Store( name: 'name', diff --git a/tests/Cache/CacheStorageStoreTest.php b/tests/Cache/CacheStorageStoreTest.php index 4dfe0070fc..0fcec2e784 100644 --- a/tests/Cache/CacheStorageStoreTest.php +++ b/tests/Cache/CacheStorageStoreTest.php @@ -243,12 +243,13 @@ public function payload(string $key): array public function testTouchUpdatesExpiration(): void { - CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + // Whole seconds make the final check reach the original rounded expiry. + CarbonImmutable::setTestNow($now = CarbonImmutable::now()->startOfSecond()); $store = new StorageStore(new ArrayFilesystem, 'cache'); $store->put('foo', 'bar', 2); - CarbonImmutable::setTestNow($now->addSecond()); + CarbonImmutable::setTestNow($now = $now->addSecond()); $this->assertTrue($store->touch('foo', 60)); diff --git a/tests/Cache/Redis/AllTaggedCacheTest.php b/tests/Cache/Redis/AllTaggedCacheTest.php index ec9dbc379a..5fc80ba160 100644 --- a/tests/Cache/Redis/AllTaggedCacheTest.php +++ b/tests/Cache/Redis/AllTaggedCacheTest.php @@ -26,6 +26,7 @@ use Hypervel\Cache\RedisStore; use Hypervel\Cache\Repository; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Support\CarbonImmutable; use Mockery as m; use RuntimeException; use Swoole\Coroutine\CanceledException; @@ -415,10 +416,6 @@ public function testTouchUpdatesKeyAndTagScores(): void $key = hash('xxh128', '_all:tag:users:entries') . ':name'; - $connection->shouldReceive('get') - ->once() - ->with("prefix:{$key}") - ->andReturn(serialize('John')); $connection->shouldReceive('evalWithShaCache') ->once() ->withArgs(function (string $script, array $keys, array $args) use ($key): bool { @@ -436,98 +433,20 @@ public function testTouchUpdatesKeyAndTagScores(): void $this->assertTrue($result); } - public function testTouchUpdatesCachedNullKeyAndTagScores(): void - { - $connection = $this->mockConnection(); - - $key = hash('xxh128', '_all:tag:users:entries') . ':name'; - - $connection->shouldReceive('get') - ->once() - ->with("prefix:{$key}") - ->andReturn(serialize(NullSentinel::VALUE)); - $connection->shouldReceive('evalWithShaCache') - ->once() - ->withArgs(function (string $script, array $keys, array $args) use ($key): bool { - $this->assertSame(["prefix:{$key}", 'prefix:_all:tag:users:entries'], $keys); - $this->assertSame(60, $args[0]); - $this->assertSame($key, $args[2]); - - return true; - }) - ->andReturn(true); - - $store = $this->createStore($connection); - $result = $store->tags(['users'])->touch('name', 60); - - $this->assertTrue($result); - } - - public function testTouchWithNullTtlStoresItemForeverWithTags(): void - { - $connection = $this->mockConnection(); - - $key = hash('xxh128', '_all:tag:users:entries') . ':name'; - - $connection->shouldReceive('get') - ->once() - ->with("prefix:{$key}") - ->andReturn(serialize('John')); - $connection->shouldReceive('pipeline')->once()->andReturn($connection); - $connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:users:entries', -1, $key)->andReturn($connection); - $connection->shouldReceive('set')->once()->with("prefix:{$key}", serialize('John'))->andReturn($connection); - $connection->shouldReceive('exec')->once()->andReturn([true, 1]); - - $store = $this->createStore($connection); - $result = $store->tags(['users'])->touch('name', null); - - $this->assertTrue($result); - } - - public function testTouchWithNullTtlPreservesCachedNullSentinel(): void + public function testTouchWithDatetimeInPastOrZeroSecondsRemovesOldItem(): void { $connection = $this->mockConnection(); $key = hash('xxh128', '_all:tag:users:entries') . ':name'; - $connection->shouldReceive('get') - ->once() - ->with("prefix:{$key}") - ->andReturn(serialize(NullSentinel::VALUE)); - $connection->shouldReceive('pipeline')->once()->andReturn($connection); - $connection->shouldReceive('zadd')->once()->with('prefix:_all:tag:users:entries', -1, $key)->andReturn($connection); - $connection->shouldReceive('set') - ->once() - ->with( - "prefix:{$key}", - m::on(fn (string $serialized): bool => unserialize($serialized) === NullSentinel::VALUE) - ) - ->andReturn($connection); - $connection->shouldReceive('exec')->once()->andReturn([true, 1]); - - $store = $this->createStore($connection); - $result = $store->tags(['users'])->touch('name', null); - - $this->assertTrue($result); - } - - public function testTouchReturnsFalseForMissingKey(): void - { - $connection = $this->mockConnection(); - - $key = hash('xxh128', '_all:tag:users:entries') . ':name'; - - $connection->shouldReceive('get') - ->once() - ->with("prefix:{$key}") - ->andReturnNull(); $connection->shouldNotReceive('evalWithShaCache'); - $connection->shouldNotReceive('pipeline'); + $connection->expects('del')->twice()->with("prefix:{$key}")->andReturn(1); $store = $this->createStore($connection); - $result = $store->tags(['users'])->touch('name', 60); + $tagged = $store->tags(['users']); - $this->assertFalse($result); + $this->assertTrue($tagged->touch('name', CarbonImmutable::now()->subMinute())); + $this->assertTrue($tagged->touch('name', 0)); } public function testIncrementWithCustomValue(): void diff --git a/tests/Concurrency/ConcurrencyTest.php b/tests/Concurrency/ConcurrencyTest.php index eb805e850d..7bc299fb85 100644 --- a/tests/Concurrency/ConcurrencyTest.php +++ b/tests/Concurrency/ConcurrencyTest.php @@ -14,6 +14,7 @@ use Hypervel\Coroutine\Coroutine; use Hypervel\Engine\Channel; use Hypervel\Process\Factory as ProcessFactory; +use Hypervel\Process\FakeProcessResult; use Hypervel\Process\PendingProcess; use Hypervel\Support\Defer\DeferredCallback; use Hypervel\Support\Defer\DeferredCallbackCollection; @@ -22,14 +23,22 @@ use Hypervel\Testbench\Attributes\UsesVendor; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Concurrency\Fixtures\ConcurrentProcessExceptionFixtures; +use Hypervel\Tests\Concurrency\Fixtures\ExceptionWithFalseyParam; +use Hypervel\Tests\Concurrency\Fixtures\ExceptionWithoutParam; +use Hypervel\Tests\Concurrency\Fixtures\ExceptionWithParam; use Hypervel\Tests\Context\Fixtures\ThrowingReplicableContext; +use PHPUnit\Framework\Attributes\DataProvider; use RuntimeException; use Swoole\Coroutine as SwooleCoroutine; +use Swoole\Process as SwooleProcess; class ConcurrencyTest extends TestCase { private CoroutineDriver $coroutineDriver; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); @@ -37,22 +46,22 @@ protected function setUp(): void $this->coroutineDriver = new CoroutineDriver; } - public function testRunReturnsConcurrentResults() + public function testRunReturnsConcurrentResults(): void { [$first, $second] = $this->coroutineDriver->run([ - fn () => 1 + 1, - fn () => 2 + 2, + fn (): int => 1 + 1, + fn (): int => 2 + 2, ]); $this->assertSame(2, $first); $this->assertSame(4, $second); } - public function testRunPreservesStringKeys() + public function testRunPreservesStringKeys(): void { $results = $this->coroutineDriver->run([ - 'first' => fn () => 1 + 1, - 'second' => fn () => 2 + 2, + 'first' => fn (): int => 1 + 1, + 'second' => fn (): int => 2 + 2, ]); $this->assertArrayHasKey('first', $results); @@ -61,18 +70,18 @@ public function testRunPreservesStringKeys() $this->assertSame(4, $results['second']); } - public function testRunPreservesOrderRegardlessOfCompletionTime() + public function testRunPreservesOrderRegardlessOfCompletionTime(): void { [$first, $second, $third] = $this->coroutineDriver->run([ - function () { + function (): string { usleep(50000); return 'first'; }, - function () { + function (): string { usleep(25000); return 'second'; }, - function () { + function (): string { return 'third'; }, ]); @@ -82,22 +91,22 @@ function () { $this->assertSame('third', $third); } - public function testRunRethrowsExceptions() + public function testRunRethrowsExceptions(): void { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('something went wrong'); + $this->expectExceptionMessageIsOrContains('something went wrong'); $this->coroutineDriver->run([ - fn () => throw new RuntimeException('something went wrong'), - fn () => 'ok', + fn (): never => throw new RuntimeException('something went wrong'), + fn (): string => 'ok', ]); } - public function testRunRethrowsCustomExceptionWithOriginalMessage() + public function testRunRethrowsCustomExceptionWithOriginalMessage(): void { try { $this->coroutineDriver->run([ - fn () => throw new ConcurrencyTestException('https://api.example.com', 400), + fn (): never => throw new ConcurrencyTestException('https://api.example.com', 400), ]); $this->fail('Expected exception was not thrown'); @@ -108,18 +117,18 @@ public function testRunRethrowsCustomExceptionWithOriginalMessage() } } - public function testRunRethrowsExceptionFromEarliestInputPositionWhenMultipleTasksFail() + public function testRunRethrowsExceptionFromEarliestInputPositionWhenMultipleTasksFail(): void { $caught = null; try { $this->coroutineDriver->run([ - function () { + function (): never { // Task 0: fails later (50ms). usleep(50000); throw new RuntimeException('first in input'); }, - function () { + function (): never { // Task 1: fails immediately, before task 0. throw new RuntimeException('second in input'); }, @@ -132,48 +141,48 @@ function () { $this->assertSame('first in input', $caught->getMessage()); } - public function testRunWithEmptyArrayReturnsEmptyArray() + public function testRunWithEmptyArrayReturnsEmptyArray(): void { $results = $this->coroutineDriver->run([]); $this->assertSame([], $results); } - public function testRunWithSingleTask() + public function testRunWithSingleTask(): void { $results = $this->coroutineDriver->run([ - fn () => 42, + fn (): int => 42, ]); $this->assertSame([42], $results); } - public function testRunWithSingleClosure() + public function testRunWithSingleClosure(): void { - $results = $this->coroutineDriver->run(fn () => 42); + $results = $this->coroutineDriver->run(fn (): int => 42); $this->assertSame([42], $results); } - public function testRunExecutesConcurrently() + public function testRunExecutesConcurrently(): void { $results = $this->coroutineDriver->run([ - fn () => Coroutine::id(), - fn () => Coroutine::id(), - fn () => Coroutine::id(), + fn (): int => Coroutine::id(), + fn (): int => Coroutine::id(), + fn (): int => Coroutine::id(), ]); // Each task runs in its own coroutine, so IDs must be unique. $this->assertCount(3, array_unique($results)); } - public function testRunPropagatesParentContext() + public function testRunPropagatesParentContext(): void { CoroutineContext::set('test_key', 'test_value'); $results = $this->coroutineDriver->run([ - fn () => CoroutineContext::get('test_key'), - fn () => CoroutineContext::get('test_key'), + fn (): mixed => CoroutineContext::get('test_key'), + fn (): mixed => CoroutineContext::get('test_key'), ]); $this->assertSame(['test_value', 'test_value'], $results); @@ -205,10 +214,10 @@ public function testRunSurfacesContextReplicationFailureWithoutStrandingTheWaitG $this->assertSame('Unable to replicate context.', $outcome->getMessage()); } - public function testRunChildContextDoesNotLeakToParent() + public function testRunChildContextDoesNotLeakToParent(): void { $this->coroutineDriver->run([ - function () { + function (): void { CoroutineContext::set('child_key', 'child_value'); }, ]); @@ -216,15 +225,15 @@ function () { $this->assertNull(CoroutineContext::get('child_key')); } - public function testRunChildContextDoesNotLeakBetweenTasks() + public function testRunChildContextDoesNotLeakBetweenTasks(): void { $results = $this->coroutineDriver->run([ - function () { + function (): mixed { CoroutineContext::set('task_key', 'from_task_1'); usleep(10000); return CoroutineContext::get('task_key'); }, - function () { + function (): mixed { usleep(5000); return CoroutineContext::get('task_key'); }, @@ -234,28 +243,28 @@ function () { $this->assertNull($results[1]); } - public function testDeferReturnsDeferredCallback() + public function testDeferReturnsDeferredCallback(): void { $collection = new DeferredCallbackCollection; - $this->app->scoped(DeferredCallbackCollection::class, fn () => $collection); + $this->app->scoped(DeferredCallbackCollection::class, fn (): DeferredCallbackCollection => $collection); $result = $this->coroutineDriver->defer([ - fn () => 1 + 1, + fn (): int => 1 + 1, ]); $this->assertInstanceOf(DeferredCallback::class, $result); $this->assertCount(1, $collection); } - public function testDeferExecutesTasksWhenInvoked() + public function testDeferExecutesTasksWhenInvoked(): void { $collection = new DeferredCallbackCollection; - $this->app->scoped(DeferredCallbackCollection::class, fn () => $collection); + $this->app->scoped(DeferredCallbackCollection::class, fn (): DeferredCallbackCollection => $collection); $executed = false; $this->coroutineDriver->defer([ - function () use (&$executed) { + function () use (&$executed): void { $executed = true; }, ]); @@ -269,16 +278,16 @@ function () use (&$executed) { $this->assertTrue($executed); } - public function testDeferPropagatesContext() + public function testDeferPropagatesContext(): void { $collection = new DeferredCallbackCollection; - $this->app->scoped(DeferredCallbackCollection::class, fn () => $collection); + $this->app->scoped(DeferredCallbackCollection::class, fn (): DeferredCallbackCollection => $collection); CoroutineContext::set('defer_key', 'defer_value'); $capturedValue = null; $this->coroutineDriver->defer([ - function () use (&$capturedValue) { + function () use (&$capturedValue): void { $capturedValue = CoroutineContext::get('defer_key'); }, ]); @@ -288,36 +297,36 @@ function () use (&$capturedValue) { $this->assertSame('defer_value', $capturedValue); } - public function testFacadeRun() + public function testFacadeRun(): void { [$first, $second] = ConcurrencyFacade::run([ - fn () => 1 + 1, - fn () => 2 + 2, + fn (): int => 1 + 1, + fn (): int => 2 + 2, ]); $this->assertSame(2, $first); $this->assertSame(4, $second); } - public function testFacadeDefer() + public function testFacadeDefer(): void { $collection = new DeferredCallbackCollection; - $this->app->scoped(DeferredCallbackCollection::class, fn () => $collection); + $this->app->scoped(DeferredCallbackCollection::class, fn (): DeferredCallbackCollection => $collection); $result = ConcurrencyFacade::defer([ - fn () => 1 + 1, + fn (): int => 1 + 1, ]); $this->assertInstanceOf(DeferredCallback::class, $result); $this->assertCount(1, $collection); } - public function testFacadeResolvesManager() + public function testFacadeResolvesManager(): void { $this->assertInstanceOf(ConcurrencyManager::class, ConcurrencyFacade::getFacadeRoot()); } - public function testManagerDefaultDriverIsCoroutine() + public function testManagerDefaultDriverIsCoroutine(): void { $manager = $this->app->make(ConcurrencyManager::class); @@ -337,21 +346,21 @@ public function testChangingDefaultDriverPreservesDriverConfiguration(): void $this->assertSame($driverConfig, $manager->getInstanceConfig('sync')); } - public function testManagerResolvesCoroutineDriver() + public function testManagerResolvesCoroutineDriver(): void { $manager = $this->app->make(ConcurrencyManager::class); $this->assertInstanceOf(CoroutineDriver::class, $manager->driver('coroutine')); } - public function testManagerResolvesProcessDriver() + public function testManagerResolvesProcessDriver(): void { $manager = $this->app->make(ConcurrencyManager::class); $this->assertInstanceOf(ProcessDriver::class, $manager->driver('process')); } - public function testManagerResolvesSyncDriver() + public function testManagerResolvesSyncDriver(): void { $manager = $this->app->make(ConcurrencyManager::class); @@ -362,9 +371,9 @@ public function testManagerResolvesEnumDriverIdentifiers(): void { $manager = $this->app->make(ConcurrencyManager::class); - $manager->extend('Primary', fn () => new SyncDriver); - $manager->extend('1', fn () => new SyncDriver); - $manager->extend('0', fn () => new SyncDriver); + $manager->extend('Primary', fn (): SyncDriver => new SyncDriver); + $manager->extend('1', fn (): SyncDriver => new SyncDriver); + $manager->extend('0', fn (): SyncDriver => new SyncDriver); $this->assertSame($manager->driver('Primary'), $manager->driver(ConcurrencyUnitIdentifier::Primary)); $this->assertSame($manager->driver('1'), $manager->driver(ConcurrencyIntegerIdentifier::Primary)); @@ -374,7 +383,7 @@ public function testManagerResolvesEnumDriverIdentifiers(): void $this->assertInstanceOf(SyncDriver::class, $manager->driver(ConcurrencyIntegerIdentifier::Zero)); } - public function testManagerCachesDriverInstances() + public function testManagerCachesDriverInstances(): void { $manager = $this->app->make(ConcurrencyManager::class); @@ -384,26 +393,26 @@ public function testManagerCachesDriverInstances() $this->assertSame($first, $second); } - public function testSyncDriverRunsSequentially() + public function testSyncDriverRunsSequentially(): void { $driver = new SyncDriver; [$first, $second] = $driver->run([ - fn () => 1 + 1, - fn () => 2 + 2, + fn (): int => 1 + 1, + fn (): int => 2 + 2, ]); $this->assertSame(2, $first); $this->assertSame(4, $second); } - public function testSyncDriverPreservesStringKeys() + public function testSyncDriverPreservesStringKeys(): void { $driver = new SyncDriver; $results = $driver->run([ - 'first' => fn () => 1 + 1, - 'second' => fn () => 2 + 2, + 'first' => fn (): int => 1 + 1, + 'second' => fn (): int => 2 + 2, ]); $this->assertArrayHasKey('first', $results); @@ -412,56 +421,56 @@ public function testSyncDriverPreservesStringKeys() $this->assertSame(4, $results['second']); } - public function testSyncDriverDefer() + public function testSyncDriverDefer(): void { $collection = new DeferredCallbackCollection; - $this->app->scoped(DeferredCallbackCollection::class, fn () => $collection); + $this->app->scoped(DeferredCallbackCollection::class, fn (): DeferredCallbackCollection => $collection); $driver = new SyncDriver; - $result = $driver->defer([fn () => 1 + 1]); + $result = $driver->defer([fn (): int => 1 + 1]); $this->assertInstanceOf(DeferredCallback::class, $result); $this->assertCount(1, $collection); } - public function testProcessDriverRunReturnsResults() + public function testProcessDriverRunReturnsResults(): void { $factory = $this->app->make(ProcessFactory::class); - $factory->fake(fn () => $factory->result( + $factory->fake(fn (): FakeProcessResult => $factory->result( output: json_encode(['successful' => true, 'result' => base64_encode(serialize('hello'))]) )); $driver = new ProcessDriver($factory); - $results = $driver->run([fn () => 'hello']); + $results = $driver->run([fn (): string => 'hello']); $this->assertSame(['hello'], array_values($results)); } - public function testProcessDriverUsesInvokeSerializedClosureCommand() + public function testProcessDriverUsesInvokeSerializedClosureCommand(): void { $factory = $this->app->make(ProcessFactory::class); - $factory->fake(fn () => $factory->result( + $factory->fake(fn (): FakeProcessResult => $factory->result( output: json_encode(['successful' => true, 'result' => base64_encode(serialize(null))]) )); $driver = new ProcessDriver($factory); - $driver->run([fn () => null]); + $driver->run([fn (): null => null]); - $factory->assertRan(fn ($process) => str_contains($process->command, 'invoke-serialized-closure')); + $factory->assertRan(fn (PendingProcess $process): bool => str_contains($process->command, 'invoke-serialized-closure')); } - public function testProcessDriverSetsEnvironmentVariable() + public function testProcessDriverSetsEnvironmentVariable(): void { $factory = $this->app->make(ProcessFactory::class); - $factory->fake(fn () => $factory->result( + $factory->fake(fn (): FakeProcessResult => $factory->result( output: json_encode(['successful' => true, 'result' => base64_encode(serialize(null))]) )); $driver = new ProcessDriver($factory); - $driver->run([fn () => null]); + $driver->run([fn (): null => null]); - $factory->assertRan(function ($process) { + $factory->assertRan(function (PendingProcess $process): bool { return isset($process->environment['HYPERVEL_INVOKABLE_CLOSURE']) && $process->environment['HYPERVEL_INVOKABLE_CLOSURE'] !== ''; }); @@ -483,7 +492,7 @@ public function testProcessDriverPreservesPublicFalseyExceptionParameters(): voi $caught = null; try { - $driver->run(static fn () => null); + $driver->run(static fn (): null => null); } catch (Exception $exception) { $caught = $exception; } @@ -499,16 +508,145 @@ public function testProcessDriverPreservesPublicFalseyExceptionParameters(): voi public function testProcessDriverReportsFailedChildProcessesBeforeDecoding(): void { $factory = $this->app->make(ProcessFactory::class); - $factory->fake(fn () => $factory->result( + $factory->fake(fn (): FakeProcessResult => $factory->result( errorOutput: 'child failed', exitCode: 5, )); $driver = new ProcessDriver($factory); $this->expectException(Exception::class); - $this->expectExceptionMessage('Concurrent process failed with exit code [5]. Message: child failed'); + $this->expectExceptionMessageIsOrContains('Concurrent process failed with exit code [5]. Message: child failed'); + + $driver->run(static fn (): null => null); + } + + #[UsesVendor] + public function testRunHandlerProcessErrorCode(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessageIsOrContains('Concurrent process failed with exit code [143].'); + + $processDriver = new ProcessDriver($this->app->make(ProcessFactory::class)); + $processDriver->run([ + // exit() throws inside a coroutine, so terminate the child process directly. + fn (): bool => SwooleProcess::kill(getmypid()), + ]); + } + + #[UsesVendor] + public function testOutputIsMappedToArrayInput(): void + { + $input = [ + 'first' => fn (): int => 1 + 1, + 'second' => fn (): int => 2 + 2, + ]; + + $processOutput = ConcurrencyFacade::driver('process')->run($input); + + $this->assertIsArray($processOutput); + $this->assertArrayHasKey('first', $processOutput); + $this->assertArrayHasKey('second', $processOutput); + + $syncOutput = ConcurrencyFacade::driver('sync')->run($input); + + $this->assertIsArray($syncOutput); + $this->assertArrayHasKey('first', $syncOutput); + $this->assertArrayHasKey('second', $syncOutput); + } + + public function testProcessDriverRunMayUseCustomTimeout(): void + { + $factory = $this->app->make(ProcessFactory::class); + + $factory->fake(fn (): FakeProcessResult => $factory->result(output: json_encode([ + 'successful' => true, + 'result' => base64_encode(serialize('result')), + ]))); + + $result = (new ProcessDriver($factory))->run([ + fn (): string => 'result', + ], timeout: 120); + + $this->assertSame(['result'], $result); + + $factory->assertRan(function (PendingProcess $process): bool { + return $process->timeout === 120; + }); + } + + public function testDriverCanBeResolvedUsingBackedEnum(): void + { + $this->assertInstanceOf( + SyncDriver::class, + ConcurrencyFacade::driver(ConcurrencyDriverEnum::Sync), + ); + } + + #[UsesVendor] + public function testRunHandlerProcessErrorWithDefaultExceptionWithoutParam(): void + { + $this->expectExceptionObject(new Exception('This is a different exception')); + + ConcurrencyFacade::driver('process')->run([ + fn (): never => throw new Exception( + 'This is a different exception', + ), + ]); + } + + #[UsesVendor] + public function testRunHandlerProcessErrorWithCustomExceptionWithoutParam(): void + { + $this->expectExceptionObject(new ExceptionWithoutParam('Test')); + ConcurrencyFacade::driver('process')->run([ + fn (): never => throw new ExceptionWithoutParam('Test'), + ]); + } + + #[UsesVendor] + public function testRunHandlerProcessErrorWithCustomExceptionWithParam(): void + { + $this->expectException(ExceptionWithParam::class); + $this->expectExceptionMessageIsOrContains('API request to https://api.example.com failed with status 400 Bad Request'); + ConcurrencyFacade::driver('process')->run([ + fn (): never => throw new ExceptionWithParam( + 'https://api.example.com', + 400, + 'Bad Request', + 'Invalid payload' + ), + ]); + } + + #[UsesVendor] + #[DataProvider('falseyExceptionParameters')] + public function testRunHandlerProcessErrorWithFalseyParam(int|bool|string $value): void + { + try { + ConcurrencyFacade::driver('process')->run([ + fn (): never => throw new ExceptionWithFalseyParam($value), + ]); + } catch (ExceptionWithFalseyParam $e) { + $this->assertSame($value, $e->value); + + return; + } + + $this->fail('The expected exception was not thrown.'); + } - $driver->run(static fn () => null); + /** + * Get falsey constructor parameters. + * + * @return array + */ + public static function falseyExceptionParameters(): array + { + return [ + 'zero' => [0], + 'false' => [false], + 'empty string' => [''], + ]; } #[UsesVendor] @@ -540,6 +678,46 @@ public function testContextIsPropagatedToDeferredConcurrentProcesses(): void $factory->assertRan(static fn (PendingProcess $process): bool => ($process->environment['__HYPERVEL_CONTEXT'] ?? null) === base64_encode(serialize(Context::dehydrate()))); } + #[UsesVendor] + #[DataProvider('getConcurrencyDrivers')] + public function testRunPreservesCallbackOrder(string $driver): void + { + [$first, $second, $third] = ConcurrencyFacade::driver($driver)->run([ + function (): string { + usleep(1000000); + + return 'first'; + }, + function (): string { + usleep(500000); + + return 'second'; + }, + function (): string { + usleep(200000); + + return 'third'; + }, + ]); + + $this->assertSame('first', $first); + $this->assertSame('second', $second); + $this->assertSame('third', $third); + } + + /** + * Get the concurrency drivers. + * + * @return array + */ + public static function getConcurrencyDrivers(): array + { + return [ + ['sync'], + ['process'], + ]; + } + #[UsesVendor] public function testBinaryContextIsPropagatedToConcurrentProcesses(): void { @@ -556,7 +734,7 @@ public function testBinaryContextIsPropagatedToConcurrentProcesses(): void public function testProcessDriverAppliesCustomTimeouts(): void { $factory = $this->app->make(ProcessFactory::class); - $factory->fake(fn () => $factory->result( + $factory->fake(fn (): FakeProcessResult => $factory->result( output: json_encode([ 'successful' => true, 'result' => base64_encode(serialize('result')), @@ -566,21 +744,21 @@ public function testProcessDriverAppliesCustomTimeouts(): void $driver = new ProcessDriver($factory); $this->assertSame(['result'], $driver->run( - static fn () => 'result', + static fn (): string => 'result', timeout: CarbonInterval::seconds(120), )); - $factory->assertRan(fn ($process) => $process->timeout === 120); + $factory->assertRan(fn (PendingProcess $process): bool => $process->timeout === 120); } public function testCoroutineAndSyncDriversAcceptProcessOnlyTimeouts(): void { $this->assertSame(['coroutine'], $this->coroutineDriver->run( - static fn () => 'coroutine', + static fn (): string => 'coroutine', timeout: 1, )); $this->assertSame(['sync'], (new SyncDriver)->run( - static fn () => 'sync', + static fn (): string => 'sync', timeout: 1, )); } @@ -593,7 +771,7 @@ public function testCoroutineAndSyncDriversAcceptProcessOnlyTimeouts(): void private function processDriverFor(array $payload): ProcessDriver { $factory = $this->app->make(ProcessFactory::class); - $factory->fake(fn () => $factory->result( + $factory->fake(fn (): FakeProcessResult => $factory->result( output: json_encode($payload, JSON_THROW_ON_ERROR) )); @@ -603,6 +781,9 @@ private function processDriverFor(array $payload): ProcessDriver class ConcurrencyTestException extends Exception { + /** + * Create an exception for the failed request. + */ public function __construct( public readonly string $uri, public readonly int $statusCode, @@ -621,3 +802,8 @@ enum ConcurrencyIntegerIdentifier: int case Primary = 1; case Zero = 0; } + +enum ConcurrencyDriverEnum: string +{ + case Sync = 'sync'; +} diff --git a/tests/Concurrency/Fixtures/ExceptionWithFalseyParam.php b/tests/Concurrency/Fixtures/ExceptionWithFalseyParam.php new file mode 100644 index 0000000000..8dab5d0c78 --- /dev/null +++ b/tests/Concurrency/Fixtures/ExceptionWithFalseyParam.php @@ -0,0 +1,18 @@ +|string $responseBody + */ + public function __construct( + public string $uri, + public int $statusCode, + public string $reason, + public string|array $responseBody = '', + ) { + parent::__construct("API request to {$uri} failed with status {$statusCode} {$reason}"); + } +} diff --git a/tests/Concurrency/Fixtures/ExceptionWithoutParam.php b/tests/Concurrency/Fixtures/ExceptionWithoutParam.php new file mode 100644 index 0000000000..aa4757ac04 --- /dev/null +++ b/tests/Concurrency/Fixtures/ExceptionWithoutParam.php @@ -0,0 +1,11 @@ +createStub(Application::class); - $dispatcher = $this->createStub(Dispatcher::class); + $dispatcher = new EventsDispatcher($container); return new ConsoleApplication($container, $dispatcher, '1.0'); } @@ -47,7 +52,7 @@ private function getCommandMap(ConsoleApplication $app): array // extractCommandName (tested indirectly through resolve) // --------------------------------------------------------------- - public function testResolveLazilyRegistersCommandWithAsCommandAttribute() + public function testResolveLazilyRegistersCommandWithAsCommandAttribute(): void { $app = $this->createApp(); @@ -57,7 +62,7 @@ public function testResolveLazilyRegistersCommandWithAsCommandAttribute() $this->assertArrayHasKey('test:attributed', $this->getCommandMap($app)); } - public function testResolveLazilyRegistersCommandWithSignatureProperty() + public function testResolveLazilyRegistersCommandWithSignatureProperty(): void { $app = $this->createApp(); @@ -67,7 +72,7 @@ public function testResolveLazilyRegistersCommandWithSignatureProperty() $this->assertArrayHasKey('test:signed', $this->getCommandMap($app)); } - public function testResolveLazilyRegistersCommandWithSignatureAttribute() + public function testResolveLazilyRegistersCommandWithSignatureAttribute(): void { $app = $this->createApp(); @@ -77,7 +82,7 @@ public function testResolveLazilyRegistersCommandWithSignatureAttribute() $this->assertArrayHasKey('test:signed-attribute', $this->getCommandMap($app)); } - public function testResolveLazilyRegistersCommandWithNameProperty() + public function testResolveLazilyRegistersCommandWithNameProperty(): void { $app = $this->createApp(); @@ -87,7 +92,7 @@ public function testResolveLazilyRegistersCommandWithNameProperty() $this->assertArrayHasKey('test:named', $this->getCommandMap($app)); } - public function testResolveRegistersAllPipeAliases() + public function testResolveRegistersAllPipeAliases(): void { $app = $this->createApp(); @@ -111,10 +116,11 @@ public function testResolveEagerlyResolvesCommandWithoutStaticName(): void $result = $artisan->resolve(StubDynamicCommand::class); $this->assertSame($command, $result); + $this->assertSame($command, $artisan->get('test:dynamic')); $this->assertArrayNotHasKey('test:dynamic', $this->getCommandMap($artisan)); } - public function testAsCommandAttributeTakesPriorityOverSignature() + public function testAsCommandAttributeTakesPriorityOverSignature(): void { $app = $this->createApp(); @@ -125,7 +131,7 @@ public function testAsCommandAttributeTakesPriorityOverSignature() $this->assertArrayNotHasKey('test:from-signature', $map); } - public function testResolveEagerlyAddsCommandInstance() + public function testResolveEagerlyAddsCommandInstance(): void { $app = $this->createApp($this->app); @@ -185,7 +191,7 @@ public function testResolveCommandsAcceptsMixedAndMultipleArrays(): void // Loader refresh // --------------------------------------------------------------- - public function testResolveRefreshesLoaderWhenAlreadySet() + public function testResolveRefreshesLoaderWhenAlreadySet(): void { $app = $this->createApp(); $app->setContainerCommandLoader(); @@ -203,7 +209,7 @@ public function testResolveRefreshesLoaderWhenAlreadySet() $this->assertNotSame($loaderBefore, $loaderAfter); } - public function testResolveDoesNotRefreshLoaderWhenNotYetSet() + public function testResolveDoesNotRefreshLoaderWhenNotYetSet(): void { $app = $this->createApp(); @@ -219,7 +225,7 @@ public function testResolveDoesNotRefreshLoaderWhenNotYetSet() // add (container propagation) // --------------------------------------------------------------- - public function testAddCommandSetsHypervelOnHypervelCommands() + public function testAddCommandSetsHypervelOnHypervelCommands(): void { $artisan = $this->getMockConsole(['addToParent']); @@ -232,7 +238,7 @@ public function testAddCommandSetsHypervelOnHypervelCommands() $this->assertSame($command, $result); } - public function testAddCommandDoesNotSetHypervelOnSymfonyCommands() + public function testAddCommandDoesNotSetHypervelOnSymfonyCommands(): void { $artisan = $this->getMockConsole(['addToParent']); @@ -249,7 +255,7 @@ public function testAddCommandDoesNotSetHypervelOnSymfonyCommands() // Alias resolution via AsCommand attribute and $aliases property // --------------------------------------------------------------- - public function testResolvingCommandsWithAliasViaAttribute() + public function testResolvingCommandsWithAliasViaAttribute(): void { $app = $this->createApp($this->app); $app->resolve(StubCommandWithAttributeAlias::class); @@ -261,7 +267,7 @@ public function testResolvingCommandsWithAliasViaAttribute() $this->assertArrayHasKey('alias-test:attr-alias', $app->all()); } - public function testResolvingCommandsWithAliasViaProperty() + public function testResolvingCommandsWithAliasViaProperty(): void { $app = $this->createApp($this->app); $app->resolve(StubCommandWithPropertyAlias::class); @@ -273,7 +279,7 @@ public function testResolvingCommandsWithAliasViaProperty() $this->assertArrayHasKey('alias-test:prop-alias', $app->all()); } - public function testResolveRegistersPropertyAliasesInCommandMap() + public function testResolveRegistersPropertyAliasesInCommandMap(): void { $app = $this->createApp(); @@ -284,7 +290,7 @@ public function testResolveRegistersPropertyAliasesInCommandMap() $this->assertArrayHasKey('alias-test:prop-alias', $map); } - public function testPropertyAliasResolvesDirectlyWithoutPrimaryName() + public function testPropertyAliasResolvesDirectlyWithoutPrimaryName(): void { $app = $this->createApp($this->app); $app->resolve(StubCommandWithPropertyAlias::class); @@ -294,7 +300,7 @@ public function testPropertyAliasResolvesDirectlyWithoutPrimaryName() $this->assertInstanceOf(StubCommandWithPropertyAlias::class, $app->get('alias-test:prop-alias')); } - public function testSignatureCommandWithAliasesResolvesDirectlyByAlias() + public function testSignatureCommandWithAliasesResolvesDirectlyByAlias(): void { $app = $this->createApp($this->app); $app->resolve(StubSignatureWithAliasCommand::class); @@ -304,7 +310,7 @@ public function testSignatureCommandWithAliasesResolvesDirectlyByAlias() $this->assertInstanceOf(StubSignatureWithAliasCommand::class, $app->get('test:signed-alias')); } - public function testSignatureAttributeCommandWithAliasesResolvesDirectlyByAlias() + public function testSignatureAttributeCommandWithAliasesResolvesDirectlyByAlias(): void { $app = $this->createApp($this->app); $app->resolve(StubSignatureAttributeCommand::class); @@ -496,7 +502,7 @@ public function testSequentialCallsUseFreshCommandInstances(): void $this->assertNotSame($recorder->commands[0], $recorder->commands[1]); } - public function testConcurrentCallsUseIsolatedCommandInstances() + public function testConcurrentCallsUseIsolatedCommandInstances(): void { $artisan = $this->createApp($this->app); $artisan->resolve(StubStatefulCommand::class); @@ -506,11 +512,11 @@ public function testConcurrentCallsUseIsolatedCommandInstances() $outputB = new BufferedOutput; [$exitCodeA, $exitCodeB] = parallel([ - fn () => $artisan->call('test:stateful', [ + fn (): int => $artisan->call('test:stateful', [ 'value' => 'alpha', '--sleep' => 5000, ], $outputA), - function () use ($artisan, $outputB) { + function () use ($artisan, $outputB): int { usleep(2500); return $artisan->call('test:stateful', [ @@ -526,7 +532,7 @@ function () use ($artisan, $outputB) { $this->assertSame("bravo\n", $outputB->fetch()); } - public function testConcurrentNestedCallsUseIsolatedCommandInstances() + public function testConcurrentNestedCallsUseIsolatedCommandInstances(): void { $artisan = $this->createApp($this->app); $artisan->resolve(StubNestedCallerCommand::class); @@ -537,11 +543,11 @@ public function testConcurrentNestedCallsUseIsolatedCommandInstances() $outputB = new BufferedOutput; [$exitCodeA, $exitCodeB] = parallel([ - fn () => $artisan->call('test:nested-caller', [ + fn (): int => $artisan->call('test:nested-caller', [ 'value' => 'alpha', '--sleep' => 5000, ], $outputA), - function () use ($artisan, $outputB) { + function () use ($artisan, $outputB): int { usleep(2500); return $artisan->call('test:nested-caller', [ @@ -567,9 +573,9 @@ function () use ($artisan, $outputB) { private function getCommandLoader(ConsoleApplication $app): ?ContainerCommandLoader { // Access the commandLoader via Symfony's private property. - $ref = new ReflectionProperty(\Symfony\Component\Console\Application::class, 'commandLoader'); + $reflection = new ReflectionProperty(SymfonyApplication::class, 'commandLoader'); - return $ref->getValue($app); + return $reflection->getValue($app); } /** @@ -594,6 +600,9 @@ private function getMockConsole(array $methods): ConsoleApplication #[AsCommand(name: 'test:attributed')] class StubAttributedCommand extends Command { + /** + * Execute the console command. + */ public function handle(): void { } @@ -603,6 +612,9 @@ class StubSignatureCommand extends Command { protected ?string $signature = 'test:signed {--option}'; + /** + * Execute the console command. + */ public function handle(): void { } @@ -611,6 +623,9 @@ public function handle(): void #[Signature('test:signed-attribute {--option}', aliases: ['test:signed-attribute-alias'])] class StubSignatureAttributeCommand extends Command { + /** + * Execute the console command. + */ public function handle(): void { } @@ -620,6 +635,9 @@ public function handle(): void #[Aliases(['test:aliases-attribute-alias'])] class StubAliasesAttributeCommand extends Command { + /** + * Execute the console command. + */ public function handle(): void { } @@ -629,6 +647,9 @@ public function handle(): void #[Aliases(['test:aliases-attribute-override'])] class StubAliasesAttributeOverridesSignatureCommand extends Command { + /** + * Execute the console command. + */ public function handle(): void { } @@ -638,6 +659,9 @@ class StubNamedCommand extends Command { protected ?string $name = 'test:named'; + /** + * Execute the console command. + */ public function handle(): void { } @@ -647,6 +671,9 @@ class StubAliasedCommand extends Command { protected ?string $name = 'test:primary|test:alias'; + /** + * Execute the console command. + */ public function handle(): void { } @@ -657,6 +684,9 @@ public function handle(): void */ class StubDynamicCommand extends SymfonyCommand { + /** + * Create a command with a dynamically assigned name. + */ public function __construct() { parent::__construct('test:dynamic'); @@ -668,6 +698,9 @@ class StubAttributeOverridesSignatureCommand extends Command { protected ?string $signature = 'test:from-signature {--option}'; + /** + * Execute the console command. + */ public function handle(): void { } @@ -676,6 +709,9 @@ public function handle(): void #[AsCommand(name: 'test:late')] class StubLateCommand extends Command { + /** + * Execute the console command. + */ public function handle(): void { } @@ -684,6 +720,9 @@ public function handle(): void #[AsCommand(name: 'alias-test:attr', aliases: ['alias-test:attr-alias'])] class StubCommandWithAttributeAlias extends Command { + /** + * Execute the console command. + */ public function handle(): void { } @@ -695,6 +734,9 @@ class StubCommandWithPropertyAlias extends Command protected array $aliases = ['alias-test:prop-alias']; + /** + * Execute the console command. + */ public function handle(): void { } @@ -704,6 +746,9 @@ class StubCommandWithoutPropertyAlias extends Command { protected ?string $name = 'alias-test:no-alias'; + /** + * Execute the console command. + */ public function handle(): void { } @@ -715,6 +760,9 @@ class StubSignatureWithAliasCommand extends Command protected array $aliases = ['test:signed-alias']; + /** + * Execute the console command. + */ public function handle(): void { } @@ -724,6 +772,9 @@ class StubStatefulCommand extends Command { protected ?string $signature = 'test:stateful {value} {--sleep=0}'; + /** + * Execute the console command. + */ public function handle(): int { usleep((int) $this->option('sleep')); @@ -737,11 +788,17 @@ public function handle(): int #[AsCommand(name: 'test:execution-identity')] class StubExecutionIdentityCommand extends Command { + /** + * Create a command that records each execution instance. + */ public function __construct(private readonly StubCommandExecutionRecorder $recorder) { parent::__construct(); } + /** + * Execute the console command. + */ public function handle(): int { $this->recorder->commands[] = $this; @@ -762,6 +819,9 @@ class StubNestedCallerCommand extends Command { protected ?string $signature = 'test:nested-caller {value} {--sleep=0}'; + /** + * Execute the console command. + */ public function handle(): int { return $this->call('test:stateful', [ diff --git a/tests/Console/Fixtures/FakeEventMutex.php b/tests/Console/Fixtures/FakeEventMutex.php new file mode 100644 index 0000000000..60a3966304 --- /dev/null +++ b/tests/Console/Fixtures/FakeEventMutex.php @@ -0,0 +1,34 @@ +container->instance(Filesystem::class, new Filesystem); } + // REMOVED: Laravel's buildCommand() and user() tests; coroutine tasks do not use shell wrappers. + public function testSendOutputToWithIsNotFile(): void { $event = new Event(m::mock(EventMutex::class), 'php -v'); @@ -163,50 +167,64 @@ public function testNeverCheckedRepeatableEventIsNotReadyToRepeat(): void $this->assertFalse($event->shouldRepeatNow()); } - public function testEventMarksSkippedWhenMutexAlreadyExists(): void + public function testRunIndicatesWhenSkippedBecauseOverlapping(): void { + $beforeCallbackCalled = false; $eventMutex = m::mock(EventMutex::class); - $eventMutex->shouldReceive('create')->once()->andReturnFalse(); + $event = new class($eventMutex, 'php -i') extends Event { + public bool $executed = false; - $event = new CallbackEvent($eventMutex, function () { - return 0; - }); - $event->name('test'); + /** + * Run the command process. + */ + protected function execute(ContainerContract $container): int + { + $this->executed = true; + + return 0; + } + }; + + $eventMutex->expects('create')->with($event)->andReturnFalse(); $event->withoutOverlapping(); + $event->before(function () use (&$beforeCallbackCalled): void { + $beforeCallbackCalled = true; + }); $this->assertNull($event->run($this->container)); $this->assertTrue($event->skippedBecauseOverlapping); + $this->assertFalse($event->executed); + $this->assertFalse($beforeCallbackCalled); } - public function testEventResetsSkippedBecauseOverlappingWhenItRuns(): void + public function testRunResetsSkippedBecauseOverlapping(): void { $eventMutex = m::mock(EventMutex::class); - $eventMutex->shouldReceive('create')->andReturnFalse(); - - $event = new CallbackEvent($eventMutex, function () { - return 0; - }); - $event->name('test'); + $event = new class($eventMutex, 'php -i') extends Event { + public int $executions = 0; + + /** + * Run the command process. + */ + protected function execute(ContainerContract $container): int + { + ++$this->executions; + + return 0; + } + }; + + $eventMutex->expects('create')->times(2)->with($event)->andReturn(false, true); + $eventMutex->expects('forget')->with($event); $event->withoutOverlapping(); $this->assertNull($event->run($this->container)); $this->assertTrue($event->skippedBecauseOverlapping); - $eventMutex = m::mock(EventMutex::class); - $eventMutex->shouldReceive('create')->once()->andReturnTrue(); - $eventMutex->shouldReceive('forget')->once(); - - $event = new CallbackEvent($eventMutex, function () { - return 0; - }); - $event->name('test'); - $event->withoutOverlapping(); - $event->skippedBecauseOverlapping = true; - - $this->container->instance(Filesystem::class, new Filesystem); + $event->run($this->container); - $this->assertSame(0, $event->run($this->container)); $this->assertFalse($event->skippedBecauseOverlapping); + $this->assertSame(1, $event->executions); } public function testReleaseMutexOnTerminationSignalReleasesOwnedMutex(): void @@ -587,25 +605,51 @@ public function testBeforeAndAfterCallbacksCanReceiveEvent(): void $this->assertSame($event, $afterEvent); } - public function testFilterCallbacksCanReceiveEventAndMayBeInvokableObjects(): void + public function testFilterCallbacksCanReceiveEvent(): void { $filterEvent = null; - $reject = new EventTestInvokableFilter(false); + $rejectEvent = null; + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->when(function (Event $event) use (&$filterEvent): bool { + $filterEvent = $event; + + return true; + }); + $event->skip(function (Event $event) use (&$rejectEvent): bool { + $rejectEvent = $event; + + return false; + }); + + $this->assertTrue($event->filtersPass($this->container)); + $this->assertSame($event, $filterEvent); + $this->assertSame($event, $rejectEvent); + } + + public function testEventCallbackResolvesByTypeRegardlessOfParameterName(): void + { + $beforeEvent = null; + $filterEvent = null; $event = new Event(m::mock(EventMutex::class), 'php -i'); + $event->before(function (Event $scheduledEvent) use (&$beforeEvent): void { + $beforeEvent = $scheduledEvent; + }); $event->when(function (Event $scheduledEvent) use (&$filterEvent): bool { $filterEvent = $scheduledEvent; return true; }); - $event->skip($reject); + $event->callBeforeCallbacks($this->container); $this->assertTrue($event->filtersPass($this->container)); + + $this->assertSame($event, $beforeEvent); $this->assertSame($event, $filterEvent); - $this->assertSame(1, $reject->calls); } - public function testEventCallbackDoesNotReplaceUnrelatedTypedParameters(): void + public function testEventCallbackDoesNotInjectIntoUnrelatedTypedParameters(): void { $value = new Stringable('injected-string'); $received = null; @@ -621,6 +665,20 @@ public function testEventCallbackDoesNotReplaceUnrelatedTypedParameters(): void $this->assertSame($value, $received); } + public function testFilterCallbacksMayBeInvokableObjects(): void + { + $filter = new EventTestInvokableFilter(true); + $reject = new EventTestInvokableFilter(false); + $event = new Event(m::mock(EventMutex::class), 'php -i'); + + $event->when($filter); + $event->skip($reject); + + $this->assertTrue($event->filtersPass($this->container)); + $this->assertSame(1, $filter->calls); + $this->assertSame(1, $reject->calls); + } + public function testSuccessFailureAndOutputCallbacksCanReceiveEvent(): void { $successEvent = null; @@ -767,17 +825,13 @@ public function testEventIsDueAtUsesGivenTime(): void $app->shouldReceive('isDownForMaintenance')->andReturn(false); $app->shouldReceive('environment')->andReturn('production'); - try { - CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-05-29 13:00:00')); + CarbonImmutable::setTestNow(CarbonImmutable::parse('2026-05-29 13:00:00')); - $event = new Event(m::mock(EventMutex::class), 'php foo'); - $event->dailyAt('13:00'); + $event = new Event(m::mock(EventMutex::class), 'php foo'); + $event->dailyAt('13:00'); - $this->assertFalse($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 12:59:59'))); - $this->assertTrue($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 13:00:00'))); - } finally { - CarbonImmutable::setTestNow(); - } + $this->assertFalse($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 12:59:59'))); + $this->assertTrue($event->isDueAt($app, CarbonImmutable::parse('2026-05-29 13:00:00'))); } public function testEventIsDueAtUsesEventTimezone(): void @@ -800,7 +854,7 @@ public function testTimeBetweenChecks(): void $app->shouldReceive('environment')->andReturn('production'); $app->shouldReceive('call')->andReturnUsing(fn (callable $callback) => $callback()); - CarbonImmutable::setTestNow(CarbonImmutable::now()->startOfDay()->addHours(9)); + CarbonImmutable::setTestNow(CarbonImmutable::today()->addHours(9)); $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); $this->assertTrue($event->between('8:00', '10:00')->filtersPass($app)); @@ -862,7 +916,7 @@ public function testTimeUnlessBetweenChecks(): void $app->shouldReceive('environment')->andReturn('production'); $app->shouldReceive('call')->andReturnUsing(fn (callable $callback) => $callback()); - CarbonImmutable::setTestNow(CarbonImmutable::now()->startOfDay()->addHours(9)); + CarbonImmutable::setTestNow(CarbonImmutable::today()->addHours(9)); $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); $this->assertFalse($event->unlessBetween('8:00', '10:00')->filtersPass($app)); @@ -888,10 +942,16 @@ class EventTestInvokableFilter { public int $calls = 0; + /** + * Create a new filter instance. + */ public function __construct(protected bool $result) { } + /** + * Evaluate the filter. + */ public function __invoke(): bool { ++$this->calls; @@ -902,16 +962,25 @@ public function __invoke(): bool class EventTestExecutableEvent extends Event { + /** + * Create a new executable event. + */ public function __construct(EventMutex $mutex) { parent::__construct($mutex, 'test:command'); } + /** + * Execute the event successfully. + */ protected function execute(ContainerContract $container): int { return 0; } + /** + * Get the event output. + */ public function getOutput(ContainerContract $container): ?string { return 'output'; @@ -920,11 +989,17 @@ public function getOutput(ContainerContract $container): ?string class EventTestFailingExitCodeEvent extends EventTestExecutableEvent { + /** + * Mark the event mutex as acquired. + */ public function acquireMutexForTest(): void { $this->mutexAcquired = true; } + /** + * Fail when publishing the exit code. + */ protected function setExitCode(int $exitCode): void { throw new RuntimeException('exit publication failed'); @@ -933,6 +1008,9 @@ protected function setExitCode(int $exitCode): void class EventTestProcessEvent extends Event { + /** + * Create a new process event. + */ public function __construct( EventMutex $mutex, protected Process $process, @@ -941,11 +1019,17 @@ public function __construct( parent::__construct($mutex, 'test:process', isSystem: true); } + /** + * Determine whether the process remains in coroutine context. + */ public function hasRetainedProcess(): bool { return CoroutineContext::has($this->processContextKey()); } + /** + * Retain the process and simulate its execution outcome. + */ protected function execute(ContainerContract $container): int { CoroutineContext::set($this->processContextKey(), $this->process); diff --git a/tests/Console/Scheduling/FrequencyTest.php b/tests/Console/Scheduling/FrequencyTest.php index 132e6115db..16d1f15f9f 100644 --- a/tests/Console/Scheduling/FrequencyTest.php +++ b/tests/Console/Scheduling/FrequencyTest.php @@ -5,33 +5,31 @@ namespace Hypervel\Tests\Console\Scheduling; use Hypervel\Console\Scheduling\Event; -use Hypervel\Console\Scheduling\EventMutex; +use Hypervel\Tests\Console\Fixtures\FakeEventMutex; use Hypervel\Tests\TestCase; use InvalidArgumentException; -use Mockery as m; class FrequencyTest extends TestCase { - /** @var \Hypervel\Console\Scheduling\Event */ - protected $event; + protected Event $event; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); - $this->event = new Event( - m::mock(EventMutex::class), - 'php foo' - ); + $this->event = new Event(new FakeEventMutex, 'php foo'); } - public function testEveryMinute() + public function testEveryMinute(): void { $this->assertSame('* * * * *', $this->event->getExpression()); $this->assertSame('* * * * *', $this->event->everyMinute()->getExpression()); } - public function testEveryXMinutes() + public function testEveryXMinutes(): void { $this->assertSame('*/2 * * * *', $this->event->everyTwoMinutes()->getExpression()); $this->assertSame('*/3 * * * *', $this->event->everyThreeMinutes()->getExpression()); @@ -42,42 +40,42 @@ public function testEveryXMinutes() $this->assertSame('*/30 * * * *', $this->event->everyThirtyMinutes()->getExpression()); } - public function testDaily() + public function testDaily(): void { $this->assertSame('0 0 * * *', $this->event->daily()->getExpression()); } - public function testDailyAt() + public function testDailyAt(): void { $this->assertSame('8 13 * * *', $this->event->dailyAt('13:08')->getExpression()); } - public function testDailyAtParsesMinutesAndIgnoresSecondsWhenSecondsAreDefined() + public function testDailyAtParsesMinutesAndIgnoresSecondsWhenSecondsAreDefined(): void { $this->assertSame('8 13 * * *', $this->event->dailyAt('13:08:10')->getExpression()); } - public function testTwiceDaily() + public function testTwiceDaily(): void { $this->assertSame('0 3,15 * * *', $this->event->twiceDaily(3, 15)->getExpression()); } - public function testTwiceDailyAt() + public function testTwiceDailyAt(): void { $this->assertSame('5 3,15 * * *', $this->event->twiceDailyAt(3, 15, 5)->getExpression()); } - public function testWeekly() + public function testWeekly(): void { $this->assertSame('0 0 * * 0', $this->event->weekly()->getExpression()); } - public function testWeeklyOn() + public function testWeeklyOn(): void { $this->assertSame('0 8 * * 1', $this->event->weeklyOn(1, '8:00')->getExpression()); } - public function testOverrideWithHourly() + public function testOverrideWithHourly(): void { $this->assertSame('0 * * * *', $this->event->everyFiveMinutes()->hourly()->getExpression()); $this->assertSame('37 * * * *', $this->event->hourlyAt(37)->getExpression()); @@ -85,7 +83,7 @@ public function testOverrideWithHourly() $this->assertSame('15,30,45 * * * *', $this->event->hourlyAt([15, 30, 45])->getExpression()); } - public function testHourly() + public function testHourly(): void { $this->assertSame('0 1-23/2 * * *', $this->event->everyOddHour()->getExpression()); $this->assertSame('0 */2 * * *', $this->event->everyTwoHours()->getExpression()); @@ -112,12 +110,12 @@ public function testHourly() $this->assertSame('15,30,45 */6 * * *', $this->event->everySixHours([15, 30, 45])->getExpression()); } - public function testMonthly() + public function testMonthly(): void { $this->assertSame('0 0 1 * *', $this->event->monthly()->getExpression()); } - public function testMonthlyOn() + public function testMonthlyOn(): void { $this->assertSame('0 15 4 * *', $this->event->monthlyOn(4, '15:00')->getExpression()); } @@ -143,104 +141,109 @@ public function testRepeatEveryRejectsNegativeValues(): void (fn () => $this->repeatEvery(-5))->call($this->event); } - public function testTwiceMonthly() + public function testTwiceMonthly(): void { $this->assertSame('0 0 1,16 * *', $this->event->twiceMonthly(1, 16)->getExpression()); } - public function testTwiceMonthlyAtTime() + public function testTwiceMonthlyAtTime(): void { $this->assertSame('30 1 1,16 * *', $this->event->twiceMonthly(1, 16, '1:30')->getExpression()); } - public function testMonthlyOnWithMinutes() + public function testMonthlyOnWithMinutes(): void { $this->assertSame('15 15 4 * *', $this->event->monthlyOn(4, '15:15')->getExpression()); } - public function testWeekdaysDaily() + public function testWeekdaysDaily(): void { $this->assertSame('0 0 * * 1-5', $this->event->weekdays()->daily()->getExpression()); } - public function testWeekdaysHourly() + public function testWeekdaysHourly(): void { $this->assertSame('0 * * * 1-5', $this->event->weekdays()->hourly()->getExpression()); } - public function testWeekdays() + public function testWeekdays(): void { $this->assertSame('* * * * 1-5', $this->event->weekdays()->getExpression()); } - public function testWeekends() + public function testWeekends(): void { $this->assertSame('* * * * 6,0', $this->event->weekends()->getExpression()); } - public function testSundays() + public function testSundays(): void { $this->assertSame('* * * * 0', $this->event->sundays()->getExpression()); } - public function testMondays() + public function testMondays(): void { $this->assertSame('* * * * 1', $this->event->mondays()->getExpression()); } - public function testTuesdays() + public function testTuesdays(): void { $this->assertSame('* * * * 2', $this->event->tuesdays()->getExpression()); } - public function testWednesdays() + public function testWednesdays(): void { $this->assertSame('* * * * 3', $this->event->wednesdays()->getExpression()); } - public function testThursdays() + public function testThursdays(): void { $this->assertSame('* * * * 4', $this->event->thursdays()->getExpression()); } - public function testFridays() + public function testFridays(): void { $this->assertSame('* * * * 5', $this->event->fridays()->getExpression()); } - public function testSaturdays() + public function testSaturdays(): void { $this->assertSame('* * * * 6', $this->event->saturdays()->getExpression()); } - public function testQuarterly() + public function testQuarterly(): void { $this->assertSame('0 0 1 1-12/3 *', $this->event->quarterly()->getExpression()); } - public function testYearly() + public function testQuarterlyOn(): void + { + $this->assertSame('0 15 4 1-12/3 *', $this->event->quarterlyOn(4, '15:00')->getExpression()); + } + + public function testYearly(): void { $this->assertSame('0 0 1 1 *', $this->event->yearly()->getExpression()); } - public function testYearlyOn() + public function testYearlyOn(): void { $this->assertSame('8 15 5 4 *', $this->event->yearlyOn(4, 5, '15:08')->getExpression()); } - public function testYearlyOnMondaysOnly() + public function testYearlyOnMondaysOnly(): void { $this->assertSame('1 9 * 7 1', $this->event->mondays()->yearlyOn(7, '*', '09:01')->getExpression()); } - public function testYearlyOnTuesdaysAndDayOfMonth20() + public function testYearlyOnTuesdaysAndDayOfMonth20(): void { $this->assertSame('1 9 20 7 2', $this->event->tuesdays()->yearlyOn(7, 20, '09:01')->getExpression()); } - public function testFrequencyMacro() + public function testFrequencyMacro(): void { - Event::macro('everyXMinutes', function ($x) { + Event::macro('everyXMinutes', function (int $x): Event { return $this->spliceIntoPosition(1, "*/{$x}"); }); diff --git a/tests/Console/Scheduling/ScheduleRunCommandTest.php b/tests/Console/Scheduling/ScheduleRunCommandTest.php index 9200d1f34a..dd405a01f7 100644 --- a/tests/Console/Scheduling/ScheduleRunCommandTest.php +++ b/tests/Console/Scheduling/ScheduleRunCommandTest.php @@ -50,6 +50,8 @@ class ScheduleRunCommandTest extends TestCase { + // REMOVED: ScheduleWorkCommandTest; schedule:run owns the loop without a subprocess wrapper. + protected array $dispatched; protected Dispatcher $dispatcher; diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index ab85160738..01e34eabbc 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -3202,7 +3202,7 @@ public function testUpdateWithAlias() $this->assertEquals(1, $result); } - public function testUpdateWithAliasWithQualifiedTimestampValue() + public function testUpdateWithAliasWithQualifiedTimestampValue(): void { CarbonImmutable::setTestNow($now = '2017-10-10 10:10:10'); @@ -3218,8 +3218,6 @@ public function testUpdateWithAliasWithQualifiedTimestampValue() $result = $builder->from('table as alias')->update(['foo' => 'bar', 'alias.updated_at' => null]); $this->assertEquals(1, $result); - - CarbonImmutable::setTestNow(null); } public function testUpsert() diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index f7f1e9793c..245624a132 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -858,7 +858,7 @@ public function testFactoryCanConditionallyExecuteCode() }); } - public function testDynamicTrashedStateForSoftdeletesModels() + public function testDynamicTrashedStateForSoftdeletesModels(): void { $now = CarbonImmutable::create(2020, 6, 7, 8, 9); CarbonImmutable::setTestNow($now); @@ -870,19 +870,15 @@ public function testDynamicTrashedStateForSoftdeletesModels() $post = PostFactory::new()->trashed($deleted_at)->create(); $this->assertTrue($deleted_at->equalTo($post->deleted_at)); - - CarbonImmutable::setTestNow(); } - public function testDynamicTrashedStateRespectsExistingState() + public function testDynamicTrashedStateRespectsExistingState(): void { $now = CarbonImmutable::create(2020, 6, 7, 8, 9); CarbonImmutable::setTestNow($now); $comment = CommentFactory::new()->trashed()->create(); $this->assertTrue($comment->deleted_at->equalTo($now->subWeek())); - - CarbonImmutable::setTestNow(); } public function testDynamicTrashedStateThrowsExceptionWhenNotASoftdeletesModel() diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 6dd37cacab..ef261f802f 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -1172,7 +1172,7 @@ public function testWhereNullSafeEqualsWithSubqueryPostgres(): void $this->assertSame([1, 'bar'], $builder->getBindings()); } - public function testWhereBetweens() + public function testWhereBetweens(): void { $builder = $this->getBuilder(); $builder->select('*')->from('users')->whereBetween('id', [1, 2]); @@ -1200,19 +1200,19 @@ public function testWhereBetweens() $this->assertEquals([], $builder->getBindings()); $builder = $this->getBuilder(); - $period = now()->startOfDay()->toPeriod(now()->addDay()->startOfDay()); + $period = today()->toPeriod(now()->addDay()->startOfDay()); $builder->select('*')->from('users')->whereBetween('created_at', $period); $this->assertSame('select * from "users" where "created_at" between ? and ?', $builder->toSql()); - $this->assertEquals([now()->startOfDay(), now()->addDay()->startOfDay()], $builder->getBindings()); + $this->assertEquals([today(), now()->addDay()->startOfDay()], $builder->getBindings()); // custom long carbon period date $builder = $this->getBuilder(); - $period = now()->startOfDay()->toPeriod(now()->addMonth()->startOfDay()); + $period = today()->toPeriod(now()->addMonth()->startOfDay()); $builder->select('*')->from('users')->whereBetween('created_at', $period); $this->assertSame('select * from "users" where "created_at" between ? and ?', $builder->toSql()); - $this->assertEquals([now()->startOfDay(), now()->addMonth()->startOfDay()], $builder->getBindings()); + $this->assertEquals([today(), now()->addMonth()->startOfDay()], $builder->getBindings()); - $start = now()->startOfDay(); + $start = today(); $builder = $this->getBuilder(); $period = new DatePeriod($start, new DateInterval('P1D'), $start->addDays(5)); diff --git a/tests/Foundation/Console/ChannelListCommandTest.php b/tests/Foundation/Console/ChannelListCommandTest.php index 4a646477fc..3525f34542 100644 --- a/tests/Foundation/Console/ChannelListCommandTest.php +++ b/tests/Foundation/Console/ChannelListCommandTest.php @@ -4,19 +4,32 @@ namespace Hypervel\Tests\Foundation\Console; -class ChannelListCommandTest extends \Hypervel\Testbench\TestCase +use Hypervel\Support\Facades\Broadcast; +use Hypervel\Testbench\TestCase; + +class ChannelListCommandTest extends TestCase { - public function testDoesNotWarnAboutBroadcastServiceProvider() + public function testDoesNotWarnAboutBroadcastServiceProvider(): void { $this->artisan('channel:list') ->doesntExpectOutputToContain('BroadcastServiceProvider') ->assertSuccessful(); } - public function testOutputsErrorWhenNoChannelsRegistered() + public function testOutputsErrorWhenNoChannelsRegistered(): void { $this->artisan('channel:list') ->expectsOutputToContain("Your application doesn't have any private broadcasting channels.") ->assertSuccessful(); } + + public function testItListsRegisteredChannels(): void + { + Broadcast::channel('orders.{order}', fn (): bool => true); + + $this->artisan('channel:list') + ->expectsOutputToContain('orders.{order}') + ->expectsOutputToContain('Showing [1] private channels') + ->assertSuccessful(); + } } diff --git a/tests/Foundation/Console/RouteListCommandTest.php b/tests/Foundation/Console/RouteListCommandTest.php index 5cc66e1cff..dc4474552c 100644 --- a/tests/Foundation/Console/RouteListCommandTest.php +++ b/tests/Foundation/Console/RouteListCommandTest.php @@ -5,34 +5,32 @@ namespace Hypervel\Tests\Foundation\Console; use Hypervel\Console\Application; -use Hypervel\Console\Events\ArtisanStarting; -use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Http\Kernel as KernelContract; +use Hypervel\Events\Dispatcher; +use Hypervel\Foundation\Application as FoundationApplication; use Hypervel\Foundation\Console\RouteListCommand; use Hypervel\Foundation\Http\Kernel; use Hypervel\Routing\Router; use Hypervel\Tests\TestCase; -use Mockery as m; class RouteListCommandTest extends TestCase { protected Application $consoleApp; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); - $events = m::mock(Dispatcher::class); - $events->shouldReceive('hasListeners')->once()->with(ArtisanStarting::class)->andReturnFalse(); - $events->shouldNotReceive('dispatch'); - $this->consoleApp = new Application( - $hypervel = new \Hypervel\Foundation\Application(__DIR__), - $events, + $hypervel = new FoundationApplication(__DIR__), + new Dispatcher($hypervel), 'testing', ); - $router = new Router(m::mock('Hypervel\Events\Dispatcher')); + $router = new Router(new Dispatcher($hypervel)); $kernel = new class($hypervel, $router) extends Kernel { protected array $middlewareGroups = [ @@ -52,16 +50,16 @@ protected function setUp(): void $hypervel->instance(KernelContract::class, $kernel); - $router->get('/example', function () { + $router->get('/example', function (): string { return 'Hello World'; })->middleware('exampleMiddleware'); - $router->get('/sub-example', function () { + $router->get('/sub-example', function (): string { return 'Hello World'; })->domain('sub') ->middleware('exampleMiddleware'); - $router->get('/example-group', function () { + $router->get('/example-group', function (): string { return 'Hello Group'; })->middleware(['web', 'auth']); @@ -71,7 +69,7 @@ protected function setUp(): void $this->consoleApp->addCommands([$command]); } - public function testNoMiddlewareIfNotVerbose() + public function testNoMiddlewareIfNotVerbose(): void { $this->consoleApp->call('route:list'); $output = $this->consoleApp->output(); @@ -151,7 +149,7 @@ public function testSortRouteListPrecedence(): void } } - public function testMiddlewareGroupsAssignmentInCli() + public function testMiddlewareGroupsAssignmentInCli(): void { $this->consoleApp->call('route:list', ['-v' => true]); $output = $this->consoleApp->output(); @@ -167,7 +165,7 @@ public function testMiddlewareGroupsAssignmentInCli() $this->assertStringNotContainsString('Middleware 5', $output); } - public function testMiddlewareGroupsExpandInCliIfVeryVerbose() + public function testMiddlewareGroupsExpandInCliIfVeryVerbose(): void { $this->consoleApp->call('route:list', ['-vv' => true]); $output = $this->consoleApp->output(); @@ -183,7 +181,7 @@ public function testMiddlewareGroupsExpandInCliIfVeryVerbose() $this->assertStringNotContainsString('auth', $output); } - public function testMiddlewareGroupsAssignmentInJson() + public function testMiddlewareGroupsAssignmentInJson(): void { $this->consoleApp->call('route:list', ['--json' => true, '-v' => true]); $output = $this->consoleApp->output(); @@ -199,7 +197,7 @@ public function testMiddlewareGroupsAssignmentInJson() $this->assertStringNotContainsString('Middleware 5', $output); } - public function testMiddlewareGroupsExpandInJsonIfVeryVerbose() + public function testMiddlewareGroupsExpandInJsonIfVeryVerbose(): void { $this->consoleApp->call('route:list', ['--json' => true, '-vv' => true]); $output = $this->consoleApp->output(); @@ -258,8 +256,8 @@ public function testClosureRouteShowsPathInCli(): void public function testControllerRoutePathIsNull(): void { - $hypervel = new \Hypervel\Foundation\Application(__DIR__); - $router = new Router(m::mock('Hypervel\Events\Dispatcher')); + $hypervel = new FoundationApplication(__DIR__); + $router = new Router(new Dispatcher($hypervel)); $kernel = new class($hypervel, $router) extends Kernel { protected array $middlewareGroups = []; @@ -272,13 +270,9 @@ public function testControllerRoutePathIsNull(): void $command = new RouteListCommand($router); $command->setHypervel($hypervel); - $events = m::mock(Dispatcher::class); - $events->shouldReceive('hasListeners')->once()->with(ArtisanStarting::class)->andReturnFalse(); - $events->shouldNotReceive('dispatch'); - $app = new Application( $hypervel, - $events, + new Dispatcher($hypervel), 'testing', ); $app->addCommands([$command]); @@ -294,6 +288,9 @@ public function testControllerRoutePathIsNull(): void class RouteListCommandTestController { + /** + * Handle the controller route. + */ public function index(): string { return 'Hello World'; diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index 4fe36a9363..427488236b 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -1286,7 +1286,7 @@ public function attempt( return $this->store()->attempt($policy, $callback, $limiterName); } }); - CarbonImmutable::setTestNow(CarbonImmutable::now()->startOfDay()); + CarbonImmutable::setTestNow(CarbonImmutable::today()); for ($i = 0; $i < 100; ++$i) { $handler->report(new Exception('Something in the app went wrong.')); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 958adb79d7..b6f82ab37d 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -6316,7 +6316,7 @@ public function testTheTransferStatsAreCustomizableOnFake(): void public function testItCanAddGlobalMiddleware(): void { - CarbonImmutable::setTestNow(now()->startOfDay()); + CarbonImmutable::setTestNow(CarbonImmutable::today()); $requests = []; $responses = []; $this->factory->fake(function ($r) use (&$requests) { @@ -6329,7 +6329,7 @@ public function testItCanAddGlobalMiddleware(): void $this->factory->globalMiddleware(Middleware::mapRequest(function ($request) { // Test manipulating headers on outgoing request... - return $request->withHeader('User-Agent', 'Laravel Framework/1.0') + return $request->withHeader('User-Agent', 'Hypervel Framework/1.0') ->withAddedHeader('shared', 'global') ->withHeader('list', ['item-1', 'item-2']) ->withAddedHeader('list', ['item-3']); @@ -6348,19 +6348,19 @@ public function testItCanAddGlobalMiddleware(): void }); }; }); - $responses[] = $this->factory->post('http://forge.laravel.com'); - $responses[] = $this->factory->withHeader('shared', 'local')->post('http://vapor.laravel.com'); + $responses[] = $this->factory->post('http://forge.hypervel.com'); + $responses[] = $this->factory->withHeader('shared', 'local')->post('http://vapor.hypervel.com'); $this->assertCount(2, $requests); $this->assertCount(2, $responses); - $this->assertSame(['Laravel Framework/1.0'], $requests[0]->header('User-Agent')); + $this->assertSame(['Hypervel Framework/1.0'], $requests[0]->header('User-Agent')); $this->assertSame(['item-1', 'item-2', 'item-3'], $requests[0]->header('list')); $this->assertSame(['global'], $requests[0]->header('shared')); $this->assertSame('1', $responses[0]->header('X-Count')); $this->assertSame('6 seconds', $responses[0]->header('X-Duration')); - $this->assertSame(['Laravel Framework/1.0'], $requests[1]->header('User-Agent')); + $this->assertSame(['Hypervel Framework/1.0'], $requests[1]->header('User-Agent')); $this->assertSame(['item-1', 'item-2', 'item-3'], $requests[1]->header('list')); $this->assertSame(['local', 'global'], $requests[1]->header('shared')); $this->assertSame('2', $responses[1]->header('X-Count')); diff --git a/tests/Inertia/Commands/CheckSsrTest.php b/tests/Inertia/Commands/CheckSsrTest.php index 127178e5e3..6a8fd4cc5d 100644 --- a/tests/Inertia/Commands/CheckSsrTest.php +++ b/tests/Inertia/Commands/CheckSsrTest.php @@ -7,13 +7,13 @@ use Hypervel\Inertia\Ssr\Gateway; use Hypervel\Inertia\Ssr\HasHealthCheck; use Hypervel\Tests\Inertia\TestCase; -use Mockery; +use Mockery as m; class CheckSsrTest extends TestCase { public function testSuccessOnHealthySsrServer(): void { - $mock = Mockery::mock(Gateway::class, HasHealthCheck::class); + $mock = m::mock(Gateway::class, HasHealthCheck::class); $mock->shouldReceive('isHealthy')->andReturn(true); $this->app->instance(Gateway::class, $mock); @@ -24,7 +24,7 @@ public function testSuccessOnHealthySsrServer(): void public function testFailureOnUnhealthySsrServer(): void { - $mock = Mockery::mock(Gateway::class, HasHealthCheck::class); + $mock = m::mock(Gateway::class, HasHealthCheck::class); $mock->shouldReceive('isHealthy')->andReturn(false); $this->app->instance(Gateway::class, $mock); diff --git a/tests/Inertia/ResponseTest.php b/tests/Inertia/ResponseTest.php index 3f0de19c58..89c695ce8d 100644 --- a/tests/Inertia/ResponseTest.php +++ b/tests/Inertia/ResponseTest.php @@ -27,7 +27,7 @@ use Hypervel\Tests\Inertia\Fixtures\FakeResource; use Hypervel\Tests\Inertia\Fixtures\MergeWithSharedProp; use Hypervel\View\View; -use Mockery; +use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; class ResponseTest extends TestCase @@ -898,7 +898,7 @@ public function testPromisePropsAreResolved(): void $user = (object) ['name' => 'Jonathan']; - $promise = Mockery::mock('GuzzleHttp\Promise\PromiseInterface') + $promise = m::mock('GuzzleHttp\Promise\PromiseInterface') ->shouldReceive('wait') ->andReturn($user) ->getMock(); diff --git a/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php b/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php index 5939cbf0ed..9d5e0fb5b2 100644 --- a/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php +++ b/tests/Integration/Cache/Redis/TtlHandlingIntegrationTest.php @@ -387,19 +387,9 @@ public function testAllModeTaggedTouchPreservesCachedNullSentinel(): void })); $this->assertSame(1, $invocations); - $this->assertTrue($tagged->touch('nullable_key', null)); - $this->assertSame(-1.0, $this->redis()->zScore($this->allModeTagKey('touch_nullable'), $namespacedKey)); - $rawValue = $this->redis()->get($this->getCachePrefix() . $namespacedKey); $this->assertIsString($rawValue); $this->assertSame(NullSentinel::VALUE, unserialize($rawValue)); - - $this->assertNull($tagged->rememberNullable('nullable_key', 60, function () use (&$invocations): string { - ++$invocations; - - return 'fresh'; - })); - $this->assertSame(1, $invocations); } public function testAnyModePlainTouchExtendsKeyAndTagMetadata(): void diff --git a/tests/Integration/Console/CommandDurationThresholdTest.php b/tests/Integration/Console/CommandDurationThresholdTest.php index ba2ef84e60..c4d1adb4a3 100644 --- a/tests/Integration/Console/CommandDurationThresholdTest.php +++ b/tests/Integration/Console/CommandDurationThresholdTest.php @@ -7,7 +7,6 @@ use Carbon\CarbonInterval; use Hypervel\Contracts\Console\Kernel; use Hypervel\Support\CarbonImmutable; -use Hypervel\Support\Facades\Config; use Hypervel\Testbench\TestCase; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\ConsoleOutput; @@ -17,19 +16,19 @@ class CommandDurationThresholdTest extends TestCase public function testItCanHandleExceedingCommandDuration(): void { $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called): void { $called = true; }); - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMilliseconds(1)); + CarbonImmutable::setTestNow($now->addSecond()->addMilliseconds(1)); $kernel->terminate($input, 21); $this->assertTrue($called); @@ -38,19 +37,19 @@ public function testItCanHandleExceedingCommandDuration(): void public function testItDoesntCallWhenExactlyThresholdDuration(): void { $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called): void { $called = true; }); - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); + CarbonImmutable::setTestNow($now->addSecond()); $kernel->terminate($input, 21); $this->assertFalse($called); @@ -59,16 +58,16 @@ public function testItDoesntCallWhenExactlyThresholdDuration(): void public function testItProvidesArgsToHandler(): void { $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $args = null; - $kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(0), function () use (&$args) { + $kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(0), function () use (&$args): void { $args = func_get_args(); }); CarbonImmutable::setTestNow($startedAt = CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); + CarbonImmutable::setTestNow($startedAt->addSecond()); $kernel->terminate($input, 21); $this->assertCount(3, $args); @@ -81,19 +80,19 @@ public function testItProvidesArgsToHandler(): void public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds(): void { $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(1000, function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(1000, function () use (&$called): void { $called = true; }); - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMilliseconds(1)); + CarbonImmutable::setTestNow($now->addSecond()->addMilliseconds(1)); $kernel->terminate($input, 21); $this->assertTrue($called); @@ -102,19 +101,19 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds(): public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds(): void { $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(1000, function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan(1000, function () use (&$called): void { $called = true; }); - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $kernel->handle($input, new ConsoleOutput); $this->assertFalse($called); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); + CarbonImmutable::setTestNow($now->addSecond()); $kernel->terminate($input, 21); $this->assertFalse($called); @@ -122,14 +121,14 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds( public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime(): void { - $this->freezeSecond(); + $now = $this->freezeSecond(); $input = new StringInput('foo'); $called = false; $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); - $kernel->whenCommandLifecycleIsLongerThan(CarbonImmutable::now()->addSecond()->addMillisecond(), function () use (&$called) { + $kernel->command('foo', fn (): null => null); + $kernel->whenCommandLifecycleIsLongerThan($now->addSecond()->addMillisecond(), function () use (&$called): void { $called = true; }); @@ -137,7 +136,7 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime(): void $this->assertFalse($called); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)->addMillisecond()); + CarbonImmutable::setTestNow($now->addSecond()->addMillisecond()); $kernel->terminate($input, 21); @@ -146,12 +145,12 @@ public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime(): void public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime(): void { - $this->freezeSecond(); + $now = $this->freezeSecond(); $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $called = false; - $kernel->whenCommandLifecycleIsLongerThan(CarbonImmutable::now()->addSecond()->addMillisecond(), function () use (&$called) { + $kernel->whenCommandLifecycleIsLongerThan($now->addSecond()->addMillisecond(), function () use (&$called): void { $called = true; }); @@ -159,7 +158,7 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime(): v $this->assertFalse($called); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(1)); + CarbonImmutable::setTestNow($now->addSecond()); $kernel->terminate($input, 21); $this->assertFalse($called); @@ -168,7 +167,7 @@ public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime(): v public function testItClearsStartTimeAfterHandlingCommand(): void { $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $input = new StringInput('foo'); $this->assertNull($kernel->commandStartedAt()); @@ -184,21 +183,21 @@ public function testItClearsStartTimeAfterHandlingCommand(): void public function testUsesTheConfiguredDateTimezone(): void { - Config::set('app.timezone', 'UTC'); + config(['app.timezone' => 'UTC']); $startedAt = null; $kernel = $this->app->make(Kernel::class); - $kernel->command('foo', fn () => null); + $kernel->command('foo', fn (): null => null); $kernel->whenCommandLifecycleIsLongerThan(0, function (CarbonImmutable $started) use (&$startedAt, $kernel): void { $startedAt = $started; $this->assertSame($started, $kernel->commandStartedAt()); }); - Config::set('app.timezone', 'Australia/Melbourne'); - CarbonImmutable::setTestNow(CarbonImmutable::now()); + config(['app.timezone' => 'Australia/Melbourne']); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $kernel->handle($input = new StringInput('foo'), new ConsoleOutput); - CarbonImmutable::setTestNow(now()->addMinute()); + CarbonImmutable::setTestNow($now->addMinute()); $kernel->terminate($input, 21); $this->assertSame(CarbonImmutable::class, $startedAt::class); diff --git a/tests/Integration/Console/EnvironmentDecryptCommandTest.php b/tests/Integration/Console/EnvironmentDecryptCommandTest.php index aaaa719ab1..7489bf4439 100644 --- a/tests/Integration/Console/EnvironmentDecryptCommandTest.php +++ b/tests/Integration/Console/EnvironmentDecryptCommandTest.php @@ -17,14 +17,16 @@ class EnvironmentDecryptCommandTest extends TestCase { protected Filesystem $filesystem; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); - $this->filesystem = m::spy(Filesystem::class); + $this->filesystem = File::spy(); $this->filesystem->shouldReceive('replace'); $this->filesystem->shouldReceive('chmod')->andReturn('0640'); - File::swap($this->filesystem); } public function testItFailsWithInvalidCipherFails(): void @@ -77,7 +79,7 @@ public function testItPreservesCancellationWhileReadingTheEncryptedEnvironment() } } - public function testItFailsWhenEncryptionFileCannotBeFound(): void + public function testItFailsWhenEnvironmentFileExists(): void { $this->filesystem->shouldReceive('exists')->andReturn(true); @@ -86,7 +88,7 @@ public function testItFailsWhenEncryptionFileCannotBeFound(): void ->assertExitCode(1); } - public function testItFailsWhenEnvironmentFileExists(): void + public function testItFailsWhenEncryptionFileCannotBeFound(): void { $this->filesystem->shouldReceive('exists')->andReturn(false); diff --git a/tests/Integration/Console/EnvironmentEncryptCommandTest.php b/tests/Integration/Console/EnvironmentEncryptCommandTest.php index eda3c53152..0fcaab102d 100644 --- a/tests/Integration/Console/EnvironmentEncryptCommandTest.php +++ b/tests/Integration/Console/EnvironmentEncryptCommandTest.php @@ -17,16 +17,18 @@ class EnvironmentEncryptCommandTest extends TestCase { protected Filesystem $filesystem; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); - $this->filesystem = m::spy(Filesystem::class); + $this->filesystem = File::spy(); $this->filesystem->shouldReceive('get') ->andReturn('APP_NAME=Hypervel'); $this->filesystem->shouldReceive('replace'); $this->filesystem->shouldReceive('chmod')->andReturn('0640'); - File::swap($this->filesystem); } public function testItFailsWithInvalidCipherFails(): void @@ -196,29 +198,25 @@ public function testItEncryptsWithGivenGeneratedBase64KeyAndDisplaysIt(): void public function testItEncryptsInReadableFormat(): void { - $filesystem = m::mock(Filesystem::class); - $filesystem->shouldReceive('exists') + File::swap(m::mock(Filesystem::class)); + + File::expects('exists') ->with(base_path('.env')) - ->once() ->andReturn(true); - $filesystem->shouldReceive('exists') + File::expects('exists') ->with(base_path('.env.encrypted')) - ->once() ->andReturn(false); - $filesystem->shouldReceive('get') + File::expects('get') ->with(base_path('.env')) - ->once() ->andReturn("APP_NAME=Hypervel\nAPP_ENV=local"); - $filesystem->shouldReceive('replace') - ->once() - ->with(base_path('.env.encrypted'), m::on(function ($content) { + File::expects('replace') + ->with(base_path('.env.encrypted'), m::on(function (string $content): bool { $lines = explode("\n", rtrim($content)); return count($lines) === 2 && str_starts_with($lines[0], 'APP_NAME=') && str_starts_with($lines[1], 'APP_ENV='); }), null); - File::swap($filesystem); $this->artisan('env:encrypt', ['--readable' => true, '--key' => 'ANvVbPbE0tWMHpUySh6liY4WaCmAYKXP']) ->expectsOutputToContain('Environment successfully encrypted') @@ -227,22 +225,19 @@ public function testItEncryptsInReadableFormat(): void public function testItSkipsCommentsAndBlankLinesInReadableFormat(): void { - $filesystem = m::mock(Filesystem::class); - $filesystem->shouldReceive('exists') + File::swap(m::mock(Filesystem::class)); + + File::expects('exists') ->with(base_path('.env')) - ->once() ->andReturn(true); - $filesystem->shouldReceive('exists') + File::expects('exists') ->with(base_path('.env.encrypted')) - ->once() ->andReturn(false); - $filesystem->shouldReceive('get') + File::expects('get') ->with(base_path('.env')) - ->once() ->andReturn("# Comment\nAPP_NAME=Hypervel\n\nAPP_ENV=local"); - $filesystem->shouldReceive('replace') - ->once() - ->with(base_path('.env.encrypted'), m::on(function ($content) { + File::expects('replace') + ->with(base_path('.env.encrypted'), m::on(function (string $content): bool { $lines = explode("\n", rtrim($content)); // Comments and blank lines are skipped @@ -250,7 +245,6 @@ public function testItSkipsCommentsAndBlankLinesInReadableFormat(): void && str_starts_with($lines[0], 'APP_NAME=') && str_starts_with($lines[1], 'APP_ENV='); }), null); - File::swap($filesystem); $this->artisan('env:encrypt', ['--readable' => true, '--key' => 'ANvVbPbE0tWMHpUySh6liY4WaCmAYKXP']) ->expectsOutputToContain('Environment successfully encrypted') @@ -273,27 +267,23 @@ public function testItEncryptsMultiLineValuesInReadableFormat(): void $encryptedOutput = null; - $filesystem = m::mock(Filesystem::class); - $filesystem->shouldReceive('exists') + File::swap(m::mock(Filesystem::class)); + + File::expects('exists') ->with(base_path('.env')) - ->once() ->andReturn(true); - $filesystem->shouldReceive('exists') + File::expects('exists') ->with(base_path('.env.encrypted')) - ->once() ->andReturn(false); - $filesystem->shouldReceive('get') + File::expects('get') ->with(base_path('.env')) - ->once() ->andReturn($originalContent); - $filesystem->shouldReceive('replace') - ->once() - ->with(base_path('.env.encrypted'), m::on(function ($content) use (&$encryptedOutput) { + File::expects('replace') + ->with(base_path('.env.encrypted'), m::on(function (string $content) use (&$encryptedOutput): bool { $encryptedOutput = $content; return true; }), null); - File::swap($filesystem); $this->artisan('env:encrypt', ['--readable' => true, '--key' => $key]) ->expectsOutputToContain('Environment successfully encrypted') @@ -326,27 +316,23 @@ public function testItEncryptsVariableReferencesInReadableFormat(): void $encryptedOutput = null; - $filesystem = m::mock(Filesystem::class); - $filesystem->shouldReceive('exists') + File::swap(m::mock(Filesystem::class)); + + File::expects('exists') ->with(base_path('.env')) - ->once() ->andReturn(true); - $filesystem->shouldReceive('exists') + File::expects('exists') ->with(base_path('.env.encrypted')) - ->once() ->andReturn(false); - $filesystem->shouldReceive('get') + File::expects('get') ->with(base_path('.env')) - ->once() ->andReturn($originalContent); - $filesystem->shouldReceive('replace') - ->once() - ->with(base_path('.env.encrypted'), m::on(function ($content) use (&$encryptedOutput) { + File::expects('replace') + ->with(base_path('.env.encrypted'), m::on(function (string $content) use (&$encryptedOutput): bool { $encryptedOutput = $content; return true; }), null); - File::swap($filesystem); $this->artisan('env:encrypt', ['--readable' => true, '--key' => $key]) ->expectsOutputToContain('Environment successfully encrypted') @@ -380,27 +366,23 @@ public function testItSkipsInvalidEnvLinesInReadableFormat(): void $encryptedOutput = null; - $filesystem = m::mock(Filesystem::class); - $filesystem->shouldReceive('exists') + File::swap(m::mock(Filesystem::class)); + + File::expects('exists') ->with(base_path('.env')) - ->once() ->andReturn(true); - $filesystem->shouldReceive('exists') + File::expects('exists') ->with(base_path('.env.encrypted')) - ->once() ->andReturn(false); - $filesystem->shouldReceive('get') + File::expects('get') ->with(base_path('.env')) - ->once() ->andReturn($originalContent); - $filesystem->shouldReceive('replace') - ->once() - ->with(base_path('.env.encrypted'), m::on(function ($content) use (&$encryptedOutput) { + File::expects('replace') + ->with(base_path('.env.encrypted'), m::on(function (string $content) use (&$encryptedOutput): bool { $encryptedOutput = $content; return true; }), null); - File::swap($filesystem); $this->artisan('env:encrypt', ['--readable' => true, '--key' => $key]) ->expectsOutputToContain('Environment successfully encrypted') @@ -433,27 +415,23 @@ public function testItEncryptsSpecialCharactersInReadableFormat(): void $encryptedOutput = null; - $filesystem = m::mock(Filesystem::class); - $filesystem->shouldReceive('exists') + File::swap(m::mock(Filesystem::class)); + + File::expects('exists') ->with(base_path('.env')) - ->once() ->andReturn(true); - $filesystem->shouldReceive('exists') + File::expects('exists') ->with(base_path('.env.encrypted')) - ->once() ->andReturn(false); - $filesystem->shouldReceive('get') + File::expects('get') ->with(base_path('.env')) - ->once() ->andReturn($originalContent); - $filesystem->shouldReceive('replace') - ->once() - ->with(base_path('.env.encrypted'), m::on(function ($content) use (&$encryptedOutput) { + File::expects('replace') + ->with(base_path('.env.encrypted'), m::on(function (string $content) use (&$encryptedOutput): bool { $encryptedOutput = $content; return true; }), null); - File::swap($filesystem); $this->artisan('env:encrypt', ['--readable' => true, '--key' => $key]) ->expectsOutputToContain('Environment successfully encrypted') diff --git a/tests/Integration/Console/Scheduling/CallbackEventTest.php b/tests/Integration/Console/Scheduling/CallbackEventTest.php index e7238e3221..e18833941b 100644 --- a/tests/Integration/Console/Scheduling/CallbackEventTest.php +++ b/tests/Integration/Console/Scheduling/CallbackEventTest.php @@ -7,19 +7,32 @@ use Exception; use Hypervel\Console\Scheduling\CallbackEvent; use Hypervel\Console\Scheduling\EventMutex; +use Hypervel\Support\Stringable; use Hypervel\Testbench\TestCase; -use Mockery as m; +use Hypervel\Tests\Console\Fixtures\FakeEventMutex; class CallbackEventTest extends TestCase { - public function testDefaultResultIsSuccess() + private EventMutex $mutex; + + /** + * Set up the test environment. + */ + protected function setUp(): void + { + parent::setUp(); + + $this->mutex = new FakeEventMutex; + } + + public function testDefaultResultIsSuccess(): void { $success = null; - $event = (new CallbackEvent(m::mock(EventMutex::class), function () { - }))->onSuccess(function () use (&$success) { + $event = (new CallbackEvent($this->mutex, function (): void { + }))->onSuccess(function () use (&$success): void { $success = true; - })->onFailure(function () use (&$success) { + })->onFailure(function () use (&$success): void { $success = false; }); @@ -28,15 +41,15 @@ public function testDefaultResultIsSuccess() $this->assertTrue($success); } - public function testFalseResponseIsFailure() + public function testFalseResponseIsFailure(): void { $success = null; - $event = (new CallbackEvent(m::mock(EventMutex::class), function () { + $event = (new CallbackEvent($this->mutex, function (): bool { return false; - }))->onSuccess(function () use (&$success) { + }))->onSuccess(function () use (&$success): void { $success = true; - })->onFailure(function () use (&$success) { + })->onFailure(function () use (&$success): void { $success = false; }); @@ -45,15 +58,15 @@ public function testFalseResponseIsFailure() $this->assertFalse($success); } - public function testExceptionIsFailure() + public function testExceptionIsFailure(): void { $success = null; - $event = (new CallbackEvent(m::mock(EventMutex::class), function () { + $event = (new CallbackEvent($this->mutex, function (): never { throw new Exception; - }))->onSuccess(function () use (&$success) { + }))->onSuccess(function () use (&$success): void { $success = true; - })->onFailure(function () use (&$success) { + })->onFailure(function () use (&$success): void { $success = false; }); @@ -65,9 +78,9 @@ public function testExceptionIsFailure() $this->assertFalse($success); } - public function testExceptionBubbles() + public function testExceptionBubbles(): void { - $event = new CallbackEvent(m::mock(EventMutex::class), function () { + $event = new CallbackEvent($this->mutex, function (): never { throw new Exception; }); @@ -75,4 +88,55 @@ public function testExceptionBubbles() $event->run($this->app); } + + public function testOnSuccessCallbackCanReceiveEvent(): void + { + $callbackEvent = null; + + $event = (new CallbackEvent($this->mutex, function (): void { + }))->onSuccess(function (CallbackEvent $event) use (&$callbackEvent): void { + $callbackEvent = $event; + }); + + $event->run($this->app); + + $this->assertSame($event, $callbackEvent); + } + + public function testOnFailureCallbackCanReceiveEvent(): void + { + $callbackEvent = null; + + $event = (new CallbackEvent($this->mutex, function (): bool { + return false; + }))->onFailure(function (CallbackEvent $event) use (&$callbackEvent): void { + $callbackEvent = $event; + }); + + $event->run($this->app); + + $this->assertSame($event, $callbackEvent); + } + + public function testOutputCallbackCanReceiveEvent(): void + { + $callbackEvent = null; + $outputValue = null; + + $event = (new CallbackEvent($this->mutex, function (): void { + }))->onSuccess(function (Stringable $output, CallbackEvent $event) use (&$callbackEvent, &$outputValue): void { + $callbackEvent = $event; + $outputValue = (string) $output; + }); + $event->sendOutputTo($outputPath = $this->app->storagePath('logs/callback-event-output-test.log')); + + try { + $event->run($this->app); + + $this->assertSame($event, $callbackEvent); + $this->assertSame('', $outputValue); + } finally { + $this->app->make('files')->delete($outputPath); + } + } } diff --git a/tests/Integration/Console/Scheduling/EventPingTest.php b/tests/Integration/Console/Scheduling/EventPingTest.php index 60d8968224..249bb826a6 100644 --- a/tests/Integration/Console/Scheduling/EventPingTest.php +++ b/tests/Integration/Console/Scheduling/EventPingTest.php @@ -10,19 +10,18 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response as Psr7Response; use Hypervel\Console\Scheduling\Event; -use Hypervel\Console\Scheduling\EventMutex; use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\Debug\ExceptionHandler; use Hypervel\Testbench\TestCase; +use Hypervel\Tests\Console\Fixtures\FakeEventMutex; use Mockery as m; class EventPingTest extends TestCase { - public function testPingRescuesTransferExceptions() + public function testPingRescuesTransferExceptions(): void { $this->spy(ExceptionHandler::class) - ->shouldReceive('report') - ->once() + ->expects('report') ->with(m::type(ServerException::class)); $httpMock = new HttpClient([ @@ -33,12 +32,12 @@ public function testPingRescuesTransferExceptions() $this->swap(HttpClient::class, $httpMock); - $event = new Event(m::mock(EventMutex::class), 'php -i'); + $event = new Event(new FakeEventMutex, 'php -i'); $thenCalled = false; $event->pingBefore('https://httpstat.us/500') - ->then(function () use (&$thenCalled) { + ->then(function () use (&$thenCalled): void { $thenCalled = true; }); diff --git a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php index 45662e52ba..9be854448f 100644 --- a/tests/Integration/Console/Scheduling/ScheduleGroupTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleGroupTest.php @@ -2,26 +2,28 @@ declare(strict_types=1); -namespace Hypervel\Tests\Integration\Console\Scheduling\ScheduleGroupTest; +namespace Hypervel\Tests\Integration\Console\Scheduling; use Carbon\CarbonInterface; use Hypervel\Console\Scheduling\Event; use Hypervel\Console\Scheduling\Schedule as ScheduleClass; +use Hypervel\Contracts\Foundation\Application; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schedule; +use Hypervel\Support\Stringable; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Queue\Fixtures\JobToTestWithSchedule; use PHPUnit\Framework\Attributes\DataProvider; class ScheduleGroupTest extends TestCase { - public function testGroupCanSetScheduleCronExpression() + public function testGroupCanSetScheduleCronExpression(): void { $schedule = new ScheduleClass; $schedule ->daily() - ->group(function (ScheduleClass $schedule) { + ->group(function (ScheduleClass $schedule): void { $schedule->command('inspire'); }); @@ -29,9 +31,9 @@ public function testGroupCanSetScheduleCronExpression() $this->assertSame('0 0 * * *', $events[0]->expression); } - public function testGroupedScheduleCanOverrideGroupCronExpression() + public function testGroupedScheduleCanOverrideGroupCronExpression(): void { - Schedule::daily()->group(function () { + Schedule::daily()->group(function (): void { Schedule::command('inspire'); Schedule::command('inspire') ->twiceDaily(); @@ -42,11 +44,11 @@ public function testGroupedScheduleCanOverrideGroupCronExpression() $this->assertSame('0 1,13 * * *', $events[1]->expression); } - public function testGroupCanSetScheduleRepeatSeconds() + public function testGroupCanSetScheduleRepeatSeconds(): void { Schedule::everyMinute() ->everyThirtySeconds() - ->group(function () { + ->group(function (): void { Schedule::command('inspire'); }); @@ -55,11 +57,11 @@ public function testGroupCanSetScheduleRepeatSeconds() $this->assertSame('* * * * *', $events[0]->expression); } - public function testGroupedScheduleCanOverrideGroupRepeatSeconds() + public function testGroupedScheduleCanOverrideGroupRepeatSeconds(): void { Schedule::everyMinute() ->everyThirtySeconds() - ->group(function () { + ->group(function (): void { Schedule::command('inspire'); Schedule::command('inspire') ->everyTwentySeconds(); @@ -73,13 +75,13 @@ public function testGroupedScheduleCanOverrideGroupRepeatSeconds() $this->assertSame('* * * * *', $events[1]->expression); } - public function testGroupedScheduleCanBeNested() + public function testGroupedScheduleCanBeNested(): void { Schedule::daily() ->timezone('UTC') - ->group(function () { + ->group(function (): void { Schedule::command('inspire'); - Schedule::timezone('Asia/Dhaka')->group(function () { + Schedule::timezone('Asia/Dhaka')->group(function (): void { Schedule::command('inspire'); }); }); @@ -89,9 +91,9 @@ public function testGroupedScheduleCanBeNested() $this->assertSame('Asia/Dhaka', $events[1]->timezone); } - public function testGroupCanApplyAttributesToSchedules() + public function testGroupCanApplyAttributesToSchedules(): void { - Schedule::withAttributes(['team' => 'platform'])->group(function () { + Schedule::withAttributes(['team' => 'platform'])->group(function (): void { Schedule::command('inspire'); }); @@ -100,9 +102,9 @@ public function testGroupCanApplyAttributesToSchedules() $this->assertSame(['team' => 'platform'], $events[0]->attributes); } - public function testGroupAttributesAreNotDuplicatedOnPendingSchedules() + public function testGroupAttributesAreNotDuplicatedOnPendingSchedules(): void { - Schedule::withAttributes(['team' => 'platform'])->group(function () { + Schedule::withAttributes(['team' => 'platform'])->group(function (): void { Schedule::dailyAt('09:00')->command('inspire'); }); @@ -112,9 +114,9 @@ public function testGroupAttributesAreNotDuplicatedOnPendingSchedules() $this->assertSame('0 9 * * *', $events[0]->expression); } - public function testGroupAttributesAreMergedWithPendingAttributes() + public function testGroupAttributesAreMergedWithPendingAttributes(): void { - Schedule::withAttributes(['team' => 'platform'])->group(function () { + Schedule::withAttributes(['team' => 'platform'])->group(function (): void { Schedule::withAttributes(['tagName' => 'import-premium-podcasts']) ->command('audio:import-podcasts --only-premium'); }); @@ -128,9 +130,9 @@ public function testGroupAttributesAreMergedWithPendingAttributes() } #[DataProvider('groupAttributes')] - public function testGroupCanApplyAttributeToSchedules(string $property, mixed $value) + public function testGroupCanApplyAttributeToSchedules(string $property, mixed $value): void { - Schedule::$property($value)->group(function () { + Schedule::$property($value)->group(function (): void { Schedule::command('inspire'); }); @@ -145,10 +147,13 @@ public function testGroupCanApplyAttributeToSchedules(string $property, mixed $v } } + /** + * Get the group attribute cases. + */ public static function groupAttributes(): array { return [ - 'user' => ['user', fake()->userName()], + // REMOVED: user(); coroutine tasks share the scheduler's OS user. 'timezone' => ['timezone', fake()->timezone()], 'onOneServer' => ['onOneServer', true], 'environments' => [ @@ -168,13 +173,13 @@ public function testGroupedScheduleExecution(CarbonInterface $time, array $expec CarbonImmutable::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::days([1, 2, 3, 4, 5, 6])->group(function (): void { + Schedule::between('07:00', '08:00')->group(function (): void { + Schedule::call(fn (): string => 'Task 1')->everyMinute(); + Schedule::call(fn (): string => 'Task 2')->everyFiveMinutes(); }); - Schedule::call(fn () => 'Task 3')->at('08:05'); + Schedule::call(fn (): string => 'Task 3')->at('08:05'); }); $events = Schedule::events(); @@ -189,6 +194,9 @@ public function testGroupedScheduleExecution(CarbonInterface $time, array $expec } } + /** + * Get the grouped execution cases. + */ public static function scheduleTestCases(): array { return [ @@ -213,7 +221,10 @@ public static function scheduleTestCases(): array ]; } - private function assertTaskExecution($event, $app, $expected, $message): void + /** + * Assert whether the scheduled task should run. + */ + private function assertTaskExecution(Event $event, Application $app, bool $expected, string $message): void { $this->assertSame( $expected, @@ -222,10 +233,10 @@ private function assertTaskExecution($event, $app, $expected, $message): void ); } - public function testGroupedPendingEventAttribute() + public function testGroupedPendingEventAttribute(): void { $schedule = new ScheduleClass; - $schedule->weekdays()->group(function ($schedule) { + $schedule->weekdays()->group(function (ScheduleClass $schedule): void { $schedule->command('inspire')->at('00:00'); // this is event, not pending attribute $schedule->at('01:00')->command('inspire'); // this is pending attribute $schedule->command('inspire'); // this goes back to group pending attribute @@ -237,10 +248,10 @@ public function testGroupedPendingEventAttribute() $this->assertSame('* * * * 1-5', $events[2]->expression); } - public function testGroupedPendingEventAttributesWithoutOverlapping() + public function testGroupedPendingEventAttributesWithoutOverlapping(): void { $schedule = new ScheduleClass; - $schedule->weekdays()->withoutOverlapping()->group(function ($schedule) { + $schedule->weekdays()->withoutOverlapping()->group(function (ScheduleClass $schedule): void { $schedule->command('inspire')->at('14:00'); // this is event, not pending attribute $schedule->at('03:00')->command('inspire'); // this is pending attribute $schedule->command('inspire'); // this goes back to group pending attribute @@ -254,12 +265,12 @@ public function testGroupedPendingEventAttributesWithoutOverlapping() $this->assertSame('0 4 * * 1-5', $events[3]->expression); } - public function testGroupCanOptOutOfReleaseOnTerminationSignals() + public function testGroupCanOptOutOfReleaseOnTerminationSignals(): void { $schedule = new ScheduleClass; $schedule->daily() ->withoutOverlapping(1440, releaseOnTerminationSignals: false) - ->group(function ($schedule) { + ->group(function (ScheduleClass $schedule): void { $schedule->command('inspire'); }); @@ -268,42 +279,338 @@ public function testGroupCanOptOutOfReleaseOnTerminationSignals() $this->assertFalse($events[0]->releaseOnTerminationSignals); } - public function testGroupAppliesEventMacrosToAllEvents() + public function testGroupAppliesEventMacrosToAllEvents(): void { - Event::macro('groupTestAttribute', function () { - return $this->withAttributes(['macro' => 'applied']); + Event::macro('sentryMonitor', function (): Event { + return $this->withAttributes(['sentryMonitored' => true]); }); $schedule = new ScheduleClass; - $schedule->daily()->groupTestAttribute()->group(function ($schedule) { + $schedule->daily()->sentryMonitor()->group(function (ScheduleClass $schedule): void { $schedule->command('inspire'); $schedule->command('inspire'); }); $events = $schedule->events(); - $this->assertSame(['macro' => 'applied'], $events[0]->attributes); - $this->assertSame(['macro' => 'applied'], $events[1]->attributes); + $this->assertTrue($events[0]->attributes['sentryMonitored']); + $this->assertTrue($events[1]->attributes['sentryMonitored']); $this->assertSame('0 0 * * *', $events[0]->expression); $this->assertSame('0 0 * * *', $events[1]->expression); } - public function testGroupAppliesLifecycleCallbacksToAllEvents() + public function testGroupAppliesEventMacroCalledBeforeBuiltInAttributes(): void { - $calls = 0; + Event::macro('sentryMonitor', function (): Event { + return $this->withAttributes(['sentryMonitored' => true]); + }); $schedule = new ScheduleClass; - $schedule->daily()->after(function () use (&$calls) { - ++$calls; - })->group(function ($schedule) { + $schedule->sentryMonitor()->daily()->onOneServer()->group(function (ScheduleClass $schedule): void { $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertTrue($events[0]->attributes['sentryMonitored']); + $this->assertTrue($events[0]->onOneServer); + $this->assertSame('0 0 * * *', $events[0]->expression); + } + + public function testGroupAppliesMultipleEventMacros(): void + { + Event::macro('sentryMonitor', function (): Event { + return $this->withAttributes(['sentryMonitored' => true]); + }); + Event::macro('customTag', function (string $tag): Event { + return $this->withAttributes(['customTag' => $tag]); + }); + + $schedule = new ScheduleClass; + $schedule->daily()->sentryMonitor()->customTag('billing')->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertTrue($events[0]->attributes['sentryMonitored']); + $this->assertSame('billing', $events[0]->attributes['customTag']); + $this->assertTrue($events[1]->attributes['sentryMonitored']); + $this->assertSame('billing', $events[1]->attributes['customTag']); + } + + public function testNestedGroupInheritsEventMacros(): void + { + Event::macro('sentryMonitor', function (): Event { + return $this->withAttributes(['sentryMonitored' => true]); + }); + + $schedule = new ScheduleClass; + $schedule->daily()->sentryMonitor()->group(function (ScheduleClass $schedule): void { $schedule->command('inspire'); + $schedule->weekly()->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + }); }); $events = $schedule->events(); + $this->assertTrue($events[0]->attributes['sentryMonitored']); + $this->assertSame('0 0 * * *', $events[0]->expression); + $this->assertTrue($events[1]->attributes['sentryMonitored']); + $this->assertSame('0 0 * * 0', $events[1]->expression); + } + + public function testGroupAppliesEventMacrosOnceToPendingSchedules(): void + { + Event::macro('sentryMonitor', function (): Event { + return $this->withAttributes(['sentryMonitored' => ($this->attributes['sentryMonitored'] ?? 0) + 1]); + }); + + $schedule = new ScheduleClass; + $schedule->daily()->sentryMonitor()->group(function (ScheduleClass $schedule): void { + $schedule->at('09:00')->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertSame(1, $events[0]->attributes['sentryMonitored']); + $this->assertSame('0 9 * * *', $events[0]->expression); + } + + public function testGroupAppliesOnFailureCallbackToAllEvents(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls): void { + $calls[] = 'group-failure'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $this->assertCount(2, $events); + + $events[0]->finish(app(), 1); + $events[1]->finish(app(), 1); + + $this->assertSame(['group-failure', 'group-failure'], $calls); + } + + public function testGroupOnFailureCallbackDoesNotRunOnSuccess(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls): void { + $calls[] = 'group-failure'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + + $this->assertSame([], $calls); + } + + public function testGroupAppliesOnSuccessCallbackToAllEvents(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onSuccess(function () use (&$calls): void { + $calls[] = 'group-success'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + $events[1]->finish(app(), 0); + + $this->assertSame(['group-success', 'group-success'], $calls); + } + + public function testGroupAppliesBeforeAndAfterCallbacksToAllEvents(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->before(function () use (&$calls): void { + $calls[] = 'before'; + }) + ->after(function () use (&$calls): void { + $calls[] = 'after'; + }) + ->then(function () use (&$calls): void { + $calls[] = 'then'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->callBeforeCallbacks(app()); + $events[0]->finish(app(), 0); + + $this->assertSame(['before', 'after', 'then'], $calls); + } + + public function testGroupAppliesAfterCallbackOnceToPendingSchedules(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->after(function () use (&$calls): void { + $calls[] = 'after'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->at('09:00')->command('inspire'); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + + $this->assertSame(['after'], $calls); + $this->assertSame('0 9 * * *', $events[0]->expression); + } + + public function testGroupCallbacksCombineWithEventLevelCallbacks(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls): void { + $calls[] = 'group'; + }) + ->group(function (ScheduleClass $schedule) use (&$calls): void { + $schedule->command('inspire')->onFailure(function () use (&$calls): void { + $calls[] = 'event'; + }); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 1); + + $this->assertSame(['group', 'event'], $calls); + } + + public function testNestedGroupInheritsLifecycleCallbacks(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule->daily() + ->onFailure(function () use (&$calls): void { + $calls[] = 'outer'; + }) + ->group(function (ScheduleClass $schedule) use (&$calls): void { + $schedule->command('inspire'); + $schedule->weekly() + ->onFailure(function () use (&$calls): void { + $calls[] = 'inner'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + }); + }); + + $events = $schedule->events(); + $this->assertCount(2, $events); + + $events[0]->finish(app(), 1); + $this->assertSame(['outer'], $calls); + + $events[1]->finish(app(), 1); + $this->assertSame(['outer', 'outer', 'inner'], $calls); + } + + public function testNestedGroupInheritsLifecycleCallbacksOnce(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->after(function () use (&$calls): void { + $calls[] = 'outer'; + }) + ->group(function (ScheduleClass $schedule) use (&$calls): void { + $schedule + ->after(function () use (&$calls): void { + $calls[] = 'inner'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire'); + }); + }); + + $events = $schedule->events(); + $events[0]->finish(app(), 0); + + $this->assertSame(['outer', 'inner'], $calls); + } + + public function testGroupCanStartWithLifecycleCallbackWithoutFrequency(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->before(function () use (&$calls): void { + $calls[] = 'before'; + }) + ->onSuccess(function () use (&$calls): void { + $calls[] = 'success'; + }) + ->onFailure(function () use (&$calls): void { + $calls[] = 'failure'; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire')->daily(); + $schedule->command('inspire')->weekly(); + }); + + $events = $schedule->events(); + $this->assertCount(2, $events); + $this->assertSame('0 0 * * *', $events[0]->expression); + $this->assertSame('0 0 * * 0', $events[1]->expression); + + $events[0]->callBeforeCallbacks(app()); + $events[0]->finish(app(), 0); + $events[1]->callBeforeCallbacks(app()); + $events[1]->finish(app(), 1); + + $this->assertSame(['before', 'success', 'before', 'failure'], $calls); + } + + public function testGroupCanStartWithOutputCallbackWithoutFrequency(): void + { + $calls = []; + + $schedule = new ScheduleClass; + $schedule + ->onFailureWithOutput(function (Event $event, Stringable $output) use (&$calls): void { + $calls[] = 'failure:' . $output; + }) + ->group(function (ScheduleClass $schedule): void { + $schedule->command('inspire')->daily(); + }); + + $events = $schedule->events(); + $this->assertCount(1, $events); + $this->assertSame('0 0 * * *', $events[0]->expression); - $events[0]->callAfterCallbacks(app()); - $events[1]->callAfterCallbacks(app()); + $events[0]->finish(app(), 1); - $this->assertSame(2, $calls); + $this->assertCount(1, $calls); } } diff --git a/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php new file mode 100644 index 0000000000..913d1de3c2 --- /dev/null +++ b/tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php @@ -0,0 +1,295 @@ +app->make(Schedule::class); + $task = $schedule->exec('exit 1') + ->everyMinute(); + + // Allow the task through its filters. + $task->when(function (): bool { + return true; + }); + + // Execute the scheduler + $this->runSchedule($task); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertDispatched(ScheduledTaskFailed::class, function (ScheduledTaskFailed $event) use ($task): bool { + return $event->task === $task + && $event->exception->getMessage() === 'Scheduled command [exit 1] failed with exit code [1].'; + }); + } + + /** + * @throws BindingResolutionException + */ + public function testFailingCommandInBackgroundTriggersEvent(): void + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + // Create a schedule and add the command + $schedule = $this->app->make(Schedule::class); + $task = $schedule->exec('exit 1') + ->everyMinute() + ->runInBackground(); + + // Allow the task through its filters. + $task->when(function (): bool { + return true; + }); + + // Execute the scheduler + $this->runSchedule($task); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + // Background coroutines remain observable, including the process exit status. + Event::assertDispatched(ScheduledTaskFailed::class, function (ScheduledTaskFailed $event) use ($task): bool { + return $event->task === $task + && $event->exception->getMessage() === 'Scheduled command [exit 1] failed with exit code [1].'; + }); + } + + /** + * @throws BindingResolutionException + */ + public function testSuccessfulCommandDoesNotTriggerEvent(): void + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + // Create a schedule and add the command + $schedule = $this->app->make(Schedule::class); + $task = $schedule->exec('exit 0') + ->everyMinute(); + + // Allow the task through its filters. + $task->when(function (): bool { + return true; + }); + + // Execute the scheduler + $this->runSchedule($task); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertNotDispatched(ScheduledTaskFailed::class); + } + + /** + * @throws BindingResolutionException + */ + public function testCommandWithNoExplicitReturnDoesNotTriggerEvent(): void + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + // Create a schedule and add the command that just performs an action without explicit exit + $schedule = $this->app->make(Schedule::class); + $command = PHP_OS_FAMILY === 'Windows' ? 'cmd /c exit 0' : 'true'; + $task = $schedule->exec($command) + ->everyMinute(); + + // Allow the task through its filters. + $task->when(function (): bool { + return true; + }); + + // Execute the scheduler + $this->runSchedule($task); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertNotDispatched(ScheduledTaskFailed::class); + } + + /** + * @throws BindingResolutionException + */ + public function testSuccessfulCommandInBackgroundDoesNotTriggerEvent(): void + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + // Create a schedule and add the command + $schedule = $this->app->make(Schedule::class); + $task = $schedule->exec('exit 0') + ->everyMinute() + ->runInBackground(); + + // Allow the task through its filters. + $task->when(function (): bool { + return true; + }); + + // Execute the scheduler + $this->runSchedule($task); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertNotDispatched(ScheduledTaskFailed::class); + } + + public function testOverlappingTaskFinishedEventIndicatesSkipped(): void + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + $this->app->instance(EventMutex::class, new FakeEventMutex); + + $ran = false; + $schedule = $this->app->make(Schedule::class); + $task = $schedule->call(function () use (&$ran): void { + $ran = true; + })->name('test')->withoutOverlapping()->everyMinute(); + + $this->runSchedule($task); + + Event::assertDispatched(ScheduledTaskStarting::class, function (ScheduledTaskStarting $event) use ($task): bool { + return $event->task === $task; + }); + Event::assertDispatched(ScheduledTaskFinished::class, function (ScheduledTaskFinished $event) use ($task): bool { + return $event->task === $task + && $event->task->skippedBecauseOverlapping === true; + }); + Event::assertNotDispatched(ScheduledTaskFailed::class); + $this->assertFalse($ran); + } + + /** + * @throws BindingResolutionException + */ + public function testCommandWithNoExplicitReturnInBackgroundDoesNotTriggerEvent(): void + { + Event::fake([ + ScheduledTaskStarting::class, + ScheduledTaskFinished::class, + ScheduledTaskFailed::class, + ]); + + // Create a schedule and add the command that just performs an action without explicit exit + $schedule = $this->app->make(Schedule::class); + $command = PHP_OS_FAMILY === 'Windows' ? 'cmd /c exit 0' : 'true'; + $task = $schedule->exec($command) + ->everyMinute() + ->runInBackground(); + + // Allow the task through its filters. + $task->when(function (): bool { + return true; + }); + + // Execute the scheduler + $this->runSchedule($task); + + // Verify the event sequence + Event::assertDispatched(ScheduledTaskStarting::class); + Event::assertDispatched(ScheduledTaskFinished::class); + Event::assertNotDispatched(ScheduledTaskFailed::class); + } + + public function testRepeatEventsDoesNotMutateStartedAt(): void + { + CarbonImmutable::setTestNow('2026-03-25 12:00:30'); + + $command = new ScheduleRunCommand; + $this->app->instance(ScheduleRunCommand::class, $command); + + $reflection = new ReflectionProperty($command, 'startedAt'); + // runOnce() uses Date::now(); a mutable date exercises the copy() guard. + $reflection->setValue($command, Carbon::now()); + $startedAt = $reflection->getValue($command); + + $originalTimestamp = $startedAt->getTimestamp(); + $originalMicro = $startedAt->micro; + + // Call repeatEvents with an empty collection so it exits immediately + $reflection = new ReflectionMethod($command, 'repeatEvents'); + $command->setHypervel($this->app); + + // Set test time past the minute boundary so the while loop exits immediately + CarbonImmutable::setTestNow('2026-03-25 12:01:01'); + $reflection->invoke($command, collect()); + + // startedAt should not have been mutated to end of minute + $startedAtAfter = (new ReflectionProperty($command, 'startedAt'))->getValue($command); + $this->assertEquals($originalTimestamp, $startedAtAfter->getTimestamp()); + $this->assertEquals($originalMicro, $startedAtAfter->micro); + } + + /** + * Run one scheduler iteration and join its task coroutine before returning. + */ + private function runSchedule(ScheduledEvent $task): void + { + $coroutineId = null; + $task->before(function () use (&$coroutineId): void { + $coroutineId = Coroutine::id(); + }); + + try { + $this->artisan('schedule:run', ['--once' => true])->assertSuccessful(); + } finally { + if ($coroutineId !== null) { + Coroutine::join([$coroutineId], 5); + $this->assertFalse(Coroutine::exists($coroutineId), 'The scheduled task did not finish within five seconds.'); + } + } + } +} diff --git a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php index f06abe188a..0d964d32db 100644 --- a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php +++ b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Hypervel\Tests\Integration\Console\Scheduling\SubMinuteSchedulingTest; +namespace Hypervel\Tests\Integration\Console\Scheduling; use Hypervel\Cache\Repository; use Hypervel\Cache\WorkerArrayStore; @@ -26,23 +26,28 @@ class SubMinuteSchedulingTest extends TestCase { protected Schedule $schedule; + /** + * Set up the test environment. + */ protected function setUp(): void { - $this->beforeApplicationDestroyed(function () { - @unlink(storage_path('framework/down')); - }); - parent::setUp(); $cache = new class implements Factory { public Repository $store; + /** + * Create a cache factory for scheduler mutexes. + */ public function __construct() { // Use worker-array because scheduling mutexes must survive across scheduler coroutines. $this->store = new Repository(new WorkerArrayStore(true)); } + /** + * Get the shared cache store. + */ public function store(UnitEnum|string|null $name = null): Repository { return $this->store; @@ -102,6 +107,9 @@ public function testItRunsSubMinuteCallbacks(string $frequency, int $expectedRun $this->assertEquals($expectedRuns, $runs); } + /** + * Get the sub-minute frequencies and expected execution counts. + */ public static function frequencyProvider(): array { return [ @@ -212,7 +220,7 @@ public function testSubMinuteEventsCanBeRunInMaintenanceMode(): void Sleep::whenFakingSleep(function ($duration) use ($startedAt) { CarbonImmutable::setTestNow(now()->add($duration)); - if (now()->diffInSeconds($startedAt) >= 30 && ! $this->app->isDownForMaintenance()) { + if ($startedAt->diffInSeconds() >= 30 && ! $this->app->isDownForMaintenance()) { $this->artisan('down'); } }); @@ -222,6 +230,7 @@ public function testSubMinuteEventsCanBeRunInMaintenanceMode(): void Sleep::assertSleptTimes(600); $this->assertEquals(60, $runs); + $this->assertTrue($this->app->isDownForMaintenance()); } public function testSubMinuteEventsCanBeRunWhenScheduleIsPaused(): void diff --git a/tests/Integration/Cookie/CookieTest.php b/tests/Integration/Cookie/CookieTest.php new file mode 100644 index 0000000000..0950881294 --- /dev/null +++ b/tests/Integration/Cookie/CookieTest.php @@ -0,0 +1,60 @@ + true]); + + Route::get('/', function (): string { + return 'hello world'; + })->middleware('web'); + + $response = $this->get('/'); + $this->assertCount(2, $response->headers->getCookies()); + $this->assertEquals(0, $response->headers->getCookies()[1]->getExpiresTime()); + } + + public function testCookieIsSentBackWithProperExpireTimeWithRespectToLifetime(): void + { + config(['session.expire_on_close' => false, 'session.lifetime' => 1]); + + Route::get('/', function (): string { + return 'hello world'; + })->middleware('web'); + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $response = $this->get('/'); + $this->assertCount(2, $response->headers->getCookies()); + $this->assertEquals($now->addMinute()->getTimestamp(), $response->headers->getCookies()[1]->getExpiresTime()); + } + + /** + * Define the test environment. + */ + protected function defineEnvironment(Application $app): void + { + Exceptions::spy()->shouldReceive('render')->andReturn(new Response); + + $app->make('config')->set('app.key', Str::random(32)); + $app->make('config')->set('session.driver', 'fake-null'); + + Session::extend('fake-null', function (): NullSessionHandler { + return new NullSessionHandler; + }); + } +} diff --git a/tests/Integration/Database/MariaDb/EloquentCastTest.php b/tests/Integration/Database/MariaDb/EloquentCastTest.php index 3c857d1bd1..ef3534a92e 100644 --- a/tests/Integration/Database/MariaDb/EloquentCastTest.php +++ b/tests/Integration/Database/MariaDb/EloquentCastTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Integration\Database\MariaDb; +use Hypervel\Database\Schema\Blueprint; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; use Hypervel\Tests\Integration\Database\Fixtures\Models\IntTimestampCasts\UserWithIntTimestampsViaAttribute; @@ -20,14 +21,14 @@ class EloquentCastTest extends MariaDbTestCase */ protected function afterRefreshingDatabase(): void { - Schema::create('users', function ($table) { + Schema::create('users', function (Blueprint $table): void { $table->increments('id'); $table->string('email')->unique(); $table->integer('created_at'); $table->integer('updated_at'); }); - Schema::create('users_nullable_timestamps', function ($table) { + Schema::create('users_nullable_timestamps', function (Blueprint $table): void { $table->increments('id'); $table->string('email')->unique(); $table->timestamp('created_at')->nullable(); @@ -41,12 +42,13 @@ protected function afterRefreshingDatabase(): void protected function destroyDatabaseMigrations(): void { Schema::drop('users'); + Schema::drop('users_nullable_timestamps'); } public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): void { - CarbonImmutable::setTestNow(now()); - $createdAt = now()->timestamp; + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $createdAt = $now->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -58,12 +60,12 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): v 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($createdAt, $castUser->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($createdAt, $attributeUser->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->updated_at->getTimestamp()); $castUser->update([ 'email' => fake()->unique()->email, @@ -75,21 +77,21 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): v 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($createdAt, $castUser->updated_at->timestamp); - $this->assertSame($createdAt, $castUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($createdAt, $attributeUser->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->fresh()->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->fresh()->updated_at->getTimestamp()); } public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void { - CarbonImmutable::setTestNow(now()); - $createdAt = now()->timestamp; + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $createdAt = $now->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -101,15 +103,15 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($createdAt, $castUser->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($createdAt, $attributeUser->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->updated_at->getTimestamp()); - CarbonImmutable::setTestNow(now()->addSecond()); - $updatedAt = now()->timestamp; + CarbonImmutable::setTestNow($now = $now->addSecond()); + $updatedAt = $now->getTimestamp(); $castUser->update([ 'email' => fake()->unique()->email, @@ -121,20 +123,20 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($updatedAt, $castUser->updated_at->timestamp); - $this->assertSame($updatedAt, $castUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($updatedAt, $attributeUser->updated_at->timestamp); - $this->assertSame($updatedAt, $attributeUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($updatedAt, $mutatorUser->updated_at->timestamp); - $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($updatedAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $castUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($updatedAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $attributeUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($updatedAt, $mutatorUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->getTimestamp()); } public function testItCastTimestampsUpdatedByAMutator(): void { - CarbonImmutable::setTestNow(now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $mutatorUser = UserWithUpdatedAtViaMutator::create([ 'email' => fake()->unique()->email, @@ -142,14 +144,14 @@ public function testItCastTimestampsUpdatedByAMutator(): void $this->assertNull($mutatorUser->updated_at); - CarbonImmutable::setTestNow(now()->addSecond()); - $updatedAt = now()->timestamp; + CarbonImmutable::setTestNow($now = $now->addSecond()); + $updatedAt = $now->getTimestamp(); $mutatorUser->update([ 'email' => fake()->unique()->email, ]); - $this->assertSame($updatedAt, $mutatorUser->updated_at->timestamp); - $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->timestamp); + $this->assertSame($updatedAt, $mutatorUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->getTimestamp()); } } diff --git a/tests/Integration/Database/MySql/EloquentCastTest.php b/tests/Integration/Database/MySql/EloquentCastTest.php index 8b5755fdac..f72de3086a 100644 --- a/tests/Integration/Database/MySql/EloquentCastTest.php +++ b/tests/Integration/Database/MySql/EloquentCastTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Integration\Database\MySql; +use Hypervel\Database\Schema\Blueprint; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Schema; use Hypervel\Tests\Integration\Database\Fixtures\Models\IntTimestampCasts\UserWithIntTimestampsViaAttribute; @@ -20,14 +21,14 @@ class EloquentCastTest extends MySqlTestCase */ protected function afterRefreshingDatabase(): void { - Schema::create('users', function ($table) { + Schema::create('users', function (Blueprint $table): void { $table->increments('id'); $table->string('email')->unique(); $table->integer('created_at'); $table->integer('updated_at'); }); - Schema::create('users_nullable_timestamps', function ($table) { + Schema::create('users_nullable_timestamps', function (Blueprint $table): void { $table->increments('id'); $table->string('email')->unique(); $table->timestamp('created_at')->nullable(); @@ -41,12 +42,13 @@ protected function afterRefreshingDatabase(): void protected function destroyDatabaseMigrations(): void { Schema::drop('users'); + Schema::drop('users_nullable_timestamps'); } public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): void { - CarbonImmutable::setTestNow(now()); - $createdAt = now()->timestamp; + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $createdAt = $now->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -58,12 +60,12 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): v 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($createdAt, $castUser->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($createdAt, $attributeUser->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->updated_at->getTimestamp()); $castUser->update([ 'email' => fake()->unique()->email, @@ -75,21 +77,21 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasNotPassed(): v 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($createdAt, $castUser->updated_at->timestamp); - $this->assertSame($createdAt, $castUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($createdAt, $attributeUser->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->fresh()->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->fresh()->updated_at->getTimestamp()); } public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void { - CarbonImmutable::setTestNow(now()); - $createdAt = now()->timestamp; + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $createdAt = $now->getTimestamp(); $castUser = UserWithIntTimestampsViaCasts::create([ 'email' => fake()->unique()->email, @@ -101,15 +103,15 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($createdAt, $castUser->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($createdAt, $attributeUser->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->updated_at->getTimestamp()); - CarbonImmutable::setTestNow(now()->addSecond()); - $updatedAt = now()->timestamp; + CarbonImmutable::setTestNow($now = $now->addSecond()); + $updatedAt = $now->getTimestamp(); $castUser->update([ 'email' => fake()->unique()->email, @@ -121,20 +123,20 @@ public function testItCastTimestampsCreatedByTheBuilderWhenTimeHasPassed(): void 'email' => fake()->unique()->email, ]); - $this->assertSame($createdAt, $castUser->created_at->timestamp); - $this->assertSame($updatedAt, $castUser->updated_at->timestamp); - $this->assertSame($updatedAt, $castUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $attributeUser->created_at->timestamp); - $this->assertSame($updatedAt, $attributeUser->updated_at->timestamp); - $this->assertSame($updatedAt, $attributeUser->fresh()->updated_at->timestamp); - $this->assertSame($createdAt, $mutatorUser->created_at->timestamp); - $this->assertSame($updatedAt, $mutatorUser->updated_at->timestamp); - $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->timestamp); + $this->assertSame($createdAt, $castUser->created_at->getTimestamp()); + $this->assertSame($updatedAt, $castUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $castUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $attributeUser->created_at->getTimestamp()); + $this->assertSame($updatedAt, $attributeUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $attributeUser->fresh()->updated_at->getTimestamp()); + $this->assertSame($createdAt, $mutatorUser->created_at->getTimestamp()); + $this->assertSame($updatedAt, $mutatorUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->getTimestamp()); } public function testItCastTimestampsUpdatedByAMutator(): void { - CarbonImmutable::setTestNow(now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $mutatorUser = UserWithUpdatedAtViaMutator::create([ 'email' => fake()->unique()->email, @@ -142,14 +144,14 @@ public function testItCastTimestampsUpdatedByAMutator(): void $this->assertNull($mutatorUser->updated_at); - CarbonImmutable::setTestNow(now()->addSecond()); - $updatedAt = now()->timestamp; + CarbonImmutable::setTestNow($now = $now->addSecond()); + $updatedAt = $now->getTimestamp(); $mutatorUser->update([ 'email' => fake()->unique()->email, ]); - $this->assertSame($updatedAt, $mutatorUser->updated_at->timestamp); - $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->timestamp); + $this->assertSame($updatedAt, $mutatorUser->updated_at->getTimestamp()); + $this->assertSame($updatedAt, $mutatorUser->fresh()->updated_at->getTimestamp()); } } diff --git a/tests/Integration/Foundation/Exceptions/RendererTest.php b/tests/Integration/Foundation/Exceptions/RendererTest.php index e68de37903..946edc3538 100644 --- a/tests/Integration/Foundation/Exceptions/RendererTest.php +++ b/tests/Integration/Foundation/Exceptions/RendererTest.php @@ -10,6 +10,7 @@ use Hypervel\Foundation\Exceptions\Renderer\Renderer; use Hypervel\Foundation\Providers\FoundationServiceProvider; use Hypervel\Routing\Router; +use Hypervel\Support\Facades\Event; use Hypervel\Testbench\Attributes\WithConfig; use Hypervel\Testbench\TestCase; use Mockery as m; @@ -18,10 +19,13 @@ class RendererTest extends TestCase { + /** + * Define the test routes. + */ protected function defineRoutes(Router $router): void { - $router->get('failed', fn () => throw new RuntimeException('Bad route!')); - $router->get('failed-with-previous', function () { + $router->get('failed', fn (): never => throw new RuntimeException('Bad route!')); + $router->get('failed-with-previous', function (): never { throw new RuntimeException( 'First exception', previous: new RuntimeException( @@ -35,7 +39,7 @@ protected function defineRoutes(Router $router): void } #[WithConfig('app.debug', true)] - public function testItCanRenderExceptionPage() + public function testItCanRenderExceptionPage(): void { $this->assertTrue($this->app->bound(Renderer::class)); @@ -46,7 +50,7 @@ public function testItCanRenderExceptionPage() } #[WithConfig('app.debug', false)] - public function testItCanRenderExceptionPageUsingSymfonyIfRendererIsNotDefined() + public function testItCanRenderExceptionPageUsingSymfonyIfRendererIsNotDefined(): void { config(['app.debug' => true]); @@ -59,10 +63,13 @@ public function testItCanRenderExceptionPageUsingSymfonyIfRendererIsNotDefined() } #[WithConfig('app.debug', true)] - public function testItCanRenderExceptionPageWithRendererWhenDebugEnabled() + public function testItCanRenderExceptionPageWithRendererWhenDebugEnabled(): void { - $this->app->singleton(ExceptionRenderer::class, function () { + $this->app->singleton(ExceptionRenderer::class, function (): ExceptionRenderer { return new class implements ExceptionRenderer { + /** + * Render the exception as HTML. + */ public function render(Throwable $throwable): string { return 'Custom Exception Renderer: ' . $throwable->getMessage(); @@ -78,10 +85,13 @@ public function render(Throwable $throwable): string } #[WithConfig('app.debug', false)] - public function testItDoesNotRenderExceptionPageWithRendererWhenDebugDisabled() + public function testItDoesNotRenderExceptionPageWithRendererWhenDebugDisabled(): void { - $this->app->singleton(ExceptionRenderer::class, function () { + $this->app->singleton(ExceptionRenderer::class, function (): ExceptionRenderer { return new class implements ExceptionRenderer { + /** + * Render the exception as HTML. + */ public function render(Throwable $throwable): string { return 'Custom Exception Renderer: ' . $throwable->getMessage(); @@ -97,7 +107,7 @@ public function render(Throwable $throwable): string } #[WithConfig('app.debug', false)] - public function testItDoesNotRegisterListenersWhenDebugDisabled() + public function testItDoesNotRegisterListenersWhenDebugDisabled(): void { $this->app->forgetInstance(ExceptionRenderer::class); $this->assertFalse($this->app->bound(ExceptionRenderer::class)); @@ -106,17 +116,20 @@ public function testItDoesNotRegisterListenersWhenDebugDisabled() $listener->shouldReceive('registerListeners')->never(); $this->app->instance(Listener::class, $listener); - $this->app->instance(Dispatcher::class, m::mock(Dispatcher::class)); + Event::swap(m::mock(Dispatcher::class, ['listen' => null])); $provider = $this->app->getProvider(FoundationServiceProvider::class); $provider->boot(); } #[WithConfig('app.debug', true)] - public function testItDoesNotRegisterListenersWhenRendererBound() + public function testItDoesNotRegisterListenersWhenRendererBound(): void { - $this->app->singleton(ExceptionRenderer::class, function () { + $this->app->singleton(ExceptionRenderer::class, function (): ExceptionRenderer { return new class implements ExceptionRenderer { + /** + * Render the exception as HTML. + */ public function render(Throwable $throwable): string { return 'Custom Exception Renderer: ' . $throwable->getMessage(); @@ -130,14 +143,14 @@ public function render(Throwable $throwable): string $listener->shouldReceive('registerListeners')->never(); $this->app->instance(Listener::class, $listener); - $this->app->instance(Dispatcher::class, m::mock(Dispatcher::class)); + Event::swap(m::mock(Dispatcher::class, ['listen' => null])); $provider = $this->app->getProvider(FoundationServiceProvider::class); $provider->boot(); } #[WithConfig('app.debug', true)] - public function testItRegistersListenersWhenRendererNotBound() + public function testItRegistersListenersWhenRendererNotBound(): void { $this->app->forgetInstance(ExceptionRenderer::class); $this->assertFalse($this->app->bound(ExceptionRenderer::class)); @@ -146,14 +159,14 @@ public function testItRegistersListenersWhenRendererNotBound() $listener->shouldReceive('registerListeners')->once(); $this->app->instance(Listener::class, $listener); - $this->app->instance(Dispatcher::class, m::mock(Dispatcher::class)); + Event::swap(m::mock(Dispatcher::class, ['listen' => null])); $provider = $this->app->getProvider(FoundationServiceProvider::class); $provider->boot(); } #[WithConfig('app.debug', true)] - public function testItRendersPreviousExceptions() + public function testItRendersPreviousExceptions(): void { $this->assertTrue($this->app->bound(Renderer::class)); @@ -171,11 +184,14 @@ public function testItRendersPreviousExceptions() // REMOVED: testItExcludesDecorativeAsciiArtInNonBrowserContexts - Laravel ASCII art component was removed #[WithConfig('app.debug', true)] - public function testItFallsBackToSymfonyWhenRendererThrows() + public function testItFallsBackToSymfonyWhenRendererThrows(): void { // Replace the Renderer with one that always throws - $this->app->singleton(Renderer::class, function () { + $this->app->singleton(Renderer::class, function (): object { return new class { + /** + * Fail while rendering the exception. + */ public function render(): never { throw new RuntimeException('Renderer broke'); diff --git a/tests/Integration/Foundation/MaintenanceModeTest.php b/tests/Integration/Foundation/MaintenanceModeTest.php index c216721b0e..27b8eb3350 100644 --- a/tests/Integration/Foundation/MaintenanceModeTest.php +++ b/tests/Integration/Foundation/MaintenanceModeTest.php @@ -224,6 +224,8 @@ public function testMaintenanceModeDoesNotRedirectJsonRequests(): void $response->assertJson(['message' => 'Service Unavailable']); } + // REMOVED: The maintenance.php stub test; requests reach the already-booted worker's middleware. + public function testDownCommandPrerendersTemplateIntoMaintenancePayload(): void { file_put_contents(resource_path('views/errors/503.blade.php'), 'Rendered {{ $retryAfter }}'); @@ -501,8 +503,6 @@ public function testMaintenanceModeRetryCanAcceptDatetime(string $datetime): voi $expectedDate = CarbonImmutable::parse($datetime)->toRfc7231String(); $this->assertSame($expectedDate, $data['retry']); - - CarbonImmutable::setTestNow(); } /** diff --git a/tests/Integration/Http/RequestDurationThresholdTest.php b/tests/Integration/Http/RequestDurationThresholdTest.php new file mode 100644 index 0000000000..affa924ea4 --- /dev/null +++ b/tests/Integration/Http/RequestDurationThresholdTest.php @@ -0,0 +1,200 @@ + 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $called = false; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function () use (&$called): void { + $called = true; + }); + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSecond()->addMillisecond()); + $kernel->terminate($request, $response); + + $this->assertTrue($called); + } + + public function testItDoesntCallWhenExactlyThresholdDuration(): void + { + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $called = false; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function () use (&$called): void { + $called = true; + }); + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSecond()); + $kernel->terminate($request, $response); + + $this->assertFalse($called); + } + + public function testItProvidesRequestToHandler(): void + { + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $url = null; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function (CarbonImmutable $startedAt, Request $request) use (&$url): void { + $url = $request->url(); + }); + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSeconds(2)); + $kernel->terminate($request, $response); + + $this->assertSame('http://localhost/test-route', $url); + } + + public function testUsesTheConfiguredDateTimezone(): void + { + config(['app.timezone' => 'UTC']); + Route::get('test-route', fn (): string => 'ok'); + $kernel = $this->app->make(Kernel::class); + $startedAt = null; + $kernel->whenRequestLifecycleIsLongerThan(CarbonInterval::second(), function (CarbonImmutable $started) use (&$startedAt): void { + $startedAt = $started; + }); + + config(['app.timezone' => 'Australia/Melbourne']); + CarbonImmutable::setTestNow($now = CarbonImmutable::today()); + $kernel->handle($request = Request::create('http://localhost/test-route')); + CarbonImmutable::setTestNow($now->addMinute()); + $kernel->terminate($request, new Response); + + $this->assertSame('Australia/Melbourne', $startedAt->timezone->getName()); + } + + public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds(): void + { + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $called = false; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan(1000, function () use (&$called): void { + $called = true; + }); + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSecond()->addMillisecond()); + $kernel->terminate($request, $response); + + $this->assertTrue($called); + } + + public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds(): void + { + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $called = false; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan(1000, function () use (&$called): void { + $called = true; + }); + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSecond()); + $kernel->terminate($request, $response); + + $this->assertFalse($called); + } + + public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime(): void + { + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $called = false; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan($now->addSecond(), function () use (&$called): void { + $called = true; + }); + + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSecond()->addMillisecond()); + $kernel->terminate($request, $response); + + $this->assertTrue($called); + } + + public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime(): void + { + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + $called = false; + $kernel = $this->app->make(Kernel::class); + $kernel->whenRequestLifecycleIsLongerThan($now->addSecond(), function () use (&$called): void { + $called = true; + }); + + $kernel->handle($request); + + CarbonImmutable::setTestNow($now->addSecond()); + $kernel->terminate($request, $response); + + $this->assertFalse($called); + } + + public function testItClearsStartTimeAfterHandlingRequest(): void + { + $kernel = $this->app->make(Kernel::class); + Route::get('test-route', fn (): string => 'ok'); + $request = Request::create('http://localhost/test-route'); + $response = new Response; + + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); + $kernel->handle($request); + $this->assertTrue($now->eq($kernel->requestStartedAt())); + + $kernel->terminate($request, $response); + $this->assertNull($kernel->requestStartedAt()); + } + + public function testItHandlesCallingTerminateWithoutHandle(): void + { + $this->app->make(Kernel::class)->terminate(Request::create('http://localhost/test-route'), new Response); + + // this is a placeholder just to show that the above did not throw an exception. + $this->assertTrue(true); + } +} diff --git a/tests/Integration/Mail/SendingMailWithLocaleTest.php b/tests/Integration/Mail/SendingMailWithLocaleTest.php index b29aebbb0a..f4012c4c50 100644 --- a/tests/Integration/Mail/SendingMailWithLocaleTest.php +++ b/tests/Integration/Mail/SendingMailWithLocaleTest.php @@ -18,6 +18,9 @@ class SendingMailWithLocaleTest extends TestCase { + /** + * Define the test environment. + */ protected function defineEnvironment(ApplicationContract $app): void { $app->make('config')->set('mail', [ @@ -92,8 +95,6 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled(): void ); $this->assertSame('en', CarbonImmutable::getLocale()); - - CarbonImmutable::setTestNow(null); } public function testLocaleIsSentWithModelPreferredLocale(): void @@ -198,6 +199,9 @@ class SendingLocaleTestEmailLocaleUser extends Model implements HasLocalePrefere 'email_locale', ]; + /** + * Get the preferred locale. + */ public function preferredLocale(): string { return $this->email_locale; diff --git a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php index a87cdd7885..0b211451db 100644 --- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php +++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php @@ -25,6 +25,9 @@ #[WithConfig('database.default', 'testing')] class SendingNotificationsWithLocaleTest extends TestCase { + /** + * Define the test environment. + */ protected function defineEnvironment(ApplicationContract $app): void { $config = $app->make('config'); @@ -46,11 +49,14 @@ protected function defineEnvironment(ApplicationContract $app): void ]); } + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); - Schema::create('users', function (Blueprint $table) { + Schema::create('users', function (Blueprint $table): void { $table->increments('id'); $table->string('email'); $table->string('name')->nullable(); @@ -60,7 +66,7 @@ protected function setUp(): void public function testMailIsSentWithDefaultLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -75,7 +81,7 @@ public function testMailIsSentWithDefaultLocale(): void public function testMailIsSentWithFacadeSelectedLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -91,11 +97,11 @@ public function testMailIsSentWithNotificationSelectedLocale(): void { $users = [ NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]), NotifiableLocalizedUser::forceCreate([ - 'email' => 'mohamed@laravel.com', + 'email' => 'mohamed@hypervel.com', 'name' => 'Mohamed Said', ]), ]; @@ -116,7 +122,7 @@ public function testMailIsSentWithNotificationSelectedLocale(): void public function testMailableIsSentWithSelectedLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -132,12 +138,12 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled(): void { CarbonImmutable::setTestNow('2018-07-25'); - Event::listen(LocaleUpdated::class, function ($event) { + Event::listen(LocaleUpdated::class, function (LocaleUpdated $event): void { CarbonImmutable::setLocale($event->locale); }); $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -156,8 +162,6 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled(): void $this->assertTrue($this->app->isLocale('en')); $this->assertSame('en', CarbonImmutable::getLocale()); - - CarbonImmutable::setTestNow(null); } public function testLocaleIsSentWithNotifiablePreferredLocale(): void @@ -264,6 +268,9 @@ class NotifiableEmailLocalePreferredUser extends Model implements HasLocalePrefe 'email_locale', ]; + /** + * Get the preferred locale. + */ public function preferredLocale(): ?string { return $this->email_locale; @@ -272,11 +279,17 @@ public function preferredLocale(): ?string class GreetingMailNotification extends Notification { + /** + * Get the notification channels. + */ public function via(mixed $notifiable): array { return [MailChannel::class]; } + /** + * Get the mail representation of the notification. + */ public function toMail(mixed $notifiable): MailMessage { return (new MailMessage) @@ -287,12 +300,18 @@ public function toMail(mixed $notifiable): MailMessage class GreetingMailNotificationWithMailable extends Notification { - public function via($notifiable) + /** + * Get the notification channels. + */ + public function via(mixed $notifiable): array { return [MailChannel::class]; } - public function toMail($notifiable) + /** + * Get the mail representation of the notification. + */ + public function toMail(mixed $notifiable): GreetingMailable { return (new GreetingMailable) ->to($notifiable->email); @@ -301,7 +320,10 @@ public function toMail($notifiable) class GreetingMailable extends Mailable { - public function build() + /** + * Build the message. + */ + public function build(): static { return $this->view('greeting'); } diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index e128e366d3..a052f92934 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -10,7 +10,6 @@ use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Database\UniqueConstraintViolationException; use Hypervel\Foundation\Bus\Dispatchable; -use Hypervel\Foundation\Testing\DatabaseMigrations; use Hypervel\Queue\Worker; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Facades\Artisan; @@ -26,8 +25,6 @@ #[WithMigration('queue')] class WorkCommandTest extends QueueTestCase { - use DatabaseMigrations; - /** * Define the test environment. */ @@ -38,9 +35,12 @@ protected function defineEnvironment(ApplicationContract $app): void $app->make('config')->set('queue.default', env('QUEUE_CONNECTION', 'database')); } + /** + * Set up the test environment. + */ protected function setUp(): void { - $this->beforeApplicationDestroyed(function () { + $this->beforeApplicationDestroyed(function (): void { FirstJob::$ran = false; SecondJob::$ran = false; ThirdJob::$ran = false; @@ -51,7 +51,7 @@ protected function setUp(): void $this->markTestSkippedWhenUsingSyncQueueDriver(); } - public function testRunningOneJob() + public function testRunningOneJob(): void { Queue::push(new FirstJob); Queue::push(new SecondJob); @@ -91,12 +91,7 @@ public function testQueueOptionPreservesZeroAndDefaultsEmptyString(): void public function testConnectionArgumentPreservesZero(): void { - $config = $this->app->make('config'); - - $config->set( - 'queue.connections.0', - $config->get('queue.connections.database'), - ); + config(['queue.connections.0' => config('queue.connections.database')]); Queue::connection('0')->push(new FirstJob); @@ -109,7 +104,7 @@ public function testConnectionArgumentPreservesZero(): void $this->assertTrue(FirstJob::$ran); } - public function testOnceDoesNotRunInMaintenanceModeUnlessForced() + public function testOnceDoesNotRunInMaintenanceModeUnlessForced(): void { Queue::push(new FirstJob); @@ -156,7 +151,7 @@ public function testRunTimestampOutputWithDefaultAppTimezone(): void public function testRunTimestampOutputWithDifferentLogTimezone(): void { - $this->app->make('config')->set('queue.output_timezone', 'Europe/Helsinki'); + config(['queue.output_timezone' => 'Europe/Helsinki']); $this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11)); Queue::push(new FirstJob); @@ -170,7 +165,7 @@ public function testRunTimestampOutputWithDifferentLogTimezone(): void public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault(): void { - $this->app->make('config')->set('queue.output_timezone', 'UTC'); + config(['queue.output_timezone' => 'UTC']); $this->travelTo(CarbonImmutable::create(2023, 1, 18, 10, 10, 11)); Queue::push(new FirstJob); @@ -182,7 +177,7 @@ public function testRunTimestampOutputWithSameAppDefaultAndQueueLogDefault(): vo ->assertExitCode(0); } - public function testDaemon() + public function testDaemon(): void { Queue::push(new FirstJob); Queue::push(new SecondJob); @@ -198,7 +193,7 @@ public function testDaemon() $this->assertTrue(SecondJob::$ran); } - public function testDaemonWritesOutputFromJobCoroutine() + public function testDaemonWritesOutputFromJobCoroutine(): void { $this->markTestSkippedWhenUsingQueueDrivers(['redis', 'beanstalkd']); @@ -215,7 +210,7 @@ public function testDaemonWritesOutputFromJobCoroutine() $this->assertStringContainsString(FirstJob::class, Artisan::output()); } - public function testMemoryExceeded() + public function testMemoryExceeded(): void { Queue::push(new FirstJob); Queue::push(new SecondJob); @@ -223,7 +218,7 @@ public function testMemoryExceeded() $this->artisan('queue:work', [ '--daemon' => true, '--stop-when-empty' => true, - '--memory' => 0.1, + '--memory' => 1, ])->assertExitCode(12); // Memory limit isn't checked until after the first job is attempted. @@ -272,7 +267,7 @@ public function testMaxTimeExceeded(): void $this->assertFalse(SecondJob::$ran); } - public function testMemoryExitCode() + public function testMemoryExitCode(): void { $this->markTestSkippedWhenUsingQueueDrivers(['redis', 'beanstalkd']); @@ -282,7 +277,7 @@ public function testMemoryExitCode() Queue::push(new SecondJob); $this->artisan('queue:work', [ - '--memory' => 0.1, + '--memory' => 1, ])->assertExitCode(0); // Memory limit isn't checked until after the first job is attempted. @@ -351,7 +346,7 @@ public function testDisablePauseQueueCheck(): void Worker::$pausable = true; } - public function testFailedJobListenerOnlyRunsOnce() + public function testFailedJobListenerOnlyRunsOnce(): void { $this->markTestSkippedWhenUsingQueueDrivers(['redis', 'beanstalkd']); @@ -401,6 +396,9 @@ class FirstJob implements ShouldQueue public static bool $ran = false; + /** + * Handle the first job. + */ public function handle(): void { static::$ran = true; @@ -414,6 +412,9 @@ class SecondJob implements ShouldQueue public static bool $ran = false; + /** + * Handle the second job. + */ public function handle(): void { static::$ran = true; @@ -427,6 +428,9 @@ class ThirdJob implements ShouldQueue public static bool $ran = false; + /** + * Handle the slow job. + */ public function handle(): void { sleep(1); @@ -440,7 +444,10 @@ class JobWillFail implements ShouldQueue use Dispatchable; use Queueable; - public function handle(): void + /** + * Fail while handling the job. + */ + public function handle(): never { throw new RuntimeException; } diff --git a/tests/Integration/Session/Database/DatabaseSessionHandlerTestCase.php b/tests/Integration/Session/Database/DatabaseSessionHandlerTestCase.php index 72a646d3a7..9c28af0113 100644 --- a/tests/Integration/Session/Database/DatabaseSessionHandlerTestCase.php +++ b/tests/Integration/Session/Database/DatabaseSessionHandlerTestCase.php @@ -90,18 +90,18 @@ public function testGarbageCollector(): void $connection = $this->app->make('db')->connection(); $handler = new DatabaseSessionHandler($resolver, null, 'sessions', 1, $this->app); - CarbonImmutable::setTestNow(CarbonImmutable::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); $handler->write('simple_id_1', 'abcd'); $this->assertSame(0, $handler->gc(1)); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(2)); + CarbonImmutable::setTestNow($now = $now->addSeconds(2)); $handler = new DatabaseSessionHandler($resolver, null, 'sessions', 1, $this->app); $handler->write('simple_id_2', 'abcd'); $this->assertSame(1, $handler->gc(2)); $this->assertSame(1, $connection->table('sessions')->count()); - CarbonImmutable::setTestNow(CarbonImmutable::now()->addSeconds(2)); + CarbonImmutable::setTestNow($now->addSeconds(2)); $this->assertSame(1, $handler->gc(1)); $this->assertSame(0, $connection->table('sessions')->count()); diff --git a/tests/Integration/View/BladeTest.php b/tests/Integration/View/BladeTest.php index edf00e2f16..6598aceab6 100644 --- a/tests/Integration/View/BladeTest.php +++ b/tests/Integration/View/BladeTest.php @@ -279,10 +279,9 @@ public function testViewCacheCommandDeduplicatesPathsBeforeCompiling(): void View::addNamespace('templates', join_paths(__DIR__, 'Fixtures', 'templates')); View::addNamespace('components', join_paths(__DIR__, 'Fixtures', 'templates', 'components')); - $compiler = m::mock(app('blade.compiler'))->makePartial(); - $compiler->shouldReceive('compile')->with(realpath(__DIR__ . '/Fixtures/templates/components/panel.blade.php'))->once(); - - $this->instance('blade.compiler', $compiler); + // Unmatched compile() calls need the configured compiler; a class-based partial mock skips its constructor. + Blade::swap(m::mock(Blade::getFacadeRoot())->makePartial()); + Blade::expects('compile')->with(realpath(__DIR__ . '/Fixtures/templates/components/panel.blade.php')); $this->artisan('view:cache'); } diff --git a/tests/Integration/View/ClearCommandTest.php b/tests/Integration/View/ClearCommandTest.php new file mode 100644 index 0000000000..9dcebe9e3a --- /dev/null +++ b/tests/Integration/View/ClearCommandTest.php @@ -0,0 +1,28 @@ +andReturn($globResult); + + File::expects('isDirectory')->with($globResult[0])->andReturnFalse(); + File::expects('isDirectory')->with($globResult[1])->andReturnTrue(); + File::expects('delete')->with($globResult[0])->andReturnTrue(); + File::expects('deleteDirectory')->with($globResult[1])->andReturnTrue(); + + $this->artisan(ViewClearCommand::class); + } +} diff --git a/tests/Queue/DatabaseFailedJobProviderTest.php b/tests/Queue/DatabaseFailedJobProviderTest.php index e8db9a09d3..e64ca6c3ee 100644 --- a/tests/Queue/DatabaseFailedJobProviderTest.php +++ b/tests/Queue/DatabaseFailedJobProviderTest.php @@ -10,7 +10,6 @@ use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Queue\Failed\DatabaseFailedJobProvider; use Hypervel\Support\CarbonImmutable; -use Hypervel\Support\Facades\Date; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; use RuntimeException; @@ -125,17 +124,17 @@ public function testCanPruneFailedJobsWithRelativeHoursAndMinutes(): void public function testCanFlushFailedJobs(): void { - Date::setTestNow(Date::now()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); - $this->createFailedJobsRecord(['failed_at' => Date::now()->subDays(10)]); + $this->createFailedJobsRecord(['failed_at' => $now->subDays(10)]); $this->provider->flush(); $this->assertSame(0, $this->failedJobsTable()->count()); - $this->createFailedJobsRecord(['failed_at' => Date::now()->subDays(10)]); + $this->createFailedJobsRecord(['failed_at' => $now->subDays(10)]); $this->provider->flush(15 * 24); $this->assertSame(1, $this->failedJobsTable()->count()); - $this->createFailedJobsRecord(['failed_at' => Date::now()->subDays(10)]); + $this->createFailedJobsRecord(['failed_at' => $now->subDays(10)]); $this->provider->flush(10 * 24); $this->assertSame(0, $this->failedJobsTable()->count()); } @@ -231,7 +230,7 @@ protected function createFailedJobsRecord(array $overrides = []): bool 'queue' => 'default', 'payload' => json_encode(['uuid' => (string) Str::uuid()]), 'exception' => new Exception('Whoops!'), - 'failed_at' => Date::now()->subDays(10), + 'failed_at' => CarbonImmutable::now()->subDays(10), ], $overrides)); } } diff --git a/tests/Queue/FileFailedJobProviderTest.php b/tests/Queue/FileFailedJobProviderTest.php index 4bc44b140b..06564a2c92 100644 --- a/tests/Queue/FileFailedJobProviderTest.php +++ b/tests/Queue/FileFailedJobProviderTest.php @@ -25,6 +25,9 @@ class FileFailedJobProviderTest extends TestCase protected Filesystem $filesystem; + /** + * Set up the test environment. + */ protected function setUp(): void { parent::setUp(); @@ -37,6 +40,9 @@ protected function setUp(): void $this->provider = new FileFailedJobProvider($this->path); } + /** + * Clean up the test environment. + */ protected function tearDown(): void { $this->filesystem->deleteDirectory($this->tempDirectory); @@ -73,6 +79,9 @@ public function testLogGeneratesAnIdentifierWhilePreservingUnsupportedPayloads(s $this->assertSame($payload, $this->provider->find($id)->payload); } + /** + * Provide payloads without usable identifiers. + */ public static function payloadsWithoutUsableIdentifiers(): array { return [ @@ -86,37 +95,33 @@ public static function payloadsWithoutUsableIdentifiers(): array public function testCanRetrieveAllFailedJobs(): void { - try { - CarbonImmutable::setTestNow(now()); - - [$uuidOne, $exceptionOne] = $this->logFailedJob(); - [$uuidTwo, $exceptionTwo] = $this->logFailedJob(); - - $failedJobs = $this->provider->all(); - - $this->assertEquals([ - (object) [ - 'id' => $uuidTwo, - 'connection' => 'connection', - 'queue' => 'queue', - 'payload' => json_encode(['uuid' => $uuidTwo]), - 'exception' => (string) mb_convert_encoding((string) $exceptionTwo, 'UTF-8'), - 'failed_at' => $failedJobs[1]->failed_at, - 'failed_at_timestamp' => $failedJobs[1]->failed_at_timestamp, - ], - (object) [ - 'id' => $uuidOne, - 'connection' => 'connection', - 'queue' => 'queue', - 'payload' => json_encode(['uuid' => $uuidOne]), - 'exception' => (string) mb_convert_encoding((string) $exceptionOne, 'UTF-8'), - 'failed_at' => $failedJobs[0]->failed_at, - 'failed_at_timestamp' => $failedJobs[0]->failed_at_timestamp, - ], - ], $failedJobs); - } finally { - CarbonImmutable::setTestNow(); - } + CarbonImmutable::setTestNow(now()); + + [$uuidOne, $exceptionOne] = $this->logFailedJob(); + [$uuidTwo, $exceptionTwo] = $this->logFailedJob(); + + $failedJobs = $this->provider->all(); + + $this->assertEquals([ + (object) [ + 'id' => $uuidTwo, + 'connection' => 'connection', + 'queue' => 'queue', + 'payload' => json_encode(['uuid' => $uuidTwo]), + 'exception' => (string) mb_convert_encoding((string) $exceptionTwo, 'UTF-8'), + 'failed_at' => $failedJobs[1]->failed_at, + 'failed_at_timestamp' => $failedJobs[1]->failed_at_timestamp, + ], + (object) [ + 'id' => $uuidOne, + 'connection' => 'connection', + 'queue' => 'queue', + 'payload' => json_encode(['uuid' => $uuidOne]), + 'exception' => (string) mb_convert_encoding((string) $exceptionOne, 'UTF-8'), + 'failed_at' => $failedJobs[0]->failed_at, + 'failed_at_timestamp' => $failedJobs[0]->failed_at_timestamp, + ], + ], $failedJobs); } public function testCanFindFailedJobs(): void @@ -336,7 +341,11 @@ public function testJobsCanBeCountedByQueueAndConnection(): void $this->assertSame(2, $this->provider->count('connection-2', 'queue-1')); } - /** @return array{string, Exception} */ + /** + * Log a failed job. + * + * @return array{string, Exception} + */ public function logFailedJob(string $connection = 'connection', string $queue = 'queue'): array { $uuid = Str::uuid(); diff --git a/tests/Queue/QueuePauseResumeTest.php b/tests/Queue/QueuePauseResumeTest.php index 5c1e38bbd6..2caeb499d1 100644 --- a/tests/Queue/QueuePauseResumeTest.php +++ b/tests/Queue/QueuePauseResumeTest.php @@ -80,7 +80,7 @@ public function store(?string $name = null): CacheRepository return new QueueManager($container); } - public function testPauseQueueWithConnection() + public function testPauseQueueWithConnection(): void { $this->manager->pause('redis', 'default'); @@ -89,7 +89,6 @@ public function testPauseQueueWithConnection() public function testPauseQueueWithTTL(): void { - CarbonImmutable::setTestNow(); $this->manager->pauseFor('redis', 'default', 30); $this->assertTrue($this->manager->isPaused('redis', 'default')); @@ -100,7 +99,6 @@ public function testPauseQueueWithTTL(): void public function testPauseQueueIndefinitely(): void { - CarbonImmutable::setTestNow(); $this->manager->pause('redis', 'default'); $this->assertTrue($this->manager->isPaused('redis', 'default')); @@ -109,7 +107,7 @@ public function testPauseQueueIndefinitely(): void $this->assertTrue($this->manager->isPaused('redis', 'default')); } - public function testResumeQueue() + public function testResumeQueue(): void { $this->manager->pause('redis', 'default'); $this->assertTrue($this->manager->isPaused('redis', 'default')); @@ -118,7 +116,7 @@ public function testResumeQueue() $this->assertFalse($this->manager->isPaused('redis', 'default')); } - public function testPausingQueueOnOneConnectionDoesNotAffectAnother() + public function testPausingQueueOnOneConnectionDoesNotAffectAnother(): void { $this->manager->pause('redis', 'default'); @@ -126,7 +124,7 @@ public function testPausingQueueOnOneConnectionDoesNotAffectAnother() $this->assertFalse($this->manager->isPaused('database', 'default')); } - public function testPausingDifferentQueuesOnSameConnection() + public function testPausingDifferentQueuesOnSameConnection(): void { $this->manager->pause('redis', 'emails'); $this->manager->pause('redis', 'notifications'); @@ -136,7 +134,7 @@ public function testPausingDifferentQueuesOnSameConnection() $this->assertFalse($this->manager->isPaused('redis', 'default')); } - public function testResumingOnlyAffectsSpecificQueue() + public function testResumingOnlyAffectsSpecificQueue(): void { $this->manager->pause('redis', 'emails'); $this->manager->pause('redis', 'notifications'); @@ -147,11 +145,11 @@ public function testResumingOnlyAffectsSpecificQueue() $this->assertTrue($this->manager->isPaused('redis', 'notifications')); } - public function testPauseDispatchesQueuePausedEvent() + public function testPauseDispatchesQueuePausedEvent(): void { $dispatchedEvent = null; - $this->events->listen(QueuePaused::class, function (QueuePaused $event) use (&$dispatchedEvent) { + $this->events->listen(QueuePaused::class, function (QueuePaused $event) use (&$dispatchedEvent): void { $dispatchedEvent = $event; }); @@ -163,11 +161,11 @@ public function testPauseDispatchesQueuePausedEvent() $this->assertNull($dispatchedEvent->ttl); } - public function testPauseForDispatchesQueuePausedEventWithTTL() + public function testPauseForDispatchesQueuePausedEventWithTTL(): void { $dispatchedEvent = null; - $this->events->listen(QueuePaused::class, function (QueuePaused $event) use (&$dispatchedEvent) { + $this->events->listen(QueuePaused::class, function (QueuePaused $event) use (&$dispatchedEvent): void { $dispatchedEvent = $event; }); @@ -179,11 +177,11 @@ public function testPauseForDispatchesQueuePausedEventWithTTL() $this->assertSame(60, $dispatchedEvent->ttl); } - public function testResumeDispatchesQueueResumedEvent() + public function testResumeDispatchesQueueResumedEvent(): void { $dispatchedEvent = null; - $this->events->listen(QueueResumed::class, function (QueueResumed $event) use (&$dispatchedEvent) { + $this->events->listen(QueueResumed::class, function (QueueResumed $event) use (&$dispatchedEvent): void { $dispatchedEvent = $event; }); @@ -308,13 +306,16 @@ public function testResumeAllDispatchesQueuesResumedEvent(): void $this->assertInstanceOf(QueuesResumed::class, $dispatchedEvent); } - public function testParsingQueueString() + public function testParsingQueueString(): void { $parser = new class { use ParsesQueue; private Container $hypervel; + /** + * Create the queue parser. + */ public function __construct() { $this->hypervel = new Container; @@ -325,6 +326,9 @@ public function __construct() ])); } + /** + * Parse a queue connection and name. + */ public function parse(string $queue): array { return $this->parseQueue($queue); diff --git a/tests/RateLimiter/LimiterTest.php b/tests/RateLimiter/LimiterTest.php index de6adcf2e3..82769af850 100644 --- a/tests/RateLimiter/LimiterTest.php +++ b/tests/RateLimiter/LimiterTest.php @@ -175,8 +175,8 @@ public function testCooldownDurationValidationHappensBeforeKeyOrStoreAccess(): v $this->assertSame(0, $store->calls); } - // REMOVED: Laravel's primitive counter and fallback-key tests are replaced - // by atomic policy decisions and canonical identity coverage. + // REMOVED: Laravel's primitive counter, key-sanitization and fallback-key tests + // are replaced by atomic policy decisions and canonical identity coverage. // REMOVED: Laravel's callback-before-hit attempt() coverage is replaced by // one atomic consume before the callback. @@ -188,9 +188,35 @@ public function testAttemptConsumesBeforeInvokingTheCallback(): void new KeyResolver('app', static fn (): ?string => null), ); $policy = Limit::perMinute(1)->by('attempt'); + $executions = 0; - $this->assertTrue($limiter->attempt($policy, static fn (): null => null)); - $this->assertFalse($limiter->attempt($policy, static fn (): string => 'not executed')); + $this->assertTrue($limiter->attempt($policy, function () use ($limiter, $policy, &$executions): void { + ++$executions; + + $this->assertTrue($limiter->inspect($policy)->denied()); + })); + $this->assertSame(1, $executions); + + $this->assertFalse($limiter->attempt($policy, static function () use (&$executions): void { + ++$executions; + })); + $this->assertSame(1, $executions); + } + + public function testAttemptsCallbackReturnsCallbackReturn(): void + { + $limiter = new Limiter( + new WorkerArrayStore, + new KeyResolver('app', static fn (): ?string => null), + ); + $policy = Limit::perMinute(6)->by('callback-return'); + + $this->assertSame('foo', $limiter->attempt($policy, static fn (): string => 'foo')); + $this->assertFalse($limiter->attempt($policy, static fn (): false => false)); + $this->assertSame([], $limiter->attempt($policy, static fn (): array => [])); + $this->assertSame(0, $limiter->attempt($policy, static fn (): int => 0)); + $this->assertSame(0.0, $limiter->attempt($policy, static fn (): float => 0.0)); + $this->assertSame('', $limiter->attempt($policy, static fn (): string => '')); } public function testAttemptRetainsTheChargeWhenTheCallbackThrows(): void @@ -216,6 +242,9 @@ class LimiterCountingStore implements Store { public int $calls = 0; + /** + * Count the call and return an allowed decision. + */ public function consume(string $key, AdmissionPolicy $policy): LimitResult { ++$this->calls; @@ -223,6 +252,9 @@ public function consume(string $key, AdmissionPolicy $policy): LimitResult return new LimitResult(true, 1, 0, 0, 1_000_000); } + /** + * Count the call and return a blocked cooldown. + */ public function block(string $key, int $durationMicroseconds): CooldownResult { ++$this->calls; @@ -230,6 +262,9 @@ public function block(string $key, int $durationMicroseconds): CooldownResult return new CooldownResult(false, $durationMicroseconds); } + /** + * Count the call and return an allowed decision for the policy. + */ public function inspect( string $key, AdmissionPolicy|Backoff|Cooldown $policy, @@ -243,6 +278,9 @@ public function inspect( }; } + /** + * Count the call and return an allowed backoff decision. + */ public function recordFailure(string $key, Backoff $backoff): BackoffResult { ++$this->calls; @@ -250,6 +288,9 @@ public function recordFailure(string $key, Backoff $backoff): BackoffResult return new BackoffResult(true, 1, 0); } + /** + * Count the call and report successful clearing. + */ public function clear(string $key): bool { ++$this->calls; @@ -260,6 +301,9 @@ public function clear(string $key): bool readonly class UnsupportedAdmissionPolicy extends AdmissionPolicy { + /** + * Create a policy instance with the given settings. + */ protected function newInstance( string $key, int $cost, diff --git a/tests/Sentry/IntegrationMetaTagTest.php b/tests/Sentry/IntegrationMetaTagTest.php index 96bc6c6935..38b002a912 100644 --- a/tests/Sentry/IntegrationMetaTagTest.php +++ b/tests/Sentry/IntegrationMetaTagTest.php @@ -5,7 +5,7 @@ namespace Hypervel\Tests\Sentry; use Hypervel\Sentry\Integration; -use Mockery; +use Mockery as m; use Sentry\State\Scope; use Sentry\Tracing\Span; @@ -84,7 +84,7 @@ public function testSentryBaggageMetaReturnsAWellFormedMetaTag(): void private function setDangerousSpanValues(string $traceparent, string $baggage): void { - $span = Mockery::mock(Span::class); + $span = m::mock(Span::class); $span->shouldReceive('toTraceparent')->andReturn($traceparent)->zeroOrMoreTimes(); $span->shouldReceive('toBaggage')->andReturn($baggage)->zeroOrMoreTimes(); diff --git a/tests/Sentry/IntegrationTest.php b/tests/Sentry/IntegrationTest.php index 658b9f3b26..b6089f18da 100644 --- a/tests/Sentry/IntegrationTest.php +++ b/tests/Sentry/IntegrationTest.php @@ -8,7 +8,7 @@ use Hypervel\Routing\Events\RouteMatched; use Hypervel\Routing\Route; use Hypervel\Sentry\Integration; -use Mockery; +use Mockery as m; use RuntimeException; use Sentry\Event; use Sentry\State\Scope; @@ -31,7 +31,7 @@ public function testTransactionIsSetWhenRouteMatchedEventIsFired(): void $event = new RouteMatched( new Route('GET', $routeUrl = '/sentry-route-matched-event', []), - Mockery::mock(Request::class)->makePartial() + m::mock(Request::class)->makePartial() ); $this->dispatchHypervelEvent($event); diff --git a/tests/Session/ArraySessionHandlerTest.php b/tests/Session/ArraySessionHandlerTest.php index 32ed0f0d75..a6a1f1b38b 100644 --- a/tests/Session/ArraySessionHandlerTest.php +++ b/tests/Session/ArraySessionHandlerTest.php @@ -47,10 +47,10 @@ public function testReadDataFromAlmostExpiredSession(): void { $handler = new ArraySessionHandler(10); - CarbonImmutable::setTestNow(Date::now()); + CarbonImmutable::setTestNow($now = Date::now()); $handler->write('foo', 'bar'); - CarbonImmutable::setTestNow(Date::now()->addMinutes(10)); + CarbonImmutable::setTestNow($now->addMinutes(10)); $this->assertSame('bar', $handler->read('foo')); } @@ -59,10 +59,10 @@ public function testReadDataFromExpiredSession(): void { $handler = new ArraySessionHandler(10); - CarbonImmutable::setTestNow(Date::now()); + CarbonImmutable::setTestNow($now = Date::now()); $handler->write('foo', 'bar'); - CarbonImmutable::setTestNow(Date::now()->addMinutes(10)->addSecond()); + CarbonImmutable::setTestNow($now->addMinutes(10)->addSecond()); $this->assertSame('', $handler->read('foo')); } @@ -103,16 +103,16 @@ public function testCleanOldSession(): void $this->assertSame(0, $handler->gc(300)); - CarbonImmutable::setTestNow(Date::now()); + CarbonImmutable::setTestNow($now = Date::now()); $handler->write('foo', 'bar'); $this->assertSame(0, $handler->gc(300)); $this->assertSame('bar', $handler->read('foo')); - CarbonImmutable::setTestNow(Date::now()->addSecond()); + CarbonImmutable::setTestNow($now = $now->addSecond()); $handler->write('baz', 'qux'); - CarbonImmutable::setTestNow(Date::now()->addMinutes(5)); + CarbonImmutable::setTestNow($now->addMinutes(5)); $this->assertSame(1, $handler->gc(300)); $this->assertSame('', $handler->read('foo')); diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php index 758c8a40af..a42a44b1c4 100644 --- a/tests/Support/SleepTest.php +++ b/tests/Support/SleepTest.php @@ -17,7 +17,7 @@ class SleepTest extends TestCase { - public function testItSleepsForSeconds() + public function testItSleepsForSeconds(): void { $start = microtime(true); Sleep::for(1)->seconds(); @@ -26,12 +26,12 @@ public function testItSleepsForSeconds() $this->assertEqualsWithDelta(1, $end - $start, 0.03); } - public function testCallbacksMayBeExecutedUsingThen() + public function testCallbacksMayBeExecutedUsingThen(): void { $this->assertEquals(123, Sleep::for(1)->milliseconds()->then(fn () => 123)); } - public function testSleepRespectsWhile() + public function testSleepRespectsWhile(): void { $_SERVER['__sleep.while'] = 0; @@ -48,7 +48,7 @@ public function testSleepRespectsWhile() unset($_SERVER['__sleep.while']); } - public function testItSleepsForSecondsWithMilliseconds() + public function testItSleepsForSecondsWithMilliseconds(): void { $start = microtime(true); Sleep::for(1.5)->seconds(); @@ -57,7 +57,7 @@ public function testItSleepsForSecondsWithMilliseconds() $this->assertEqualsWithDelta(1.5, round($end - $start, 1, PHP_ROUND_HALF_DOWN), 0.03); } - public function testItCanFakeSleeping() + public function testItCanFakeSleeping(): void { Sleep::fake(); @@ -79,7 +79,7 @@ public function testItCanSpecifyMinutes(float $duration, float $microseconds): v $this->assertSame($microseconds, $sleep->duration->totalMicroseconds); } - public function testItCanSpecifyMinute() + public function testItCanSpecifyMinute(): void { Sleep::fake(); @@ -99,7 +99,7 @@ public function testItCanSpecifySeconds(float $duration, float $microseconds): v $this->assertSame($microseconds, $sleep->duration->totalMicroseconds); } - public function testItCanSpecifySecond() + public function testItCanSpecifySecond(): void { Sleep::fake(); @@ -120,7 +120,7 @@ public function testItCanSpecifyMilliseconds(float $duration, float $microsecond $this->assertSame($microseconds, $sleep->duration->totalMicroseconds); } - public function testItCanSpecifyMillisecond() + public function testItCanSpecifyMillisecond(): void { Sleep::fake(); @@ -141,7 +141,7 @@ public function testItCanSpecifyMicroseconds(float $duration, float $microsecond $this->assertSame($microseconds, $sleep->duration->totalMicroseconds); } - public function testItCanSpecifyMicrosecond() + public function testItCanSpecifyMicrosecond(): void { Sleep::fake(); @@ -150,7 +150,7 @@ public function testItCanSpecifyMicrosecond() $this->assertSame((float) $sleep->duration->totalMicroseconds, 1.0); } - public function testItCanChainDurations() + public function testItCanChainDurations(): void { Sleep::fake(); @@ -160,7 +160,7 @@ public function testItCanChainDurations() $this->assertSame((float) $sleep->duration->totalMicroseconds, 1000500.0); } - public function testItCanUseDateInterval() + public function testItCanUseDateInterval(): void { Sleep::fake(); @@ -169,7 +169,7 @@ public function testItCanUseDateInterval() $this->assertSame((float) $sleep->duration->totalMicroseconds, 1_005_000.0); } - public function testItThrowsForUnknownTimeUnit() + public function testItThrowsForUnknownTimeUnit(): void { try { Sleep::for(5); @@ -179,7 +179,7 @@ public function testItThrowsForUnknownTimeUnit() } } - public function testItCanAssertSequence() + public function testItCanAssertSequence(): void { Sleep::fake(); @@ -192,7 +192,7 @@ public function testItCanAssertSequence() ]); } - public function testItFailsSequenceAssertion() + public function testItFailsSequenceAssertion(): void { Sleep::fake(); @@ -210,7 +210,7 @@ public function testItFailsSequenceAssertion() } } - public function testItCanUseSleep() + public function testItCanUseSleep(): void { Sleep::fake(); @@ -221,7 +221,7 @@ public function testItCanUseSleep() ]); } - public function testItCanUseUSleep() + public function testItCanUseUSleep(): void { Sleep::fake(); @@ -232,43 +232,43 @@ public function testItCanUseUSleep() ]); } - public function testItCanSleepTillGivenTime() + public function testItCanSleepTillGivenTime(): void { Sleep::fake(); - Carbon::setTestNow(now()->startOfDay()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); - Sleep::until(now()->addMinute()); + Sleep::until($now->addMinute()); Sleep::assertSequence([ Sleep::for(60)->seconds(), ]); } - public function testItCanSleepTillGivenTimestamp() + public function testItCanSleepTillGivenTimestamp(): void { Sleep::fake(); - Carbon::setTestNow(now()->startOfDay()); + CarbonImmutable::setTestNow($today = CarbonImmutable::today()); - Sleep::until(now()->addMinute()->timestamp); + Sleep::until($today->addMinute()->getTimestamp()); Sleep::assertSequence([ Sleep::for(60)->seconds(), ]); } - public function testItCanSleepTillGivenTimestampAsString() + public function testItCanSleepTillGivenTimestampAsString(): void { Sleep::fake(); - Carbon::setTestNow(now()->startOfDay()); + CarbonImmutable::setTestNow($today = CarbonImmutable::today()); - Sleep::until((string) now()->addMinute()->timestamp); + Sleep::until((string) $today->addMinute()->getTimestamp()); Sleep::assertSequence([ Sleep::for(60)->seconds(), ]); } - public function testItCanSleepTillGivenTimestampAsStringWithMilliseconds() + public function testItCanSleepTillGivenTimestampAsStringWithMilliseconds(): void { Sleep::fake(); Carbon::setTestNow('2000-01-01 00:00:00.000'); // 946684800 @@ -282,19 +282,19 @@ public function testItCanSleepTillGivenTimestampAsStringWithMilliseconds() ]); } - public function testItSleepsForZeroTimeWithNegativeDateTime() + public function testItSleepsForZeroTimeWithNegativeDateTime(): void { Sleep::fake(); - Carbon::setTestNow(now()->startOfDay()); + CarbonImmutable::setTestNow($now = CarbonImmutable::now()); - Sleep::until(now()->subMinutes(100)); + Sleep::until($now->subMinutes(100)); Sleep::assertSequence([ Sleep::for(0)->seconds(), ]); } - public function testSleepingForZeroTime() + public function testSleepingForZeroTime(): void { Sleep::fake(); @@ -310,7 +310,7 @@ public function testSleepingForZeroTime() } } - public function testItFailsWhenSequenceContainsTooManySleeps() + public function testItFailsWhenSequenceContainsTooManySleeps(): void { Sleep::fake(); @@ -327,7 +327,7 @@ public function testItFailsWhenSequenceContainsTooManySleeps() } } - public function testSilentlySetsDurationToZeroForNegativeValues() + public function testSilentlySetsDurationToZeroForNegativeValues(): void { Sleep::fake(); @@ -338,7 +338,7 @@ public function testSilentlySetsDurationToZeroForNegativeValues() ]); } - public function testItDoesntCaptureAssertionInstances() + public function testItDoesntCaptureAssertionInstances(): void { Sleep::fake(); @@ -359,7 +359,7 @@ public function testItDoesntCaptureAssertionInstances() } } - public function testAssertNeverSlept() + public function testAssertNeverSlept(): void { Sleep::fake(); @@ -375,7 +375,7 @@ public function testAssertNeverSlept() } } - public function testAssertNeverAgainstZeroSecondSleep() + public function testAssertNeverAgainstZeroSecondSleep(): void { Sleep::fake(); @@ -391,7 +391,7 @@ public function testAssertNeverAgainstZeroSecondSleep() } } - public function testItCanAssertNoSleepingOccurred() + public function testItCanAssertNoSleepingOccurred(): void { Sleep::fake(); @@ -412,7 +412,7 @@ public function testItCanAssertNoSleepingOccurred() } } - public function testItCanAssertSleepCount() + public function testItCanAssertSleepCount(): void { Sleep::fake(); @@ -437,7 +437,7 @@ public function testItCanAssertSleepCount() } } - public function testAssertSlept() + public function testAssertSlept(): void { Sleep::fake(); @@ -469,7 +469,7 @@ public function testAssertSlept() } } - public function testItCanCreateMacrosViaMacroable() + public function testItCanCreateMacrosViaMacroable(): void { Sleep::fake(); @@ -500,7 +500,7 @@ public function testItCanCreateMacrosViaMacroable() $this->assertSame((float) $sleep->duration->totalMicroseconds, 1234567.0); } - public function testItCanReplacePreviouslyDefinedDurations() + public function testItCanReplacePreviouslyDefinedDurations(): void { Sleep::fake(); @@ -518,7 +518,7 @@ public function testItCanReplacePreviouslyDefinedDurations() $this->assertSame((float) $sleep->duration->totalMicroseconds, 500000.0); } - public function testItCanSleepConditionallyWhen() + public function testItCanSleepConditionallyWhen(): void { Sleep::fake(); @@ -556,7 +556,7 @@ public function testItCanSleepConditionallyWhen() Sleep::assertSlept(fn () => true, 4); } - public function testItCanRegisterCallbacksToRunInTests() + public function testItCanRegisterCallbacksToRunInTests(): void { $countA = 0; $countB = 0; @@ -580,7 +580,7 @@ public function testItCanRegisterCallbacksToRunInTests() $this->assertSame(3.0, (float) $countB); } - public function testItDoesntRunCallbacksWhenNotFaking() + public function testItDoesntRunCallbacksWhenNotFaking(): void { Sleep::whenFakingSleep(function () { throw new Exception('Should not run without faking.'); @@ -591,7 +591,7 @@ public function testItDoesntRunCallbacksWhenNotFaking() $this->assertTrue(true); } - public function testItDoesNotSyncCarbon() + public function testItDoesNotSyncCarbon(): void { Carbon::setTestNow('2000-01-01 00:00:00'); Sleep::fake(); @@ -661,7 +661,7 @@ public function testFakeCanSetSyncWithCarbon(bool $syncWithCarbon, string $datet $this->assertSame($datetime, Date::now()->toDateTimeString()); } - public function testFakeDoesNotNeedToSyncWithCarbon() + public function testFakeDoesNotNeedToSyncWithCarbon(): void { Carbon::setTestNow('2000-01-01 00:00:00'); Sleep::fake(); diff --git a/tests/Support/SupportLazyCollectionTest.php b/tests/Support/SupportLazyCollectionTest.php index 05e2bac727..c96b0e724c 100644 --- a/tests/Support/SupportLazyCollectionTest.php +++ b/tests/Support/SupportLazyCollectionTest.php @@ -80,10 +80,10 @@ public function testCanCreateCollectionFromGeneratorFunction(): void public function testCanCreateCollectionFromNonGeneratorFunction(): void { $data = LazyCollection::make(function () { - return 'laravel'; + return 'hypervel'; }); - $this->assertSame(['laravel'], $data->all()); + $this->assertSame(['hypervel'], $data->all()); } public function testDoesNotCreateCollectionFromGenerator(): void @@ -292,8 +292,6 @@ public function testThrottle(): void }, times: 3); $this->assertSame([1, 2, 3], $data); - - Sleep::fake(false); } public function testThrottleAccountsForTimePassed(): void @@ -303,16 +301,16 @@ public function testThrottleAccountsForTimePassed(): void $data = LazyCollection::times(3) ->throttle(3) - ->tapEach(function ($value, $index) { - if ($index == 1) { + ->tapEach(function (int $value, int $index): void { + if ($index === 1) { // Travel in time... (new Wormhole(1))->second(); } }) ->all(); - Sleep::assertSlept(function (Duration $duration, int $index) { - $expectation = $index == 1 ? 2_000_000 : 3_000_000; + Sleep::assertSlept(function (Duration $duration, int $index): bool { + $expectation = $index === 1 ? 2_000_000 : 3_000_000; $this->assertEqualsWithDelta( $expectation, @@ -324,9 +322,6 @@ public function testThrottleAccountsForTimePassed(): void }, times: 3); $this->assertSame([1, 2, 3], $data); - - Sleep::fake(false); - CarbonImmutable::setTestNow(); } public function testUniqueDoubleEnumeration(): void @@ -532,8 +527,6 @@ public function testWithHeartbeat(): void ], $output->all(), ); - - CarbonImmutable::setTestNow(); } public function testRandomPreservesKeys(): void diff --git a/tests/Telescope/Watchers/ScheduleWatcherTest.php b/tests/Telescope/Watchers/ScheduleWatcherTest.php index 8491824013..96ee6fe205 100644 --- a/tests/Telescope/Watchers/ScheduleWatcherTest.php +++ b/tests/Telescope/Watchers/ScheduleWatcherTest.php @@ -43,7 +43,6 @@ public function testScheduleRegistersEntryWithoutACommandStartEvent(): void $this->assertSame('command description', $entry->content['description']); $this->assertSame('* * * * *', $entry->content['expression']); $this->assertSame('UTC', $entry->content['timezone']); - $this->assertSame('user', $entry->content['user']); $this->assertSame('command output', $entry->content['output']); $this->assertSame('finished', $entry->content['status']); $this->assertSame(0, $entry->content['exit_code']); @@ -192,7 +191,7 @@ public function testDifferentSchedulesRegisterSeparateEntriesInTheSameCoroutine( public function testIgnoredSchedulerDoesNotRegisterAnEntry(): void { - config()->set('telescope.ignore_commands', ['schedule:run']); + config(['telescope.ignore_commands' => ['schedule:run']]); Telescope::stopRecording(); $task = m::mock(Event::class); @@ -273,7 +272,6 @@ protected function makeTask( $task->description = $command . ' description'; $task->expression = '* * * * *'; $task->timezone = 'UTC'; - $task->user = 'user'; $task->shouldReceive('exitCode')->andReturn($exitCode); $task->shouldReceive('wasSkippedDueToOverlapping')->andReturn($skippedBecauseOverlapping); $task->shouldReceive('getOutput') diff --git a/types/Collections/Collection.php b/types/Collections/Collection.php index 6b9c4425e6..b904832c94 100644 --- a/types/Collections/Collection.php +++ b/types/Collections/Collection.php @@ -1003,6 +1003,7 @@ function ($collection, $count) { assertType('Hypervel\Support\Collection', $collection::make([1])->pad(2, 0)); assertType('Hypervel\Support\Collection', $collection::make([1])->pad(2, 'string')); assertType('Hypervel\Support\Collection', $collection->pad(2, 0)); +assertType('Hypervel\Support\Collection', $associativeCollection->pad(2, 0)); assertType('Hypervel\Support\Collection<(int|string), int>', $collection::make([1])->countBy()); assertType('Hypervel\Support\Collection<(int|string), int>', $collection::make(['string' => 'string'])->countBy('string')); diff --git a/types/Collections/Enumerable.php b/types/Collections/Enumerable.php index c2fcafed73..6d4ff85c28 100644 --- a/types/Collections/Enumerable.php +++ b/types/Collections/Enumerable.php @@ -69,6 +69,7 @@ function assertEnumerableTypes(Enumerable $enumerable): void assertType('Hypervel\Support\Enumerable', $enumerable->flatten()); assertType('Hypervel\Support\Enumerable', $enumerable->random(2)); assertType('Hypervel\Support\Enumerable', $enumerable->random(2, true)); + assertType('Hypervel\Support\Enumerable', $enumerable->pad(3, 0)); assertType('float|int', $enumerable->sum(static fn (int $value): int => $value)); assertType('mixed', $enumerable->sum('amount')); diff --git a/types/Collections/LazyCollection.php b/types/Collections/LazyCollection.php index 6d0c50f673..e4890a492e 100644 --- a/types/Collections/LazyCollection.php +++ b/types/Collections/LazyCollection.php @@ -886,6 +886,7 @@ public function toArray(): array assertType('Hypervel\Support\LazyCollection', $collection::make([1])->pad(2, 0)); assertType('Hypervel\Support\LazyCollection', $collection::make([1])->pad(2, 'string')); assertType('Hypervel\Support\LazyCollection', $collection->pad(2, 0)); +assertType('Hypervel\Support\LazyCollection', $associativeCollection->pad(2, 0)); assertType('Hypervel\Support\LazyCollection<(int|string), int>', $collection::make([1])->countBy()); assertType('Hypervel\Support\LazyCollection<(int|string), int>', $collection::make(['string' => 'string'])->countBy('string')); diff --git a/types/Database/Eloquent/Collection.php b/types/Database/Eloquent/Collection.php index ee84134aa5..ef7a45fd84 100644 --- a/types/Database/Eloquent/Collection.php +++ b/types/Database/Eloquent/Collection.php @@ -226,6 +226,7 @@ function assertEloquentCollectionAggregateExpressionTypes(Collection $collection assertType('Hypervel\Support\Collection', $collection->pad(2, 0)); assertType('Hypervel\Support\Collection', $collection->pad(2, 'string')); +assertType('Hypervel\Support\Collection', (new Collection(['first' => new User]))->pad(2, 0)); assertType('array', $collection->getQueueableIds());