From 73123faf7d40d1c4a9ab6bf25f9a6c9bc3c5cb9a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:26:02 +0000 Subject: [PATCH 01/15] Complete immutable-clock cleanup and duration coverage Port the remaining clock captures, expiry checks and cookie/request-duration cases from Laravel's test cleanup history. Retain cumulative advances when dates are immutable, and rely on the existing global clock reset instead of duplicating cleanup in individual tests. Cookie expiry comparisons use the equivalent isPast/isFuture methods with their existing zero-expiry guards. Keep caller-owned timezone restoration in Testbench and the validation cases; forcing UTC globally would change the environment of consuming packages. Freeze DateTime duration tests before registering their deadline so crossing a real second boundary cannot change the intended threshold. Make the storage-cache touch test reach the original expiry boundary, preserve the distinct explicit-TTL expired-lock case, and remove both tables owned by the MySQL and MariaDB cast tests during teardown. Upstream: https://github.com/laravel/framework/pull/60761 https://github.com/laravel/framework/pull/60793 https://github.com/laravel/framework/pull/61199 Porting source: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. Validation: full composer fix, immediate affected-file checks, dedicated MySQL/MariaDB cast tests and SQLite/MySQL/MariaDB/PostgreSQL session tests. The final deterministic-clock adjustment passes its complete file and the formatter. Existing Hypervel-specific coverage remains intact. --- src/testing/src/TestResponse.php | 4 +- tests/Cache/CacheArrayStoreTest.php | 48 +++-- tests/Cache/CacheFileStoreTest.php | 9 +- tests/Cache/CacheSessionStoreTest.php | 63 +++--- tests/Cache/CacheStorageStoreTest.php | 5 +- .../Database/DatabaseEloquentBuilderTest.php | 4 +- .../Database/DatabaseEloquentFactoryTest.php | 8 +- tests/Database/DatabaseQueryBuilderTest.php | 12 +- .../FoundationExceptionsHandlerTest.php | 2 +- tests/Http/HttpClientTest.php | 12 +- .../Console/CommandDurationThresholdTest.php | 67 +++--- tests/Integration/Cookie/CookieTest.php | 60 ++++++ .../Database/MariaDb/EloquentCastTest.php | 88 ++++---- .../Database/MySql/EloquentCastTest.php | 88 ++++---- .../Http/RequestDurationThresholdTest.php | 200 ++++++++++++++++++ .../Mail/SendingMailWithLocaleTest.php | 8 +- .../SendingNotificationsWithLocaleTest.php | 48 +++-- .../DatabaseSessionHandlerTestCase.php | 6 +- tests/Queue/DatabaseFailedJobProviderTest.php | 11 +- tests/Queue/FileFailedJobProviderTest.php | 73 ++++--- tests/Queue/QueuePauseResumeTest.php | 32 +-- tests/Session/ArraySessionHandlerTest.php | 14 +- tests/Support/SleepTest.php | 90 ++++---- tests/Support/SupportLazyCollectionTest.php | 19 +- 24 files changed, 639 insertions(+), 332 deletions(-) create mode 100644 tests/Integration/Cookie/CookieTest.php create mode 100644 tests/Integration/Http/RequestDurationThresholdTest.php 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/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 366d362e5b..fa5f663969 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); @@ -157,12 +157,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 +233,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 +264,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 +279,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 +446,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 +469,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 +513,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 +719,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/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/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/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/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 d1cc1ac4c5..b8d7799a85 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -6270,7 +6270,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) { @@ -6283,7 +6283,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']); @@ -6302,19 +6302,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/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/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/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 b7239b4839..32a8631c61 100644 --- a/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php +++ b/tests/Integration/Notifications/SendingNotificationsWithLocaleTest.php @@ -23,6 +23,9 @@ class SendingNotificationsWithLocaleTest extends TestCase { + /** + * Define the test environment. + */ protected function defineEnvironment(ApplicationContract $app): void { $config = $app->make('config'); @@ -44,11 +47,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(); @@ -58,7 +64,7 @@ protected function setUp(): void public function testMailIsSentWithDefaultLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -73,7 +79,7 @@ public function testMailIsSentWithDefaultLocale(): void public function testMailIsSentWithFacadeSelectedLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -89,11 +95,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', ]), ]; @@ -114,7 +120,7 @@ public function testMailIsSentWithNotificationSelectedLocale(): void public function testMailableIsSentWithSelectedLocale(): void { $user = NotifiableLocalizedUser::forceCreate([ - 'email' => 'taylor@laravel.com', + 'email' => 'taylor@hypervel.com', 'name' => 'Taylor Otwell', ]); @@ -130,12 +136,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', ]); @@ -154,8 +160,6 @@ public function testMailIsSentWithLocaleUpdatedListenersCalled(): void $this->assertTrue($this->app->isLocale('en')); $this->assertSame('en', CarbonImmutable::getLocale()); - - CarbonImmutable::setTestNow(null); } public function testLocaleIsSentWithNotifiablePreferredLocale(): void @@ -262,6 +266,9 @@ class NotifiableEmailLocalePreferredUser extends Model implements HasLocalePrefe 'email_locale', ]; + /** + * Get the preferred locale. + */ public function preferredLocale(): ?string { return $this->email_locale; @@ -270,11 +277,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) @@ -285,12 +298,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); @@ -299,7 +318,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/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/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/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 From d690bf2871a0a08886279cc8f59a7afd2a968754 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:26:45 +0000 Subject: [PATCH 02/15] Apply inherited scheduler callbacks once and complete scheduler coverage When a group seeds pending event attributes, apply that pending copy once and return. Applying the group again duplicated lifecycle callbacks and macros. Use the current Laravel merge order, remove unreachable dependency guards, and retain Hypervel's coroutine execution and event observation boundaries. Restore the complete applicable grouping, callback, quarterly-frequency and run-command tests. Use a shared stateless event mutex fixture and join the task coroutine owned by each background-command test before asserting its outcome. Preserve mutable dates in the repeat-start test so it protects the copy guard. Correct the reversed elapsed-time comparison that kept the sub-minute maintenance test from ever entering maintenance mode. Remove the ineffective scheduled-task user API and its pending attributes, generated facade annotation and Telescope metadata/UI. Tasks share the scheduler's OS user; document running the scheduler under the required user or using an explicit system command. Keep the long-running schedule:run replacement for schedule:work and document the --once cron adaptation. Complete the linked maintenance-view deployment guidance alongside these porting-guide changes. Upstream scheduler history: https://github.com/laravel/framework/pull/58926 https://github.com/laravel/framework/pull/60133 https://github.com/laravel/framework/pull/60144 https://github.com/laravel/framework/pull/60148 https://github.com/laravel/framework/pull/60190 https://github.com/laravel/framework/pull/60197 https://github.com/laravel/framework/pull/60255 https://github.com/laravel/framework/pull/60712 https://github.com/laravel/framework/pull/60311 https://github.com/laravel/framework/pull/60469 https://github.com/laravel/framework/pull/55624 https://github.com/laravel/framework/pull/57621 https://github.com/laravel/framework/pull/59331 Related complete test and documentation reconciliation: https://github.com/laravel/framework/pull/60761 https://github.com/laravel/framework/pull/60793 https://github.com/laravel/framework/pull/61199 https://github.com/laravel/framework/pull/60595 https://github.com/laravel/framework/pull/61117 Porting source: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. Validation: full composer fix, both complete scheduler suites after review corrections, immediate test-file checks, facade regeneration and Telescope frontend build. The repeat-start assertion fails when copy() is removed; the maintenance-state assertion fails before correcting the elapsed-time comparison. Formatting and diff checks pass. --- src/console/README.md | 8 +- src/console/src/ConsoleServiceProvider.php | 1 + src/console/src/Scheduling/Event.php | 2 + .../src/Scheduling/ManagesAttributes.php | 15 +- .../src/Scheduling/PendingEventAttributes.php | 4 - src/console/src/Scheduling/Schedule.php | 33 +- src/docs/porting-from-laravel.md | 8 + src/docs/scheduling.md | 8 +- src/docs/telescope.md | 2 +- src/support/src/Facades/Schedule.php | 3 +- src/telescope/dist/app.js | 2 +- .../resources/js/screens/schedule/preview.vue | 7 - .../src/Watchers/ScheduleWatcher.php | 1 - tests/Console/Fixtures/FakeEventMutex.php | 34 ++ tests/Console/Scheduling/EventTest.php | 170 ++++++-- tests/Console/Scheduling/FrequencyTest.php | 87 ++-- .../Scheduling/ScheduleRunCommandTest.php | 2 + .../Console/Scheduling/CallbackEventTest.php | 94 +++- .../Console/Scheduling/EventPingTest.php | 11 +- .../Console/Scheduling/ScheduleGroupTest.php | 401 ++++++++++++++++-- .../Scheduling/ScheduleRunCommandTest.php | 295 +++++++++++++ .../Scheduling/SubMinuteSchedulingTest.php | 21 +- .../Watchers/ScheduleWatcherTest.php | 4 +- 23 files changed, 994 insertions(+), 219 deletions(-) create mode 100644 tests/Console/Fixtures/FakeEventMutex.php create mode 100644 tests/Integration/Console/Scheduling/ScheduleRunCommandTest.php 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/docs/porting-from-laravel.md b/src/docs/porting-from-laravel.md index 3c14e7189a..3248630449 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,6 +475,13 @@ 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). + + +### 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 72981e67a4..292fcec841 100644 --- a/src/docs/scheduling.md +++ b/src/docs/scheduling.md @@ -147,6 +147,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 @@ -438,14 +440,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 @@ -498,7 +500,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 d3ff0ce447..125243b5ee 100644 --- a/src/docs/telescope.md +++ b/src/docs/telescope.md @@ -544,7 +544,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/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/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 99d4d24174..2328eb869b 100644 --- a/tests/Console/Scheduling/ScheduleRunCommandTest.php +++ b/tests/Console/Scheduling/ScheduleRunCommandTest.php @@ -46,6 +46,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/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/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') From 29dfac345f311c8eff2d6fef2f4e01535e2da75c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:27:07 +0000 Subject: [PATCH 03/15] Record worker-served maintenance views and complete JSON test reconciliation Document that Hypervel serves rendered maintenance views through its running workers instead of Laravel's pre-bootstrap maintenance.php stub. Record the intentional omission at the source and matching test location, and point deployments that need a page while Hypervel is unavailable to their reverse proxy or load balancer. The shared porting guide contains the same deployment action and links to the existing feature documentation. The middleware and existing tests already cover the JSON redirect/template guards from Laravel. Preserve those cases, remove the duplicate final clock reset owned by the test subscriber, and complete native test-method typing. No maintenance runtime behavior changes. Upstream: https://github.com/laravel/framework/pull/60595 https://github.com/laravel/framework/pull/60761 https://github.com/laravel/framework/pull/61199 Porting source: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. Validation: complete MaintenanceModeTest, full composer fix and diff checks. --- src/foundation/README.md | 2 ++ src/foundation/src/Console/DownCommand.php | 2 ++ tests/Integration/Foundation/MaintenanceModeTest.php | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) 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/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(); } /** From 52542ede3a10df20cc77ebe17f60318de8166749 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:27:16 +0000 Subject: [PATCH 04/15] Complete framework test double cleanup and missing view-clear coverage Use real event dispatchers where the tests need normal dispatch behavior and apply the current facade spy, swap and expectation forms. Preserve exact event counts, the owning container bindings, command execution boundaries, and Hypervel's atomic environment-file replacement coverage. Keep Blade's initialized compiler behind its proxy mock: a constructor-free partial mock lacks filesystem state when the real compile method runs. Preserve the real session exception handler, which executes the deferred session save, rather than replacing that behavior with a no-op spy. Port the original view-clear test alongside the existing Hypervel filesystem cases. It checks deletion of both compiled files and parallel-test directories with successful native bool returns. Restore missing channel-list assertions, retain the queue worker's fractional memory-limit contract, and update the two upstream memory-test values without changing worker source behavior. Upstream: https://github.com/laravel/framework/pull/61199 https://github.com/laravel/framework/pull/61117 https://github.com/laravel/framework/pull/59068 https://github.com/laravel/framework/pull/59049 https://github.com/laravel/framework/pull/60761 Porting source: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. This completes the encountered cleanup slice; the wider 270-file #61117 port remains in progress. Existing Hypervel-specific tests are preserved. Validation: each changed test file, the real session/view-clear baseline, the database queue worker tests, and full composer fix all pass. --- .../Auth/AuthDatabaseTokenRepositoryTest.php | 107 ++++++++-------- tests/Auth/AuthenticateMiddlewareTest.php | 2 +- .../Console/ConsoleApplicationResolveTest.php | 112 +++++++++++++---- .../Console/ChannelListCommandTest.php | 19 ++- .../Console/RouteListCommandTest.php | 47 ++++---- .../Console/EnvironmentDecryptCommandTest.php | 10 +- .../Console/EnvironmentEncryptCommandTest.php | 114 +++++++----------- .../Foundation/Exceptions/RendererTest.php | 52 +++++--- tests/Integration/Queue/WorkCommandTest.php | 51 ++++---- tests/Integration/View/BladeTest.php | 7 +- tests/Integration/View/ClearCommandTest.php | 28 +++++ 11 files changed, 331 insertions(+), 218 deletions(-) create mode 100644 tests/Integration/View/ClearCommandTest.php 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/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/Console/ConsoleApplicationResolveTest.php b/tests/Console/ConsoleApplicationResolveTest.php index 291f0dc567..03cb56262f 100644 --- a/tests/Console/ConsoleApplicationResolveTest.php +++ b/tests/Console/ConsoleApplicationResolveTest.php @@ -12,11 +12,13 @@ use Hypervel\Console\Events\ArtisanStarting; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Foundation\Application; +use Hypervel\Events\Dispatcher as EventsDispatcher; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Console\Fixtures\FakeCommandWithArrayInputPrompting; use Hypervel\Tests\Console\Fixtures\FakeCommandWithInputPrompting; use Mockery as m; use ReflectionProperty; +use Symfony\Component\Console\Application as SymfonyApplication; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Exception\CommandNotFoundException; @@ -27,10 +29,13 @@ class ConsoleApplicationResolveTest extends TestCase { + /** + * Create a console application for command resolution. + */ private function createApp(?Application $container = null): ConsoleApplication { $container ??= $this->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/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/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/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/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/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); + } +} From 36e0cd3989c845ba2d402c5a476f46b53f751a59 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:09:54 +0000 Subject: [PATCH 05/15] Complete native rate limiter callback coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconcile the applicable CacheRateLimiterTest cases from Laravel PR #61117 against Hypervel’s policy-based rate limiter. Preserve every callback result, including false, empty arrays and strings, and distinct integer and float zero values. Make the existing admission test observe that capacity is consumed before callback execution and that denied callbacks remain uncalled. Use the real worker-array store without restoring Laravel’s replaced primitive counter API. Complete the existing omission comments and inline helper method titles. Validated the changed test file, the rate-limiter suite, repository formatting and full source and type-fixture analysis. Upstream: https://github.com/laravel/framework/pull/61117 Source revision: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2 --- src/rate-limiter/src/Limiter.php | 4 +-- tests/RateLimiter/LimiterTest.php | 52 ++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) 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/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, From d87d1f66c2af95f6348579529c27b085d29849aa Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:29:17 +0000 Subject: [PATCH 06/15] Preserve numeric cache keys when dispatching batch read events PHP converts numeric-string array keys to integers. Batched cache reads passed those integer result keys to strictly typed hit and miss event constructors, causing a TypeError whenever the corresponding listeners were registered. Normalize the two event arguments to strings inside their existing listener guards. Returned maps and reads without listeners remain unchanged. Restore the original numeric-key regression from Laravel PR #48423, which had been changed to alphabetic keys and no longer tested numeric-position defaults. Add a focused real-store test for numeric hit and miss event keys. Existing cached-null coverage remains unchanged. Both constructor failures were reproduced before the correction. The repository tests, Cache suite, configured formatter and full source/type analysis pass. Upstream: https://github.com/laravel/framework/pull/48423 Encountered during: https://github.com/laravel/framework/pull/61117 Source revision: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2 --- src/cache/src/Repository.php | 5 +++-- tests/Cache/CacheRepositoryTest.php | 30 ++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/cache/src/Repository.php b/src/cache/src/Repository.php index 7d6e533a44..c8d96545f8 100644 --- a/src/cache/src/Repository.php +++ b/src/cache/src/Repository.php @@ -1372,15 +1372,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/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 7deef807e2..07ccd5f763 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() @@ -1356,6 +1356,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(); From e5d4657ebf77be82f4c5945c2d4c2696e02dce2c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:04:26 +0000 Subject: [PATCH 07/15] Complete cache touch expiration handling Require an explicit TTL and dispatch positive lifetimes directly to the store. Remove an item through forget() when its computed lifetime is zero or negative. The previous read-before-touch path could make an expired item permanent in array stores and left Redis items alive after their requested expiration. Apply the same expiration boundary to all-mode tagged Redis caches, retain enum-key normalization, and align the contract, any-mode override and generated Cache facade. Removing the preliminary read also avoids an unnecessary storage round trip and value deserialization. Cached-null sentinels remain untouched by finite lifetime updates. Reconcile the complete touch history against Laravel 13.x at 01d008c9b5f32cb7c5e50a9a22273113d810b2a2, including existing store implementations and established unsupported-driver exclusions. Preserve the pinned repository cases with immutable dates and Hypervel enum naming, and retain real Redis sentinel and missing-key coverage. Remove obsolete null-TTL rewrite tests and document removal for nonpositive lifetimes. Validation: affected test files, Cache ParaTest, isolated Redis TTL integration tests, generated-facade tests, configured formatting and full source/type PHPStan checks pass. Existing environment-dependent skips remain in the Cache suite. Laravel PRs: https://github.com/laravel/framework/pull/55954 https://github.com/laravel/framework/pull/59121 https://github.com/laravel/framework/pull/59864 https://github.com/laravel/framework/pull/60878 --- src/cache/src/AnyModeTaggedCache.php | 2 +- src/cache/src/Redis/AllTaggedCache.php | 15 ++- src/cache/src/Repository.php | 15 ++- src/contracts/src/Cache/Repository.php | 4 +- src/docs/cache.md | 4 +- src/support/src/Facades/Cache.php | 2 +- tests/Cache/CacheArrayStoreTest.php | 7 +- tests/Cache/CacheRepositoryTest.php | 64 ++++++------- tests/Cache/Redis/AllTaggedCacheTest.php | 93 ++----------------- .../Redis/TtlHandlingIntegrationTest.php | 10 -- 10 files changed, 57 insertions(+), 159 deletions(-) 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 c8d96545f8..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); } /** 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/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/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/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index fa5f663969..1db55858f5 100644 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -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 diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 07ccd5f763..2bfc7e53e2 100644 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -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() 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/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 From 49af02d86f4c1adcc03fc38a42908e2e728d188b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:52:49 +0000 Subject: [PATCH 08/15] Complete process concurrency test coverage Restore the current upstream tests for transported exceptions, falsey constructor arguments, failed child processes, keyed results, result order, string-backed driver enums and integer timeouts. Keep the existing coroutine, direct sync-driver and CarbonInterval coverage alongside these cases. Select the process driver explicitly because Hypervel defaults to coroutines. Extract the exception fixtures into separate PSR-4 files so fresh children can autoload them. Terminate the failed-child fixture through Swoole's process API: exit() inside its command coroutine throws a task exception and does not produce the failed process result this test must exercise. Check the stable exit-code message without depending on shell error-output wording. Complete the modified test file's native typing and replace deprecated substring assertions without changing their matching semantics. Add the missing ProcessDriver::run throws annotation; runtime source is unchanged. Ported from Laravel 13.x at 01d008c9b5f32cb7c5e50a9a22273113d810b2a2: https://github.com/laravel/framework/pull/54705 https://github.com/laravel/framework/pull/60822 https://github.com/laravel/framework/pull/53135 https://github.com/laravel/framework/pull/53712 https://github.com/laravel/framework/pull/55161 https://github.com/laravel/framework/pull/59801 https://github.com/laravel/framework/pull/60105 https://github.com/laravel/framework/pull/59602 https://github.com/laravel/framework/pull/54732 This completes the Concurrency assertion changes encountered in https://github.com/laravel/framework/pull/61049; the rest of that PR remains under reconciliation. Validation: changed-file PHPUnit and Concurrency ParaTest pass, scoped formatting is clean, full source and type-fixture PHPStan passes, and facade generation lint plus FacadeDocblocksTest pass. --- src/concurrency/src/ProcessDriver.php | 3 + tests/Concurrency/ConcurrencyTest.php | 366 +++++++++++++----- .../Fixtures/ExceptionWithFalseyParam.php | 18 + .../Fixtures/ExceptionWithParam.php | 24 ++ .../Fixtures/ExceptionWithoutParam.php | 11 + 5 files changed, 332 insertions(+), 90 deletions(-) create mode 100644 tests/Concurrency/Fixtures/ExceptionWithFalseyParam.php create mode 100644 tests/Concurrency/Fixtures/ExceptionWithParam.php create mode 100644 tests/Concurrency/Fixtures/ExceptionWithoutParam.php 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/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 @@ + Date: Sat, 12 Sep 2026 23:33:25 +0000 Subject: [PATCH 09/15] Complete authentication test expectations and fixtures Reconcile the provider, password broker, token guard, verification listener and authorization middleware tests against the pinned Laravel 13.x tests. Use exact Mockery expectations for required calls while retaining default stubs, native fixture types and the existing coroutine-aware request and provider behavior. Restore string-backed broker-name and non-string token coverage alongside the existing integer-zero and request-isolation cases. Capture password reset arguments locally, remove the unreachable throttled-notification stub, and leave global container cleanup with the PHPUnit subscriber. No authentication source behavior changes. This completes these test files within the ongoing port of https://github.com/laravel/framework/pull/61117, using source pin 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. The token input cases also reconcile the complete direct upstream commit https://github.com/laravel/framework/commit/9b21ce0a9b. Validation: immediate tests for every edited file, full composer fix for the checkpoint, and focused token-guard tests plus configured formatting after the final fixture-name correction. --- tests/Auth/AuthDatabaseUserProviderTest.php | 104 +++++++------- tests/Auth/AuthEloquentUserProviderTest.php | 128 ++++++++--------- ...ficationNotificationHandleFunctionTest.php | 6 +- tests/Auth/AuthPasswordBrokerManagerTest.php | 29 +++- tests/Auth/AuthPasswordBrokerTest.php | 65 +++++---- tests/Auth/AuthTokenGuardTest.php | 123 +++++++++++------ tests/Auth/AuthorizeMiddlewareTest.php | 130 +++++++++--------- 7 files changed, 326 insertions(+), 259 deletions(-) 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/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); } From 9818083e3ae49c38d1d071d042c2fe8028d8e1fd Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:33:40 +0000 Subject: [PATCH 10/15] Complete broadcast payload and authentication test coverage Reconcile the Ably, Pusher, Redis and queued broadcast event tests against the pinned Laravel 13.x surface. Restore exact event payload checks and the explicit false missing-model deletion case while preserving Hypervel queue attributes, connection routing and channel authorization behavior. Exercise the real Pusher SDK local authentication signer against the known upstream signature instead of returning the expected signature from a mock. Required signing and publishing calls use exact expectations; request lookup stubs remain loose where their call count is not the contract. Complete native callback and fixture types without changing production code. Upstream: https://github.com/laravel/framework/pull/61117 and the complete source/test reconciliation of https://github.com/laravel/framework/pull/61074. Porting source: 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. The broader Mockery port remains in progress. Validation: each changed test file passed immediately, full composer fix passed for the checkpoint, and both broadcaster files plus configured formatting passed after restoring the final request stubs. --- tests/Broadcasting/AblyBroadcasterTest.php | 39 ++++--- tests/Broadcasting/BroadcastEventTest.php | 107 ++++++++++++++----- tests/Broadcasting/PusherBroadcasterTest.php | 83 +++++++------- tests/Broadcasting/RedisBroadcasterTest.php | 68 ++++++------ 4 files changed, 188 insertions(+), 109 deletions(-) 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); From 2bf271b2c89be6f34e822db9e6621cedd2abde0c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:33:54 +0000 Subject: [PATCH 11/15] Complete bus dispatch and batch test expectations Reconcile the five bus test files against Laravel 13.x, including the missing true Conditionable delay case. Preserve routing, guarded batch events, batch failure callbacks, deferred dispatch and unique-lock ownership assertions. Required calls now use exact Mockery expectations and fixture callbacks carry their actual native types. Keep stored and returned batch mocks distinct, construct valid Batchable jobs before testing failed-add cleanup, and match native database batch result types. Preserve the two queue lookups and three connection lookups made by the bulk dispatch scenario. Remove only container cleanup already owned by the PHPUnit subscriber; retain explicit destruction and lock-release checks. Upstream: https://github.com/laravel/framework/pull/61117, source pin 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. This is one complete test-file group in the ongoing broader port, with no runtime source or public API changes. Validation: immediate per-file PHPUnit runs, the complete Bus ParaTest suite, configured formatting, and full composer fix for the accumulated checkpoint. --- tests/Bus/BusBatchTest.php | 219 ++++++++++++++++----------- tests/Bus/BusBatchableTest.php | 10 +- tests/Bus/BusDispatcherTest.php | 164 ++++++++++---------- tests/Bus/BusPendingBatchTest.php | 135 +++++++++-------- tests/Bus/BusPendingDispatchTest.php | 183 ++++++++++++---------- 5 files changed, 388 insertions(+), 323 deletions(-) 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; From 83a4cb17d89c3bcb13bf6911772a0cb8500bcc77 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:34:06 +0000 Subject: [PATCH 12/15] Restore cache event and manager test assertions Restore pinned Laravel batched read and putMany event checks and verify store names on ordinary and tagged cache events. Preserve Hypervel listener guards, failure and cancellation coverage, and tagged-cache payload behavior. Counted dispatch expectations replace redundant literal-true assertions. Complete the manager fixture types, missing unbound-dispatcher assertion, strict default-driver assertion and required resolver expectations. Preserve custom repositories, explicit event disabling and coroutine-local memoized stores. No cache runtime behavior changes. Upstream: https://github.com/laravel/framework/pull/61117, using current source pin 01d008c9b5f32cb7c5e50a9a22273113d810b2a2. These two files are reconciled within the ongoing broader test port. Validation: immediate PHPUnit runs for both files, configured formatting, and full composer fix across the accumulated checkpoint. --- tests/Cache/CacheEventsTest.php | 209 ++++++++++++++++++------------- tests/Cache/CacheManagerTest.php | 58 +++++---- 2 files changed, 152 insertions(+), 115 deletions(-) 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/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); From 9308ecb81d4feaed8c369293f788bb7558dd2d29 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:34:18 +0000 Subject: [PATCH 13/15] Use the standard Mockery alias in Inertia tests Normalize the remaining Mockery imports and calls in the SSR health-command and response tests to the framework-wide m alias. Assertions, test bodies and production behavior remain unchanged. These local convention outliers were found while reconciling https://github.com/laravel/framework/pull/61117. Hypervel retains its established alias rather than adopting a conflicting upstream spelling; this does not introduce an Inertia upstream catch-up. Validation: both affected test files passed immediately and the full checkpoint passed composer fix. --- tests/Inertia/Commands/CheckSsrTest.php | 6 +++--- tests/Inertia/ResponseTest.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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(); From 6d8c245212367bc1b2df3162dde392b3f50b76c5 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:34:31 +0000 Subject: [PATCH 14/15] Use the standard Mockery alias in Sentry tests Normalize the remaining Mockery imports and calls in the Sentry integration and meta-tag tests to the framework-wide m alias. Keep their existing span, route and output assertions unchanged. These local convention outliers were found during https://github.com/laravel/framework/pull/61117 reconciliation. The change follows the established Hypervel alias and introduces no Sentry runtime changes or additional upstream port. Validation: both affected test files passed immediately and full composer fix passed for the checkpoint. --- tests/Sentry/IntegrationMetaTagTest.php | 4 ++-- tests/Sentry/IntegrationTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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); From 3ad6b7c717e5da12981afd87b66fb1b085985eb6 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:34:34 +0000 Subject: [PATCH 15/15] Correct collection key analysis for PHPStan 2.2.14 Collection::pad() preserves string keys while adding integer keys. Reflect that behavior in the eager, lazy, Enumerable and Eloquent return annotations and cover each boundary with a keyed type assertion. Keep existing list assertions and all runtime implementations unchanged. Require PHPStan 2.2.14 in the root and database development manifests. Older versions incorrectly collapse the int|TKey union, so they cannot validate these corrected return types. The new analyzer also falsely rejects generic ArrayAccess keys at four existing collection assignments. A standalone comparison with 2.2.13 confirms the regression, including when local generic types are explicit. Ignore only that diagnostic at those four lines, explaining the limitation without changing collection algorithms or adding runtime work. Validated with full source and type-fixture analysis on installed PHPStan 2.2.14, formatting, each changed type fixture, Composer manifest consistency and the existing eager, lazy and Eloquent collection suites under ParaTest. --- composer.json | 2 +- src/collections/src/Collection.php | 4 +++- src/collections/src/Enumerable.php | 2 +- src/collections/src/LazyCollection.php | 4 +++- src/database/composer.json | 2 +- src/database/src/Eloquent/Collection.php | 4 +++- types/Collections/Collection.php | 1 + types/Collections/Enumerable.php | 1 + types/Collections/LazyCollection.php | 1 + types/Database/Eloquent/Collection.php | 1 + 10 files changed, 16 insertions(+), 6 deletions(-) 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/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/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/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());