From fd21911d85d04834b648bf0c27e5d17bd91ad3c9 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:40:46 +0000 Subject: [PATCH 1/5] Store FakeJob release delays of every accepted type FakeJob::release() accepts DateInterval|DateTimeInterface|int, matching Laravel's fake, but stored the value in an int property. Releasing the fake with a date or interval therefore threw a TypeError instead of recording the delay. The $releaseDelay property now uses the same union, so direct calls keep the supplied value. The $failedWith property moves above the constructor to match upstream's member order. A data-provided FakeJobTest case covers integer, date and interval delays and checks the job is marked as released. --- src/queue/src/Jobs/FakeJob.php | 14 +++++++------- tests/Queue/FakeJobTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/queue/src/Jobs/FakeJob.php b/src/queue/src/Jobs/FakeJob.php index f113e05bc1..449e1f88b1 100644 --- a/src/queue/src/Jobs/FakeJob.php +++ b/src/queue/src/Jobs/FakeJob.php @@ -12,15 +12,20 @@ class FakeJob extends Job { /** - * The number of seconds the released job was delayed. + * The delay the job was released with. */ - public int $releaseDelay = 0; + public DateInterval|DateTimeInterface|int $releaseDelay = 0; /** * The number of attempts made to process the job. */ public int $attempts = 1; + /** + * The exception the job failed with. + */ + public ?Throwable $failedWith = null; + public function __construct( public array $payload = [], protected string $connectionName = 'sync', @@ -28,11 +33,6 @@ public function __construct( ) { } - /** - * The exception the job failed with. - */ - public ?Throwable $failedWith = null; - /** * Get the job identifier. */ diff --git a/tests/Queue/FakeJobTest.php b/tests/Queue/FakeJobTest.php index 55dfcf1212..34ee174a65 100644 --- a/tests/Queue/FakeJobTest.php +++ b/tests/Queue/FakeJobTest.php @@ -4,11 +4,15 @@ namespace Hypervel\Tests\Queue; +use DateInterval; +use DateTimeImmutable; +use DateTimeInterface; use Hypervel\Container\Container; use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\SyncQueue; use Hypervel\Support\ClassInvoker; use Hypervel\Tests\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class FakeJobTest extends TestCase { @@ -38,4 +42,27 @@ public function testSyncQueueNormalizesANullQueueBeforeConstructingTheJob(): voi $this->assertSame('sync', (new ClassInvoker($job))->queue); } + + #[DataProvider('releaseDelays')] + public function testReleaseRecordsTheGivenDelay(DateInterval|DateTimeInterface|int $delay): void + { + $job = new FakeJob; + + $job->release($delay); + + $this->assertTrue($job->isReleased()); + $this->assertSame($delay, $job->releaseDelay); + } + + /** + * Provide release delays of each supported type. + */ + public static function releaseDelays(): array + { + return [ + 'seconds' => [30], + 'date' => [new DateTimeImmutable('2026-01-01 00:00:00')], + 'interval' => [new DateInterval('PT1M')], + ]; + } } From 686fe516daa7f9eb80b1c3f09ce99bfcfa4f16d5 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:40:51 +0000 Subject: [PATCH 2/5] Remove dead returns from InteractsWithQueue delete() and release() ended their final if blocks with a bare return in void methods, which had no effect. Removing them does not change behavior. --- src/queue/src/InteractsWithQueue.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/queue/src/InteractsWithQueue.php b/src/queue/src/InteractsWithQueue.php index 221d716987..8cb20f4c90 100644 --- a/src/queue/src/InteractsWithQueue.php +++ b/src/queue/src/InteractsWithQueue.php @@ -37,7 +37,6 @@ public function delete(): void { if ($this->job) { $this->job->delete(); - return; } } @@ -64,7 +63,6 @@ public function release(DateInterval|DateTimeInterface|int $delay = 0): void if ($this->job) { $this->job->release($delay); - return; } } From 6709452dbd17d2f72c8444ae5fae97935a5dbaaa Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:47:10 +0000 Subject: [PATCH 3/5] Port Laravel's real-object test conversions Laravel replaced many mocks in the framework test suite with real objects, fakes and in-memory connections (laravel/framework#61663). A follow-up rewrote several of the same files further (laravel/framework#61739). This ports #61663, except the direct-connection console test, which covers a feature Hypervel does not support, at the 13.x revision 5d7797528723107cd4f495326838a7d7d67fbb20. It includes the #61739 changes to the files the two pull requests share and the two support files #61739 adds. The remaining #61739 files are left for a later port. Hypervel adaptations: - Use PdoConnection where upstream builds its base Connection, and a Capsule-backed InteractsWithInMemoryConnection test concern in place of upstream's RestoresConnectionResolver, since the test subscriber already resets the model connection resolver. - Build guards with a real container that holds the request. - Expect Hypervel's grouped local-scope constraints in real-query assertions. - Use resolver and repository fakes in the migrator tests, because Hypervel switches connections through the coroutine context instead of the resolver's shared default. - Keep typed mocks without expectations where upstream swaps in a real dispatcher or bus without assertions, so unexpected calls still fail. - In the queue worker tests, use upstream's event and exception handler fakes, and switch to a dispatcher spy only in tests that must intercept dispatches for ordering, listener failures or listener guards. Coverage kept, added or corrected: - Add custom password-column rehash tests for both user providers, since the real-object rehash tests no longer check the configured column. - Keep the local builder macro test and the array and collection whereKey and whereKeyNot tests that upstream deleted, as nothing else covers them. - Seed the null MorphTo association test with a key, type and loaded relation, so it proves they are cleared. - Keep the password verifier's timeout and exact padding header checks. - Add a request-event listener-guard test for the HTTP client, since the fake conversions removed the only assertions on that guard. - Catch the prebuilt exception in the failed-notification test, since upstream's assertions after the throwing call never run. Verified with php-cs-fixer, PHPStan and the affected test files, including the Redis integration tests. --- tests/Auth/AuthDatabaseUserProviderTest.php | 195 +++---- tests/Auth/AuthEloquentUserProviderTest.php | 200 +++---- tests/Auth/AuthGuardTest.php | 397 +++++++------- tests/Auth/AuthenticateMiddlewareTest.php | 4 +- tests/Auth/AuthorizeMiddlewareTest.php | 5 +- tests/Broadcasting/BroadcastEventTest.php | 9 +- tests/Broadcasting/PusherBroadcasterTest.php | 15 +- tests/Broadcasting/RedisBroadcasterTest.php | 15 +- tests/Bus/BusBatchTest.php | 112 ++-- tests/Bus/BusDispatcherTest.php | 93 ++-- tests/Bus/BusPendingBatchTest.php | 38 +- tests/Bus/BusPendingDispatchTest.php | 2 +- tests/Cache/CacheFailoverStoreTest.php | 5 +- tests/Cache/CacheRepositoryTest.php | 13 +- tests/Console/CommandTest.php | 37 +- .../Concerns/CreatesAnsweredOutputStyles.php | 27 + tests/Console/ConsoleApplicationTest.php | 16 +- .../ContextualAttributeBindingTest.php | 21 +- .../InteractsWithInMemoryConnection.php | 30 ++ .../DatabaseAbstractSchemaGrammarTest.php | 6 +- .../DatabaseConnectionFactoryTest.php | 2 +- tests/Database/DatabaseConnectionTest.php | 49 +- ...oquentBelongsToManyWithoutTouchingTest.php | 59 ++- .../DatabaseEloquentBelongsToTest.php | 124 +++-- .../Database/DatabaseEloquentBuilderTest.php | 486 +++++++----------- .../DatabaseEloquentCollectionTest.php | 155 ++---- .../Database/DatabaseEloquentHasManyTest.php | 42 +- tests/Database/DatabaseEloquentHasOneTest.php | 51 +- .../DatabaseEloquentInverseRelationTest.php | 27 +- tests/Database/DatabaseEloquentModelTest.php | 484 +++++++---------- tests/Database/DatabaseEloquentMorphTest.php | 60 ++- .../Database/DatabaseEloquentMorphToTest.php | 78 ++- tests/Database/DatabaseEloquentPivotTest.php | 26 +- .../Database/DatabaseEloquentRelationTest.php | 164 +++--- .../DatabaseEloquentRelationshipsTest.php | 6 +- .../DatabaseMigrationMigrateCommandTest.php | 24 +- tests/Database/DatabaseMigratorTest.php | 157 +++++- tests/Database/DatabaseMySqlBuilderTest.php | 2 +- .../DatabaseMySqlQueryGrammarTest.php | 7 +- .../DatabasePostgresQueryGrammarTest.php | 7 +- tests/Database/DatabaseQueryBuilderTest.php | 111 ++-- tests/Database/DatabaseQueryGrammarTest.php | 25 +- tests/Database/DatabaseSeederTest.php | 4 +- .../DatabaseSoftDeletingScopeTest.php | 147 +++--- .../EloquentHasOneOrManyDeprecationTest.php | 56 +- tests/Database/SeedCommandTest.php | 4 +- tests/Events/BroadcastedEventsTest.php | 20 +- tests/Events/QueuedEventsTest.php | 29 +- tests/Filesystem/FilesystemAdapterTest.php | 50 +- .../Configuration/MiddlewareTest.php | 13 +- .../Exceptions/Renderer/ListenerTest.php | 41 +- .../Foundation/FoundationApplicationTest.php | 39 +- .../FoundationAuthenticationTest.php | 111 +++- .../Testing/DatabaseTruncationTest.php | 9 +- tests/Http/HttpClientTest.php | 47 +- tests/Http/HttpRedirectResponseTest.php | 47 +- tests/Http/HttpRequestTest.php | 49 +- tests/Http/HttpResponseTest.php | 54 +- tests/Http/JsonResourceTest.php | 15 +- .../Middleware/PreventRequestForgeryTest.php | 11 +- tests/Image/ImageManagerTest.php | 54 +- .../Database/EloquentMassPrunableTest.php | 6 +- .../Foundation/Exceptions/RendererTest.php | 33 +- .../Queue/CallQueuedHandlerTest.php | 39 +- tests/Integration/Queue/RateLimitedTest.php | 40 +- .../Queue/Redis/RateLimitedRedisStoreTest.php | 28 +- .../ThrottlesExceptionsRedisStoreTest.php | 36 +- .../Queue/ReleaseMiddlewareTest.php | 26 +- .../Integration/Queue/SkipMiddlewareTest.php | 12 +- .../Queue/ThrottlesExceptionsTest.php | 55 +- .../Queue/WithoutOverlappingJobsTest.php | 39 +- tests/Log/LogLoggerTest.php | 69 +-- tests/Mail/MailMailerTest.php | 76 ++- tests/Mail/MailSesV2TransportTest.php | 15 +- tests/Notifications/Fixtures/ChannelSpy.php | 30 ++ .../NotificationBroadcastChannelTest.php | 11 +- .../NotificationChannelManagerTest.php | 365 +++++++------ .../NotificationDatabaseChannelTest.php | 7 +- .../Notifications/NotificationSenderTest.php | 264 +++++----- tests/Queue/FailoverQueueTest.php | 16 +- tests/Queue/QueueDatabaseQueueUnitTest.php | 17 +- tests/Queue/QueueManagerTest.php | 85 ++- tests/Queue/QueueRedisQueueTest.php | 2 +- tests/Queue/QueueRetryCommandTest.php | 3 +- tests/Queue/QueueSqsQueueTest.php | 28 +- tests/Queue/QueueWorkerTest.php | 377 ++++++++------ tests/Redis/RedisEventsTest.php | 77 ++- tests/Routing/RoutingRedirectorTest.php | 17 +- tests/Support/SupportHelpersTest.php | 24 +- tests/Support/SupportTestingEventFakeTest.php | 17 +- tests/Validation/ValidationFactoryTest.php | 48 +- .../ValidationNotPwnedVerifierTest.php | 127 ++--- tests/Validation/ValidationValidatorTest.php | 94 ++-- tests/View/ViewFactoryTest.php | 40 +- 94 files changed, 3123 insertions(+), 3186 deletions(-) create mode 100644 tests/Console/Concerns/CreatesAnsweredOutputStyles.php create mode 100644 tests/Database/Concerns/InteractsWithInMemoryConnection.php create mode 100644 tests/Notifications/Fixtures/ChannelSpy.php diff --git a/tests/Auth/AuthDatabaseUserProviderTest.php b/tests/Auth/AuthDatabaseUserProviderTest.php index 1eaa45d7eb..5d6e758b05 100755 --- a/tests/Auth/AuthDatabaseUserProviderTest.php +++ b/tests/Auth/AuthDatabaseUserProviderTest.php @@ -10,10 +10,13 @@ use Hypervel\Contracts\Hashing\Hasher; use Hypervel\Database\ConnectionInterface; use Hypervel\Database\ConnectionResolverInterface; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder; +use Hypervel\Database\SQLiteConnection; +use Hypervel\Hashing\BcryptHasher; use Hypervel\Tests\TestCase; use Mockery as m; -use stdClass; +use PDO; use function Hypervel\Coroutine\parallel; @@ -21,28 +24,18 @@ class AuthDatabaseUserProviderTest extends TestCase { public function testRetrieveByIDReturnsUserWhenUserIsFound(): void { - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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); + $provider = $this->newProvider(); + $user = $provider->retrieveById(3); $this->assertInstanceOf(GenericUser::class, $user); - $this->assertSame(1, $user->getAuthIdentifier()); - $this->assertSame('Dayle', $user->name); + $this->assertSame(3, $user->getAuthIdentifier()); + $this->assertSame('taylor', $user->name); } public function testRetrieveByIDReturnsNullWhenUserIsNotFound(): void { - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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); + $provider = $this->newProvider(); + $user = $provider->retrieveById(99); $this->assertNull($user); } @@ -126,44 +119,24 @@ public function testResolverContractTakesPrecedenceForObjectsImplementingBothCon public function testRetrieveByTokenReturnsUser(): void { - $mockUser = new stdClass; - $mockUser->remember_token = 'a'; + $provider = $this->newProvider(); + $user = $provider->retrieveByToken(3, 'a'); - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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'); - - $this->assertEquals(new GenericUser((array) $mockUser), $user); + $this->assertInstanceOf(GenericUser::class, $user); + $this->assertSame(3, $user->getAuthIdentifier()); } public function testRetrieveTokenWithBadIdentifierReturnsNull(): void { - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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'); + $provider = $this->newProvider(); + $user = $provider->retrieveByToken(99, 'a'); $this->assertNull($user); } public function testRetrieveByBadTokenReturnsNull(): void { - $mockUser = new stdClass; - $mockUser->remember_token = null; - - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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'); + $provider = $this->newProvider(); $user = $provider->retrieveByToken(1, 'a'); $this->assertNull($user); @@ -171,31 +144,17 @@ public function testRetrieveByBadTokenReturnsNull(): void public function testRetrieveByCredentialsReturnsUserWhenUserIsFound(): void { - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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'); + $provider = $this->newProvider(); $user = $provider->retrieveByCredentials(['username' => 'dayle', 'password' => 'foo', 'group' => ['one', 'two']]); $this->assertInstanceOf(GenericUser::class, $user); - $this->assertSame(1, $user->getAuthIdentifier()); + $this->assertSame(3, $user->getAuthIdentifier()); $this->assertSame('taylor', $user->name); } public function testRetrieveByCredentialsAcceptsCallback(): void { - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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'); + $provider = $this->newProvider(); $user = $provider->retrieveByCredentials([function (Builder $builder): void { $builder->where('username', 'dayle'); @@ -203,28 +162,22 @@ public function testRetrieveByCredentialsAcceptsCallback(): void }]); $this->assertInstanceOf(GenericUser::class, $user); - $this->assertSame(1, $user->getAuthIdentifier()); + $this->assertSame(3, $user->getAuthIdentifier()); $this->assertSame('taylor', $user->name); } public function testRetrieveByCredentialsReturnsNullWhenUserIsFound(): void { - $conn = m::mock(ConnectionInterface::class); - $query = m::mock(Builder::class); - $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']); + $provider = $this->newProvider(); + $user = $provider->retrieveByCredentials(['username' => 'nobody']); $this->assertNull($user); } public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull(): void { - $conn = m::mock(ConnectionInterface::class); - $hasher = m::mock(Hasher::class); + $conn = new PdoConnection(new PDO('sqlite::memory:')); + $hasher = new BcryptHasher; $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); $user = $provider->retrieveByCredentials([ 'password' => 'dayle', @@ -236,12 +189,10 @@ public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull(): voi public function testCredentialValidation(): void { - $conn = m::mock(ConnectionInterface::class); - $hasher = m::mock(Hasher::class); - $hasher->expects('check')->with('plain', 'hash')->andReturn(true); + $conn = new PdoConnection(new PDO('sqlite::memory:')); + $hasher = new BcryptHasher; $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); + $user = new GenericUser(['password' => $hasher->make('plain')]); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertTrue($result); @@ -249,20 +200,18 @@ public function testCredentialValidation(): void public function testCredentialValidationFails(): void { - $conn = m::mock(ConnectionInterface::class); - $hasher = m::mock(Hasher::class); - $hasher->expects('check')->with('plain', 'hash')->andReturn(false); + $conn = new PdoConnection(new PDO('sqlite::memory:')); + $hasher = new BcryptHasher; $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); - $result = $provider->validateCredentials($user, ['password' => 'plain']); + $user = new GenericUser(['password' => $hasher->make('plain')]); + $result = $provider->validateCredentials($user, ['password' => 'wrong']); $this->assertFalse($result); } public function testCredentialValidationFailsGracefullyWithNullPassword(): void { - $conn = m::mock(ConnectionInterface::class); + $conn = new PdoConnection(new PDO('sqlite::memory:')); $hasher = m::mock(Hasher::class); $hasher->shouldReceive('check')->never(); $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); @@ -275,51 +224,73 @@ public function testCredentialValidationFailsGracefullyWithNullPassword(): void public function testRehashPasswordIfRequired(): void { - $hasher = m::mock(Hasher::class); - $hasher->expects('needsRehash')->with('hash')->andReturn(true); - $hasher->expects('make')->with('plain')->andReturn('rehashed'); + $connection = $this->newConnection(); + $connection->table('foo')->where('id', 3)->update(['password' => (new BcryptHasher(['rounds' => 4]))->make('plain')]); + + $hasher = new BcryptHasher(['rounds' => 5]); + $provider = new DatabaseUserProvider($connection, $hasher, 'foo'); + $provider->rehashPasswordIfRequired($provider->retrieveById(3), ['password' => 'plain']); + + $hash = $connection->table('foo')->where('id', 3)->value('password'); + $this->assertTrue($hasher->check('plain', $hash)); + $this->assertFalse($hasher->needsRehash($hash)); + } - $conn = m::mock(ConnectionInterface::class); - $table = m::mock(Builder::class); - $conn->expects('table')->with('foo')->andReturn($table); - $table->expects('where')->with('id', 1)->andReturnSelf(); - $table->expects('update')->with(['password_attribute' => 'rehashed']); + public function testRehashPasswordIfRequiredWithCustomPasswordColumn(): void + { + $connection = $this->newConnection(); + $connection->statement('alter table "foo" rename column "password" to "secret"'); - $user = new class(['id' => 1, 'password_attribute' => 'hash']) extends GenericUser { + $user = new class(['id' => 3, 'secret' => (new BcryptHasher(['rounds' => 4]))->make('plain')]) extends GenericUser { /** - * Get the custom password attribute name. + * Get the name of the password attribute for the user. */ public function getAuthPasswordName(): string { - return 'password_attribute'; + return 'secret'; } }; - $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); + $hasher = new BcryptHasher(['rounds' => 5]); + $provider = new DatabaseUserProvider($connection, $hasher, 'foo'); $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); - $this->assertSame('rehashed', $user->getAuthPassword()); + $hash = $connection->table('foo')->where('id', 3)->value('secret'); + $this->assertTrue($hasher->check('plain', $hash)); + $this->assertSame($hash, $user->getAuthPassword()); } public function testDontRehashPasswordIfNotRequired(): void { - $hasher = m::mock(Hasher::class); - $hasher->expects('needsRehash')->with('hash')->andReturn(false); - $hasher->shouldNotReceive('make'); + $hasher = new BcryptHasher(['rounds' => 5]); + $connection = $this->newConnection(); + $connection->table('foo')->where('id', 3)->update(['password' => $hash = $hasher->make('plain')]); - $conn = m::mock(ConnectionInterface::class); - $table = m::mock(Builder::class); - $conn->shouldNotReceive('table'); - $table->shouldNotReceive('where'); - $table->shouldNotReceive('update'); + $provider = new DatabaseUserProvider($connection, $hasher, 'foo'); + $provider->rehashPasswordIfRequired($provider->retrieveById(3), ['password' => 'plain']); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); - $user->shouldNotReceive('getAuthIdentifierName'); - $user->shouldNotReceive('getAuthIdentifier'); - $user->shouldNotReceive('getAuthPasswordName'); + $this->assertSame($hash, $connection->table('foo')->where('id', 3)->value('password')); + } - $provider = new DatabaseUserProvider($conn, $hasher, 'foo'); - $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + /** + * Create an in-memory connection with seeded users. + */ + protected function newConnection(): SQLiteConnection + { + $pdo = new PDO('sqlite::memory:'); + $pdo->exec('create table "foo" ("id" integer primary key, "username" text, "group" text, "name" text, "password" text, "remember_token" text)'); + $pdo->exec("insert into \"foo\" values (1, 'dayle', 'three', 'other', null, null)"); + $pdo->exec("insert into \"foo\" values (2, 'sam', 'one', 'third', null, null)"); + $pdo->exec("insert into \"foo\" values (3, 'dayle', 'one', 'taylor', null, 'a')"); + + return new SQLiteConnection($pdo); + } + + /** + * Create a provider backed by the seeded connection. + */ + protected function newProvider(): DatabaseUserProvider + { + return new DatabaseUserProvider($this->newConnection(), new BcryptHasher, 'foo'); } } diff --git a/tests/Auth/AuthEloquentUserProviderTest.php b/tests/Auth/AuthEloquentUserProviderTest.php index e88c1b15db..63615f751f 100755 --- a/tests/Auth/AuthEloquentUserProviderTest.php +++ b/tests/Auth/AuthEloquentUserProviderTest.php @@ -4,93 +4,58 @@ namespace Hypervel\Tests\Auth; +use Hypervel\Auth\Authenticatable as AuthenticatableTrait; use Hypervel\Auth\EloquentUserProvider; +use Hypervel\Auth\GenericUser; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Hashing\Hasher; use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Model; use Hypervel\Foundation\Auth\User; +use Hypervel\Hashing\BcryptHasher; +use Hypervel\Tests\Database\Concerns\InteractsWithInMemoryConnection; use Hypervel\Tests\TestCase; use Mockery as m; -use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; class AuthEloquentUserProviderTest extends TestCase { + use InteractsWithInMemoryConnection; + public function testRetrieveByIDReturnsUser(): void { - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $expectedUser = m::mock(Authenticatable::class); - $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); + $user = $this->newProvider()->retrieveById(3); + + $this->assertInstanceOf(EloquentProviderUserStub::class, $user); + $this->assertSame('taylor', $user->name); } public function testRetrieveByTokenReturnsUser(): void { - $mockUser = m::mock(Authenticatable::class); - $mockUser->expects('getRememberToken')->andReturn('a'); - - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $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); + $user = $this->newProvider()->retrieveByToken(3, 'a'); + + $this->assertInstanceOf(EloquentProviderUserStub::class, $user); + $this->assertSame('taylor', $user->name); } public function testRetrieveTokenWithBadIdentifierReturnsNull(): void { - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $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); + $this->assertNull($this->newProvider()->retrieveByToken(99, 'a')); } public function testRetrievingWithOnlyPasswordCredentialReturnsNull(): void { - $provider = $this->getProviderMock(); - $provider->expects($this->never())->method('createModel'); - $user = $provider->retrieveByCredentials(['api_password' => 'foo']); + $user = $this->newProvider()->retrieveByCredentials(['api_password' => 'foo']); $this->assertNull($user); } public function testRetrieveByBadTokenReturnsNull(): void { - $mockUser = m::mock(Authenticatable::class); - $mockUser->expects('getRememberToken')->andReturn(null); - - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $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'); + $provider = $this->newProvider(); - $this->assertNull($user); + $this->assertNull($provider->retrieveByToken(1, 'a')); + $this->assertNull($provider->retrieveByToken(3, 'b')); } public function testUpdateRememberTokenRestoresTimestampsAfterSaving(): void @@ -138,44 +103,25 @@ public function testUpdateRememberTokenRestoresTimestampsAfterSaveFailure(): voi public function testRetrieveByCredentialsReturnsUser(): void { - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $expectedUser = m::mock(Authenticatable::class); - $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); + $user = $this->newProvider()->retrieveByCredentials(['username' => 'dayle', 'password' => 'foo', 'group' => ['one', 'two']]); + + $this->assertInstanceOf(EloquentProviderUserStub::class, $user); + $this->assertSame('taylor', $user->name); } public function testRetrieveByCredentialsAcceptsCallback(): void { - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $expectedUser = m::mock(Authenticatable::class); - $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 $builder): void { + $user = $this->newProvider()->retrieveByCredentials([function (Builder $builder): void { $builder->where('username', 'dayle'); $builder->whereIn('group', ['one', 'two']); }]); - $this->assertSame($expectedUser, $user); + $this->assertSame('taylor', $user->name); } public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull(): void { - $provider = $this->getProviderMock(); - $provider->expects($this->never())->method('createModel'); - $user = $provider->retrieveByCredentials([ + $user = $this->newProvider()->retrieveByCredentials([ 'password' => 'dayle', 'password2' => 'night', ]); @@ -185,11 +131,9 @@ public function testRetrieveByCredentialsWithMultiplyPasswordsReturnsNull(): voi public function testCredentialValidation(): void { - $hasher = m::mock(Hasher::class); - $hasher->expects('check')->with('plain', 'hash')->andReturn(true); + $hasher = new BcryptHasher; $provider = new EloquentUserProvider($hasher, 'foo'); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); + $user = new GenericUser(['password' => $hasher->make('plain')]); $result = $provider->validateCredentials($user, ['password' => 'plain']); $this->assertTrue($result); @@ -197,12 +141,10 @@ public function testCredentialValidation(): void public function testCredentialValidationFailed(): void { - $hasher = m::mock(Hasher::class); - $hasher->expects('check')->with('plain', 'hash')->andReturn(false); + $hasher = new BcryptHasher; $provider = new EloquentUserProvider($hasher, 'foo'); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); - $result = $provider->validateCredentials($user, ['password' => 'plain']); + $user = new GenericUser(['password' => $hasher->make('plain')]); + $result = $provider->validateCredentials($user, ['password' => 'wrong']); $this->assertFalse($result); } @@ -221,18 +163,31 @@ public function testCredentialValidationFailsGracefullyWithNullPassword(): void public function testRehashPasswordIfRequired(): void { - $hasher = m::mock(Hasher::class); - $hasher->expects('needsRehash')->with('hash')->andReturn(true); - $hasher->expects('make')->with('plain')->andReturn('rehashed'); + $hasher = new BcryptHasher(['rounds' => 5]); + $provider = $this->newProvider($hasher); + $user = EloquentProviderUserStub::find(3); + $user->forceFill(['password' => (new BcryptHasher(['rounds' => 4]))->make('plain')])->save(); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); - $user->expects('getAuthPasswordName')->andReturn('password_attribute'); - $user->expects('forceFill')->with(['password_attribute' => 'rehashed'])->andReturnSelf(); - $user->expects('save'); + $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + + $hash = $user->fresh()->password; + $this->assertTrue($hasher->check('plain', $hash)); + $this->assertFalse($hasher->needsRehash($hash)); + } + + public function testRehashPasswordIfRequiredWithCustomPasswordColumn(): void + { + $hasher = new BcryptHasher(['rounds' => 5]); + $provider = $this->newProvider($hasher); + (new EloquentProviderCustomPasswordUserStub)->getConnection()->statement('alter table "users" rename column "password" to "secret"'); + $user = EloquentProviderCustomPasswordUserStub::find(3); + $user->forceFill(['secret' => (new BcryptHasher(['rounds' => 4]))->make('plain')])->save(); - $provider = new EloquentUserProvider($hasher, 'foo'); $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + + $hash = $user->fresh()->secret; + $this->assertTrue($hasher->check('plain', $hash)); + $this->assertFalse($hasher->needsRehash($hash)); } public function testDontRehashPasswordIfNotRequired(): void @@ -241,19 +196,16 @@ public function testDontRehashPasswordIfNotRequired(): void $hasher->expects('needsRehash')->with('hash')->andReturn(false); $hasher->shouldNotReceive('make'); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthPassword')->andReturn('hash'); - $user->shouldNotReceive('getAuthPasswordName'); - $user->shouldNotReceive('forceFill'); - $user->shouldNotReceive('save'); - - $provider = new EloquentUserProvider($hasher, 'foo'); + $provider = $this->newProvider($hasher); + $user = EloquentProviderUserStub::find(3); $provider->rehashPasswordIfRequired($user, ['password' => 'plain']); + + $this->assertSame('hash', $user->fresh()->password); } public function testModelsCanBeCreated(): void { - $hasher = m::mock(Hasher::class); + $hasher = new BcryptHasher; $provider = new EloquentUserProvider($hasher, EloquentProviderUserStub::class); $model = $provider->createModel(); @@ -266,37 +218,43 @@ public function testRegistersQueryHandler(): void $builder->whereIn('group', ['one', 'two']); }; - $provider = $this->getProviderMock(); - $model = m::mock(Model::class); - $builder = m::mock(Builder::class); - $model->expects('newQuery')->andReturn($builder); - $builder->expects('where')->with('username', 'dayle'); - $builder->expects('whereIn')->with('group', ['one', 'two']); - $expectedUser = m::mock(Authenticatable::class); - $builder->expects('first')->andReturn($expectedUser); - $provider->expects($this->once())->method('createModel')->willReturn($model); + $provider = $this->newProvider(); $provider->withQuery($callback); $user = $provider->retrieveByCredentials([function (Builder $builder): void { $builder->where('username', 'dayle'); }]); - $this->assertSame($expectedUser, $user); + $this->assertSame('taylor', $user->name); $this->assertSame($callback, $provider->getQueryCallback()); } /** - * Create a user provider with a mocked model factory. + * Create a user provider backed by seeded in-memory users. */ - protected function getProviderMock(): EloquentUserProvider&MockObject + protected function newProvider(?Hasher $hasher = null): EloquentUserProvider { - $hasher = m::mock(Hasher::class); + $pdo = $this->useInMemoryConnection()->getPdo(); + $pdo->exec('create table "users" ("id" integer primary key, "username" text, "group" text, "name" text, "remember_token" text, "password" text)'); + $pdo->exec("insert into \"users\" values (1, 'dayle', 'three', 'other', null, null)"); + $pdo->exec("insert into \"users\" values (2, 'sam', 'one', 'third', null, null)"); + $pdo->exec("insert into \"users\" values (3, 'dayle', 'two', 'taylor', 'a', 'hash')"); - return $this->getMockBuilder(EloquentUserProvider::class)->onlyMethods(['createModel'])->setConstructorArgs([$hasher, 'foo'])->getMock(); + return new EloquentUserProvider($hasher ?? new BcryptHasher, EloquentProviderUserStub::class); } } -class EloquentProviderUserStub extends Model +class EloquentProviderUserStub extends Model implements Authenticatable +{ + use AuthenticatableTrait; + + protected ?string $table = 'users'; + + public bool $timestamps = false; +} + +class EloquentProviderCustomPasswordUserStub extends EloquentProviderUserStub { + protected string $authPasswordName = 'secret'; } class EloquentProviderRememberUserStub extends User diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php index 4a646a9d63..d65bf5ff58 100755 --- a/tests/Auth/AuthGuardTest.php +++ b/tests/Auth/AuthGuardTest.php @@ -13,15 +13,20 @@ use Hypervel\Auth\Events\Login; use Hypervel\Auth\Events\Logout; use Hypervel\Auth\Events\Validated; +use Hypervel\Auth\GenericUser; use Hypervel\Auth\Recaller; use Hypervel\Auth\SessionGuard; use Hypervel\Container\Container; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Auth\UserProvider; -use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Contracts\Events\Dispatcher as DispatcherContract; use Hypervel\Contracts\Session\Session; use Hypervel\Cookie\CookieJar; use Hypervel\Coroutine\Waiter; +use Hypervel\Events\Dispatcher; +use Hypervel\Session\ArraySessionHandler; +use Hypervel\Session\Store; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Support\Timebox; use Hypervel\Tests\TestCase; use Mockery as m; @@ -35,131 +40,142 @@ class AuthGuardTest extends TestCase { public function testBasicReturnsNullOnValidAttempt(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $basicRequest = Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret']); - $app->shouldReceive('make')->with('request')->andReturn($basicRequest); - $guard = m::mock(SessionGuard::class . '[check,attempt]', ['default', $provider, $session, $app]); - $guard->expects('check')->andReturn(false); - $guard->expects('attempt')->with(['email' => 'foo@bar.com', 'password' => 'secret'])->andReturn(true); + [$guard, $provider, $session] = $this->getRealGuard(Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret'])); - $guard->basic('email'); + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $credentials = ['email' => 'foo@bar.com', 'password' => 'secret']; + $provider->expects('retrieveByCredentials')->with($credentials)->andReturn($user); + $provider->expects('validateCredentials')->with($user, $credentials)->andReturn(true); + $provider->expects('rehashPasswordIfRequired')->with($user, $credentials); + + $this->assertNull($guard->basic('email')); + $this->assertSame($user, $guard->user()); + $this->assertSame(10, $session->get($guard->getName())); } public function testBasicReturnsNullWhenAlreadyLoggedIn(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = m::mock(SessionGuard::class . '[check]', ['default', $provider, $session, $app]); - $guard->expects('check')->andReturn(true); - $guard->shouldReceive('attempt')->never(); + [$guard, $provider] = $this->getRealGuard(Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret'])); - $guard->basic('email'); + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $guard->setUser($user); + $provider->shouldNotReceive('retrieveByCredentials'); + + $this->assertNull($guard->basic('email')); + $this->assertSame($user, $guard->user()); } public function testBasicReturnsResponseOnFailure(): void { $this->expectException(UnauthorizedHttpException::class); - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $basicRequest = Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret']); - $app->shouldReceive('make')->with('request')->andReturn($basicRequest); - $guard = m::mock(SessionGuard::class . '[check,attempt]', ['default', $provider, $session, $app]); - $guard->expects('check')->andReturn(false); - $guard->expects('attempt')->with(['email' => 'foo@bar.com', 'password' => 'secret'])->andReturn(false); + [$guard, $provider] = $this->getRealGuard(Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret'])); + + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $credentials = ['email' => 'foo@bar.com', 'password' => 'secret']; + $provider->expects('retrieveByCredentials')->with($credentials)->andReturn($user); + $provider->expects('validateCredentials')->with($user, $credentials)->andReturn(false); + $provider->shouldNotReceive('rehashPasswordIfRequired'); + $guard->basic('email'); } public function testBasicWithExtraConditions(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $basicRequest = Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret']); - $app->shouldReceive('make')->with('request')->andReturn($basicRequest); - $guard = m::mock(SessionGuard::class . '[check,attempt]', ['default', $provider, $session, $app]); - $guard->expects('check')->andReturn(false); - $guard->expects('attempt')->with(['email' => 'foo@bar.com', 'password' => 'secret', 'active' => 1])->andReturn(true); + [$guard, $provider, $session] = $this->getRealGuard(Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret'])); - $guard->basic('email', ['active' => 1]); + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $credentials = ['email' => 'foo@bar.com', 'password' => 'secret', 'active' => 1]; + $provider->expects('retrieveByCredentials')->with($credentials)->andReturn($user); + $provider->expects('validateCredentials')->with($user, $credentials)->andReturn(true); + $provider->expects('rehashPasswordIfRequired')->with($user, $credentials); + + $this->assertNull($guard->basic('email', ['active' => 1])); + $this->assertSame($user, $guard->user()); + $this->assertSame(10, $session->get($guard->getName())); } public function testBasicWithExtraArrayConditions(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $basicRequest = Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret']); - $app->shouldReceive('make')->with('request')->andReturn($basicRequest); - $guard = m::mock(SessionGuard::class . '[check,attempt]', ['default', $provider, $session, $app]); - $guard->expects('check')->andReturn(false); - $guard->expects('attempt')->with(['email' => 'foo@bar.com', 'password' => 'secret', 'active' => 1, 'type' => [1, 2, 3]])->andReturn(true); + [$guard, $provider, $session] = $this->getRealGuard(Request::create('/', 'GET', [], [], [], ['PHP_AUTH_USER' => 'foo@bar.com', 'PHP_AUTH_PW' => 'secret'])); + + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $credentials = ['email' => 'foo@bar.com', 'password' => 'secret', 'active' => 1, 'type' => [1, 2, 3]]; + $provider->expects('retrieveByCredentials')->with($credentials)->andReturn($user); + $provider->expects('validateCredentials')->with($user, $credentials)->andReturn(true); + $provider->expects('rehashPasswordIfRequired')->with($user, $credentials); - $guard->basic('email', ['active' => 1, 'type' => [1, 2, 3]]); + $this->assertNull($guard->basic('email', ['active' => 1, 'type' => [1, 2, 3]])); + $this->assertSame($user, $guard->user()); + $this->assertSame(10, $session->get($guard->getName())); } public function testAttemptCallsRetrieveByCredentials(): void { $guard = $this->getGuard(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $guard->setDispatcher($events); $timebox = $guard->getTimebox(); $timebox->expects('call')->andReturnUsing(function (Closure $callback) use ($timebox): bool { return $callback($timebox); }); - $events->expects('dispatch')->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Failed::class)); - $events->shouldNotReceive('dispatch')->with(m::type(Validated::class)); $guard->getProvider()->expects('retrieveByCredentials')->with(['foo']); $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $guard->attempt(['foo']); + + $events->assertDispatchedOnce(Attempting::class); + $events->assertDispatchedOnce(Failed::class); + $events->assertNotDispatched(Validated::class); } public function testAttemptReturnsUserInterface(): void { [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); $guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login'])->setConstructorArgs(['default', $provider, $session, $app, $timebox])->getMock(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $guard->setDispatcher($events); $timebox->expects('call')->andReturnUsing(function (Closure $callback, int $microseconds) use ($timebox): bool { return $callback($timebox->expects('returnEarly')->getMock()); }); - $events->expects('dispatch')->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Validated::class)); $user = $this->createStub(Authenticatable::class); $guard->getProvider()->expects('retrieveByCredentials')->andReturn($user); $guard->getProvider()->expects('validateCredentials')->with($user, ['foo'])->andReturn(true); $guard->getProvider()->expects('rehashPasswordIfRequired')->with($user, ['foo']); $guard->expects($this->once())->method('login')->with($user); $this->assertTrue($guard->attempt(['foo'])); + + $events->assertDispatchedOnce(Attempting::class); + $events->assertDispatchedOnce(Validated::class); } public function testAttemptReturnsFalseIfUserNotGiven(): void { $mock = $this->getGuard(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $mock->setDispatcher($events); $timebox = $mock->getTimebox(); $timebox->expects('call')->andReturnUsing(function (Closure $callback, int $microseconds) use ($timebox): bool { return $callback($timebox); }); - $events->expects('dispatch')->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Failed::class)); - $events->shouldNotReceive('dispatch')->with(m::type(Validated::class)); $mock->getProvider()->expects('retrieveByCredentials')->andReturn(null); $mock->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $this->assertFalse($mock->attempt(['foo'])); + + $events->assertDispatchedOnce(Attempting::class); + $events->assertDispatchedOnce(Failed::class); + $events->assertNotDispatched(Validated::class); } public function testAttemptAndWithCallbacks(): void { [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['getName'])->setConstructorArgs(['default', $provider, $session, $app, $timebox])->getMock(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $mock->setDispatcher($events); $timebox->shouldReceive('call')->andReturnUsing(function (Closure $callback) use ($timebox): bool { return $callback($timebox->shouldReceive('returnEarly')->getMock()); }); $user = m::mock(Authenticatable::class); - $events->expects('dispatch')->times(3)->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Login::class)); - $events->expects('dispatch')->with(m::type(Authenticated::class)); - $events->expects('dispatch')->times(2)->with(m::type(Validated::class)); - $events->expects('dispatch')->times(2)->with(m::type(Failed::class)); $mock->expects($this->once())->method('getName')->willReturn('foo'); $user->expects('getAuthIdentifier')->andReturn('bar'); $user->expects('getAuthPassword')->andReturn(null); @@ -192,25 +208,32 @@ public function testAttemptAndWithCallbacks(): void })); $this->assertFalse($executed); + + $events->assertDispatchedTimes(Attempting::class, 3); + $events->assertDispatchedOnce(Login::class); + $events->assertDispatchedOnce(Authenticated::class); + $events->assertDispatchedTimes(Validated::class, 2); + $events->assertDispatchedTimes(Failed::class, 2); } public function testAttemptRehashesPasswordWhenRequired(): void { [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); $guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login'])->setConstructorArgs(['default', $provider, $session, $app, $timebox])->getMock(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $guard->setDispatcher($events); $timebox->expects('call')->andReturnUsing(function (Closure $callback, int $microseconds) use ($timebox): bool { return $callback($timebox->expects('returnEarly')->getMock()); }); - $events->expects('dispatch')->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Validated::class)); $user = $this->createStub(Authenticatable::class); $guard->getProvider()->expects('retrieveByCredentials')->andReturn($user); $guard->getProvider()->expects('validateCredentials')->with($user, ['foo'])->andReturn(true); $guard->getProvider()->expects('rehashPasswordIfRequired')->with($user, ['foo']); $guard->expects($this->once())->method('login')->with($user); $this->assertTrue($guard->attempt(['foo'])); + + $events->assertDispatchedOnce(Attempting::class); + $events->assertDispatchedOnce(Validated::class); } public function testAttemptDoesntRehashPasswordWhenDisabled(): void @@ -219,19 +242,20 @@ public function testAttemptDoesntRehashPasswordWhenDisabled(): void $guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login']) ->setConstructorArgs(['default', $provider, $session, $app, $timebox, false]) ->getMock(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $guard->setDispatcher($events); $timebox->expects('call')->andReturnUsing(function (Closure $callback, int $microseconds) use ($timebox): bool { return $callback($timebox->expects('returnEarly')->getMock()); }); - $events->expects('dispatch')->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Validated::class)); $user = $this->createStub(Authenticatable::class); $guard->getProvider()->expects('retrieveByCredentials')->andReturn($user); $guard->getProvider()->expects('validateCredentials')->with($user, ['foo'])->andReturn(true); $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $guard->expects($this->once())->method('login')->with($user); $this->assertTrue($guard->attempt(['foo'])); + + $events->assertDispatchedOnce(Attempting::class); + $events->assertDispatchedOnce(Validated::class); } public function testLoginStoresIdentifierInSession(): void @@ -250,19 +274,18 @@ public function testLoginStoresIdentifierInSession(): void public function testLoginStoresPasswordHashInSession(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = new SessionGuard('default', $provider, $session, $app); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthIdentifier')->andReturn('foo'); - $user->expects('getAuthPassword')->andReturn('bar'); - $session->expects('put')->with($guard->getName(), 'foo'); - $session->expects('regenerate'); - $session->expects('put')->with( - 'password_hash_default', - hash_hmac('sha256', 'bar', 'base-key-for-password-hash-mac') - ); + [$guard, , $session] = $this->getRealGuard(); + $sessionId = $session->getId(); + $user = new GenericUser(['id' => 'foo', 'password' => 'bar']); $guard->login($user); + + $this->assertSame('foo', $session->get($guard->getName())); + $this->assertSame( + hash_hmac('sha256', 'bar', 'base-key-for-password-hash-mac'), + $session->get('password_hash_default') + ); + $this->assertNotSame($sessionId, $session->getId()); } public function testSessionGuardIsMacroable(): void @@ -283,11 +306,9 @@ public function testLoginFiresLoginAndAuthenticatedEvents(): void { [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['getName'])->setConstructorArgs(['default', $provider, $session, $app])->getMock(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $mock->setDispatcher($events); $user = m::mock(Authenticatable::class); - $events->expects('dispatch')->with(m::type(Login::class)); - $events->expects('dispatch')->with(m::type(Authenticated::class)); $mock->expects($this->once())->method('getName')->willReturn('foo'); $user->expects('getAuthIdentifier')->andReturn('bar'); $user->expects('getAuthPassword')->andReturn(null); @@ -295,28 +316,32 @@ public function testLoginFiresLoginAndAuthenticatedEvents(): void $mock->getSession()->expects('remove')->with('password_hash_default'); $session->expects('regenerate'); $mock->login($user); + + $events->assertDispatchedOnce(Login::class); + $events->assertDispatchedOnce(Authenticated::class); } public function testFailedAttemptFiresFailedEvent(): void { $guard = $this->getGuard(); - $events = $this->mockEventDispatcher(); + $events = new EventFake(new Dispatcher); $guard->setDispatcher($events); $timebox = $guard->getTimebox(); $timebox->expects('call')->andReturnUsing(function (Closure $callback, int $microseconds) use ($timebox): bool { return $callback($timebox); }); - $events->expects('dispatch')->with(m::type(Attempting::class)); - $events->expects('dispatch')->with(m::type(Failed::class)); - $events->shouldNotReceive('dispatch')->with(m::type(Validated::class)); $guard->getProvider()->expects('retrieveByCredentials')->with(['foo'])->andReturn(null); $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); $guard->attempt(['foo']); + + $events->assertDispatchedOnce(Attempting::class); + $events->assertDispatchedOnce(Failed::class); + $events->assertNotDispatched(Validated::class); } public function testAuthenticateReturnsUserWhenUserIsNotNull(): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $guard = $this->getGuard(); $guard->setUser($user); @@ -325,27 +350,27 @@ public function testAuthenticateReturnsUserWhenUserIsNotNull(): void public function testSetUserFiresAuthenticatedEvent(): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $guard = $this->getGuard(); - $events = $this->mockEventDispatcher(); - $events->expects('dispatch')->with(m::type(Authenticated::class)); + $events = new EventFake(new Dispatcher); $guard->setDispatcher($events); $guard->setUser($user); + + $events->assertDispatchedOnce(Authenticated::class); } public function testAuthenticateThrowsWhenUserIsNull(): void { $this->expectExceptionObject(new AuthenticationException('Unauthenticated.')); - $guard = $this->getGuard(); - $guard->getSession()->expects('get')->andReturn(null); + [$guard] = $this->getRealGuard(); $guard->authenticate(); } public function testHasUserReturnsTrueWhenUserIsNotNull(): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $guard = $this->getGuard(); $guard->setUser($user); @@ -363,7 +388,7 @@ public function testHasUserReturnsFalseWhenUserIsNull(): void public function testIsAuthedReturnsTrueWhenUserIsNotNull(): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $mock = $this->getGuard(); $mock->setUser($user); $this->assertTrue($mock->check()); @@ -372,16 +397,15 @@ public function testIsAuthedReturnsTrueWhenUserIsNotNull(): void public function testIsAuthedReturnsFalseWhenUserIsNull(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['user'])->setConstructorArgs(['default', $provider, $session, $app])->getMock(); - $mock->expects($this->exactly(2))->method('user')->willReturn(null); - $this->assertFalse($mock->check()); - $this->assertTrue($mock->guest()); + [$guard] = $this->getRealGuard(); + + $this->assertFalse($guard->check()); + $this->assertTrue($guard->guest()); } public function testUserMethodReturnsCachedUser(): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $mock = $this->getGuard(); $mock->setUser($user); $this->assertSame($user, $mock->user()); @@ -413,12 +437,13 @@ public static function sessionStartedStates(): array public function testUserIsSetToRetrievedUser(): void { - $mock = $this->getGuard(); - $mock->getSession()->expects('get')->andReturn(1); - $user = m::mock(Authenticatable::class); - $mock->getProvider()->expects('retrieveById')->with(1)->andReturn($user); - $this->assertSame($user, $mock->user()); - $this->assertSame($user, $mock->getUser()); + [$guard, $provider, $session] = $this->getRealGuard(); + $session->put($guard->getName(), 1); + $user = new GenericUser([]); + $provider->expects('retrieveById')->with(1)->andReturn($user); + + $this->assertSame($user, $guard->user()); + $this->assertSame($user, $guard->getUser()); } public function testLogoutRemovesSessionTokenAndRememberMeCookie(): void @@ -435,7 +460,7 @@ public function testLogoutRemovesSessionTokenAndRememberMeCookie(): void $mock->expects($this->once())->method('recaller')->willReturn(new Recaller('id|token|hash')); $provider->expects('updateRememberToken'); - $cookie = m::mock(Cookie::class); + $cookie = new Cookie('bar'); $cookies->expects('forget')->with('bar')->andReturn($cookie); $cookies->expects('queue')->with($cookie); $cookies->expects('unqueue')->with($recallerName); @@ -467,17 +492,18 @@ public function testLogoutDoesNotEnqueueRememberMeCookieForDeletionIfCookieDoesn public function testLogoutFiresLogoutEvent(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['clearUserDataFromStorage'])->setConstructorArgs(['default', $provider, $session, $app])->getMock(); - $mock->expects($this->once())->method('clearUserDataFromStorage'); - $events = $this->mockEventDispatcher(); - $mock->setDispatcher($events); - $user = m::mock(Authenticatable::class); - $user->expects('getRememberToken')->andReturn(null); - $events->expects('dispatch')->with(m::type(Authenticated::class)); - $mock->setUser($user); - $events->expects('dispatch')->with(m::type(Logout::class)); - $mock->logout(); + [$guard, , $session] = $this->getRealGuard(); + $guard->setCookieJar(new CookieJar); + $events = new EventFake(new Dispatcher); + $guard->setDispatcher($events); + $user = new GenericUser(['id' => 10, 'remember_token' => null]); + $guard->setUser($user); + $session->put($guard->getName(), $user->getAuthIdentifier()); + $guard->logout(); + + $this->assertNull($session->get($guard->getName())); + $events->assertDispatchedOnce(Authenticated::class); + $events->assertDispatchedOnce(Logout::class); } public function testLogoutDoesNotSetRememberTokenIfNotPreviouslySet(): void @@ -501,12 +527,12 @@ public function testLogoutCurrentDeviceRemovesRememberMeCookie(): void $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['getName', 'getRecallerName', 'recaller'])->setConstructorArgs(['default', $provider, $session, $app])->getMock(); $cookies = m::mock(CookieJar::class); $mock->setCookieJar($cookies); - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $mock->expects($this->once())->method('getName')->willReturn('foo'); $mock->expects($this->exactly(2))->method('getRecallerName')->willReturn($recallerName = 'bar'); $mock->expects($this->once())->method('recaller')->willReturn(new Recaller('id|token|hash')); - $cookie = m::mock(Cookie::class); + $cookie = new Cookie('bar'); $cookies->expects('forget')->with('bar')->andReturn($cookie); $cookies->expects('queue')->with($cookie); $cookies->expects('unqueue')->with($recallerName); @@ -522,7 +548,7 @@ public function testLogoutCurrentDeviceDoesNotEnqueueRememberMeCookieForDeletion $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['getName', 'getRecallerName', 'recaller'])->setConstructorArgs(['default', $provider, $session, $app])->getMock(); $cookies = m::mock(CookieJar::class); $mock->setCookieJar($cookies); - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $mock->expects($this->once())->method('getName')->willReturn('foo'); $mock->expects($this->once())->method('getRecallerName')->willReturn($recallerName = 'bar'); $mock->expects($this->once())->method('recaller')->willReturn(null); @@ -536,19 +562,21 @@ public function testLogoutCurrentDeviceDoesNotEnqueueRememberMeCookieForDeletion public function testLogoutCurrentDeviceFiresLogoutEvent(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['clearUserDataFromStorage'])->setConstructorArgs(['default', $provider, $session, $app])->getMock(); - $mock->expects($this->exactly(2))->method('clearUserDataFromStorage'); - $events = $this->mockEventDispatcher(); - $mock->setDispatcher($events); - $user = m::mock(Authenticatable::class); - $events->expects('dispatch')->with(m::type(Authenticated::class)); - $mock->setUser($user); - $events->expects('dispatch')->with(m::on(fn (CurrentDeviceLogout $event): bool => $event->guard === 'default' && $event->user === $user)); - $mock->logoutCurrentDevice(); + [$guard, , $session] = $this->getRealGuard(); + $guard->setCookieJar(new CookieJar); + $events = new EventFake(new Dispatcher); + $guard->setDispatcher($events); + $user = new GenericUser(['id' => 10]); + $guard->setUser($user); + $session->put($guard->getName(), $user->getAuthIdentifier()); + $guard->logoutCurrentDevice(); + $guard->logoutCurrentDevice(); - $events->expects('dispatch')->with(m::on(fn (CurrentDeviceLogout $event): bool => $event->guard === 'default' && $event->user === null)); - $mock->logoutCurrentDevice(); + $this->assertNull($session->get($guard->getName())); + $events->assertDispatchedOnce(Authenticated::class); + $events->assertDispatchedTimes(CurrentDeviceLogout::class, 2); + $events->assertDispatched(CurrentDeviceLogout::class, fn (CurrentDeviceLogout $event): bool => $event->guard === 'default' && $event->user === $user); + $events->assertDispatched(CurrentDeviceLogout::class, fn (CurrentDeviceLogout $event): bool => $event->guard === 'default' && $event->user === null); } public function testLoginMethodQueuesCookieWhenRemembering(): void @@ -637,68 +665,62 @@ public function testLoginMethodCreatesRememberTokenIfOneDoesntExist(): void public function testLoginUsingIdLogsInWithUser(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - - $guard = m::mock(SessionGuard::class, ['default', $provider, $session, $app])->makePartial(); + [$guard, $provider, $session] = $this->getRealGuard(); - $user = m::mock(Authenticatable::class); - $guard->getProvider()->expects('retrieveById')->with(10)->andReturn($user); - $guard->expects('login')->with($user, false); + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $provider->expects('retrieveById')->with(10)->andReturn($user); $this->assertSame($user, $guard->loginUsingId(10)); + $this->assertSame($user, $guard->user()); + $this->assertSame(10, $session->get($guard->getName())); } public function testLoginUsingIdFailure(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = m::mock(SessionGuard::class, ['default', $provider, $session, $app])->makePartial(); + [$guard, $provider, $session] = $this->getRealGuard(); - $guard->getProvider()->expects('retrieveById')->with(11)->andReturn(null); - $guard->shouldNotReceive('login'); + $provider->expects('retrieveById')->with(11)->andReturn(null); $this->assertFalse($guard->loginUsingId(11)); + $this->assertNull($guard->user()); + $this->assertNull($session->get($guard->getName())); } public function testOnceUsingIdSetsUser(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = m::mock(SessionGuard::class, ['default', $provider, $session, $app])->makePartial(); + [$guard, $provider, $session] = $this->getRealGuard(); - $user = m::mock(Authenticatable::class); - $guard->getProvider()->expects('retrieveById')->with(10)->andReturn($user); - $guard->expects('setUser')->with($user); + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $provider->expects('retrieveById')->with(10)->andReturn($user); $this->assertSame($user, $guard->onceUsingId(10)); + $this->assertSame($user, $guard->user()); + $this->assertNull($session->get($guard->getName())); } public function testOnceUsingIdFailure(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = m::mock(SessionGuard::class, ['default', $provider, $session, $app])->makePartial(); + [$guard, $provider] = $this->getRealGuard(); - $guard->getProvider()->expects('retrieveById')->with(11)->andReturn(null); - $guard->shouldNotReceive('setUser'); + $provider->expects('retrieveById')->with(11)->andReturn(null); $this->assertFalse($guard->onceUsingId(11)); + $this->assertNull($guard->user()); } #[DataProvider('rememberCookiePasswords')] public function testUserUsesRememberCookieIfItExists(?string $passwordHash): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = new SessionGuard('default', $provider, $session, $app); - $cookieRequest = Request::create('/', 'GET', [], [ + $guard = $this->getGuard(); + [$guard, $provider, $session] = $this->getRealGuard(Request::create('/', 'GET', [], [ $guard->getRecallerName() => 'id|recaller|' . $guard->hashPasswordForCookie($passwordHash), - ]); - $app->shouldReceive('make')->with('request')->andReturn($cookieRequest); - $guard->getSession()->expects('get')->with($guard->getName())->andReturn(null); + ])); + $sessionId = $session->getId(); $user = m::mock(Authenticatable::class); - $guard->getProvider()->expects('retrieveByToken')->with('id', 'recaller')->andReturn($user); + $provider->expects('retrieveByToken')->with('id', 'recaller')->andReturn($user); $user->expects('getAuthIdentifier')->twice()->andReturn('bar'); $user->expects('getAuthPassword')->twice()->andReturn($passwordHash); - $guard->getSession()->expects('put')->twice()->with($guard->getName(), 'bar'); - $session->expects('regenerate')->twice(); - $events = m::mock(Dispatcher::class); + $events = m::mock(DispatcherContract::class); $events->shouldReceive('hasListeners')->andReturnUsing(static fn (string $event): bool => $event === Login::class); $rememberedLogins = []; $events->expects('dispatch')->twice()->with(m::on(function (Login $event) use ($guard, &$rememberedLogins): bool { @@ -710,16 +732,18 @@ public function testUserUsesRememberCookieIfItExists(?string $passwordHash): voi $this->assertSame($user, $guard->user()); $this->assertTrue($guard->viaRemember()); - - if ($passwordHash !== null) { - $session->expects('put')->with('password_hash_default', $guard->hashPasswordForCookie($passwordHash)); - } else { - $session->expects('remove')->with('password_hash_default'); - } + $this->assertSame('bar', $session->get($guard->getName())); + $this->assertNotSame($sessionId, $rememberedSessionId = $session->getId()); + $this->assertFalse($session->has('password_hash_default')); $guard->login($user); $this->assertFalse($guard->viaRemember()); $this->assertSame([true, false], $rememberedLogins); + $this->assertNotSame($rememberedSessionId, $session->getId()); + $this->assertSame( + $passwordHash === null ? null : $guard->hashPasswordForCookie($passwordHash), + $session->get('password_hash_default') + ); } /** @@ -791,36 +815,34 @@ public static function invalidRememberCookieHashes(): array public function testLoginOnceSetsUser(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = m::mock(SessionGuard::class, ['default', $provider, $session, $app, $timebox])->makePartial(); - $user = m::mock(Authenticatable::class); - $timebox->expects('call')->andReturnUsing(function (Closure $callback) use ($timebox): bool { - return $callback($timebox->expects('returnEarly')->getMock()); - }); - $guard->getProvider()->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); - $guard->getProvider()->expects('validateCredentials')->with($user, ['foo'])->andReturn(true); - $guard->getProvider()->expects('rehashPasswordIfRequired')->with($user, ['foo']); - $guard->expects('setUser')->with($user); + [$guard, $provider, $session] = $this->getRealGuard(); + + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $provider->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); + $provider->expects('validateCredentials')->with($user, ['foo'])->andReturn(true); + $provider->expects('rehashPasswordIfRequired')->with($user, ['foo']); + $this->assertTrue($guard->once(['foo'])); + $this->assertSame($user, $guard->user()); + $this->assertNull($session->get($guard->getName())); } public function testLoginOnceFailure(): void { - [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); - $guard = m::mock(SessionGuard::class, ['default', $provider, $session, $app, $timebox])->makePartial(); - $user = m::mock(Authenticatable::class); - $timebox->expects('call')->andReturnUsing(function (Closure $callback) use ($timebox): bool { - return $callback($timebox); - }); - $guard->getProvider()->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); - $guard->getProvider()->expects('validateCredentials')->with($user, ['foo'])->andReturn(false); - $guard->getProvider()->shouldNotReceive('rehashPasswordIfRequired'); + [$guard, $provider] = $this->getRealGuard(); + + $user = new GenericUser(['id' => 10, 'password' => 'secret']); + $provider->expects('retrieveByCredentials')->with(['foo'])->andReturn($user); + $provider->expects('validateCredentials')->with($user, ['foo'])->andReturn(false); + $provider->shouldNotReceive('rehashPasswordIfRequired'); + $this->assertFalse($guard->once(['foo'])); + $this->assertNull($guard->user()); } public function testForgetUserSetsUserToNull(): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $guard = $this->getGuard(); $guard->setUser($user); $this->assertTrue($guard->hasUser()); @@ -1173,7 +1195,7 @@ public function testAttemptingRegistersEventListener(): void [$session, $provider, $request, $cookie, $timebox, $app] = $this->getMocks(); $guard = new SessionGuard('default', $provider, $session, $app, $timebox); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = m::mock(DispatcherContract::class); $dispatcher->shouldReceive('listen') ->with(Attempting::class, m::type('callable')) ->once(); @@ -1185,7 +1207,7 @@ public function testAttemptingRegistersEventListener(): void public function testAttemptSkipsEventDispatchWhenNoListenersAreRegistered(): void { $guard = $this->getGuard(); - $guard->setDispatcher($events = m::mock(Dispatcher::class)); + $guard->setDispatcher($events = m::mock(DispatcherContract::class)); $events->shouldReceive('hasListeners')->withAnyArgs()->andReturn(false); $events->shouldNotReceive('dispatch'); @@ -1248,6 +1270,21 @@ public function testGetNameContainsGuardName(): void $this->assertSame('remember_default_' . hash('xxh128', SessionGuard::class), $guard->getRecallerName()); } + /** + * Create a guard backed by a real session and a mocked user provider. + * + * @return array{SessionGuard, UserProvider, Store} + */ + protected function getRealGuard(?Request $request = null): array + { + $session = new Store('test', new ArraySessionHandler(10)); + $provider = m::mock(UserProvider::class); + $app = new Container; + $app->instance('request', $request ?? Request::create('/', 'GET')); + + return [new SessionGuard('default', $provider, $session, $app, timeboxDuration: 0), $provider, $session]; + } + /** * Create a guard with mocked dependencies. */ @@ -1283,9 +1320,9 @@ protected function getMocks(): array /** * Create an event dispatcher with listeners enabled. */ - protected function mockEventDispatcher(): Dispatcher + protected function mockEventDispatcher(): DispatcherContract { - $events = m::mock(Dispatcher::class); + $events = m::mock(DispatcherContract::class); $events->shouldReceive('hasListeners')->byDefault()->andReturn(true); return $events; diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index f5fb7e0172..6f280ee95d 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -273,9 +273,7 @@ protected function createAuthDriver(string $name, bool $authenticated): RequestG */ protected function authenticate(string ...$guards): void { - $request = m::mock(Request::class); - - $request->shouldReceive('expectsJson')->andReturn(false); + $request = Request::create('/'); $nextParam = null; $response = new Response; diff --git a/tests/Auth/AuthorizeMiddlewareTest.php b/tests/Auth/AuthorizeMiddlewareTest.php index 3e6fb72c48..e39230e64c 100644 --- a/tests/Auth/AuthorizeMiddlewareTest.php +++ b/tests/Auth/AuthorizeMiddlewareTest.php @@ -20,7 +20,6 @@ use Hypervel\Routing\Router; use Hypervel\Tests\Auth\Fixtures\AbilitiesEnum; use Hypervel\Tests\TestCase; -use Mockery as m; use stdClass; use Symfony\Component\HttpFoundation\Response; @@ -365,7 +364,7 @@ public function testModelAuthorized(): void public function testModelInstanceAsParameter(): void { - $instance = m::mock(Model::class); + $instance = new class extends Model {}; $this->gate()->define('success', function (stdClass $user, Model $model) use ($instance): bool { $this->assertSame($model, $instance); @@ -373,7 +372,7 @@ public function testModelInstanceAsParameter(): void return true; }); - $request = m::mock(Request::class); + $request = new Request; $next = function (): Response { return new Response; diff --git a/tests/Broadcasting/BroadcastEventTest.php b/tests/Broadcasting/BroadcastEventTest.php index 1546ff9598..e41a773ecd 100644 --- a/tests/Broadcasting/BroadcastEventTest.php +++ b/tests/Broadcasting/BroadcastEventTest.php @@ -219,21 +219,22 @@ public function testDeletingWhenMissingModelsCanBeDisabled(): void public function testMiddlewareProxiesFailedHandlerFromUnderlyingEvent(): void { $event = new class { + public ?Throwable $failedWith = null; + /** * Handle a job failure. */ public function failed(?Throwable $e = null): void { - $e->validateCall(); + $this->failedWith = $e; } }; $job = new BroadcastEvent($event); - $exception = m::mock(Exception::class); - $exception->expects('validateCall'); + $job->failed($exception = new Exception); - $job->failed($exception); + $this->assertSame($exception, $event->failedWith); } public function testArrayBackoffIsReadFromTheUnderlyingEvent(): void diff --git a/tests/Broadcasting/PusherBroadcasterTest.php b/tests/Broadcasting/PusherBroadcasterTest.php index e5ba0a675e..9ab7e052b8 100644 --- a/tests/Broadcasting/PusherBroadcasterTest.php +++ b/tests/Broadcasting/PusherBroadcasterTest.php @@ -18,6 +18,7 @@ use Hypervel\Routing\ResponseFactory; use Hypervel\Tests\TestCase; use Mockery as m; +use Mockery\MockInterface; use Pusher\Pusher; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -330,16 +331,13 @@ public function testExplicitJsonpOptInWithoutCallbackReturnsJson(): void */ protected function getMockRequestWithUserForChannel(string $channel): Request { - $request = m::mock(Request::class); - $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - $request->shouldReceive('input')->with('socket_id')->andReturn('abcd.1234'); - $request->shouldReceive('input')->with('callback', false)->andReturn(false); + $request = Request::create('/', 'POST', ['channel_name' => $channel, 'socket_id' => 'abcd.1234']); $user = m::mock('User'); $user->shouldReceive('getAuthIdentifierForBroadcasting')->andReturn(42); $user->shouldReceive('getAuthIdentifier')->andReturn(42); - $request->shouldReceive('user')->andReturn($user); + $request->setUserResolver(fn (): MockInterface => $user); return $request; } @@ -349,12 +347,7 @@ protected function getMockRequestWithUserForChannel(string $channel): Request */ protected function getMockRequestWithoutUserForChannel(string $channel): Request { - $request = m::mock(Request::class); - $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - - $request->shouldReceive('user')->andReturn(null); - - return $request; + return Request::create('/', 'POST', ['channel_name' => $channel]); } } diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php index 8aebb3b448..22e9685816 100644 --- a/tests/Broadcasting/RedisBroadcasterTest.php +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -15,6 +15,7 @@ use Hypervel\Tests\TestCase; use JsonException; use Mockery as m; +use Mockery\MockInterface; use RedisClusterException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -24,7 +25,7 @@ class RedisBroadcasterTest extends TestCase protected Container $container; - protected Redis&m\MockInterface $redis; + protected Redis&MockInterface $redis; /** * Set up the test environment. @@ -385,14 +386,13 @@ public function testBroadcastPayloadDoesNotDuplicateSocketInData(): void */ protected function getMockRequestWithUserForChannel(string $channel): Request { - $request = m::mock(Request::class); - $request->shouldReceive('input')->with('channel_name')->andReturn($channel); + $request = Request::create('/', 'POST', ['channel_name' => $channel]); $user = m::mock('User'); $user->shouldReceive('getAuthIdentifierForBroadcasting')->andReturn(42); $user->shouldReceive('getAuthIdentifier')->andReturn(42); - $request->shouldReceive('user')->andReturn($user); + $request->setUserResolver(fn (): MockInterface => $user); return $request; } @@ -402,11 +402,6 @@ protected function getMockRequestWithUserForChannel(string $channel): Request */ protected function getMockRequestWithoutUserForChannel(string $channel): Request { - $request = m::mock(Request::class); - $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - - $request->shouldReceive('user')->andReturn(null); - - return $request; + return Request::create('/', 'POST', ['channel_name' => $channel]); } } diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 63a09b3356..155bb7d0e4 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -21,6 +21,7 @@ use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\PostgresConnection; use Hypervel\Database\Query\Builder; +use Hypervel\Events\Dispatcher as EventsDispatcher; use Hypervel\Foundation\Bus\Dispatchable; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Queue\CallQueuedClosure; @@ -28,6 +29,8 @@ use Hypervel\Support\Collection; use Hypervel\Support\Facades\Bus; use Hypervel\Support\Facades\Queue; +use Hypervel\Support\Testing\Fakes\EventFake; +use Hypervel\Support\Testing\Fakes\QueueFake; use Hypervel\Testbench\TestCase; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; @@ -251,7 +254,7 @@ public function testJobsCanBeAddedToThePendingBatchFromIterable(): void public function testProcessedJobsCanBeCalculated(): void { - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); @@ -303,49 +306,38 @@ public function testSuccessfulJobsCanBeRecorded(): void public function testBatchFinishedEventIsDispatched(): void { - $events = m::mock(EventDispatcher::class); + $events = new EventFake(new EventsDispatcher); $this->app->instance(EventDispatcher::class, $events); - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); $job = new class { use Batchable; }; - $connection = m::mock(QueueContract::class); - $queue->expects('connection') - ->with('test-connection') - ->andReturn($connection); - - $connection->expects('bulk'); - $batch = $batch->add([$job]); - $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->expects('hasListeners')->with(BatchFinished::class)->andReturnTrue(); + $batch->recordSuccessfulJob('test-id'); - $events->expects('dispatch')->with(m::on(function (object $event) use ($batch): bool { - return $event instanceof BatchFinished - && $event->batch->id === $batch->id + $events->assertDispatchedOnce(BatchStarted::class); + $events->assertDispatched(BatchStarted::class, function (BatchStarted $event) use ($batch): bool { + return $event->batch === $batch; + }); + $events->assertDispatchedOnce(BatchFinished::class); + $events->assertDispatched(BatchFinished::class, function (BatchFinished $event) use ($batch): bool { + return $event->batch->id === $batch->id && $event->batch->finished() && $event->batch->pendingJobs === 0; - })); - - $batch->recordSuccessfulJob('test-id'); + }); } public function testBatchStartedEventIsDispatched(): void { - $events = m::mock(EventDispatcher::class); + $events = new EventFake(new EventsDispatcher); $this->app->instance(EventDispatcher::class, $events); - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); $job = new class { @@ -356,37 +348,24 @@ public function testBatchStartedEventIsDispatched(): void use Batchable; }; - $connection = m::mock(QueueContract::class); - $queue->expects('connection') - ->with('test-connection') - ->andReturn($connection); - - $connection->expects('bulk'); - $batch = $batch->add([$job, $secondJob]); - $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->expects('hasListeners')->with(BatchFinished::class)->andReturnTrue(); - - $events->expects('dispatch')->with(m::on(function (object $event): bool { - return $event instanceof BatchFinished; - })); - $batch->recordSuccessfulJob('test-id-1'); $batch->recordSuccessfulJob('test-id-2'); + + $events->assertDispatchedOnce(BatchStarted::class); + $events->assertDispatched(BatchStarted::class, function (BatchStarted $event) use ($batch): bool { + return $event->batch === $batch; + }); + $events->assertDispatchedOnce(BatchFinished::class); } public function testBatchStartedEventIsDispatchedWhenFirstJobFails(): void { - $events = m::mock(EventDispatcher::class); + $events = new EventFake(new EventsDispatcher); $this->app->instance(EventDispatcher::class, $events); - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue, $allowFailures = true); $job = new class { @@ -397,23 +376,15 @@ public function testBatchStartedEventIsDispatchedWhenFirstJobFails(): void use Batchable; }; - $connection = m::mock(QueueContract::class); - $queue->expects('connection') - ->with('test-connection') - ->andReturn($connection); - - $connection->expects('bulk'); - $batch = $batch->add([$job, $secondJob]); - $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; - })); - $batch->recordFailedJob('test-id-1', new RuntimeException('Something went wrong.')); $batch->recordFailedJob('test-id-2', new RuntimeException('Something else went wrong.')); + + $events->assertDispatchedOnce(BatchStarted::class); + $events->assertDispatched(BatchStarted::class, function (BatchStarted $event) use ($batch): bool { + return $event->batch === $batch; + }); } public function testFailedJobsCanBeRecordedWhileNotAllowingFailures(): void @@ -579,7 +550,7 @@ function (Batch $batch, ?Throwable $e): void { public function testBatchCanBeCancelled(): void { - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); @@ -592,28 +563,27 @@ public function testBatchCanBeCancelled(): void public function testBatchCancelledEventIsDispatched(): void { - $events = m::mock(EventDispatcher::class); + $events = new EventFake(new EventsDispatcher); $this->app->instance(EventDispatcher::class, $events); - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); $exception = new RuntimeException('Something went wrong.'); - $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 + $batch->cancel($exception); + + $events->assertDispatchedOnce(BatchCanceled::class); + $events->assertDispatched(BatchCanceled::class, function (BatchCanceled $event) use ($batch, $exception): bool { + return $event->batch->id === $batch->id && $event->batch->cancelled() && $event->exception === $exception; - })); - - $batch->cancel($exception); + }); } public function testBatchCanBeDeleted(): void { - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); @@ -655,7 +625,7 @@ public function testDeletedBatchIgnoresLateJobResultsAndCallbacks(): void public function testBatchStateCanBeInspected(): void { - $queue = m::mock(Factory::class); + $queue = new QueueFake($this->app); $batch = $this->createTestBatch($queue); diff --git a/tests/Bus/BusDispatcherTest.php b/tests/Bus/BusDispatcherTest.php index 30a490f1c2..90d198c025 100644 --- a/tests/Bus/BusDispatcherTest.php +++ b/tests/Bus/BusDispatcherTest.php @@ -14,6 +14,7 @@ use Hypervel\Queue\Attributes\Queue as QueueAttribute; use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\QueueRoutes; +use Hypervel\Support\Testing\Fakes\QueueFake; use Hypervel\Tests\TestCase; use Mockery as m; use RuntimeException; @@ -23,44 +24,33 @@ class BusDispatcherTest extends TestCase public function testCommandsThatShouldQueueIsQueued(): void { $container = new Container; - $queueRoutes = m::mock(QueueRoutes::class); - $queueRoutes->expects('getQueue')->andReturn(null); - $queueRoutes->expects('getConnection')->andReturn(null); - $container->instance('queue.routes', $queueRoutes); + $container->instance('queue.routes', new QueueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function (): Queue { - $mock = m::mock(Queue::class); - $mock->expects('push'); + $queue = new QueueFake($container); + $dispatcher = new Dispatcher($container, fn (): Queue => $queue); - return $mock; - }); + $dispatcher->dispatch(new BusDispatcherQueueable); - $dispatcher->dispatch(m::mock(ShouldQueue::class)); + $queue->assertPushedOnce(BusDispatcherQueueable::class); } public function testCommandsThatShouldQueueIsQueuedUsingCustomHandler(): void { $container = new Container; - $queueRoutes = m::mock(QueueRoutes::class); - $queueRoutes->expects('getConnection')->andReturn(null); - $container->instance('queue.routes', $queueRoutes); + $container->instance('queue.routes', new QueueRoutes); Container::setInstance($container); - $dispatcher = new Dispatcher($container, function (): Queue { - $mock = m::mock(Queue::class); - $mock->expects('push'); - - return $mock; - }); + $queue = new QueueFake($container); + $dispatcher = new Dispatcher($container, fn (): Queue => $queue); $dispatcher->dispatch(new BusDispatcherTestCustomQueueCommand); + + $queue->assertPushedOnce(BusDispatcherTestCustomQueueCommand::class); } public function testCommandsThatShouldQueueIsQueuedUsingCustomQueueAndDelay(): void { $container = new Container; - $queueRoutes = m::mock(QueueRoutes::class); - $queueRoutes->expects('getConnection')->andReturn(null); - $container->instance('queue.routes', $queueRoutes); + $container->instance('queue.routes', new QueueRoutes); Container::setInstance($container); $dispatcher = new Dispatcher($container, function (): Queue { $mock = m::mock(Queue::class); @@ -109,19 +99,16 @@ public function testCommandDelayPropertyOverridesDelayAttribute(): void public function testCommandsAreDispatchedWithQueueRoute(): void { Container::setInstance($container = new Container); - $queueRoutes = m::mock(QueueRoutes::class); - $queueRoutes->expects('getQueue')->andReturn('high-priority'); - $queueRoutes->expects('getConnection')->andReturn(null); + $queueRoutes = new QueueRoutes; + $queueRoutes->set(BusDispatcherQueueable::class, 'high-priority'); $container->instance('queue.routes', $queueRoutes); - $mock = m::mock(Queue::class); - $mock->expects('push')->with(BusDispatcherQueueable::class, '', 'high-priority'); - - $dispatcher = new Dispatcher($container, function () use ($mock): Queue { - return $mock; - }); + $queue = new QueueFake($container); + $dispatcher = new Dispatcher($container, fn (): Queue => $queue); $dispatcher->dispatch(new BusDispatcherQueueable); + + $queue->assertPushedOn('high-priority', BusDispatcherQueueable::class); } public function testCommandsAreForwardedToConnectionByQueueName(): void @@ -131,20 +118,19 @@ public function testCommandsAreForwardedToConnectionByQueueName(): void $queueRoutes->forward('reports', 'processing', 'cloud'); $container->instance('queue.routes', $queueRoutes); - $mock = m::mock(Queue::class); - $mock->expects('push')->with(m::type(BusDispatcherQueueable::class), '', 'reports'); - + $queue = new QueueFake($container); $usedConnection = false; - $dispatcher = new Dispatcher($container, function (?string $connection) use ($mock, &$usedConnection): Queue { + $dispatcher = new Dispatcher($container, function (?string $connection) use ($queue, &$usedConnection): Queue { $usedConnection = $connection; - return $mock; + return $queue; }); $dispatcher->dispatch((new BusDispatcherQueueable)->onQueue('reports')); $this->assertSame('cloud', $usedConnection); + $queue->assertPushedOn('reports', BusDispatcherQueueable::class); } public function testExplicitConnectionWinsOverForwardedQueue(): void @@ -173,28 +159,26 @@ public function testExplicitConnectionWinsOverForwardedQueue(): void public function testDispatchNowShouldNeverQueue(): void { $container = new Container; - $mock = m::mock(Queue::class); - $mock->shouldReceive('push')->never(); - $dispatcher = new Dispatcher($container, function () use ($mock): Queue { - return $mock; - }); + $queue = new QueueFake($container); + $dispatcher = new Dispatcher($container, fn (): Queue => $queue); $dispatcher->dispatch(new BusDispatcherBasicCommand); + + $queue->assertNothingPushed(); } public function testDispatcherCanDispatchStandAloneHandler(): void { $container = new Container; - $mock = m::mock(Queue::class); - $dispatcher = new Dispatcher($container, function () use ($mock): Queue { - return $mock; - }); + $queue = new QueueFake($container); + $dispatcher = new Dispatcher($container, fn (): Queue => $queue); $dispatcher->map([StandAloneCommand::class => StandAloneHandler::class]); $response = $dispatcher->dispatch(new StandAloneCommand); $this->assertInstanceOf(StandAloneCommand::class, $response); + $queue->assertNothingPushed(); } public function testDisabledDispatchAfterResponseUsesExplicitHandler(): void @@ -226,29 +210,22 @@ public function testOnConnectionOnJobWhenDispatching(): void ], ]); }); - $queueRoutes = m::mock(QueueRoutes::class); - $queueRoutes->expects('getQueue')->andReturn(null); - $container->instance('queue.routes', $queueRoutes); - - $dispatcher = new Dispatcher($container, function (): Queue { - $mock = m::mock(Queue::class); - $mock->expects('push'); + $container->instance('queue.routes', new QueueRoutes); - return $mock; - }); + $queue = new QueueFake($container); + $dispatcher = new Dispatcher($container, fn (): Queue => $queue); $job = (new ShouldNotBeDispatched)->onConnection('null'); $dispatcher->dispatch($job); + + $queue->assertPushedOnce(ShouldNotBeDispatched::class); } public function testDispatchBulk(): void { $container = new Container; - $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->instance('queue.routes', new QueueRoutes); Container::setInstance($container); $defaultQueue = m::mock(Queue::class); diff --git a/tests/Bus/BusPendingBatchTest.php b/tests/Bus/BusPendingBatchTest.php index eb00664312..a53b035822 100644 --- a/tests/Bus/BusPendingBatchTest.php +++ b/tests/Bus/BusPendingBatchTest.php @@ -14,7 +14,9 @@ use Hypervel\Container\Container; use Hypervel\Contracts\Bus\Dispatcher as BusDispatcher; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Events\Dispatcher as EventsDispatcher; use Hypervel\Support\Collection; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Tests\TestCase; use Mockery as m; use RuntimeException; @@ -153,10 +155,7 @@ public function testPendingBatchMayBeConfiguredAndDispatched(): void { $container = new Container; - $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); - + $eventDispatcher = new EventFake(new EventsDispatcher); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -189,6 +188,8 @@ public function testPendingBatchMayBeConfiguredAndDispatched(): void $container->instance(BatchRepository::class, $repository); $pendingBatch->dispatch(); + + $eventDispatcher->assertDispatchedOnce(BatchDispatched::class); } public function testBatchDispatchedEventIsSkippedWithoutListeners(): void @@ -273,9 +274,7 @@ public function testBatchIsDispatchedWhenDispatchifIsTrue(): void { $container = new Container; - $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); + $eventDispatcher = new EventFake(new EventsDispatcher); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -295,14 +294,15 @@ public function testBatchIsDispatchedWhenDispatchifIsTrue(): void $result = $pendingBatch->dispatchIf(true); $this->assertInstanceOf(Batch::class, $result); + + $eventDispatcher->assertDispatchedOnce(BatchDispatched::class); } public function testBatchIsNotDispatchedWhenDispatchifIsFalse(): void { $container = new Container; - $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldNotReceive('dispatch'); + $eventDispatcher = new EventFake(new EventsDispatcher); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -317,15 +317,15 @@ public function testBatchIsNotDispatchedWhenDispatchifIsFalse(): void $result = $pendingBatch->dispatchIf(false); $this->assertNull($result); + + $eventDispatcher->assertNothingDispatched(); } public function testBatchIsDispatchedWhenDispatchunlessIsFalse(): void { $container = new Container; - $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); + $eventDispatcher = new EventFake(new EventsDispatcher); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -345,14 +345,15 @@ public function testBatchIsDispatchedWhenDispatchunlessIsFalse(): void $result = $pendingBatch->dispatchUnless(false); $this->assertInstanceOf(Batch::class, $result); + + $eventDispatcher->assertDispatchedOnce(BatchDispatched::class); } public function testBatchIsNotDispatchedWhenDispatchunlessIsTrue(): void { $container = new Container; - $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->shouldNotReceive('dispatch'); + $eventDispatcher = new EventFake(new EventsDispatcher); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -367,16 +368,15 @@ public function testBatchIsNotDispatchedWhenDispatchunlessIsTrue(): void $result = $pendingBatch->dispatchUnless(true); $this->assertNull($result); + + $eventDispatcher->assertNothingDispatched(); } public function testBatchBeforeEventIsCalled(): void { $container = new Container; - $eventDispatcher = m::mock(Dispatcher::class); - $eventDispatcher->expects('hasListeners')->with(BatchDispatched::class)->andReturnTrue(); - $eventDispatcher->expects('dispatch')->with(m::type(BatchDispatched::class)); - + $eventDispatcher = new EventFake(new EventsDispatcher); $container->instance(Dispatcher::class, $eventDispatcher); $job = new class { @@ -402,6 +402,8 @@ public function testBatchBeforeEventIsCalled(): void $pendingBatch->dispatch(); $this->assertTrue($beforeCalled); + + $eventDispatcher->assertDispatchedOnce(BatchDispatched::class); } public function testItThrowsExceptionIfBatchedJobIsNotBatchable(): void diff --git a/tests/Bus/BusPendingDispatchTest.php b/tests/Bus/BusPendingDispatchTest.php index b0bd633048..83633f1ee8 100644 --- a/tests/Bus/BusPendingDispatchTest.php +++ b/tests/Bus/BusPendingDispatchTest.php @@ -244,7 +244,7 @@ public function testUniqueMetadataReachesAfterResponseDispatcherBeforeUnclaimedO public function testDynamicallyProxyMethods(): void { - $newJob = m::mock(stdClass::class); + $newJob = new stdClass; $this->job->expects('appendToChain')->with($newJob); $this->pendingDispatch->appendToChain($newJob); } diff --git a/tests/Cache/CacheFailoverStoreTest.php b/tests/Cache/CacheFailoverStoreTest.php index da10d5d876..a40fbef40e 100644 --- a/tests/Cache/CacheFailoverStoreTest.php +++ b/tests/Cache/CacheFailoverStoreTest.php @@ -20,6 +20,7 @@ use Hypervel\Contracts\Cache\Repository as RepositoryContract; use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Tests\TestCase; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; @@ -48,7 +49,7 @@ public function testFlushLocksCallsFlushLocksOnAllBackingStores(): void $cache->expects('store')->with('store-a')->andReturn(new Repository($storeA)); $cache->expects('store')->with('store-b')->andReturn(new Repository($storeB)); - $failover = new FailoverStore($cache, m::mock(Dispatcher::class), ['store-a', 'store-b']); + $failover = new FailoverStore($cache, new EventDispatcher, ['store-a', 'store-b']); $result = $failover->flushLocks(); @@ -641,7 +642,7 @@ protected function makeFailoverStore( return new FailoverStore( $cache, - $events ?? m::mock(Dispatcher::class), + $events ?? new EventDispatcher, $storeNames ?? array_keys($stores), $failoverStoreName, ); diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 0ce667f5e5..77d84f52dc 100644 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -30,13 +30,16 @@ use Hypervel\Cache\StackStore; use Hypervel\Cache\TaggableStore; use Hypervel\Cache\TaggedCache; +use Hypervel\Container\Container; use Hypervel\Contracts\Cache\AuthoritativeRawReadable; use Hypervel\Contracts\Cache\LockProvider; use Hypervel\Contracts\Cache\LockTimeoutException; use Hypervel\Contracts\Cache\Store; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Filesystem\Filesystem; use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; @@ -1153,8 +1156,7 @@ public function testFlushLocksDelegatesToStore(): void public function testTaggableRepositoriesSupportTags(): void { - $taggable = m::mock(TaggableStore::class); - $taggable->expects('supportsTags')->andReturnTrue(); + $taggable = new ArrayStore; $taggableRepo = new Repository($taggable); $this->assertTrue($taggableRepo->supportsTags()); @@ -1171,7 +1173,7 @@ public function testTaggableRepositoriesCanReportUnsupportedTags(): void public function testNonTaggableRepositoryDoesNotSupportTags(): void { - $nonTaggable = m::mock(FileStore::class); + $nonTaggable = new FileStore(new Filesystem, sys_get_temp_dir()); $nonTaggableRepo = new Repository($nonTaggable); $this->assertFalse($nonTaggableRepo->supportsTags()); @@ -1888,9 +1890,8 @@ public function testRememberWorksWithoutPinnableStore(): void */ protected function getRepository(): Repository { - $dispatcher = m::mock(Dispatcher::class); - $dispatcher->shouldReceive('hasListeners')->withAnyArgs()->andReturn(true); - $dispatcher->shouldReceive('dispatch')->with(m::any())->andReturnNull(); + // A plain dispatcher reports no listeners, so the repository would skip building events. + $dispatcher = new EventFake(new EventDispatcher(new Container)); $repository = new Repository(m::mock(Store::class)); $repository->setEventDispatcher($dispatcher); diff --git a/tests/Console/CommandTest.php b/tests/Console/CommandTest.php index d612b403bb..d9c766cc1b 100644 --- a/tests/Console/CommandTest.php +++ b/tests/Console/CommandTest.php @@ -21,6 +21,7 @@ use Hypervel\Support\CarbonImmutable; use Hypervel\Support\ClassInvoker; use Hypervel\Testbench\TestCase; +use Hypervel\Tests\Console\Concerns\CreatesAnsweredOutputStyles; use Hypervel\Tests\Console\Fixtures\Commands\DefaultSwooleFlagsCommand; use Hypervel\Tests\Console\Fixtures\Commands\FooCommand; use Hypervel\Tests\Console\Fixtures\Commands\FooExceptionCommand; @@ -35,10 +36,10 @@ use Symfony\Component\Console\Application as ConsoleApplication; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Question\ChoiceQuestion; enum CommandInputType: string { @@ -47,6 +48,8 @@ enum CommandInputType: string class CommandTest extends TestCase { + use CreatesAnsweredOutputStyles; + public function testHookFlags(): void { $command = new DefaultSwooleFlagsCommand('test:demo'); @@ -322,9 +325,9 @@ public function handle(): void $input = new ArrayInput([]); $output = new NullOutput; - $outputStyle = m::mock(OutputStyle::class)->shouldIgnoreMissing(); + $outputStyle = new OutputStyle($input, $output); $application->expects('make')->with(OutputStyle::class, ['input' => $input, 'output' => $output])->andReturn($outputStyle); - $application->expects('make')->with(Factory::class, ['output' => $outputStyle])->andReturn(m::mock(Factory::class)); + $application->expects('make')->with(Factory::class, ['output' => $outputStyle])->andReturn(new Factory($outputStyle)); $application->shouldReceive('bound')->andReturn(false); $application->expects('call')->with([$command, 'handle'])->andReturnUsing(function () use ($command, $application): void { @@ -489,12 +492,8 @@ protected function getOptions(): array public function testTheInputSetterOverwrite(): void { - $input = m::mock(InputInterface::class); - $input->expects('hasArgument')->with('foo')->andReturn(false); - $input->expects('hasArgument')->with('0')->andReturn(true); - $command = new CommandTestStubCommand; - $command->setInput($input); + $command->setInput(new ArrayInput([], new InputDefinition([new InputArgument('0')]))); $this->assertFalse($command->hasArgument('foo')); $this->assertTrue($command->hasArgument(0)); @@ -564,28 +563,22 @@ public function testAliasesProperty(): void public function testChoiceIsSingleSelectByDefault(): void { - $output = m::mock(OutputStyle::class); - $output->expects('askQuestion')->withArgs(function (ChoiceQuestion $question) { - return $question->isMultiselect() === false; - })->andReturn('yes'); - $command = new CommandTestStubCommand; - $command->setOutput($output); + $command->setOutput($this->outputStyleWithAnswer('yes')); - $command->choice('Do you need further help?', ['yes', 'no']); + $answer = $command->choice('Do you need further help?', ['yes', 'no']); + + $this->assertSame('yes', $answer); } public function testChoiceWithMultiselect(): void { - $output = m::mock(OutputStyle::class); - $output->expects('askQuestion')->withArgs(function (ChoiceQuestion $question) { - return $question->isMultiselect() === true; - })->andReturn(['option-1']); - $command = new CommandTestStubCommand; - $command->setOutput($output); + $command->setOutput($this->outputStyleWithAnswer('option-1,option-2')); + + $answer = $command->choice('Select all that apply.', ['option-1', 'option-2', 'option-3'], null, null, true); - $command->choice('Select all that apply.', ['option-1', 'option-2', 'option-3'], null, null, true); + $this->assertSame(['option-1', 'option-2'], $answer); } public function testSignatureAttributeCanSetAliases(): void diff --git a/tests/Console/Concerns/CreatesAnsweredOutputStyles.php b/tests/Console/Concerns/CreatesAnsweredOutputStyles.php new file mode 100644 index 0000000000..eda14b4d2a --- /dev/null +++ b/tests/Console/Concerns/CreatesAnsweredOutputStyles.php @@ -0,0 +1,27 @@ +setStream($stream); + + return new OutputStyle($input, new BufferedOutput); + } +} diff --git a/tests/Console/ConsoleApplicationTest.php b/tests/Console/ConsoleApplicationTest.php index 8ca77b03f3..c2fc6b87e0 100644 --- a/tests/Console/ConsoleApplicationTest.php +++ b/tests/Console/ConsoleApplicationTest.php @@ -9,8 +9,6 @@ use Hypervel\Console\Attributes\Signature; use Hypervel\Console\Command; use Hypervel\Console\ContainerCommandLoader; -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; @@ -242,8 +240,7 @@ public function testHypervelNotSetOnSymfonyCommands(): void { $artisan = $this->getMockConsole(['addToParent']); - $command = m::mock(SymfonyCommand::class); - $command->shouldNotReceive('setHypervel'); + $command = new SymfonyCommand('foo'); $artisan->expects($this->once())->method('addToParent')->with($command)->willReturn($command); $result = $artisan->add($command); @@ -385,12 +382,7 @@ public function testResolvingCommandsWithNoAliasViaProperty(): void public function testCallFullyStringCommandLine(): void { - $app = $this->createApp( - m::mock(Application::class, [ - 'version' => '1.0', - 'runningInConsole' => false, - ]), - ); + $app = $this->createApp($this->app); $codeOfCallingArrayInput = $app->call('help', [ '--raw' => true, @@ -584,9 +576,7 @@ private function getCommandLoader(ConsoleApplication $app): ?ContainerCommandLoa private function getMockConsole(array $methods): ConsoleApplication { $app = m::mock(Application::class, ['version' => '1.0']); - $events = m::mock(Dispatcher::class); - $events->shouldReceive('hasListeners')->once()->with(ArtisanStarting::class)->andReturnFalse(); - $events->shouldNotReceive('dispatch'); + $events = new EventsDispatcher; return $this->getMockBuilder(ConsoleApplication::class) ->onlyMethods($methods) diff --git a/tests/Container/ContextualAttributeBindingTest.php b/tests/Container/ContextualAttributeBindingTest.php index b514f16374..e6958dc333 100644 --- a/tests/Container/ContextualAttributeBindingTest.php +++ b/tests/Container/ContextualAttributeBindingTest.php @@ -35,6 +35,7 @@ use Hypervel\Database\ConnectionInterface; use Hypervel\Database\DatabaseManager; use Hypervel\Database\Eloquent\Model; +use Hypervel\Database\PdoConnection; use Hypervel\Filesystem\FilesystemManager; use Hypervel\Http\Request; use Hypervel\Log\Context\Repository as ContextRepository; @@ -42,6 +43,7 @@ use Hypervel\Log\LogManager; use Hypervel\Testbench\TestCase; use Mockery as m; +use PDO; use Psr\Log\LoggerInterface; use ReflectionParameter; use RuntimeException; @@ -306,15 +308,12 @@ public function testCacheAttribute(): void public function testConfigAttribute(): void { $container = new Container; - $container->singleton('config', function (): Repository { - $repository = m::mock(Repository::class); - $repository->expects('get')->with('foo', null)->andReturn('foo'); - $repository->expects('get')->with('bar', null)->andReturn('bar'); + $container->singleton('config', fn (): Repository => new Repository(['foo' => 'foo', 'bar' => 'bar'])); - return $repository; - }); + $resolved = $container->make(ConfigTest::class); - $container->make(ConfigTest::class); + $this->assertSame('foo', $resolved->foo); + $this->assertSame('bar', $resolved->bar); } public function testDatabaseAttribute(): void @@ -322,9 +321,9 @@ public function testDatabaseAttribute(): void $container = new Container; $container->singleton('db', function (): DatabaseManager { $manager = m::mock(DatabaseManager::class); - $manager->expects('connection')->with('foo')->andReturn(m::mock(ConnectionInterface::class)); - $manager->expects('connection')->with('bar')->andReturn(m::mock(ConnectionInterface::class)); - $manager->expects('connection')->with(DatabaseConnectionIntegerBackedEnum::Zero)->andReturn(m::mock(ConnectionInterface::class)); + $manager->expects('connection')->with('foo')->andReturn(new PdoConnection(new PDO('sqlite::memory:'))); + $manager->expects('connection')->with('bar')->andReturn(new PdoConnection(new PDO('sqlite::memory:'))); + $manager->expects('connection')->with(DatabaseConnectionIntegerBackedEnum::Zero)->andReturn(new PdoConnection(new PDO('sqlite::memory:'))); return $manager; }); @@ -1011,7 +1010,7 @@ public function __construct(#[Cache('foo')] CacheRepository $cache) /** * Create a new test fixture. */ - public function __construct(#[Config('foo')] string $foo, #[Config('bar')] string $bar) + public function __construct(#[Config('foo')] public string $foo, #[Config('bar')] public string $bar) { } } diff --git a/tests/Database/Concerns/InteractsWithInMemoryConnection.php b/tests/Database/Concerns/InteractsWithInMemoryConnection.php new file mode 100644 index 0000000000..390ade5152 --- /dev/null +++ b/tests/Database/Concerns/InteractsWithInMemoryConnection.php @@ -0,0 +1,30 @@ +addConnection(['driver' => 'sqlite', 'database' => ':memory:']); + $db->bootEloquent(); + + /** @var SQLiteConnection */ + return $db->getConnection(); + } +} diff --git a/tests/Database/DatabaseAbstractSchemaGrammarTest.php b/tests/Database/DatabaseAbstractSchemaGrammarTest.php index 4613214879..7810e425ed 100755 --- a/tests/Database/DatabaseAbstractSchemaGrammarTest.php +++ b/tests/Database/DatabaseAbstractSchemaGrammarTest.php @@ -5,9 +5,11 @@ namespace Hypervel\Tests\Database; use Hypervel\Database\Connection; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Schema\Grammars\Grammar; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; use PHPUnit\Framework\Attributes\TestWith; use RuntimeException; @@ -15,7 +17,7 @@ class DatabaseAbstractSchemaGrammarTest extends TestCase { public function testCreateDatabase(): void { - $connection = m::mock(Connection::class); + $connection = new PdoConnection(new PDO('sqlite::memory:')); $grammar = new class($connection) extends Grammar { }; @@ -24,7 +26,7 @@ public function testCreateDatabase(): void public function testDropDatabaseIfExists(): void { - $connection = m::mock(Connection::class); + $connection = new PdoConnection(new PDO('sqlite::memory:')); $grammar = new class($connection) extends Grammar { }; diff --git a/tests/Database/DatabaseConnectionFactoryTest.php b/tests/Database/DatabaseConnectionFactoryTest.php index 9b25cd2309..2f4707fff6 100755 --- a/tests/Database/DatabaseConnectionFactoryTest.php +++ b/tests/Database/DatabaseConnectionFactoryTest.php @@ -348,7 +348,7 @@ public function testIfDriverIsntSetExceptionIsThrown(): void { $this->expectExceptionObject(new InvalidArgumentException('A driver must be specified.')); - $container = m::mock(Container::class); + $container = new Container; $factory = new ConnectionFactory($container); $factory->createConnector(['foo']); } diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index fd42f1f2ed..16d6aaa02e 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -25,7 +25,6 @@ use Hypervel\Database\MySqlConnection; use Hypervel\Database\PdoConnection; use Hypervel\Database\PostgresConnection; -use Hypervel\Database\Query\Builder as BaseBuilder; use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Processors\Processor; use Hypervel\Database\QueryException; @@ -34,6 +33,8 @@ use Hypervel\Database\SQLiteConnection; use Hypervel\Database\StreamClosedException; use Hypervel\Database\UniqueConstraintViolationException; +use Hypervel\Events\Dispatcher as EventsDispatcher; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Testbench\TestCase; use InvalidArgumentException; use LogicException; @@ -436,7 +437,7 @@ public function testNeutralNestedConcurrencyFailureRetriesTheOuterTransaction(): public function testSettingDefaultCallsGetDefaultGrammar(): void { $connection = $this->getMockConnection(['getDefaultQueryGrammar']); - $mock = m::mock(Grammar::class); + $mock = new Grammar($connection); $connection->expects($this->once())->method('getDefaultQueryGrammar')->willReturn($mock); $connection->useDefaultQueryGrammar(); $this->assertEquals($mock, $connection->getQueryGrammar()); @@ -445,7 +446,7 @@ public function testSettingDefaultCallsGetDefaultGrammar(): void public function testSettingDefaultCallsGetDefaultPostProcessor(): void { $connection = $this->getMockConnection(['getDefaultPostProcessor']); - $mock = m::mock(Processor::class); + $mock = new Processor; $connection->expects($this->once())->method('getDefaultPostProcessor')->willReturn($mock); $connection->useDefaultPostProcessor(); $this->assertEquals($mock, $connection->getPostProcessor()); @@ -581,22 +582,20 @@ public function testBeganTransactionFiresEventsIfSet(): void { $pdo = $this->createStub(PDOStub::class); $connection = $this->getMockConnection([], $pdo); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(TransactionBeginning::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(TransactionBeginning::class)); + $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->beginTransaction(); + $events->assertDispatched(TransactionBeginning::class); } public function testCommittedFiresEventsIfSet(): void { $pdo = $this->createStub(PDOStub::class); $connection = $this->getMockConnection([], $pdo); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(TransactionCommitted::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(TransactionCommitted::class)); + $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->commit(); + $events->assertDispatched(TransactionCommitted::class); } public function testCommittingFiresEventsIfSet(): void @@ -604,13 +603,11 @@ public function testCommittingFiresEventsIfSet(): void $pdo = $this->createStub(PDOStub::class); $connection = $this->getMockConnection([], $pdo); $connection->beginTransaction(); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(TransactionCommitting::class)->andReturn(true); - $events->expects('hasListeners')->with(TransactionCommitted::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(TransactionCommitting::class)); - $events->expects('dispatch')->with(m::type(TransactionCommitted::class)); + $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->commit(); + $events->assertDispatched(TransactionCommitting::class); + $events->assertDispatched(TransactionCommitted::class); } public function testRollBackedFiresEventsIfSet(): void @@ -618,11 +615,10 @@ public function testRollBackedFiresEventsIfSet(): void $pdo = $this->createStub(PDOStub::class); $connection = $this->getMockConnection([], $pdo); $connection->beginTransaction(); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(TransactionRolledBack::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(TransactionRolledBack::class)); + $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->rollBack(); + $events->assertDispatched(TransactionRolledBack::class); } public function testBeganTransactionSkipsDispatchWhenNoListenersAreRegistered(): void @@ -640,10 +636,10 @@ public function testRedundantRollBackFiresNoEvent(): void { $pdo = $this->createStub(PDOStub::class); $connection = $this->getMockConnection([], $pdo); - $events = m::mock(Dispatcher::class); + $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); - $events->shouldNotReceive('dispatch'); $connection->rollBack(); + $events->assertNothingDispatched(); } public function testTransactionMethodRunsSuccessfully(): void @@ -1510,16 +1506,6 @@ public function testRunMethodNeverRetriesIfWithinTransaction(): void }]); } - public function testFromCreatesNewQueryBuilder(): void - { - $conn = $this->getMockConnection(); - $conn->setQueryGrammar(m::mock(Grammar::class)); - $conn->setPostProcessor(m::mock(Processor::class)); - $builder = $conn->table('users'); - $this->assertInstanceOf(BaseBuilder::class, $builder); - $this->assertSame('users', $builder->from); - } - public function testTableNormalizesIntegerBackedEnumName(): void { $connection = $this->getMockConnection(); @@ -1548,11 +1534,10 @@ public function testLogQueryFiresEventsIfSet(): void { $connection = $this->getMockConnection(); $connection->logQuery('foo', [], time()); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(QueryExecuted::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(QueryExecuted::class)); + $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->logQuery('foo', [], null); + $events->assertDispatched(QueryExecuted::class); } public function testLogQuerySkipsDispatchWhenNoListenersAreRegistered(): void diff --git a/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php b/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php index 6dbad5644b..3b7265b8f4 100644 --- a/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php +++ b/tests/Database/DatabaseEloquentBelongsToManyWithoutTouchingTest.php @@ -7,42 +7,51 @@ use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\BelongsToMany; -use Hypervel\Database\Query\Builder as QueryBuilder; -use Hypervel\Database\Query\Grammars\Grammar; +use Hypervel\Database\SQLiteConnection; +use Hypervel\Tests\Database\Concerns\InteractsWithInMemoryConnection; use Hypervel\Tests\TestCase; -use Mockery as m; class DatabaseEloquentBelongsToManyWithoutTouchingTest extends TestCase { - public function testItWillNotTouchRelatedModelsWhenUpdatingChild(): void - { - /** @var BelongsToManyWithoutTouchingArticle $related */ - $related = m::mock(BelongsToManyWithoutTouchingArticle::class)->makePartial(); - $related->shouldReceive('getUpdatedAtColumn')->never(); - $related->shouldReceive('freshTimestampString')->never(); + use InteractsWithInMemoryConnection; - $this->assertFalse($related::isIgnoringTouch()); + /** + * Create an in-memory connection with a user attached to an article. + */ + protected function newConnection(): SQLiteConnection + { + $connection = $this->useInMemoryConnection(); + $pdo = $connection->getPdo(); + $pdo->exec('create table "users" ("id" integer primary key)'); + $pdo->exec('create table "articles" ("id" integer primary key, "title" text, "updated_at" text)'); + $pdo->exec('create table "article_user" ("user_id" integer, "article_id" integer)'); + $pdo->exec('insert into "users" values (1)'); + $pdo->exec("insert into \"articles\" values (1, 'title', null)"); + $pdo->exec('insert into "article_user" values (1, 1)'); + + return $connection; + } - Model::withoutTouching(function () use ($related): void { - $this->assertTrue($related::isIgnoringTouch()); + public function testItWillNotTouchRelatedModelsWhenUpdatingChild(): void + { + $connection = $this->newConnection(); + $user = new BelongsToManyWithoutTouchingUser(['id' => 1]); + $builder = (new Builder($connection->query()))->setModel(new BelongsToManyWithoutTouchingArticle); + $relation = new BelongsToMany($builder, $user, 'article_user', 'user_id', 'article_id', 'id', 'id'); - $builder = m::mock(Builder::class); - $builder->expects('join'); - $parent = m::mock(BelongsToManyWithoutTouchingUser::class); + $this->assertFalse(BelongsToManyWithoutTouchingArticle::isIgnoringTouch()); - $parent->expects('getAttribute')->with('id')->andReturn(1); - $builder->expects('getModel')->andReturn($related); - $builder->expects('where'); - $builder->expects('getQuery')->times(2)->andReturn( - m::mock(QueryBuilder::class, ['getGrammar' => m::mock(Grammar::class, ['isExpression' => false])]) - ); - $relation = new BelongsToMany($builder, $parent, 'article_users', 'user_id', 'article_id', 'id', 'id'); - $builder->shouldReceive('update')->never(); + Model::withoutTouching(function () use ($relation): void { + $this->assertTrue(BelongsToManyWithoutTouchingArticle::isIgnoringTouch()); $relation->touch(); }); - $this->assertFalse($related::isIgnoringTouch()); + $this->assertNull($connection->scalar('select "updated_at" from "articles"')); + + $relation->touch(); + + $this->assertNotNull($connection->scalar('select "updated_at" from "articles"')); } } @@ -65,6 +74,8 @@ class BelongsToManyWithoutTouchingArticle extends Model { protected ?string $table = 'articles'; + protected ?string $dateFormat = 'Y-m-d H:i:s'; + protected array $fillable = ['id', 'title']; protected array $touches = ['user']; diff --git a/tests/Database/DatabaseEloquentBelongsToTest.php b/tests/Database/DatabaseEloquentBelongsToTest.php index 99800cea13..8cd205947d 100755 --- a/tests/Database/DatabaseEloquentBelongsToTest.php +++ b/tests/Database/DatabaseEloquentBelongsToTest.php @@ -8,9 +8,14 @@ use Hypervel\Database\Eloquent\Collection; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\BelongsTo; +use Hypervel\Database\PdoConnection; +use Hypervel\Database\Query\Builder as BaseBuilder; +use Hypervel\Database\Query\Grammars\Grammar; +use Hypervel\Database\Query\Processors\Processor; use Hypervel\Tests\Database\Fixtures\Enums\Bar; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; class DatabaseEloquentBelongsToTest extends TestCase { @@ -64,26 +69,27 @@ public function testBelongsToWithArrayDefault(): void public function testEagerConstraintsAreProperlyAdded(): void { - $relation = $this->getRelation(); - $relation->getQuery()->expects('whereIntegerInRaw')->with('relation.id', ['foreign.value', 'foreign.value.two']); - $models = [new ModelStub, new ModelStub, new AnotherModelStub]; + $relation = $this->getRelationWithRealQuery(); + $models = [$this->newModelWithKey(1), $this->newModelWithKey(2), $this->newModelWithKey(1)]; $relation->addEagerConstraints($models); + + $this->assertSame('select * from "relation" where "relation"."id" = ? and "relation"."id" in (1, 2)', $relation->toSql()); } public function testIdsInEagerConstraintsCanBeZero(): void { - $relation = $this->getRelation(); - $relation->getQuery()->expects('whereIntegerInRaw')->with('relation.id', [0, 'foreign.value']); - $models = [new ModelStub, new ModelStubWithZeroId]; - $relation->addEagerConstraints($models); + $relation = $this->getRelationWithRealQuery(); + $relation->addEagerConstraints([$this->newModelWithKey(1), new ModelStubWithZeroId]); + + $this->assertSame('select * from "relation" where "relation"."id" = ? and "relation"."id" in (0, 1)', $relation->toSql()); } public function testIdsInEagerConstraintsCanBeBackedEnum(): void { - $relation = $this->getRelation(); - $relation->getQuery()->expects('whereIntegerInRaw')->with('relation.id', [5, 'foreign.value']); - $models = [new ModelStub, new ModelStubWithBackedEnumCast]; - $relation->addEagerConstraints($models); + $relation = $this->getRelationWithRealQuery(); + $relation->addEagerConstraints([$this->newModelWithKey(1), new ModelStubWithBackedEnumCast]); + + $this->assertSame('select * from "relation" where "relation"."id" = ? and "relation"."id" in (1, 5)', $relation->toSql()); } public function testRelationIsProperlyInitialized(): void @@ -219,66 +225,80 @@ public function testModelsWithFloatKeysAreProperlyMatchedToParents(): void public function testAssociateMethodSetsForeignKeyOnModel(): void { - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('foreign_key')->andReturn('foreign.value'); - $relation = $this->getRelation($parent); - $associate = m::mock(Model::class); - $associate->expects('getAttribute')->with('id')->andReturn(1); - $parent->expects('setAttribute')->with('foreign_key', 1); - $parent->expects('setRelation')->with('relation', $associate); - - $relation->associate($associate); + $relation = $this->getRelationWithRealQuery(); + $associate = new EloquentBelongsToRelatedStub; + $associate->id = 1; + + $child = $relation->associate($associate); + + $this->assertSame(1, $child->getAttribute('foreign_key')); + $this->assertSame($associate, $child->getRelation('relation')); } public function testDissociateMethodUnsetsForeignKeyOnModel(): void { - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('foreign_key')->andReturn('foreign.value'); - $relation = $this->getRelation($parent); - $parent->expects('setAttribute')->with('foreign_key', null); + $relation = $this->getRelationWithRealQuery(); + $relation->getChild()->setAttribute('foreign_key', 5); - // Always set relation when we received Model - $parent->expects('setRelation')->with('relation', null); + $child = $relation->dissociate(); - $relation->dissociate(); + $this->assertNull($child->getAttribute('foreign_key')); + // Always set relation when we received Model + $this->assertTrue($child->relationLoaded('relation')); + $this->assertNull($child->getRelation('relation')); } public function testAssociateMethodSetsForeignKeyOnModelById(): void { - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('foreign_key')->andReturn('foreign.value'); - $relation = $this->getRelation($parent); - $parent->expects('setAttribute')->with('foreign_key', 1); + $relation = $this->getRelationWithRealQuery(); + $relation->getChild()->setRelation('relation', new EloquentBelongsToRelatedStub); - // Always unset relation when we received id, regardless of dirtiness - $parent->shouldReceive('isDirty')->never(); - $parent->expects('unsetRelation')->with($relation->getRelationName()); + $child = $relation->associate(1); - $relation->associate(1); + $this->assertSame(1, $child->getAttribute('foreign_key')); + // Always unset relation when we received id, regardless of dirtiness + $this->assertFalse($child->relationLoaded('relation')); } public function testDefaultEagerConstraintsWhenIncrementing(): void { - $relation = $this->getRelation(); - $relation->getQuery()->expects('whereIntegerInRaw')->with('relation.id', m::mustBe([])); - $models = [new MissingModelStub, new MissingModelStub]; - $relation->addEagerConstraints($models); + $relation = $this->getRelationWithRealQuery(); + $relation->addEagerConstraints([new MissingModelStub, new MissingModelStub]); + + $this->assertSame('select * from "relation" where "relation"."id" = ? and 0 = 1', $relation->toSql()); } public function testDefaultEagerConstraintsWhenIncrementingAndNonIntKeyType(): void { - $relation = $this->getRelation(null, 'string'); - $relation->getQuery()->expects('whereIn')->with('relation.id', m::mustBe([])); - $models = [new MissingModelStub, new MissingModelStub]; - $relation->addEagerConstraints($models); + $relation = $this->getRelationWithRealQuery('string'); + $relation->addEagerConstraints([$this->newModelWithKey('abc'), $this->newModelWithKey('1abc')]); + + $this->assertSame('select * from "relation" where "relation"."id" = ? and "relation"."id" in (?, ?)', $relation->toSql()); + $this->assertSame(['foreign.value', '1abc', 'abc'], $relation->getBindings()); } - public function testDefaultEagerConstraintsWhenNotIncrementing(): void + /** + * Create a child model with the given foreign key. + */ + protected function newModelWithKey(int|string $key): ModelStub { - $relation = $this->getRelation(); - $relation->getQuery()->expects('whereIntegerInRaw')->with('relation.id', m::mustBe([])); - $models = [new MissingModelStub, new MissingModelStub]; - $relation->addEagerConstraints($models); + $model = new ModelStub; + $model->foreign_key = $key; + + return $model; + } + + /** + * Get a belongs to relationship backed by a real query builder. + */ + protected function getRelationWithRealQuery(string $keyType = 'int'): BelongsTo + { + $related = new EloquentBelongsToRelatedStub; + $related->setKeyType($keyType); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $builder = (new Builder(new BaseBuilder($connection, new Grammar($connection), new Processor)))->setModel($related); + + return new BelongsTo($builder, new ModelStub, 'foreign_key', 'id', 'relation'); } /** @@ -322,14 +342,14 @@ protected function getRelationWithPartialMock(?Model $parent = null): BelongsTo } } -class ModelStub extends Model +class EloquentBelongsToRelatedStub extends Model { - public mixed $foreign_key = 'foreign.value'; + protected ?string $table = 'relation'; } -class AnotherModelStub extends Model +class ModelStub extends Model { - public string $foreign_key = 'foreign.value.two'; + public mixed $foreign_key = 'foreign.value'; } class ModelStubWithZeroId extends Model diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index c4ed5f8731..cb92efaf11 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -10,7 +10,6 @@ use Hypervel\Database\BinaryParameter; use Hypervel\Database\ClassMorphViolationException; use Hypervel\Database\Connection; -use Hypervel\Database\ConnectionInterface; use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Collection; @@ -26,9 +25,11 @@ use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Grammars\MySqlGrammar; use Hypervel\Database\Query\Processors\Processor; +use Hypervel\Database\SQLiteConnection; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Collection as BaseCollection; use Hypervel\Testbench\TestCase; +use Hypervel\Tests\Database\Concerns\InteractsWithInMemoryConnection; use InvalidArgumentException; use Mockery as m; use PDO; @@ -40,6 +41,8 @@ class DatabaseEloquentBuilderTest extends TestCase { + use InteractsWithInMemoryConnection; + public function testFindMethod(): void { $builder = m::mock(Builder::class . '[first]', [$this->getMockQueryBuilder()]); @@ -354,12 +357,13 @@ public function testFindWithManyUsingCollection(): void public function testFirstMethod(): void { - $builder = m::mock(Builder::class . '[get,take]', [$this->getMockQueryBuilder()]); - $builder->expects('limit')->with(1)->andReturnSelf(); - $builder->expects('get')->with(['*'])->andReturn(new Collection(['bar'])); + $connection = $this->newConnection(); + $builder = $this->newBuilder($connection); $result = $builder->first(); - $this->assertSame('bar', $result); + + $this->assertSame('taylor', $result->name); + $this->assertSame('select * from "table" limit 1', $connection->getQueryLog()[0]['query']); } public function testQualifyColumn(): void @@ -477,89 +481,58 @@ public function testValueOrFailMethodWithModelNotFoundThrowsModelNotFoundExcepti public function testChunkWithLastChunkComplete(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection(['foo1', 'foo2']); - $chunk2 = new Collection(['foo3', 'foo4']); - $chunk3 = new Collection([]); - - $builder->expects('getOffset')->andReturn(null); - $builder->expects('getLimit')->andReturn(null); - $builder->expects('offset')->with(0)->andReturnSelf(); - $builder->expects('offset')->with(2)->andReturnSelf(); - $builder->expects('offset')->with(4)->andReturnSelf(); - $builder->expects('limit')->times(3)->with(2)->andReturnSelf(); - $builder->expects('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); - - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk3); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3, 4]); - $builder->chunk(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $chunks = []; + $builder->orderBy('id')->chunk(2, function (Collection $results) use (&$chunks): void { + $chunks[] = $results->pluck('id')->all(); }); + + $this->assertSame([[1, 2], [3, 4]], $chunks); + $this->assertSame([ + 'select * from "table" order by "id" asc limit 2 offset 0', + 'select * from "table" order by "id" asc limit 2 offset 2', + 'select * from "table" order by "id" asc limit 2 offset 4', + ], array_column($connection->getQueryLog(), 'query')); } public function testChunkWithLastChunkPartial(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection(['foo1', 'foo2']); - $chunk2 = new Collection(['foo3']); - $builder->expects('getOffset')->andReturn(null); - $builder->expects('getLimit')->andReturn(null); - $builder->expects('offset')->with(0)->andReturnSelf(); - $builder->expects('offset')->with(2)->andReturnSelf(); - $builder->expects('limit')->times(2)->with(2)->andReturnSelf(); - $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3]); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - - $builder->chunk(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $chunks = []; + $builder->orderBy('id')->chunk(2, function (Collection $results) use (&$chunks): void { + $chunks[] = $results->pluck('id')->all(); }); + + $this->assertSame([[1, 2], [3]], $chunks); + $this->assertSame([ + 'select * from "table" order by "id" asc limit 2 offset 0', + 'select * from "table" order by "id" asc limit 2 offset 2', + ], array_column($connection->getQueryLog(), 'query')); } public function testChunkCanBeStoppedByReturningFalse(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3]); - $chunk1 = new Collection(['foo1', 'foo2']); - $chunk2 = new Collection(['foo3']); - - $builder->expects('getOffset')->andReturn(null); - $builder->expects('getLimit')->andReturn(null); - $builder->expects('offset')->with(0)->andReturnSelf(); - $builder->expects('limit')->with(2)->andReturnSelf(); - $builder->expects('get')->andReturn($chunk1); - - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk2); - - $builder->chunk(2, function (Collection $results) use ($callbackAssertor): bool { - $callbackAssertor->doSomething($results); + $chunks = []; + $builder->orderBy('id')->chunk(2, function (Collection $results) use (&$chunks): bool { + $chunks[] = $results->pluck('id')->all(); return false; }); + + $this->assertSame([[1, 2]], $chunks); + $this->assertSame([ + 'select * from "table" order by "id" asc limit 2 offset 0', + ], array_column($connection->getQueryLog(), 'query')); } public function testChunkWithCountZero(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $builder->shouldReceive('getOffset')->never(); - $builder->shouldReceive('getLimit')->never(); - $builder->shouldReceive('offset')->never(); - $builder->shouldReceive('limit')->never(); - $builder->shouldReceive('get')->never(); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3]); + $builder->orderBy('id'); foreach ([0, -1] as $count) { try { @@ -571,64 +544,41 @@ public function testChunkWithCountZero(): void $this->assertSame('The chunk size should be at least 1', $exception->getMessage()); } } + + $this->assertSame([], $connection->getQueryLog()); } public function testChunkPaginatesUsingIdWithLastChunkComplete(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,forPageAfterId,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); - $chunk2 = new Collection([(object) ['someIdField' => 10], (object) ['someIdField' => 11]]); - $chunk3 = new Collection([]); - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('forPageAfterId')->with(2, 0, 'someIdField')->andReturnSelf(); - $builder->expects('forPageAfterId')->with(2, 2, 'someIdField')->andReturnSelf(); - $builder->expects('forPageAfterId')->with(2, 11, 'someIdField')->andReturnSelf(); - $builder->expects('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); - - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk3); - - $builder->chunkById(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 10, 11], 'someIdField'); + + $chunks = []; + $builder->chunkById(2, function (Collection $results) use (&$chunks): void { + $chunks[] = $results->pluck('someIdField')->all(); }, 'someIdField'); + + $this->assertSame([[1, 2], [10, 11]], $chunks); + $this->assertSame([[], [2], [11]], $this->pagedBindings($connection)); + $this->assertSame(['select * from "table" where "someIdField" is not null order by "someIdField" asc limit 2', 'select * from "table" where "someIdField" > ? order by "someIdField" asc limit 2', 'select * from "table" where "someIdField" > ? order by "someIdField" asc limit 2'], array_column($connection->getQueryLog(), 'query')); } public function testChunkPaginatesUsingIdWithLastChunkPartial(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,forPageAfterId,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); - $chunk2 = new Collection([(object) ['someIdField' => 10]]); - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('forPageAfterId')->with(2, 0, 'someIdField')->andReturnSelf(); - $builder->expects('forPageAfterId')->with(2, 2, 'someIdField')->andReturnSelf(); - $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 10], 'someIdField'); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - - $builder->chunkById(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $chunks = []; + $builder->chunkById(2, function (Collection $results) use (&$chunks): void { + $chunks[] = $results->pluck('someIdField')->all(); }, 'someIdField'); + + $this->assertSame([[1, 2], [10]], $chunks); + $this->assertSame([[], [2]], $this->pagedBindings($connection)); + $this->assertSame(['select * from "table" where "someIdField" is not null order by "someIdField" asc limit 2', 'select * from "table" where "someIdField" > ? order by "someIdField" asc limit 2'], array_column($connection->getQueryLog(), 'query')); } public function testChunkPaginatesUsingIdWithCountZero(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,forPageAfterId,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $builder->shouldReceive('getOffset')->never(); - $builder->shouldReceive('getLimit')->never(); - $builder->shouldReceive('forPageAfterId')->never(); - $builder->shouldReceive('get')->never(); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 10], 'someIdField'); foreach ([0, -1] as $count) { try { @@ -640,6 +590,8 @@ public function testChunkPaginatesUsingIdWithCountZero(): void $this->assertSame('The chunk size should be at least 1', $exception->getMessage()); } } + + $this->assertSame([], $connection->getQueryLog()); } public function testChunkDoesNotMutateTheOriginalBuilder(): void @@ -674,60 +626,35 @@ public function testChunkDoesNotMutateTheOriginalBuilder(): void public function testLazyWithLastChunkComplete(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('offset')->with(0)->andReturnSelf(); - $builder->expects('offset')->with(2)->andReturnSelf(); - $builder->expects('offset')->with(4)->andReturnSelf(); - $builder->expects('limit')->times(3)->with(2)->andReturnSelf(); - $builder->expects('get')->times(3)->andReturn( - new Collection(['foo1', 'foo2']), - new Collection(['foo3', 'foo4']), - new Collection([]) - ); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3, 4]); - $this->assertEquals( - ['foo1', 'foo2', 'foo3', 'foo4'], - $builder->lazy(2)->all() - ); + $this->assertSame([1, 2, 3, 4], $builder->orderBy('id')->lazy(2)->map(fn (Stub $model): int => $model->id)->all()); + $this->assertSame([ + 'select * from "table" order by "id" asc limit 2 offset 0', + 'select * from "table" order by "id" asc limit 2 offset 2', + 'select * from "table" order by "id" asc limit 2 offset 4', + ], array_column($connection->getQueryLog(), 'query')); } public function testLazyWithLastChunkPartial(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('offset')->with(0)->andReturnSelf(); - $builder->expects('offset')->with(2)->andReturnSelf(); - $builder->expects('limit')->twice()->with(2)->andReturnSelf(); - $builder->expects('get')->times(2)->andReturn( - new Collection(['foo1', 'foo2']), - new Collection(['foo3']) - ); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3]); - $this->assertEquals( - ['foo1', 'foo2', 'foo3'], - $builder->lazy(2)->all() - ); + $this->assertSame([1, 2, 3], $builder->orderBy('id')->lazy(2)->map(fn (Stub $model): int => $model->id)->all()); + $this->assertSame([ + 'select * from "table" order by "id" asc limit 2 offset 0', + 'select * from "table" order by "id" asc limit 2 offset 2', + ], array_column($connection->getQueryLog(), 'query')); } public function testLazyIsLazy(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,offset,limit,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('offset')->with(0)->andReturnSelf(); - $builder->expects('limit')->with(2)->andReturnSelf(); - $builder->expects('get')->andReturn(new Collection(['foo1', 'foo2'])); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 3, 4]); - $this->assertEquals(['foo1', 'foo2'], $builder->lazy(2)->take(2)->all()); + $this->assertSame([1, 2], $builder->orderBy('id')->lazy(2)->take(2)->map(fn (Stub $model): int => $model->id)->all()); + $this->assertSame([ + 'select * from "table" order by "id" asc limit 2 offset 0', + ], array_column($connection->getQueryLog(), 'query')); } public function testLazyDoesNotMutateTheOriginalBuilder(): void @@ -757,71 +684,26 @@ public function testLazyDoesNotMutateTheOriginalBuilder(): void public function testLazyByIdWithLastChunkComplete(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,forPageAfterId,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); - $chunk2 = new Collection([(object) ['someIdField' => 10], (object) ['someIdField' => 11]]); - $chunk3 = new Collection([]); - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('forPageAfterId')->with(2, 0, 'someIdField')->andReturnSelf(); - $builder->expects('forPageAfterId')->with(2, 2, 'someIdField')->andReturnSelf(); - $builder->expects('forPageAfterId')->with(2, 11, 'someIdField')->andReturnSelf(); - $builder->expects('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); - - $this->assertEquals( - [ - (object) ['someIdField' => 1], - (object) ['someIdField' => 2], - (object) ['someIdField' => 10], - (object) ['someIdField' => 11], - ], - $builder->lazyById(2, 'someIdField')->all() - ); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 10, 11], 'someIdField'); + + $this->assertSame([1, 2, 10, 11], $builder->lazyById(2, 'someIdField')->map(fn (Stub $model): int => $model->someIdField)->all()); + $this->assertSame([[], [2], [11]], $this->pagedBindings($connection)); } public function testLazyByIdWithLastChunkPartial(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,forPageAfterId,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); - $chunk2 = new Collection([(object) ['someIdField' => 10]]); - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('forPageAfterId')->with(2, 0, 'someIdField')->andReturnSelf(); - $builder->expects('forPageAfterId')->with(2, 2, 'someIdField')->andReturnSelf(); - $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); - - $this->assertEquals( - [ - (object) ['someIdField' => 1], - (object) ['someIdField' => 2], - (object) ['someIdField' => 10], - ], - $builder->lazyById(2, 'someIdField')->all() - ); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 10], 'someIdField'); + + $this->assertSame([1, 2, 10], $builder->lazyById(2, 'someIdField')->map(fn (Stub $model): int => $model->someIdField)->all()); + $this->assertSame([[], [2]], $this->pagedBindings($connection)); } public function testLazyByIdIsLazy(): void { - $builder = m::mock(Builder::class . '[getOffset,getLimit,forPageAfterId,get]', [$this->getMockQueryBuilder()]); - $builder->getQuery()->orders[] = ['column' => 'foobar', 'direction' => 'asc']; - - $chunk1 = new Collection([(object) ['someIdField' => 1], (object) ['someIdField' => 2]]); - $builder->expects('getOffset')->andReturnNull(); - $builder->expects('getLimit')->andReturnNull(); - $builder->expects('forPageAfterId')->with(2, 0, 'someIdField')->andReturnSelf(); - $builder->expects('get')->andReturn($chunk1); + [$connection, $builder] = $this->newPagedBuilder([1, 2, 10, 11], 'someIdField'); - $this->assertEquals( - [ - (object) ['someIdField' => 1], - (object) ['someIdField' => 2], - ], - $builder->lazyById(2, 'someIdField')->take(2)->all() - ); + $this->assertSame([1, 2], $builder->lazyById(2, 'someIdField')->take(2)->map(fn (Stub $model): int => $model->someIdField)->all()); + $this->assertSame([[]], $this->pagedBindings($connection)); } public function testPluckReturnsTheMutatedAttributesOfAModel(): void @@ -948,9 +830,9 @@ public function testLocalMacrosAreCalledOnBuilder(): void { unset($_SERVER['__test.builder']); $builder = new Builder(new BaseBuilder( - m::mock(ConnectionInterface::class), + new PdoConnection(new PDO('sqlite::memory:')), m::mock(Grammar::class), - m::mock(Processor::class) + new Processor )); $builder->macro('fooBar', function ($builder) { $_SERVER['__test.builder'] = $builder; @@ -991,17 +873,13 @@ public function testMissingStaticMacrosThrowsProperException(): void public function testGetModelsProperlyHydratesModels(): void { - $builder = m::mock(Builder::class . '[get]', [$this->getMockQueryBuilder()]); - $records[] = ['name' => 'taylor', 'age' => 26]; - $records[] = ['name' => 'dayle', 'age' => 28]; - $builder->getQuery()->expects('get')->with(['foo'])->andReturn(new BaseCollection($records)); - $model = m::mock(Model::class . '[getTable,hydrate]'); - $model->expects('getTable')->andReturn('foo_table'); - $builder->setModel($model); - $model->expects('hydrate')->with($records)->andReturn(new Collection(['hydrated'])); - $models = $builder->getModels(['foo']); + $builder = $this->newBuilder($this->newConnection()); + + $models = $builder->getModels(['name', 'age']); - $this->assertEquals(['hydrated'], $models); + $this->assertContainsOnlyInstancesOf(Stub::class, $models); + $this->assertSame(['taylor', 'dayle'], array_map(fn (Stub $model): string => $model->name, $models)); + $this->assertSame(28, $models[1]->age); } public function testEagerLoadRelationsLoadTopLevelRelationships(): void @@ -1020,7 +898,7 @@ public function testEagerLoadRelationsLoadTopLevelRelationships(): void public function testEagerLoadRelationsCanBeFlushed(): void { - $builder = m::mock(Builder::class . '[eagerLoadRelation]', [$this->getMockQueryBuilder()]); + $builder = new Builder($this->getMockQueryBuilder()); $builder->setEagerLoads(['foo']); @@ -1250,39 +1128,6 @@ public function testQueryPassThru(): void $this->assertSame($expression, $builder->raw('bar')); } - public function testQueryScopes(): void - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('from'); - $builder->getQuery()->expects('where')->with('foo', 'bar'); - $builder->setModel($model = new ScopeStub); - $result = $builder->approved(); - - $this->assertEquals($builder, $result); - } - - public function testQueryDynamicScopes(): void - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('from'); - $builder->getQuery()->expects('where')->with('bar', 'foo'); - $builder->setModel($model = new DynamicScopeStub); - $result = $builder->dynamic('bar', 'foo'); - - $this->assertEquals($builder, $result); - } - - public function testQueryDynamicScopesNamed(): void - { - $builder = $this->getBuilder(); - $builder->getQuery()->shouldReceive('from'); - $builder->getQuery()->expects('where')->with('foo', 'foo'); - $builder->setModel($model = new DynamicScopeStub); - $result = $builder->dynamic(bar: 'foo'); - - $this->assertEquals($builder, $result); - } - public function testApplyScopeCallbackReceivesAndReturnsSameBuilder(): void { $model = new Stub; @@ -1610,6 +1455,15 @@ public function testFirstWhereAcceptsRelationSubquery(): void $this->assertSame(11, $result->id); } + public function testRealQueryDynamicScopesWithNamedArguments(): void + { + $model = new DynamicScopeStub; + $this->mockConnectionForModel($model, 'SQLite'); + $query = $model->newQuery()->dynamic(bar: 'baz'); + $this->assertSame('select * from "table" where ("foo" = ?)', $query->toSql()); + $this->assertEquals(['baz'], $query->getBindings()); + } + public function testRealQueryHigherOrderOrWhereScopes(): void { $model = new HigherOrderWhereScopeStub; @@ -1658,22 +1512,6 @@ public function testRealQueryChainedHigherOrderOrWhereNotScopes(): void $this->assertSame('select * from "table" where ("one" = ?) or not (("two" = ?)) or not (("three" = ?))', $query->toSql()); } - public function testSimpleWhere(): void - { - $builder = $this->getBuilder(); - $builder->getQuery()->expects('where')->with('foo', '=', 'bar'); - $result = $builder->where('foo', '=', 'bar'); - $this->assertEquals($result, $builder); - } - - public function testPostgresOperatorsWhere(): void - { - $builder = $this->getBuilder(); - $builder->getQuery()->expects('where')->with('foo', '@>', 'bar'); - $result = $builder->where('foo', '@>', 'bar'); - $this->assertEquals($result, $builder); - } - public function testWhereBelongsTo(): void { $related = new WhereBelongsToStub([ @@ -3444,7 +3282,7 @@ public function testUpdate(): void $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $model = new Stub; $this->mockConnectionForModel($model, ''); @@ -3460,7 +3298,7 @@ public function testUpdateWithTimestampValue(): void { $connection = m::mock(Connection::class); $connection->expects('getTablePrefix')->times(2)->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $model = new Stub; $this->mockConnectionForModel($model, ''); @@ -3476,7 +3314,7 @@ public function testUpdateWithQualifiedTimestampValue(): void { $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $model = new Stub; $this->mockConnectionForModel($model, ''); @@ -3492,7 +3330,7 @@ public function testUpdateWithoutTimestamp(): void { $connection = m::mock(Connection::class); $connection->expects('getTablePrefix')->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $model = new StubWithoutTimestamp; $this->mockConnectionForModel($model, ''); @@ -3510,7 +3348,7 @@ public function testUpdateWithAlias(): void $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $model = new Stub; $this->mockConnectionForModel($model, ''); @@ -3528,7 +3366,7 @@ public function testUpdateWithAliasWithQualifiedTimestampValue(): void $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $model = new Stub; $this->mockConnectionForModel($model, ''); @@ -3798,21 +3636,11 @@ public function testTouchWithoutUpdatedAtColumn(): void $this->assertFalse($result); } - public function testWithCastsMethod(): void - { - $builder = new Builder($this->getMockQueryBuilder()); - $model = $this->getMockModel(); - $builder->setModel($model); - - $model->expects('mergeCasts')->with(['foo' => 'bar']); - $builder->withCasts(['foo' => 'bar']); - } - public function testClone(): void { $connection = m::mock(Connection::class); $connection->expects('getTablePrefix')->times(2)->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = new Builder($query); $builder->select('*')->from('users'); $clone = $builder->clone()->where('email', 'foo'); @@ -3883,7 +3711,7 @@ public function testCloneModelMakesAFreshCopyOfTheModel(): void { $connection = m::mock(Connection::class); $connection->expects('getTablePrefix')->times(2)->andReturn(''); - $query = new BaseBuilder($connection, new Grammar($connection), m::mock(Processor::class)); + $query = new BaseBuilder($connection, new Grammar($connection), new Processor); $builder = (new Builder($query))->setModel(new Stub); $builder->select('*')->from('users'); @@ -4107,6 +3935,58 @@ protected function getMockModel() return $model; } + /** + * Create an in-memory table holding the given ids and a builder that queries it. + * + * @return array{SQLiteConnection, Builder} + */ + protected function newPagedBuilder(array $ids, string $column = 'id'): array + { + $connection = $this->useInMemoryConnection(); + $pdo = $connection->getPdo(); + $pdo->exec('create table "table" ("' . $column . '" integer primary key)'); + + foreach ($ids as $id) { + $pdo->exec('insert into "table" values (' . $id . ')'); + } + + $connection->enableQueryLog(); + + return [$connection, $this->newBuilder($connection)]; + } + + /** + * Get the bindings of each logged query. + */ + protected function pagedBindings(SQLiteConnection $connection): array + { + return array_column($connection->getQueryLog(), 'bindings'); + } + + /** + * Create an in-memory connection with two seeded rows. + */ + protected function newConnection(): SQLiteConnection + { + $connection = $this->useInMemoryConnection(); + $pdo = $connection->getPdo(); + $pdo->exec('create table "table" ("id" integer primary key, "name" text, "age" integer)'); + $pdo->exec("insert into \"table\" values (1, 'taylor', 26)"); + $pdo->exec("insert into \"table\" values (2, 'dayle', 28)"); + + $connection->enableQueryLog(); + + return $connection; + } + + /** + * Create a builder for the stub model on the given connection. + */ + protected function newBuilder(SQLiteConnection $connection): Builder + { + return (new Builder($connection->query()))->setModel(new Stub); + } + protected function getMockQueryBuilder() { $query = m::mock(BaseBuilder::class); @@ -4132,22 +4012,6 @@ class FreshQueryModel extends Stub protected array $with = ['related']; } -class ScopeStub extends Model -{ - public function scopeApproved($query) - { - $query->where('foo', 'bar'); - } -} - -class DynamicScopeStub extends Model -{ - public function scopeDynamic($query, $foo = 'foo', $bar = 'bar') - { - $query->where($foo, $bar); - } -} - class HigherOrderWhereScopeStub extends Model { protected ?string $table = 'table'; @@ -4168,6 +4032,16 @@ public function scopeThree($query) } } +class DynamicScopeStub extends Model +{ + protected ?string $table = 'table'; + + public function scopeDynamic($query, $foo = 'foo', $bar = 'bar') + { + $query->where($foo, $bar); + } +} + class NestedStub extends Model { use SoftDeletes; diff --git a/tests/Database/DatabaseEloquentCollectionTest.php b/tests/Database/DatabaseEloquentCollectionTest.php index 24b7ec8269..57d7c62f09 100755 --- a/tests/Database/DatabaseEloquentCollectionTest.php +++ b/tests/Database/DatabaseEloquentCollectionTest.php @@ -19,7 +19,6 @@ use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; -use stdClass; class DatabaseEloquentCollectionTest extends TestCase { @@ -106,15 +105,9 @@ public function testContainsWithMultipleArguments(): void public function testContainsIndicatesIfModelInArray(): void { - $mockModel = m::mock(Model::class); - $mockModel->shouldReceive('is')->with($mockModel)->andReturn(true); - $mockModel->shouldReceive('is')->andReturn(false); - $mockModel2 = m::mock(Model::class); - $mockModel2->shouldReceive('is')->with($mockModel2)->andReturn(true); - $mockModel2->shouldReceive('is')->andReturn(false); - $mockModel3 = m::mock(Model::class); - $mockModel3->shouldReceive('is')->with($mockModel3)->andReturn(true); - $mockModel3->shouldReceive('is')->andReturn(false); + $mockModel = (new CollectionModel)->forceFill(['id' => 1]); + $mockModel2 = (new CollectionModel)->forceFill(['id' => 2]); + $mockModel3 = (new CollectionModel)->forceFill(['id' => 3]); $c = new Collection([$mockModel, $mockModel2]); $this->assertTrue($c->contains($mockModel)); @@ -128,12 +121,8 @@ public function testContainsIndicatesIfModelInArray(): void public function testContainsIndicatesIfDifferentModelInArray(): void { - $mockModelFoo = m::namedMock('Foo', Model::class); - $mockModelFoo->shouldReceive('is')->with($mockModelFoo)->andReturn(true); - $mockModelFoo->shouldReceive('is')->andReturn(false); - $mockModelBar = m::namedMock('Bar', Model::class); - $mockModelBar->shouldReceive('is')->with($mockModelBar)->andReturn(true); - $mockModelBar->shouldReceive('is')->andReturn(false); + $mockModelFoo = (new CollectionModel)->forceFill(['id' => 1]); + $mockModelBar = (new User)->forceFill(['id' => 1]); $c = new Collection([$mockModelFoo]); $this->assertTrue($c->contains($mockModelFoo)); @@ -145,11 +134,9 @@ public function testContainsIndicatesIfDifferentModelInArray(): void public function testContainsIndicatesIfKeyedModelInArray(): void { - $mockModel = m::mock(Model::class); - $mockModel->shouldReceive('getKey')->andReturn('1'); + $mockModel = (new CollectionModel)->forceFill(['id' => '1']); $c = new Collection([$mockModel]); - $mockModel2 = m::mock(Model::class); - $mockModel2->shouldReceive('getKey')->andReturn('2'); + $mockModel2 = (new CollectionModel)->forceFill(['id' => '2']); $c->add($mockModel2); $this->assertTrue($c->contains(1)); @@ -191,12 +178,8 @@ public function testContainsRejectsNullModelKeysWithoutNarrowingScalarKeyCompati public function testContainsKeyAndValueIndicatesIfModelInArray(): void { - $mockModel1 = m::mock(Model::class); - $mockModel1->shouldReceive('offsetExists')->with('name')->andReturn(true); - $mockModel1->shouldReceive('offsetGet')->with('name')->andReturn('Taylor'); - $mockModel2 = m::mock(Model::class); - $mockModel2->shouldReceive('offsetExists')->andReturn(true); - $mockModel2->shouldReceive('offsetGet')->with('name')->andReturn('Abigail'); + $mockModel1 = (new CollectionModel)->forceFill(['name' => 'Taylor']); + $mockModel2 = (new CollectionModel)->forceFill(['name' => 'Abigail']); $c = new Collection([$mockModel1, $mockModel2]); $this->assertTrue($c->contains('name', 'Taylor')); @@ -210,10 +193,8 @@ public function testContainsKeyAndValueIndicatesIfModelInArray(): void public function testContainsClosureIndicatesIfModelInArray(): void { - $mockModel1 = m::mock(Model::class); - $mockModel1->shouldReceive('getKey')->andReturn(1); - $mockModel2 = m::mock(Model::class); - $mockModel2->shouldReceive('getKey')->andReturn(2); + $mockModel1 = (new CollectionModel)->forceFill(['id' => 1]); + $mockModel2 = (new CollectionModel)->forceFill(['id' => 2]); $c = new Collection([$mockModel1, $mockModel2]); $this->assertTrue($c->contains(function ($model) { @@ -233,8 +214,7 @@ public function testContainsClosureIndicatesIfModelInArray(): void public function testFindMethodFindsModelById(): void { - $mockModel = m::mock(Model::class); - $mockModel->expects('getKey')->times(2)->andReturn(1); + $mockModel = (new CollectionModel)->forceFill(['id' => 1]); $c = new Collection([$mockModel]); $this->assertSame($mockModel, $c->find(1)); @@ -286,8 +266,7 @@ public function testFindDoesNotMatchNullModelKeys(): void public function testFindOrFailFindsModelById(): void { - $mockModel = m::mock(Model::class); - $mockModel->expects('getKey')->andReturn(1); + $mockModel = (new CollectionModel)->forceFill(['id' => 1]); $c = new Collection([$mockModel]); $this->assertSame($mockModel, $c->findOrFail(1)); @@ -359,12 +338,12 @@ public function testFindOrFailThrowsExceptionWithoutMessageWhenOtherModelsAreNot public function testLoadMethodEagerLoadsGivenRelationships(): void { - $c = $this->getMockBuilder(Collection::class)->onlyMethods(['first'])->setConstructorArgs([['foo']])->getMock(); - $mockItem = m::mock(stdClass::class); - $c->expects($this->once())->method('first')->willReturn($mockItem); - $mockItem->expects('newQueryWithoutRelationships')->andReturn($mockItem); - $mockItem->expects('with')->with(['bar', 'baz'])->andReturn($mockItem); - $mockItem->expects('eagerLoadRelations')->with(['foo'])->andReturn(['results']); + $model = m::mock(Model::class); + $builder = m::mock(Builder::class); + $model->expects('newQueryWithoutRelationships')->andReturn($builder); + $builder->expects('with')->with(['bar', 'baz'])->andReturnSelf(); + $builder->expects('eagerLoadRelations')->with([$model])->andReturn(['results']); + $c = new Collection([$model]); $c->load('bar', 'baz'); $this->assertEquals(['results'], $c->all()); @@ -372,14 +351,11 @@ public function testLoadMethodEagerLoadsGivenRelationships(): void public function testCollectionDictionaryReturnsModelKeys(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c = new Collection([$one, $two, $three]); @@ -388,14 +364,11 @@ public function testCollectionDictionaryReturnsModelKeys(): void public function testCollectionMergesWithGivenCollection(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c1 = new Collection([$one, $two]); $c2 = new Collection([$two, $three]); @@ -425,8 +398,8 @@ public function testCollectionMergeSkipsNullModelKeys(): void public function testMap(): void { - $one = m::mock(Model::class); - $two = m::mock(Model::class); + $one = new CollectionModel; + $two = new CollectionModel; $c = new Collection([$one, $two]); @@ -440,8 +413,8 @@ public function testMap(): void public function testMappingToNonModelsReturnsABaseCollection(): void { - $one = m::mock(Model::class); - $two = m::mock(Model::class); + $one = new CollectionModel; + $two = new CollectionModel; $c = (new Collection([$one, $two]))->map(function ($item) { return 'not-a-model'; @@ -452,8 +425,8 @@ public function testMappingToNonModelsReturnsABaseCollection(): void public function testMapWithKeys(): void { - $one = m::mock(Model::class); - $two = m::mock(Model::class); + $one = new CollectionModel; + $two = new CollectionModel; $c = new Collection([$one, $two]); @@ -468,8 +441,8 @@ public function testMapWithKeys(): void public function testMapWithKeysToNonModelsReturnsABaseCollection(): void { - $one = m::mock(Model::class); - $two = m::mock(Model::class); + $one = new CollectionModel; + $two = new CollectionModel; $key = 0; $c = (new Collection([$one, $two]))->mapWithKeys(function ($item) use (&$key) { @@ -481,14 +454,11 @@ public function testMapWithKeysToNonModelsReturnsABaseCollection(): void public function testCollectionDiffsWithGivenCollection(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c1 = new Collection([$one, $two]); $c2 = new Collection([$two, $three]); @@ -556,14 +526,11 @@ public function testCollectionDuplicatesWithKey(): void public function testCollectionIntersectWithNull(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c1 = new Collection([$one, $two, $three]); @@ -572,14 +539,11 @@ public function testCollectionIntersectWithNull(): void public function testCollectionIntersectsWithGivenCollection(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c1 = new Collection([$one, $two]); $c2 = new Collection([$two, $three]); @@ -599,11 +563,9 @@ public function testCollectionIntersectExcludesNullKeyedModels(): void public function testCollectionReturnsUniqueItems(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); $c = new Collection([$one, $two, $two]); @@ -634,14 +596,11 @@ public function testCollectionReturnsUniqueStrictBasedOnKeysOnly(): void public function testOnlyReturnsCollectionWithGivenModelKeys(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c = new Collection([$one, $two, $three]); @@ -652,14 +611,11 @@ public function testOnlyReturnsCollectionWithGivenModelKeys(): void public function testExceptReturnsCollectionWithoutGivenModelKeys(): void { - $one = m::mock(Model::class); - $one->shouldReceive('getKey')->andReturn(1); + $one = (new CollectionModel)->forceFill(['id' => 1]); - $two = m::mock(Model::class); - $two->shouldReceive('getKey')->andReturn(2); + $two = (new CollectionModel)->forceFill(['id' => 2]); - $three = m::mock(Model::class); - $three->shouldReceive('getKey')->andReturn(3); + $three = (new CollectionModel)->forceFill(['id' => 3]); $c = new Collection([$one, $two, $three]); @@ -1030,12 +986,10 @@ public function testLoadExistsShouldCastBool(): void public function testWithNonScalarKey(): void { $fooKey = new TestKey('foo'); - $foo = m::mock(Model::class); - $foo->shouldReceive('getKey')->andReturn($fooKey); + $foo = (new NonIncrementingModel)->forceFill(['id' => $fooKey]); $barKey = new TestKey('bar'); - $bar = m::mock(Model::class); - $bar->shouldReceive('getKey')->andReturn($barKey); + $bar = (new NonIncrementingModel)->forceFill(['id' => $barKey]); $collection = new Collection([$foo, $bar]); @@ -1131,6 +1085,11 @@ protected function newInstance(mixed $items = []): static } } +class NonIncrementingModel extends Model +{ + public bool $incrementing = false; +} + class User extends Model { protected ?string $table = 'users'; diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index 4b37ad33a3..4f2cc558f8 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -8,9 +8,13 @@ use Hypervel\Database\Eloquent\Collection; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\HasMany; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as QueryBuilder; +use Hypervel\Database\Query\Grammars\Grammar; +use Hypervel\Database\Query\Processors\Processor; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; class DatabaseEloquentHasManyTest extends TestCase { @@ -54,40 +58,43 @@ public function testRelationUpsertFillsForeignKey(): void public function testRelationIsProperlyInitialized(): void { $relation = $this->getRelation(); - $model = m::mock(Model::class); + $model = new ModelStub; $relation->getRelated()->expects('newCollection')->andReturnUsing(function (array $array = []): Collection { return new Collection($array); }); - $model->expects('setRelation')->with('foo', m::type(Collection::class)); $models = $relation->initRelation([$model], 'foo'); $this->assertEquals([$model], $models); + $this->assertInstanceOf(Collection::class, $model->getRelation('foo')); + $this->assertCount(0, $model->getRelation('foo')); } public function testEagerConstraintsAreProperlyAdded(): void { - $relation = $this->getRelation(); - $relation->getParent()->expects('getKeyName')->andReturn('id'); - $relation->getParent()->expects('getKeyType')->andReturn('int'); - $relation->getQuery()->expects('whereIntegerInRaw')->with('table.foreign_key', [1, 2]); + $relation = $this->getRelationWithRealQuery(); $model1 = new ModelStub; $model1->id = 1; $model2 = new ModelStub; $model2->id = 2; $relation->addEagerConstraints([$model1, $model2]); + + $this->assertSame('select * from "model_stubs" where "table"."foreign_key" = ? and "table"."foreign_key" is not null and "table"."foreign_key" in (1, 2)', $relation->toSql()); + $this->assertSame([1], $relation->getBindings()); } public function testEagerConstraintsAreProperlyAddedWithStringKey(): void { - $relation = $this->getRelation(); - $relation->getParent()->expects('getKeyName')->andReturn('id'); - $relation->getParent()->expects('getKeyType')->andReturn('string'); - $relation->getQuery()->expects('whereIn')->with('table.foreign_key', [1, 2]); + $parent = new ModelStub; + $parent->setKeyType('string'); + $relation = $this->getRelationWithRealQuery($parent); $model1 = new ModelStub; $model1->id = 1; $model2 = new ModelStub; $model2->id = 2; $relation->addEagerConstraints([$model1, $model2]); + + $this->assertSame('select * from "model_stubs" where "table"."foreign_key" = ? and "table"."foreign_key" is not null and "table"."foreign_key" in (?, ?)', $relation->toSql()); + $this->assertSame(['1', 1, 2], $relation->getBindings()); } public function testModelsAreProperlyMatchedToParents(): void @@ -121,6 +128,21 @@ public function testModelsAreProperlyMatchedToParents(): void $this->assertNull($models[2]->foo); } + /** + * Get a has many relationship backed by a real query builder. + */ + protected function getRelationWithRealQuery(?Model $parent = null): HasMany + { + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $query = new QueryBuilder($connection, new Grammar($connection), new Processor); + $builder = (new Builder($query))->setModel(new ModelStub); + + $parent ??= new ModelStub; + $parent->id = 1; + + return new HasMany($builder, $parent, 'table.foreign_key', 'id'); + } + protected function getRelation() { $queryBuilder = m::mock(QueryBuilder::class); diff --git a/tests/Database/DatabaseEloquentHasOneTest.php b/tests/Database/DatabaseEloquentHasOneTest.php index d254db7bfe..ac978e48bd 100755 --- a/tests/Database/DatabaseEloquentHasOneTest.php +++ b/tests/Database/DatabaseEloquentHasOneTest.php @@ -4,14 +4,17 @@ namespace Hypervel\Tests\Database\DatabaseEloquentHasOneTest; -use Hypervel\Contracts\Database\Query\Expression; use Hypervel\Database\Eloquent\Builder; use Hypervel\Database\Eloquent\Collection; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\HasOne; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as BaseBuilder; +use Hypervel\Database\Query\Grammars\Grammar; +use Hypervel\Database\Query\Processors\Processor; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; class DatabaseEloquentHasOneTest extends TestCase { @@ -102,15 +105,15 @@ public function testRelationIsProperlyInitialized(): void public function testEagerConstraintsAreProperlyAdded(): void { - $relation = $this->getRelation(); - $relation->getParent()->expects('getKeyName')->andReturn('id'); - $relation->getParent()->expects('getKeyType')->andReturn('int'); - $relation->getQuery()->expects('whereIntegerInRaw')->with('table.foreign_key', [1, 2]); + $relation = $this->getRelationWithRealQuery(); $model1 = new ModelStub; $model1->id = 1; $model2 = new ModelStub; $model2->id = 2; $relation->addEagerConstraints([$model1, $model2]); + + $this->assertSame('select * from "model_stubs" where "table"."foreign_key" = ? and "table"."foreign_key" is not null and "table"."foreign_key" in (1, 2)', $relation->toSql()); + $this->assertSame([1], $relation->getBindings()); } public function testModelsAreProperlyMatchedToParents(): void @@ -148,25 +151,33 @@ public function __toString() public function testRelationCountQueryCanBeBuilt(): void { - $relation = $this->getRelation(); - $builder = m::mock(Builder::class); + $relation = $this->getRelationWithRealQuery(); - $baseQuery = m::mock(BaseBuilder::class); - $baseQuery->from = 'one'; - $parentQuery = m::mock(BaseBuilder::class); - $parentQuery->from = 'two'; + $query = $relation->getRelationExistenceCountQuery($this->newBuilder('one'), $this->newBuilder('two')); - $builder->expects('getQuery')->andReturn($baseQuery); - $builder->expects('getQuery')->andReturn($parentQuery); + $this->assertSame('select count(*) from "one" where "model_stubs"."id" = "table"."foreign_key"', $query->toSql()); + } - $builder->expects('select')->with(m::type(Expression::class))->andReturnSelf(); - $relation->getParent()->expects('qualifyColumn')->andReturn('table.id'); - // Return $builder (Eloquent Builder) to satisfy return type - $builder->expects('whereColumn')->with('table.id', '=', 'table.foreign_key')->andReturnSelf(); - // setBindings is called on the Eloquent Builder, which forwards to base query - $builder->expects('setBindings')->with([], 'select')->andReturnSelf(); + /** + * Create a builder for the stub model, optionally from the given table. + */ + protected function newBuilder(?string $table = null): Builder + { + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $builder = (new Builder(new BaseBuilder($connection, new Grammar($connection), new Processor)))->setModel(new ModelStub); + + return $table ? $builder->from($table) : $builder; + } + + /** + * Get a has one relationship backed by a real query builder. + */ + protected function getRelationWithRealQuery(): HasOne + { + $parent = new ModelStub; + $parent->id = 1; - $relation->getRelationExistenceCountQuery($builder, $builder); + return new HasOne($this->newBuilder(), $parent, 'table.foreign_key', 'id'); } /** diff --git a/tests/Database/DatabaseEloquentInverseRelationTest.php b/tests/Database/DatabaseEloquentInverseRelationTest.php index 0e79612ed4..e82165fc00 100755 --- a/tests/Database/DatabaseEloquentInverseRelationTest.php +++ b/tests/Database/DatabaseEloquentInverseRelationTest.php @@ -12,9 +12,11 @@ use Hypervel\Database\Eloquent\Relations\BelongsTo; use Hypervel\Database\Eloquent\Relations\Concerns\SupportsInverseRelations; use Hypervel\Database\Eloquent\Relations\Relation; +use Hypervel\Database\SQLiteConnection; use Hypervel\Support\Stringable; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; use PHPUnit\Framework\Attributes\DataProvider; use ReflectionFunction; @@ -281,24 +283,31 @@ public function testSetsGuessedInverseRelationBasedOnForeignKey(): void public function testOnlyHydratesInverseRelationOnModels(): void { - $relation = m::mock(HasInverseRelationStub::class)->shouldAllowMockingProtectedMethods()->makePartial(); - $relation->expects('getParent')->andReturn(new HasInverseRelationParentStub); - $relation->expects('applyInverseRelationToModel')->times(6); + $builder = (new Builder((new SQLiteConnection(new PDO('sqlite::memory:')))->query()))->setModel(new HasInverseRelationRelatedStub); + $parent = new HasInverseRelationParentStub; + $relation = (new HasInverseRelationStub($builder, $parent))->inverse('test'); + + $models = array_map(fn (): HasInverseRelationRelatedStub => new HasInverseRelationRelatedStub, range(1, 6)); $relation->exposeApplyInverseRelationToCollection([ - new HasInverseRelationRelatedStub, + $models[0], 12345, - new HasInverseRelationRelatedStub, - new HasInverseRelationRelatedStub, + $models[1], + $models[2], Model::class, - new HasInverseRelationRelatedStub, + $models[3], true, [], - new HasInverseRelationRelatedStub, + $models[4], 'foo', new class { }, - new HasInverseRelationRelatedStub, + $models[5], ]); + + foreach ($models as $model) { + $this->assertTrue($model->relationLoaded('test')); + $this->assertSame($parent, $model->test); + } } } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index ef4202b882..8d4829ea8a 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -5,6 +5,7 @@ namespace Hypervel\Tests\Database\DatabaseEloquentModelTest; use ArgumentCountError; +use ArrayObject as NativeArrayObject; use Carbon\CarbonInterface; use DateTime; use DateTimeImmutable; @@ -17,8 +18,8 @@ use Hypervel\Contracts\Database\Eloquent\CastsInboundAttributes; use Hypervel\Contracts\Encryption\Encrypter; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Database\Capsule\Manager as DB; use Hypervel\Database\Connection; -use Hypervel\Database\ConnectionInterface; use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\ConnectionResolverInterface as Resolver; use Hypervel\Database\Eloquent\Attributes\CollectedBy; @@ -59,6 +60,7 @@ use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Grammars\PostgresGrammar; use Hypervel\Database\Query\Processors\Processor; +use Hypervel\Database\SQLiteConnection; use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\ClassInvoker; @@ -69,12 +71,14 @@ use Hypervel\Support\Stringable; use Hypervel\Support\Uri; use Hypervel\Testbench\TestCase; +use Hypervel\Tests\Database\Concerns\InteractsWithInMemoryConnection; use Hypervel\Tests\Database\Fixtures\Enums\StringStatus; use Hypervel\Tests\Database\Fixtures\TestCast; use Hypervel\Tests\Database\Fixtures\TestValueObject; use InvalidArgumentException; use LogicException; use Mockery as m; +use PDO; use PHPUnit\Framework\Attributes\TestWith; use ReflectionClass; use RuntimeException; @@ -87,6 +91,7 @@ class DatabaseEloquentModelTest extends TestCase { + use InteractsWithInMemoryConnection; use InteractsWithTime; protected $encrypter; @@ -813,34 +818,40 @@ public function testEagerLoadingWithColumns(): void { $model = new WithoutRelationStub; $instance = $model->newInstance()->newQuery()->with('foo:bar,baz', 'hadi'); - $builder = m::mock(Builder::class); - $builder->expects('select')->with(['bar', 'baz']); $this->assertNotNull($instance->getEagerLoads()['hadi']); $this->assertNotNull($instance->getEagerLoads()['foo']); $closure = $instance->getEagerLoads()['foo']; + + $builder = $this->newBuilder()->select('id'); $closure($builder); + + $this->assertSame('select "bar", "baz"', $builder->toSql()); } public function testWithWhereHasWithSpecificColumns(): void { $model = new WithWhereHasStub; $instance = $model->newInstance()->newQuery()->withWhereHas('foo:diaa,fares'); - $builder = m::mock(Builder::class); - $builder->expects('select')->with(['diaa', 'fares']); $this->assertNotNull($instance->getEagerLoads()['foo']); $closure = $instance->getEagerLoads()['foo']; + + $builder = $this->newBuilder()->select('id'); $closure($builder); + + $this->assertSame('select "diaa", "fares"', $builder->toSql()); } public function testWithWhereHasWorksInNestedQuery(): void { $model = new WithWhereHasStub; $instance = $model->newInstance()->newQuery()->where(fn (Builder $q) => $q->withWhereHas('foo:diaa,fares')); - $builder = m::mock(Builder::class); - $builder->expects('select')->with(['diaa', 'fares']); $this->assertNotNull($instance->getEagerLoads()['foo']); $closure = $instance->getEagerLoads()['foo']; + + $builder = $this->newBuilder()->select('id'); $closure($builder); + + $this->assertSame('select "diaa", "fares"', $builder->toSql()); } public function testWithMethodCallsQueryBuilderCorrectlyWithArray(): void @@ -857,12 +868,7 @@ public function testUpdateProcess(): void $query->expects('update')->with(['name' => 'taylor'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.updating: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.updated: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.saved: ' . get_class($model), $model)->andReturn(true); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->id = 1; $model->foo = 'bar'; @@ -871,6 +877,7 @@ public function testUpdateProcess(): void $model->name = 'taylor'; $model->exists = true; $this->assertTrue($model->save()); + $this->assertSame(['eloquent.saving', 'eloquent.updating', 'eloquent.updated', 'eloquent.saved'], $events->getArrayCopy()); } public function testUpdateProcessDoesntOverrideTimestamps(): void @@ -880,10 +887,7 @@ public function testUpdateProcessDoesntOverrideTimestamps(): void $query->expects('where')->with('id', '=', 1); $query->expects('update')->with(['created_at' => 'foo', 'updated_at' => 'bar'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $events = m::mock(Dispatcher::class); - $events->expects('until')->times(2); - $events->expects('dispatch')->times(2); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->id = 1; $model->syncOriginal(); @@ -891,15 +895,16 @@ public function testUpdateProcessDoesntOverrideTimestamps(): void $model->updated_at = 'bar'; $model->exists = true; $this->assertTrue($model->save()); + $this->assertSame(['eloquent.saving', 'eloquent.updating', 'eloquent.updated', 'eloquent.saved'], $events->getArrayCopy()); } public function testSaveIsCanceledIfSavingEventReturnsFalse(): void { $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery'])->getMock(); - $query = m::mock(Builder::class); + $query = $this->newBuilder(); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(false); + $events = new EventDispatcher; + $events->listen('eloquent.saving: ' . get_class($model), fn (): bool => false); $model::setEventDispatcher($events); $model->exists = true; @@ -909,11 +914,10 @@ public function testSaveIsCanceledIfSavingEventReturnsFalse(): void public function testUpdateIsCanceledIfUpdatingEventReturnsFalse(): void { $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery'])->getMock(); - $query = m::mock(Builder::class); + $query = $this->newBuilder(); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.updating: ' . get_class($model), $model)->andReturn(false); + $events = new EventDispatcher; + $events->listen('eloquent.updating: ' . get_class($model), fn (): bool => false); $model::setEventDispatcher($events); $model->exists = true; $model->foo = 'bar'; @@ -924,10 +928,10 @@ public function testUpdateIsCanceledIfUpdatingEventReturnsFalse(): void public function testEventsCanBeFiredWithCustomEventObjects(): void { $model = $this->getMockBuilder(EventObjectStub::class)->onlyMethods(['newModelQuery'])->getMock(); - $query = m::mock(Builder::class); + $query = $this->newBuilder(); $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $events = m::mock(Dispatcher::class); - $events->expects('until')->with(m::type(SavingEventStub::class))->andReturn(false); + $events = new EventDispatcher; + $events->listen(SavingEventStub::class, fn (): bool => false); $model::setEventDispatcher($events); $model->exists = true; @@ -960,12 +964,7 @@ public function testUpdateUsesOldPrimaryKey(): void $query->expects('update')->with(['id' => 2, 'foo' => 'bar'])->andReturn(1); $model->expects($this->once())->method('newModelQuery')->willReturn($query); $model->expects($this->once())->method('updateTimestamps'); - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.updating: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.updated: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.saved: ' . get_class($model), $model)->andReturn(true); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->id = 1; $model->syncOriginal(); @@ -974,6 +973,7 @@ public function testUpdateUsesOldPrimaryKey(): void $model->exists = true; $this->assertTrue($model->save()); + $this->assertSame(['eloquent.saving', 'eloquent.updating', 'eloquent.updated', 'eloquent.saved'], $events->getArrayCopy()); } public function testTimestampsAreReturnedAsObjects(): void @@ -1017,13 +1017,7 @@ public function testTimestampsAreReturnedAsObjectsOnCreate(): void 'updated_at' => CarbonImmutable::now(), ]; $model = new DateModelStub; - $resolver = m::mock(ConnectionResolverInterface::class); - Model::setConnectionResolver($resolver); - $mockConnection = m::mock(Connection::class); - $resolver->shouldReceive('connection')->andReturn($mockConnection); - $grammar = m::mock(Grammar::class); - $mockConnection->expects('getQueryGrammar')->times(4)->andReturn($grammar); - $grammar->expects('getDateFormat')->times(4)->andReturn('Y-m-d H:i:s'); + $this->useInMemoryConnection(); $instance = $model->newInstance($timestamps); $this->assertSame(CarbonImmutable::class, $instance->updated_at::class); $this->assertSame(CarbonImmutable::class, $instance->created_at::class); @@ -1036,13 +1030,7 @@ public function testDateTimeAttributesReturnNullIfSetToNull(): void 'updated_at' => CarbonImmutable::now(), ]; $model = new DateModelStub; - $resolver = m::mock(ConnectionResolverInterface::class); - Model::setConnectionResolver($resolver); - $mockConnection = m::mock(Connection::class); - $resolver->shouldReceive('connection')->andReturn($mockConnection); - $grammar = m::mock(Grammar::class); - $mockConnection->expects('getQueryGrammar')->times(2)->andReturn($grammar); - $grammar->expects('getDateFormat')->times(2)->andReturn('Y-m-d H:i:s'); + $this->useInMemoryConnection(); $instance = $model->newInstance($timestamps); $instance->created_at = null; @@ -1118,46 +1106,24 @@ public function getDateFormat(): string public function testInsertProcess(): void { - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'taylor'], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); - - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.created: ' . get_class($model), $model); - $events->expects('dispatch')->with('eloquent.saved: ' . get_class($model), $model); - $model::setEventDispatcher($events); + $connection = $this->newStubConnection(); + $events = $this->recordEvents(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $this->assertTrue($model->save()); - $this->assertEquals(1, $model->id); + $this->assertSame(1, $model->id); $this->assertTrue($model->exists); + $this->assertSame('taylor', $connection->scalar('select "name" from "stub" where "id" = 1')); + $this->assertSame(['eloquent.saving', 'eloquent.creating', 'eloquent.created', 'eloquent.saved'], $events->getArrayCopy()); - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insert')->with([['name' => 'taylor']]); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); + $model = new ModelStub; $model->setIncrementing(false); - - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.created: ' . get_class($model), $model); - $events->expects('dispatch')->with('eloquent.saved: ' . get_class($model), $model); - $model::setEventDispatcher($events); - $model->name = 'taylor'; - $model->exists = false; $this->assertTrue($model->save()); $this->assertNull($model->id); $this->assertTrue($model->exists); + $this->assertSame(2, $connection->table('stub')->count()); } #[TestWith([['tags' => ['api'], 'tenant_id' => 42], 'insert into "stub" ("tags", "tenant_id") values (?, ?)', [['api'], 42]])] @@ -1210,140 +1176,82 @@ public function testNonIncrementingModelWithoutAttributesDoesNotInsert(): void public function testInsertIsCanceledIfCreatingEventReturnsFalse(): void { - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(false); - $model::setEventDispatcher($events); + $connection = $this->newStubConnection(); + $events = new EventDispatcher; + $events->listen('eloquent.creating: ' . ModelStub::class, fn (): bool => false); + Model::setEventDispatcher($events); + + $model = new ModelStub; + $model->name = 'taylor'; $this->assertFalse($model->save()); $this->assertFalse($model->exists); + $this->assertSame(0, $connection->table('stub')->count()); } public function testInsertOrIgnoreProcessWithIncrementing(): void { - $model = $this->getMockBuilder(ModelStub::class) - ->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh']) - ->getMock(); - $query = m::mock(Builder::class); - $baseQuery = m::mock(BaseBuilder::class); - $query->expects('toBase')->andReturn($baseQuery); - $baseQuery->expects('insertOrIgnoreReturning') - ->with([['name' => 'taylor']], ['*'], null) - ->andReturn(new BaseCollection([(object) ['id' => 1, 'name' => 'taylor']])); - $query->expects('getConnection') - ->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); - - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.created: ' . get_class($model), $model); - $events->expects('dispatch')->with('eloquent.saved: ' . get_class($model), $model); - $model::setEventDispatcher($events); + $connection = $this->newStubConnection(); + $events = $this->recordEvents(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $this->assertTrue($model->saveOrIgnore()); $this->assertSame(1, $model->id); $this->assertTrue($model->exists); $this->assertTrue($model->wasRecentlyCreated); + $this->assertSame(1, $connection->table('stub')->count()); + $this->assertSame(['eloquent.saving', 'eloquent.creating', 'eloquent.created', 'eloquent.saved'], $events->getArrayCopy()); } public function testInsertOrIgnoreProcessWithConflict(): void { - $model = $this->getMockBuilder(ModelStub::class) - ->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh']) - ->getMock(); - $query = m::mock(Builder::class); - $baseQuery = m::mock(BaseBuilder::class); - $query->expects('toBase')->andReturn($baseQuery); - $baseQuery->expects('insertOrIgnoreReturning') - ->with([['name' => 'taylor']], ['*'], null) - ->andReturn(new BaseCollection); - $query->expects('getConnection') - ->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); - - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(true); - $model::setEventDispatcher($events); + $connection = $this->newStubConnection(unique: true); + $connection->table('stub')->insert(['name' => 'taylor']); + $events = $this->recordEvents(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $this->assertFalse($model->saveOrIgnore()); $this->assertFalse($model->exists); $this->assertFalse($model->wasRecentlyCreated); + $this->assertSame(1, $connection->table('stub')->count()); + $this->assertSame(['eloquent.saving', 'eloquent.creating'], $events->getArrayCopy()); } public function testInsertOrIgnoreProcessWithNonIncrementing(): void { - $model = $this->getMockBuilder(ModelStub::class) - ->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh']) - ->getMock(); - $query = m::mock(Builder::class); - $baseQuery = m::mock(BaseBuilder::class); - $query->expects('toBase')->andReturn($baseQuery); - $baseQuery->expects('insertOrIgnoreReturning') - ->with([['name' => 'taylor']], ['*'], null) - ->andReturn(new BaseCollection([(object) ['name' => 'taylor']])); - $query->expects('getConnection') - ->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); - $model->setIncrementing(false); - - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(true); - $events->expects('dispatch')->with('eloquent.created: ' . get_class($model), $model); - $events->expects('dispatch')->with('eloquent.saved: ' . get_class($model), $model); - $model::setEventDispatcher($events); + $connection = $this->newStubConnection(); + $events = $this->recordEvents(); + $model = new ModelStub; + $model->setIncrementing(false); $model->name = 'taylor'; - $model->exists = false; $this->assertTrue($model->saveOrIgnore()); $this->assertNull($model->id); $this->assertTrue($model->exists); $this->assertTrue($model->wasRecentlyCreated); + $this->assertSame(1, $connection->table('stub')->count()); + $this->assertSame(['eloquent.saving', 'eloquent.creating', 'eloquent.created', 'eloquent.saved'], $events->getArrayCopy()); } public function testInsertOrIgnoreProcessWithNamedUnique(): void { - $model = $this->getMockBuilder(ModelStub::class) - ->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh']) - ->getMock(); - $query = m::mock(Builder::class); - $baseQuery = m::mock(BaseBuilder::class); - $query->expects('toBase')->andReturn($baseQuery); - $baseQuery->expects('insertOrIgnoreReturning') - ->with([['name' => 'taylor']], ['*'], ['name']) - ->andReturn(new BaseCollection); - $query->expects('getConnection') - ->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); - - $events = m::mock(Dispatcher::class); - $events->expects('until')->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->expects('until')->with('eloquent.creating: ' . get_class($model), $model)->andReturn(true); - $model::setEventDispatcher($events); + $connection = $this->newStubConnection(unique: true); + $connection->table('stub')->insert(['name' => 'taylor']); + $events = $this->recordEvents(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $this->assertFalse($model->saveOrIgnore([], ['name'])); $this->assertFalse($model->exists); $this->assertFalse($model->wasRecentlyCreated); + $this->assertSame(1, $connection->table('stub')->count()); + $this->assertSame(['eloquent.saving', 'eloquent.creating'], $events->getArrayCopy()); } #[TestWith([true, ['tags' => ['api'], 'name' => 'taylor'], ['name'], 'insert into "stub" ("name", "tags") values (?, ?) on conflict ("name") do nothing returning *', ['taylor', ['api']]])] @@ -1420,15 +1328,10 @@ public function testDeleteProperlyDeletesModel(): void public function testPushNoRelations(): void { - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'taylor'], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); + $this->newStubConnection(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $this->assertTrue($model->push()); $this->assertEquals(1, $model->id); @@ -1437,15 +1340,10 @@ public function testPushNoRelations(): void public function testPushEmptyOneRelation(): void { - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'taylor'], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); + $this->newStubConnection(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $model->setRelation('relationOne', null); $this->assertTrue($model->push()); @@ -1456,24 +1354,13 @@ public function testPushEmptyOneRelation(): void public function testPushOneRelation(): void { - $related1 = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'related1'], 'id')->andReturn(2); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $related1->expects($this->once())->method('newModelQuery')->willReturn($query); - $related1->expects($this->once())->method('updateTimestamps'); - $related1->name = 'related1'; - $related1->exists = false; + $this->newStubConnection(); - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'taylor'], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); + $related1 = new ModelStub; + $related1->name = 'related1'; + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $model->setRelation('relationOne', $related1); $this->assertTrue($model->push()); @@ -1487,15 +1374,10 @@ public function testPushOneRelation(): void public function testPushEmptyManyRelation(): void { - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'taylor'], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); + $this->newStubConnection(); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $model->setRelation('relationMany', new Collection([])); $this->assertTrue($model->push()); @@ -1506,33 +1388,16 @@ public function testPushEmptyManyRelation(): void public function testPushManyRelation(): void { - $related1 = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'related1'], 'id')->andReturn(2); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $related1->expects($this->once())->method('newModelQuery')->willReturn($query); - $related1->expects($this->once())->method('updateTimestamps'); + $this->newStubConnection(); + + $related1 = new ModelStub; $related1->name = 'related1'; - $related1->exists = false; - $related2 = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'related2'], 'id')->andReturn(3); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $related2->expects($this->once())->method('newModelQuery')->willReturn($query); - $related2->expects($this->once())->method('updateTimestamps'); + $related2 = new ModelStub; $related2->name = 'related2'; - $related2->exists = false; - - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with(['name' => 'taylor'], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); - $model->expects($this->once())->method('updateTimestamps'); + $model = new ModelStub; $model->name = 'taylor'; - $model->exists = false; $model->setRelation('relationMany', new Collection([$related1, $related2])); $this->assertTrue($model->push()); @@ -1564,22 +1429,6 @@ public function testPushCircularRelations(): void } } - public function testNewQueryReturnsEloquentQueryBuilder(): void - { - $connection = m::mock(Connection::class); - $grammar = m::mock(Grammar::class); - $processor = m::mock(Processor::class); - $resolver = m::mock(ConnectionResolverInterface::class); - ModelStub::setConnectionResolver($resolver); - $connection->expects('query')->andReturnUsing(function () use ($connection, $grammar, $processor): BaseBuilder { - return new BaseBuilder($connection, $grammar, $processor); - }); - $resolver->shouldReceive('connection')->andReturn($connection); - $model = new ModelStub; - $builder = $model->newQuery(); - $this->assertInstanceOf(Builder::class, $builder); - } - public function testGetAndSetTableOperations(): void { $model = new ModelStub; @@ -1598,19 +1447,20 @@ public function testGetKeyReturnsValueOfPrimaryKey(): void public function testConnectionManagement(): void { - $resolver = m::mock(ConnectionResolverInterface::class); - ModelStub::setConnectionResolver($resolver); - $model = m::mock(ModelStub::class . '[getConnectionName,connection]'); + $db = new DB; + $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:'], 'foo'); + $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:'], 'bar'); + $db->bootEloquent(); + $foo = $db->getConnection('foo'); + $bar = $db->getConnection('bar'); + $model = new ModelStub; $retval = $model->setConnection('foo'); - $this->assertEquals($retval, $model); - $this->assertSame('foo', $model->connection); - - $model->expects('getConnectionName')->andReturn('somethingElse'); - $mockConnection = m::mock(Connection::class); - $resolver->expects('connection')->with('somethingElse')->andReturn($mockConnection); + $this->assertSame($model, $retval); + $this->assertSame('foo', $model->getConnectionName()); + $this->assertSame($foo, $model->getConnection()); - $this->assertSame($mockConnection, $model->getConnection()); + $this->assertSame($bar, $model->setConnection('bar')->getConnection()); } #[TestWith(['Foo'])] @@ -2874,13 +2724,17 @@ public function testReplicatingEventIsFiredWhenReplicatingModel(): void { $model = new ModelStub; - $events = m::mock(Dispatcher::class); - $events->expects('dispatch')->with('eloquent.replicating: ' . get_class($model), m::on(function (Model $replicated) use ($model): bool { - return $replicated instanceof ModelStub && $replicated !== $model; - })); + $events = new EventDispatcher; + $replicatedWith = null; + $events->listen('eloquent.replicating: ' . get_class($model), function (Model $replica) use (&$replicatedWith): void { + $replicatedWith = $replica; + }); $model::setEventDispatcher($events); $model->replicate(); + + $this->assertInstanceOf(ModelStub::class, $replicatedWith); + $this->assertNotSame($model, $replicatedWith); } public function testReplicateQuietlyCreatesANewModelInstanceWithSameAttributeValuesAndIsQuiet(): void @@ -2891,9 +2745,7 @@ public function testReplicateQuietlyCreatesANewModelInstanceWithSameAttributeVal $model->created_at = new DateTime; $model->updated_at = new DateTime; - $events = m::mock(Dispatcher::class); - $events->shouldNotReceive('dispatch'); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $replicated = $model->replicateQuietly(); @@ -2901,6 +2753,7 @@ public function testReplicateQuietlyCreatesANewModelInstanceWithSameAttributeVal $this->assertSame('bar', $replicated->foo); $this->assertNull($replicated->created_at); $this->assertNull($replicated->updated_at); + $this->assertSame([], $events->getArrayCopy()); } public function testIncrementOnExistingModelCallsQueryAndSetsAttribute(): void @@ -2939,12 +2792,7 @@ public function testIncrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $query->expects('where')->times(2)->andReturnSelf(); $query->expects('increment')->times(2)->andReturn(1); - $events = m::mock(Dispatcher::class); - $events->shouldReceive('until')->never()->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->shouldReceive('until')->never()->with('eloquent.updating: ' . get_class($model), $model)->andReturn(true); - $events->shouldReceive('dispatch')->never()->with('eloquent.updated: ' . get_class($model), $model)->andReturn(true); - $events->shouldReceive('dispatch')->never()->with('eloquent.saved: ' . get_class($model), $model)->andReturn(true); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->publicIncrementQuietly('foo', 1); $this->assertFalse($model->isDirty()); @@ -2953,6 +2801,7 @@ public function testIncrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $this->assertEquals(4, $model->foo); $this->assertEquals(1, $model->category); $this->assertFalse($model->isDirty('category')); + $this->assertSame([], $events->getArrayCopy()); } public function testDecrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAndIsQuiet(): void @@ -2968,12 +2817,7 @@ public function testDecrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $query->expects('where')->times(2)->andReturnSelf(); $query->expects('decrement')->times(2)->andReturn(1); - $events = m::mock(Dispatcher::class); - $events->shouldReceive('until')->never()->with('eloquent.saving: ' . get_class($model), $model)->andReturn(true); - $events->shouldReceive('until')->never()->with('eloquent.updating: ' . get_class($model), $model)->andReturn(true); - $events->shouldReceive('dispatch')->never()->with('eloquent.updated: ' . get_class($model), $model)->andReturn(true); - $events->shouldReceive('dispatch')->never()->with('eloquent.saved: ' . get_class($model), $model)->andReturn(true); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->publicDecrementQuietly('foo', 1); $this->assertFalse($model->isDirty()); @@ -2982,6 +2826,7 @@ public function testDecrementQuietlyOnExistingModelCallsQueryAndSetsAttributeAnd $this->assertEquals(2, $model->foo); $this->assertEquals(1, $model->category); $this->assertFalse($model->isDirty('category')); + $this->assertSame([], $events->getArrayCopy()); } public function testIncrementReturnsFalseWhenUpdatingEventIsCancelled(): void @@ -3060,10 +2905,7 @@ public function testIncrementEachQuietlyOnExistingModelCallsQueryAndSetsAttribut $query->expects('where')->times(2)->andReturnSelf(); $query->expects('incrementEach')->times(2)->andReturn(1); - $events = m::mock(Dispatcher::class); - $events->shouldNotReceive('until'); - $events->shouldNotReceive('dispatch'); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->publicIncrementEachQuietly(['foo' => 1, 'bar' => 2]); @@ -3076,6 +2918,7 @@ public function testIncrementEachQuietlyOnExistingModelCallsQueryAndSetsAttribut $this->assertSame(4, $model->foo); $this->assertSame(1, $model->category); $this->assertFalse($model->isDirty('category')); + $this->assertSame([], $events->getArrayCopy()); } public function testDecrementEachQuietlyOnExistingModelCallsQueryAndSetsAttributeAndIsQuiet(): void @@ -3092,10 +2935,7 @@ public function testDecrementEachQuietlyOnExistingModelCallsQueryAndSetsAttribut $query->expects('where')->times(2)->andReturnSelf(); $query->expects('decrementEach')->times(2)->andReturn(1); - $events = m::mock(Dispatcher::class); - $events->shouldNotReceive('until'); - $events->shouldNotReceive('dispatch'); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->publicDecrementEachQuietly(['foo' => 3, 'bar' => 2]); @@ -3108,6 +2948,7 @@ public function testDecrementEachQuietlyOnExistingModelCallsQueryAndSetsAttribut $this->assertSame(6, $model->foo); $this->assertSame(1, $model->category); $this->assertFalse($model->isDirty('category')); + $this->assertSame([], $events->getArrayCopy()); } public function testIncrementEachQuietlyCanBeCalledDynamicallyOnModelInstance(): void @@ -3185,15 +3026,10 @@ public function testIncrementEachFiresModelEvents(): void $query->expects('where')->andReturnSelf(); $query->expects('incrementEach')->andReturn(1); - $events = m::mock(Dispatcher::class); - $events->expects('until') - ->with('eloquent.updating: ' . get_class($model), $model) - ->andReturn(true); - $events->expects('dispatch') - ->with('eloquent.updated: ' . get_class($model), $model); - $model::setEventDispatcher($events); + $events = $this->recordEvents(); $model->publicIncrementEach(['foo' => 1]); + $this->assertSame(['eloquent.updating', 'eloquent.updated'], $events->getArrayCopy()); } public function testIncrementEachReturnsFalseWhenUpdatingEventCancelled(): void @@ -3205,10 +3041,8 @@ public function testIncrementEachReturnsFalseWhenUpdatingEventCancelled(): void $model->foo = 1; $model->shouldNotReceive('newQueryWithoutScopes'); - $events = m::mock(Dispatcher::class); - $events->expects('until') - ->with('eloquent.updating: ' . get_class($model), $model) - ->andReturn(false); + $events = new EventDispatcher; + $events->listen('eloquent.updating: ' . get_class($model), fn (): bool => false); $model::setEventDispatcher($events); $result = $model->publicIncrementEach(['foo' => 1]); @@ -3882,27 +3716,22 @@ public function testNonExistingAttributeWithInternalMethodNameDoesntCallMethod() public function testIntKeyTypePreserved(): void { - $model = $this->getMockBuilder(ModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with([], 'id')->andReturn(1); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); + $this->newStubConnection(); + + $model = new ModelStub; $this->assertTrue($model->save()); - $this->assertEquals(1, $model->id); + $this->assertSame(1, $model->id); } public function testStringKeyTypePreserved(): void { - $model = $this->getMockBuilder(KeyTypeModelStub::class)->onlyMethods(['newModelQuery', 'updateTimestamps', 'refresh'])->getMock(); + $this->newStubConnection(); - $query = m::mock(Builder::class); - $query->expects('insertGetId')->with([], 'id')->andReturn('string id'); - $query->expects('getConnection')->andReturn(m::mock(ConnectionInterface::class, ['getWritableName' => 'default'])); - $model->expects($this->once())->method('newModelQuery')->willReturn($query); + $model = new KeyTypeModelStub; $this->assertTrue($model->save()); - $this->assertSame('string id', $model->id); + $this->assertSame('1', $model->id); } public function testScopesMethod(): void @@ -4442,11 +4271,9 @@ protected function addMockConnection(Model $model): void $model->setConnectionResolver($resolver); $connection = m::mock(Connection::class); $resolver->shouldReceive('connection')->andReturn($connection); - $grammar = m::mock(Grammar::class); + $grammar = new Grammar($connection); $connection->shouldReceive('getQueryGrammar')->andReturn($grammar); - $grammar->shouldReceive('getBitwiseOperators')->andReturn([]); - $grammar->shouldReceive('isExpression')->andReturnFalse(); - $processor = m::mock(Processor::class); + $processor = new Processor; $connection->shouldReceive('getPostProcessor')->andReturn($processor); $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor): BaseBuilder { return new BaseBuilder($connection, $grammar, $processor); @@ -4957,6 +4784,49 @@ public function testDefaultsMethodIsEvaluatedForEachNewModel(): void $this->assertSame(['trial_days' => 30], (new ModelWithRuntimeDefaultsStub)->getAttributes()); } + + /** + * Create the "stub" table on an in-memory connection used by every model. + */ + protected function newStubConnection(bool $unique = false): SQLiteConnection + { + $connection = $this->useInMemoryConnection(); + $pdo = $connection->getPdo(); + $pdo->exec('create table "stub" ("id" integer primary key autoincrement, "name" text, "created_at" text, "updated_at" text)'); + + if ($unique) { + $pdo->exec('create unique index "stub_name_unique" on "stub" ("name")'); + } + + return $connection; + } + + /** + * Record the names of the model events fired from now on. + */ + protected function recordEvents(): NativeArrayObject + { + $log = new NativeArrayObject; + $events = new EventDispatcher; + $events->listen('eloquent.*', function (string $event) use ($log): void { + $name = strstr($event, ':', true); + + if (! in_array($name, ['eloquent.booting', 'eloquent.booted'], true)) { + $log[] = $name; + } + }); + Model::setEventDispatcher($events); + + return $log; + } + + /** + * Create a query builder on a fresh in-memory connection. + */ + protected function newBuilder(): Builder + { + return new Builder((new SQLiteConnection(new PDO('sqlite::memory:')))->query()); + } } class CustomBuilder extends Builder @@ -5294,11 +5164,9 @@ public function setIncrementing(bool $value): static public function getConnection(): Connection { $mock = m::mock(Connection::class); - $grammar = m::mock(Grammar::class); + $grammar = new Grammar($mock); $mock->shouldReceive('getQueryGrammar')->andReturn($grammar); - $grammar->shouldReceive('getBitwiseOperators')->andReturn([]); - $grammar->shouldReceive('isExpression')->andReturnFalse(); - $processor = m::mock(Processor::class); + $processor = new Processor; $mock->shouldReceive('getPostProcessor')->andReturn($processor); $mock->shouldReceive('getWritableName')->andReturn('name'); $mock->shouldReceive('query')->andReturnUsing(function () use ($mock, $grammar, $processor): BaseBuilder { diff --git a/tests/Database/DatabaseEloquentMorphTest.php b/tests/Database/DatabaseEloquentMorphTest.php index bc9ee02f6a..b38e5cb5cb 100755 --- a/tests/Database/DatabaseEloquentMorphTest.php +++ b/tests/Database/DatabaseEloquentMorphTest.php @@ -8,30 +8,38 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\MorphMany; use Hypervel\Database\Eloquent\Relations\MorphOne; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as QueryBuilder; +use Hypervel\Database\Query\Grammars\Grammar; +use Hypervel\Database\Query\Processors\Processor; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; class DatabaseEloquentMorphTest extends TestCase { public function testMorphOneSetsProperConstraints(): void { - $this->getOneRelation(); + $relation = $this->getRelationWithRealQuery(MorphOne::class); + + $this->assertSame('select * from "reset_model_stubs" where "table"."morph_type" = ? and "table"."morph_id" = ? and "table"."morph_id" is not null', $relation->toSql()); + $this->assertSame([ResetModelStub::class, 1], $relation->getBindings()); } public function testMorphOneEagerConstraintsAreProperlyAdded(): void { - $relation = $this->getOneRelation(); - $relation->getParent()->expects('getKeyName')->andReturn('id'); - $relation->getParent()->expects('getKeyType')->andReturn('string'); - $relation->getQuery()->expects('whereIn')->with('table.morph_id', [1, 2]); - $relation->getQuery()->expects('where')->with('table.morph_type', get_class($relation->getParent())); + $parent = new ResetModelStub; + $parent->setKeyType('string'); + $relation = $this->getRelationWithRealQuery(MorphOne::class, $parent); $model1 = new ResetModelStub; $model1->id = 1; $model2 = new ResetModelStub; $model2->id = 2; $relation->addEagerConstraints([$model1, $model2]); + + $this->assertSame('select * from "reset_model_stubs" where "table"."morph_type" = ? and "table"."morph_id" = ? and "table"."morph_id" is not null and "table"."morph_id" in (?, ?) and "table"."morph_type" = ?', $relation->toSql()); + $this->assertSame([ResetModelStub::class, '1', 1, 2, ResetModelStub::class], $relation->getBindings()); } /** @@ -40,22 +48,24 @@ public function testMorphOneEagerConstraintsAreProperlyAdded(): void */ public function testMorphManySetsProperConstraints(): void { - $this->getManyRelation(); + $relation = $this->getRelationWithRealQuery(MorphMany::class); + + $this->assertSame('select * from "reset_model_stubs" where "table"."morph_type" = ? and "table"."morph_id" = ? and "table"."morph_id" is not null', $relation->toSql()); + $this->assertSame([ResetModelStub::class, 1], $relation->getBindings()); } public function testMorphManyEagerConstraintsAreProperlyAdded(): void { - $relation = $this->getManyRelation(); - $relation->getParent()->expects('getKeyName')->andReturn('id'); - $relation->getParent()->expects('getKeyType')->andReturn('int'); - $relation->getQuery()->expects('whereIntegerInRaw')->with('table.morph_id', [1, 2]); - $relation->getQuery()->expects('where')->with('table.morph_type', get_class($relation->getParent())); + $relation = $this->getRelationWithRealQuery(MorphMany::class); $model1 = new ResetModelStub; $model1->id = 1; $model2 = new ResetModelStub; $model2->id = 2; $relation->addEagerConstraints([$model1, $model2]); + + $this->assertSame('select * from "reset_model_stubs" where "table"."morph_type" = ? and "table"."morph_id" = ? and "table"."morph_id" is not null and "table"."morph_id" in (1, 2) and "table"."morph_type" = ?', $relation->toSql()); + $this->assertSame([ResetModelStub::class, 1, ResetModelStub::class], $relation->getBindings()); } public function testMorphRelationUpsertFillsForeignKey(): void @@ -95,20 +105,20 @@ public function testMorphRelationUpsertFillsForeignKey(): void ); } - protected function getOneRelation() + /** + * Get a morph one or many relationship backed by a real query builder. + * + * @param class-string $relation + */ + protected function getRelationWithRealQuery(string $relation, ?Model $parent = null): MorphMany|MorphOne { - $queryBuilder = m::mock(QueryBuilder::class); - $builder = m::mock(Builder::class, [$queryBuilder]); - $builder->expects('whereNotNull')->with('table.morph_id'); - $builder->expects('where')->with('table.morph_id', '=', 1); - $related = m::mock(Model::class); - $builder->shouldReceive('getModel')->andReturn($related); - $parent = m::mock(Model::class); - $parent->shouldReceive('getAttribute')->with('id')->andReturn(1); - $parent->shouldReceive('getMorphClass')->andReturn(get_class($parent)); - $builder->expects('where')->with('table.morph_type', get_class($parent)); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $builder = (new Builder(new QueryBuilder($connection, new Grammar($connection), new Processor)))->setModel(new ResetModelStub); + + $parent ??= new ResetModelStub; + $parent->id = 1; - return new MorphOne($builder, $parent, 'table.morph_type', 'table.morph_id', 'id'); + return new $relation($builder, $parent, 'table.morph_type', 'table.morph_id', 'id'); } protected function getManyRelation() @@ -116,7 +126,7 @@ protected function getManyRelation() $builder = m::mock(Builder::class); $builder->expects('whereNotNull')->with('table.morph_id'); $builder->expects('where')->with('table.morph_id', '=', 1); - $related = m::mock(Model::class); + $related = new ResetModelStub; $builder->shouldReceive('getModel')->andReturn($related); $parent = m::mock(Model::class); $parent->shouldReceive('getAttribute')->with('id')->andReturn(1); diff --git a/tests/Database/DatabaseEloquentMorphToTest.php b/tests/Database/DatabaseEloquentMorphToTest.php index 3bb860aa31..0f42afa586 100644 --- a/tests/Database/DatabaseEloquentMorphToTest.php +++ b/tests/Database/DatabaseEloquentMorphToTest.php @@ -12,6 +12,7 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\MorphTo; use Hypervel\Database\Eloquent\Relations\Relation; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as QueryBuilder; use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Processors\Processor; @@ -19,6 +20,7 @@ use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; +use PDO; class DatabaseEloquentMorphToTest extends TestCase { @@ -207,34 +209,29 @@ public function testMorphToWithSpecifiedClassDefault(): void public function testAssociateMethodSetsForeignKeyAndTypeOnModel(): void { - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('foreign_key')->andReturn('foreign.value'); + $relation = $this->getRelationWithRealQuery(); + $associate = new RelatedStub; + $associate->id = 1; - $relation = $this->getRelationAssociate($parent); + $parent = $relation->associate($associate); - $associate = m::mock(Model::class); - $associate->expects('getAttribute')->with('id')->andReturn(1); - $associate->expects('getMorphClass')->andReturn('Model'); - - $parent->expects('setAttribute')->with('foreign_key', 1); - $parent->expects('setAttribute')->with('morph_type', 'Model'); - $parent->expects('setRelation')->with('relation', $associate); - - $relation->associate($associate); + $this->assertSame(1, $parent->getAttribute('foreign_key')); + $this->assertSame(RelatedStub::class, $parent->getAttribute('morph_type')); + $this->assertSame($associate, $parent->getRelation('relation')); } public function testAssociateMethodIgnoresNullValue(): void { - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('foreign_key')->andReturn('foreign.value'); - - $relation = $this->getRelationAssociate($parent); + $relation = $this->getRelationWithRealQuery(); + $relation->getParent()->setAttribute('foreign_key', 5); + $relation->getParent()->setAttribute('morph_type', 'type_1'); + $relation->getParent()->setRelation('relation', new RelatedStub); - $parent->expects('setAttribute')->with('foreign_key', null); - $parent->expects('setAttribute')->with('morph_type', null); - $parent->expects('setRelation')->with('relation', null); + $parent = $relation->associate(null); - $relation->associate(null); + $this->assertNull($parent->getAttribute('foreign_key')); + $this->assertNull($parent->getAttribute('morph_type')); + $this->assertNull($parent->getRelation('relation')); } public function testAssociateMethodUsesConfiguredOwnerKeyWithZeroValue(): void @@ -360,16 +357,16 @@ public function testAssociateMethodRejectsUnmappedModelsBeforeChangingTheParent( public function testDissociateMethodDeletesUnsetsKeyAndTypeOnModel(): void { - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('foreign_key')->andReturn('foreign.value'); - - $relation = $this->getRelation($parent); + $relation = $this->getRelationWithRealQuery(); + $relation->getParent()->setAttribute('foreign_key', 5); + $relation->getParent()->setAttribute('morph_type', 'type_1'); - $parent->expects('setAttribute')->with('foreign_key', null); - $parent->expects('setAttribute')->with('morph_type', null); - $parent->expects('setRelation')->with('relation', null); + $parent = $relation->dissociate(); - $relation->dissociate(); + $this->assertNull($parent->getAttribute('foreign_key')); + $this->assertNull($parent->getAttribute('morph_type')); + $this->assertTrue($parent->relationLoaded('relation')); + $this->assertNull($parent->getRelation('relation')); } public function testMatchToMorphParentsNormalizesKeyWhenOwnerKeyIsNullAndResultKeyIsObject(): void @@ -385,7 +382,7 @@ public function __toString(): string }; $builder = m::mock(Builder::class); - $related = m::mock(Model::class); + $related = new RelatedStub; $builder->expects('getModel')->andReturn($related); $parent = new ModelStub; @@ -412,29 +409,18 @@ public function testCreateModelByTypeThrowsWhenTypeNotInMorphMapAndRequireMorphM Relation::requireMorphMap(); - $parent = new ModelStub; - $builder = m::mock(Builder::class); - $builder->shouldReceive('getModel')->andReturn(new RelatedStub); - - $relation = MorphTo::noConstraints( - fn () => new MorphTo($builder, $parent, 'foreign_key', 'id', 'morph_type', 'relation') - ); - - $relation->createModelByType('poisoned'); + $this->getRelationWithRealQuery()->createModelByType('poisoned'); } /** - * Create a morph relation with the existing parent key constrained. + * Get a morph to relationship backed by a real query builder. */ - protected function getRelationAssociate(Model $parent): MorphTo + protected function getRelationWithRealQuery(): MorphTo { - $builder = m::mock(Builder::class); - $builder->expects('where')->with('relation.id', '=', 'foreign.value'); - $related = m::mock(Model::class); - $related->expects('qualifyColumn')->andReturnUsing(fn (string $column): string => "relation.{$column}"); - $builder->expects('getModel')->andReturn($related); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $builder = (new Builder(new QueryBuilder($connection, new Grammar($connection), new Processor)))->setModel(new RelatedStub); - return new MorphTo($builder, $parent, 'foreign_key', 'id', 'morph_type', 'relation'); + return new MorphTo($builder, new ModelStub, 'foreign_key', 'id', 'morph_type', 'relation'); } public function getRelation($parent = null, $builder = null) diff --git a/tests/Database/DatabaseEloquentPivotTest.php b/tests/Database/DatabaseEloquentPivotTest.php index 5b6185ed46..8fdb3abc36 100755 --- a/tests/Database/DatabaseEloquentPivotTest.php +++ b/tests/Database/DatabaseEloquentPivotTest.php @@ -21,14 +21,13 @@ class DatabaseEloquentPivotTest extends TestCase public function testPropertiesAreSetCorrectly(): void { $parent = m::mock(Model::class . '[getConnectionName]'); - $parent->expects('getConnectionName')->times(2)->andReturn('connection'); + $parent->expects('getConnectionName')->times(1)->andReturn('connection'); $resolver = m::mock(ConnectionResolverInterface::class); $parent->setConnectionResolver($resolver); $connection = m::mock(Connection::class); - $resolver->expects('connection')->times(2)->andReturn($connection); - $grammar = m::mock(Grammar::class); - $connection->expects('getQueryGrammar')->times(2)->andReturn($grammar); - $parent->getConnection()->getQueryGrammar()->expects('getDateFormat')->andReturn('Y-m-d H:i:s'); + $resolver->expects('connection')->times(1)->andReturn($connection); + $grammar = new Grammar($connection); + $connection->expects('getQueryGrammar')->times(1)->andReturn($grammar); $parent->setDateFormat('Y-m-d H:i:s'); $pivot = Pivot::fromAttributes($parent, ['foo' => 'bar', 'created_at' => '2015-09-12'], 'table', true); @@ -41,8 +40,7 @@ public function testPropertiesAreSetCorrectly(): void public function testMutatorsAreCalledFromConstructor(): void { - $parent = m::mock(Model::class . '[getConnectionName]'); - $parent->expects('getConnectionName')->andReturn('connection'); + $parent = new DummyModel; $pivot = MutatorStub::fromAttributes($parent, ['foo' => 'bar'], 'table', true); @@ -109,8 +107,7 @@ public function testTimestampPropertyIsTrueWhenCreatingFromRawAttributes(): void public function testKeysCanBeSetProperly(): void { - $parent = m::mock(Model::class . '[getConnectionName]'); - $parent->expects('getConnectionName')->andReturn('connection'); + $parent = new DummyModel; $pivot = Pivot::fromAttributes($parent, ['foo' => 'bar'], 'table'); $pivot->setPivotKeys('foreign', 'other'); @@ -182,9 +179,7 @@ public function testRestorationQueriesUsePrimaryKeys(string $pivotClass, array|i public function testPivotModelWithParentReturnsParentsTimestampColumns(): void { - $parent = m::mock(Model::class); - $parent->expects('getCreatedAtColumn')->andReturn('parent_created_at'); - $parent->expects('getUpdatedAtColumn')->andReturn('parent_updated_at'); + $parent = new DatabaseEloquentPivotTestCustomTimestampsModel; $pivotWithParent = new Pivot; $pivotWithParent->pivotParent = $parent; @@ -281,3 +276,10 @@ class PrunableDeleteReturnPivotStub extends DeleteReturnPivotStub class DummyModel extends Model { } + +class DatabaseEloquentPivotTestCustomTimestampsModel extends Model +{ + public const ?string CREATED_AT = 'parent_created_at'; + + public const ?string UPDATED_AT = 'parent_updated_at'; +} diff --git a/tests/Database/DatabaseEloquentRelationTest.php b/tests/Database/DatabaseEloquentRelationTest.php index 8bd32be889..54256ed7a5 100755 --- a/tests/Database/DatabaseEloquentRelationTest.php +++ b/tests/Database/DatabaseEloquentRelationTest.php @@ -14,7 +14,6 @@ use Hypervel\Database\SQLiteConnection; use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; -use Mockery as m; use PDO; class DatabaseEloquentRelationTest extends TestCase @@ -39,152 +38,81 @@ public function testUnsetExistingRelation(): void public function testTouchMethodUpdatesRelatedTimestamps(): void { - $builder = m::mock(Builder::class); - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('id')->andReturn(1); - $related = m::mock(NoTouchingModelStub::class)->makePartial(); - $builder->expects('getModel')->andReturn($related); - $builder->expects('whereNotNull'); - $builder->expects('where'); - $builder->expects('withoutGlobalScopes')->andReturn($builder); - $relation = new HasOne($builder, $parent, 'foreign_key', 'id'); - $related->expects('getUpdatedAtColumn')->andReturn('updated_at'); - $now = CarbonImmutable::now()->toDateTimeString(); - $related->expects('freshTimestampString')->andReturn($now); - $builder->expects('update')->with(['updated_at' => $now])->andReturn(1); + $connection = $this->newConnection(); + $relation = $this->newHasOne($connection, new NoTouchingModelStub, new NoTouchingModelStub); $relation->touch(); + + $this->assertSame('2023-01-01 00:00:00', $this->updatedAt($connection, 'table')); } public function testCanDisableParentTouchingForAllModels(): void { - $related = m::mock(NoTouchingModelStub::class)->makePartial(); - $related->shouldReceive('getUpdatedAtColumn')->never(); - $related->shouldReceive('freshTimestampString')->never(); + $connection = $this->newConnection(); + $related = new NoTouchingModelStub; $this->assertFalse($related::isIgnoringTouch()); - Model::withoutTouching(function () use ($related): void { + Model::withoutTouching(function () use ($connection, $related): void { $this->assertTrue($related::isIgnoringTouch()); - $builder = m::mock(Builder::class); - $parent = m::mock(Model::class); - - $parent->expects('getAttribute')->with('id')->andReturn(1); - $builder->expects('getModel')->andReturn($related); - $builder->expects('whereNotNull'); - $builder->expects('where'); - $relation = new HasOne($builder, $parent, 'foreign_key', 'id'); - $builder->shouldReceive('update')->never(); - - $relation->touch(); + $this->newHasOne($connection, $related, new NoTouchingModelStub)->touch(); }); + $this->assertNull($this->updatedAt($connection, 'table')); $this->assertFalse($related::isIgnoringTouch()); } public function testCanDisableTouchingForSpecificModel(): void { - $related = m::mock(NoTouchingModelStub::class)->makePartial(); - $related->shouldReceive('getUpdatedAtColumn')->never(); - $related->shouldReceive('freshTimestampString')->never(); - - $anotherRelated = m::mock(NoTouchingAnotherModelStub::class)->makePartial(); + $connection = $this->newConnection(); + $related = new NoTouchingModelStub; + $anotherRelated = new NoTouchingAnotherModelStub; $this->assertFalse($related::isIgnoringTouch()); $this->assertFalse($anotherRelated::isIgnoringTouch()); - NoTouchingModelStub::withoutTouching(function () use ($related, $anotherRelated): void { + NoTouchingModelStub::withoutTouching(function () use ($connection, $related, $anotherRelated): void { $this->assertTrue($related::isIgnoringTouch()); $this->assertFalse($anotherRelated::isIgnoringTouch()); - $builder = m::mock(Builder::class); - $parent = m::mock(Model::class); - - $parent->expects('getAttribute')->with('id')->andReturn(1); - $builder->expects('getModel')->andReturn($related); - $builder->expects('whereNotNull'); - $builder->expects('where'); - $relation = new HasOne($builder, $parent, 'foreign_key', 'id'); - $builder->shouldReceive('update')->never(); - - $relation->touch(); - - $anotherBuilder = m::mock(Builder::class); - $anotherParent = m::mock(Model::class); - - $anotherParent->expects('getAttribute')->with('id')->andReturn(2); - $anotherBuilder->expects('getModel')->andReturn($anotherRelated); - $anotherBuilder->expects('whereNotNull'); - $anotherBuilder->expects('where'); - $anotherBuilder->expects('withoutGlobalScopes')->andReturnSelf(); - $anotherRelation = new HasOne($anotherBuilder, $anotherParent, 'foreign_key', 'id'); - $now = CarbonImmutable::now()->toDateTimeString(); - $anotherRelated->expects('freshTimestampString')->andReturn($now); - $anotherBuilder->expects('update')->with(['updated_at' => $now])->andReturn(1); - - $anotherRelation->touch(); + $this->newHasOne($connection, $related, new NoTouchingModelStub)->touch(); + $this->newHasOne($connection, $anotherRelated, new NoTouchingAnotherModelStub)->touch(); }); + $this->assertNull($this->updatedAt($connection, 'table')); + $this->assertSame('2023-01-01 00:00:00', $this->updatedAt($connection, 'another_table')); $this->assertFalse($related::isIgnoringTouch()); $this->assertFalse($anotherRelated::isIgnoringTouch()); } public function testParentModelIsNotTouchedWhenChildModelIsIgnored(): void { - $related = m::mock(NoTouchingModelStub::class)->makePartial(); - $related->shouldReceive('getUpdatedAtColumn')->never(); - $related->shouldReceive('freshTimestampString')->never(); - - $relatedChild = m::mock(NoTouchingChildModelStub::class)->makePartial(); - $relatedChild->shouldReceive('getUpdatedAtColumn')->never(); - $relatedChild->shouldReceive('freshTimestampString')->never(); + $connection = $this->newConnection(); + $related = new NoTouchingModelStub; + $relatedChild = new NoTouchingChildModelStub; $this->assertFalse($related::isIgnoringTouch()); $this->assertFalse($relatedChild::isIgnoringTouch()); - NoTouchingModelStub::withoutTouching(function () use ($related, $relatedChild): void { + NoTouchingModelStub::withoutTouching(function () use ($connection, $related, $relatedChild): void { $this->assertTrue($related::isIgnoringTouch()); $this->assertTrue($relatedChild::isIgnoringTouch()); - $builder = m::mock(Builder::class); - $parent = m::mock(Model::class); - - $parent->expects('getAttribute')->with('id')->andReturn(1); - $builder->expects('getModel')->andReturn($related); - $builder->expects('whereNotNull'); - $builder->expects('where'); - $relation = new HasOne($builder, $parent, 'foreign_key', 'id'); - $builder->shouldReceive('update')->never(); - - $relation->touch(); - - $anotherBuilder = m::mock(Builder::class); - $anotherParent = m::mock(Model::class); - - $anotherParent->expects('getAttribute')->with('id')->andReturn(2); - $anotherBuilder->expects('getModel')->andReturn($relatedChild); - $anotherBuilder->expects('whereNotNull'); - $anotherBuilder->expects('where'); - $anotherRelation = new HasOne($anotherBuilder, $anotherParent, 'foreign_key', 'id'); - $anotherBuilder->shouldReceive('update')->never(); - - $anotherRelation->touch(); + $this->newHasOne($connection, $related, new NoTouchingModelStub)->touch(); + $this->newHasOne($connection, $relatedChild, new NoTouchingChildModelStub)->touch(); }); + $this->assertNull($this->updatedAt($connection, 'table')); $this->assertFalse($related::isIgnoringTouch()); $this->assertFalse($relatedChild::isIgnoringTouch()); } public function testIgnoredModelsStateIsResetWhenThereAreExceptions(): void { - $related = m::mock(NoTouchingModelStub::class)->makePartial(); - $related->shouldReceive('getUpdatedAtColumn')->never(); - $related->shouldReceive('freshTimestampString')->never(); + $related = new NoTouchingModelStub; - $relatedChild = m::mock(NoTouchingChildModelStub::class)->makePartial(); - $relatedChild->shouldReceive('getUpdatedAtColumn')->never(); - $relatedChild->shouldReceive('freshTimestampString')->never(); + $relatedChild = new NoTouchingChildModelStub; $this->assertFalse($related::isIgnoringTouch()); $this->assertFalse($relatedChild::isIgnoringTouch()); @@ -318,7 +246,7 @@ public function testMacroable(): void }); $model = new ResetModelStub; - $builder = (new Builder((new SQLiteConnection(new PDO('sqlite::memory:')))->query()))->setModel($model); + $builder = (new Builder($this->newConnection()->query()))->setModel($model); $relation = new RelationStub($builder, $model); $result = $relation->foo(); @@ -332,6 +260,40 @@ public function testIsRelationIgnoresAttribute(): void $this->assertTrue($model->isRelation('parent')); $this->assertFalse($model->isRelation('field')); } + + /** + * Create an in-memory connection with one row in each touch target table. + */ + protected function newConnection(): SQLiteConnection + { + CarbonImmutable::setTestNow('2023-01-01 00:00:00'); + + $pdo = new PDO('sqlite::memory:'); + $pdo->exec('create table "table" ("id" integer primary key, "foreign_key" integer, "updated_at" text)'); + $pdo->exec('create table "another_table" ("id" integer primary key, "foreign_key" integer, "updated_at" text)'); + $pdo->exec('insert into "table" ("id", "foreign_key") values (1, 1)'); + $pdo->exec('insert into "another_table" ("id", "foreign_key") values (1, 2)'); + + return new SQLiteConnection($pdo); + } + + /** + * Create a has one relationship querying the given connection. + */ + protected function newHasOne(SQLiteConnection $connection, Model $related, Model $parent): HasOne + { + $builder = (new Builder($connection->query()))->setModel($related); + + return new HasOne($builder, $parent, 'foreign_key', 'id'); + } + + /** + * Get the stored updated_at value of the given table. + */ + protected function updatedAt(SQLiteConnection $connection, string $table): ?string + { + return $connection->scalar('select "updated_at" from "' . $table . '"'); + } } class ResetModelStub extends Model @@ -376,6 +338,8 @@ class NoTouchingModelStub extends Model { protected ?string $table = 'table'; + protected ?string $dateFormat = 'Y-m-d H:i:s'; + protected array $attributes = [ 'id' => 1, ]; @@ -389,6 +353,8 @@ class NoTouchingAnotherModelStub extends Model { protected ?string $table = 'another_table'; + protected ?string $dateFormat = 'Y-m-d H:i:s'; + protected array $attributes = [ 'id' => 2, ]; diff --git a/tests/Database/DatabaseEloquentRelationshipsTest.php b/tests/Database/DatabaseEloquentRelationshipsTest.php index ad62e5effc..5f07368776 100644 --- a/tests/Database/DatabaseEloquentRelationshipsTest.php +++ b/tests/Database/DatabaseEloquentRelationshipsTest.php @@ -266,11 +266,9 @@ class MockedConnectionModel extends Model public function getConnection(): Connection { $mock = m::mock(Connection::class); - $grammar = m::mock(Grammar::class); + $grammar = new Grammar($mock); $mock->shouldReceive('getQueryGrammar')->andReturn($grammar); - $grammar->shouldReceive('getBitwiseOperators')->andReturn([]); - $grammar->shouldReceive('isExpression')->andReturn(false); - $processor = m::mock(Processor::class); + $processor = new Processor; $mock->shouldReceive('getPostProcessor')->andReturn($processor); $mock->shouldReceive('getName')->andReturn('name'); $mock->shouldReceive('query')->andReturnUsing(function () use ($mock, $grammar, $processor): BaseBuilder { diff --git a/tests/Database/DatabaseMigrationMigrateCommandTest.php b/tests/Database/DatabaseMigrationMigrateCommandTest.php index c17e4765a1..5e85fcf586 100755 --- a/tests/Database/DatabaseMigrationMigrateCommandTest.php +++ b/tests/Database/DatabaseMigrationMigrateCommandTest.php @@ -17,11 +17,14 @@ use Hypervel\Database\Migrations\Migrator; use Hypervel\Database\MySqlConnection; use Hypervel\Database\PostgresConnection; +use Hypervel\Database\Schema\MySqlSchemaState; use Hypervel\Database\Schema\SchemaState; use Hypervel\Database\SQLiteDatabaseDoesNotExistException; +use Hypervel\Events\Dispatcher as EventsDispatcher; use Hypervel\Foundation\Application; use Hypervel\Prompts\ConfirmPrompt; use Hypervel\Prompts\Prompt; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Tests\TestCase; use Mockery as m; use PDO; @@ -50,7 +53,7 @@ public function testBasicMigrationsCallMigratorWithProperArguments(): void $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $migrator = m::mock(Migrator::class); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new EventsDispatcher; $command = new MigrateCommand($migrator, $dispatcher); $command->setHypervel($app); $this->expectMigrationPreflight($migrator); @@ -66,25 +69,25 @@ public function testMigrationsCanBeRunWithStoredSchema(): void $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $migrator = m::mock(Migrator::class); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new EventFake(new EventsDispatcher); $command = new MigrateCommand($migrator, $dispatcher); $command->setHypervel($app); $this->expectMigrationPreflight($migrator); $migrator->expects('hasRunAnyMigrations')->andReturn(false); - $connection = m::mock(Connection::class); + $connection = m::mock(MySqlConnection::class); $migrator->expects('resolveConnection')->andReturn($connection); $connection->expects('getName')->andReturn('mysql'); $migrator->expects('deleteRepository'); - $schemaState = m::mock(SchemaState::class); + $schemaState = m::mock(MySqlSchemaState::class); $connection->expects('getSchemaState')->andReturn($schemaState); $schemaState->expects('handleOutputUsing')->andReturnSelf(); $schemaState->expects('load')->with(__DIR__ . '/Fixtures/schema.sql'); - $dispatcher->expects('hasListeners')->with(SchemaLoaded::class)->andReturnTrue(); - $dispatcher->expects('dispatch')->with(m::type(SchemaLoaded::class)); $migrator->expects('setOutput')->andReturn($migrator); $migrator->expects('run')->with([__DIR__ . DIRECTORY_SEPARATOR . 'migrations'], ['pretend' => false, 'step' => false]); $this->runCommand($command, ['--schema-path' => __DIR__ . '/Fixtures/schema.sql']); + + $dispatcher->assertDispatchedOnce(SchemaLoaded::class); } public function testStoredSchemaDoesNotDispatchWithoutListeners(): void @@ -117,7 +120,7 @@ public function testMigrationRepositoryCreatedWhenNecessary(): void $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $migrator = m::mock(Migrator::class); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new EventsDispatcher; $params = [$migrator, $dispatcher]; $command = $this->getMockBuilder(MigrateCommand::class)->onlyMethods(['callSilent'])->setConstructorArgs($params)->getMock(); $command->setHypervel($app); @@ -135,7 +138,7 @@ public function testTheCommandMayBePretended(): void $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $migrator = m::mock(Migrator::class); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new EventsDispatcher; $command = new MigrateCommand($migrator, $dispatcher); $command->setHypervel($app); $this->expectMigrationPreflight($migrator); @@ -151,7 +154,7 @@ public function testTheDatabaseMayBeSet(): void $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $migrator = m::mock(Migrator::class); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new EventsDispatcher; $command = new MigrateCommand($migrator, $dispatcher); $command->setHypervel($app); $this->expectMigrationPreflight($migrator, 'foo'); @@ -167,7 +170,7 @@ public function testStepMayBeSet(): void $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $migrator = m::mock(Migrator::class); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new EventsDispatcher; $command = new MigrateCommand($migrator, $dispatcher); $command->setHypervel($app); $this->expectMigrationPreflight($migrator); @@ -607,7 +610,6 @@ public function __construct(array $data = []) { $mutex = m::mock(CommandMutex::class); $mutex->shouldReceive('create')->andReturn(true); - $mutex->shouldReceive('release')->andReturn(true); $this->instance(CommandMutex::class, $mutex); $this->instance('env', 'development'); diff --git a/tests/Database/DatabaseMigratorTest.php b/tests/Database/DatabaseMigratorTest.php index 6be2fffd20..82b4dcae49 100644 --- a/tests/Database/DatabaseMigratorTest.php +++ b/tests/Database/DatabaseMigratorTest.php @@ -8,17 +8,21 @@ use Hypervel\Container\Container; use Hypervel\Context\CoroutineContext; use Hypervel\Database\Connection; +use Hypervel\Database\ConnectionInterface; use Hypervel\Database\ConnectionResolver; use Hypervel\Database\ConnectionResolverInterface as Resolver; use Hypervel\Database\Migrations\MigrationRepositoryInterface; use Hypervel\Database\Migrations\Migrator; +use Hypervel\Database\PdoConnection; use Hypervel\Filesystem\Filesystem; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; +use PDO; use PHPUnit\Framework\Attributes\DataProvider; use ReflectionClass; use RuntimeException; +use UnitEnum; class DatabaseMigratorTest extends TestCase { @@ -31,9 +35,8 @@ public function testResolveConnectionLeavesExplicitSuffixesUntouched(): void 'pgsql' => ['driver' => 'pgsql', 'migrations_connection' => 'pgsql-direct'], 'pgsql-direct' => ['driver' => 'pgsql'], ]); - $resolver = m::mock(Resolver::class); - $connection = m::mock(Connection::class); - $resolver->expects('connection')->with('pgsql::write')->andReturn($connection); + $resolver = new DatabaseMigratorTestResolver; + $resolver->connections['pgsql::write'] = $connection = new PdoConnection(new PDO('sqlite::memory:')); $this->assertSame($connection, $this->migrator($resolver)->resolveConnection('pgsql::write')); } @@ -41,17 +44,16 @@ public function testResolveConnectionLeavesExplicitSuffixesUntouched(): void public function testResolveConnectionPassesThroughWhenDirectConnectionIsNotConfigured(): void { $this->bindConfig(['sqlite' => ['driver' => 'sqlite']]); - $resolver = m::mock(Resolver::class); - $connection = m::mock(Connection::class); - $resolver->expects('connection')->with('sqlite')->andReturn($connection); + $resolver = new DatabaseMigratorTestResolver; + $resolver->connections['sqlite'] = $connection = new PdoConnection(new PDO('sqlite::memory:')); $this->assertSame($connection, $this->migrator($resolver)->resolveConnection('sqlite')); } public function testCustomConnectionResolverCallbackKeepsPriority(): void { - $resolver = m::mock(Resolver::class); - $connection = m::mock(Connection::class); + $resolver = new DatabaseMigratorTestResolver; + $connection = new PdoConnection(new PDO('sqlite::memory:')); Migrator::resolveConnectionsUsing(function (Resolver $resolver, ?string $name) use ($connection): Connection { $this->assertSame('pgsql', $name); @@ -65,16 +67,17 @@ public function testCustomConnectionResolverCallbackKeepsPriority(): void public function testSetConnectionNullPreservesDefaultConnectionBehaviorWithoutDirectConnection(): void { $this->bindConfig(['sqlite' => ['driver' => 'sqlite']], 'sqlite'); - $resolver = m::mock(Resolver::class); - $repository = m::mock(MigrationRepositoryInterface::class); - $repository->expects('setSource')->with('sqlite'); - $resolver->shouldNotReceive('setDefaultConnection'); + $resolver = new DatabaseMigratorTestResolver; + $repository = new DatabaseMigratorTestRepository; $migrator = $this->migrator($resolver, $repository); $migrator->setConnection(null); $this->assertSame('sqlite', $migrator->getConnection()); + $this->assertSame('sqlite', $repository->source); $this->assertSame('sqlite', CoroutineContext::get(ConnectionResolver::DEFAULT_CONNECTION_CONTEXT_KEY)); + // Only the coroutine default changes; the worker-shared resolver default is left alone. + $this->assertSame('pgsql', $resolver->getDefaultConnection()); } public function testRunMethodPreservesWriteConnectionName(): void @@ -771,7 +774,7 @@ public function testNestedUsingConnectionPreservesEachLevelsState(): void */ protected function migrator(Resolver $resolver, ?MigrationRepositoryInterface $repository = null): DatabaseMigratorTestMigrator { - return new DatabaseMigratorTestMigrator($repository ?? m::mock(MigrationRepositoryInterface::class), $resolver, new Filesystem); + return new DatabaseMigratorTestMigrator($repository ?? new DatabaseMigratorTestRepository, $resolver, new Filesystem); } /** @@ -799,3 +802,131 @@ public function runMethodPublic(Connection $connection, object $migration, strin $this->runMethod($connection, $migration, $method); } } + +class DatabaseMigratorTestResolver implements Resolver +{ + public ?string $default = 'pgsql'; + + public array $connections = []; + + /** + * Get a database connection instance. + */ + public function connection(UnitEnum|string|null $name = null): ConnectionInterface + { + return $this->connections[$name ?? $this->default]; + } + + /** + * Get the default connection name. + */ + public function getDefaultConnection(): ?string + { + return $this->default; + } + + /** + * Set the default connection name. + */ + public function setDefaultConnection(?string $name): void + { + $this->default = $name; + } +} + +class DatabaseMigratorTestRepository implements MigrationRepositoryInterface +{ + public ?string $source = null; + + /** + * Get the completed migrations. + */ + public function getRan(): array + { + return []; + } + + /** + * Get the list of migrations. + */ + public function getMigrations(int $steps): array + { + return []; + } + + /** + * Get the list of the migrations by batch. + */ + public function getMigrationsByBatch(int $batch): array + { + return []; + } + + /** + * Get the last migration batch. + */ + public function getLast(): array + { + return []; + } + + /** + * Get the completed migrations with their batch numbers. + */ + public function getMigrationBatches(): array + { + return []; + } + + /** + * Log that a migration was run. + */ + public function log(string $file, int $batch): void + { + } + + /** + * Remove a migration from the log. + */ + public function delete(object $migration): void + { + } + + /** + * Get the next migration batch number. + */ + public function getNextBatchNumber(): int + { + return 1; + } + + /** + * Create the migration repository data store. + */ + public function createRepository(): void + { + } + + /** + * Determine if the migration repository exists. + */ + public function repositoryExists(): bool + { + return true; + } + + /** + * Delete the migration repository data store. + */ + public function deleteRepository(): void + { + } + + /** + * Set the information source to gather data. + */ + public function setSource(?string $name): void + { + $this->source = $name; + } +} diff --git a/tests/Database/DatabaseMySqlBuilderTest.php b/tests/Database/DatabaseMySqlBuilderTest.php index 63f4371867..f735ce4ee2 100644 --- a/tests/Database/DatabaseMySqlBuilderTest.php +++ b/tests/Database/DatabaseMySqlBuilderTest.php @@ -50,7 +50,7 @@ public function testDropDatabaseIfExists(): void public function testDeleteWithJoinCompilesOrderByAndLimit(): void { $connection = m::mock(Connection::class); - $processor = m::mock(Processor::class); + $processor = new Processor; $grammar = new MySqlGrammar($connection); $connection->expects('getTablePrefix')->times(5)->andReturn(''); diff --git a/tests/Database/DatabaseMySqlQueryGrammarTest.php b/tests/Database/DatabaseMySqlQueryGrammarTest.php index 015aa7d6dd..787ef7d232 100755 --- a/tests/Database/DatabaseMySqlQueryGrammarTest.php +++ b/tests/Database/DatabaseMySqlQueryGrammarTest.php @@ -6,6 +6,7 @@ use Hypervel\Database\Connection; use Hypervel\Database\MySqlConnection; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder; use Hypervel\Database\Query\Grammars\MySqlGrammar; use Hypervel\Database\Query\Processors\Processor; @@ -195,14 +196,12 @@ public function testTimeoutThrowsExceptionForNegativeValue(): void */ protected function getBuilder(): Builder { - $connection = m::mock(Connection::class); - $connection->shouldReceive('getDatabaseName')->andReturn('database'); - $connection->shouldReceive('getTablePrefix')->andReturn(''); + $connection = new PdoConnection(new PDO('sqlite::memory:'), 'database'); return new Builder( $connection, new MySqlGrammar($connection), - m::mock(Processor::class) + new Processor ); } } diff --git a/tests/Database/DatabasePostgresQueryGrammarTest.php b/tests/Database/DatabasePostgresQueryGrammarTest.php index c035ebe02d..2b3cb9dd2b 100755 --- a/tests/Database/DatabasePostgresQueryGrammarTest.php +++ b/tests/Database/DatabasePostgresQueryGrammarTest.php @@ -5,11 +5,14 @@ namespace Hypervel\Tests\Database; use Hypervel\Database\Connection; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder; use Hypervel\Database\Query\Grammars\PostgresGrammar; +use Hypervel\Database\Query\Processors\Processor; use Hypervel\Tests\TestCase; use JsonException; use Mockery as m; +use PDO; class DatabasePostgresQueryGrammarTest extends TestCase { @@ -54,7 +57,7 @@ public function testCustomOperators(): void PostgresGrammar::customOperators(['@@@', '@>', '']); PostgresGrammar::customOperators(['@@>', 1]); - $connection = m::mock(Connection::class); + $connection = new PdoConnection(new PDO('sqlite::memory:')); $grammar = new PostgresGrammar($connection); $operators = $grammar->getOperators(); @@ -73,7 +76,7 @@ public function testCompileTruncate(): void $connection->expects('getTablePrefix')->times(3)->andReturn(''); $postgres = new PostgresGrammar($connection); - $builder = m::mock(Builder::class); + $builder = new Builder($connection, $postgres, new Processor); $builder->from = 'users'; $this->assertEquals([ diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index f886870cca..e6914ebb24 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -4785,7 +4785,7 @@ public function testFindReturnsFirstResultByID(): void public function testFindOrReturnsFirstResultByID(): void { $builder = $this->getMockQueryBuilder(); - $data = m::mock(stdClass::class); + $data = new stdClass; $builder->expects('get')->with(['*'])->andReturn(new Collection([$data])); $builder->expects('get')->with(['column'])->andReturn(new Collection([$data])); $builder->expects('get')->with(['*'])->andReturn(new Collection); @@ -5804,11 +5804,11 @@ public function testUpdateMethodWorksWithQueryAsValue(): void public function testUpdateOrInsertMethod(): void { - $connection = m::mock(Connection::class); + $connection = new PdoConnection(new PDO('sqlite::memory:')); $builder = m::mock(Builder::class . '[where,exists,insert]', [ $connection, new Grammar($connection), - m::mock(Processor::class), + new Processor, ]); $builder->expects('where')->with(['email' => 'foo'])->andReturn(m::self()); @@ -5817,11 +5817,11 @@ public function testUpdateOrInsertMethod(): void $this->assertTrue($builder->updateOrInsert(['email' => 'foo'], ['name' => 'bar'])); - $connection = m::mock(Connection::class); + $connection = new PdoConnection(new PDO('sqlite::memory:')); $builder = m::mock(Builder::class . '[where,exists,update]', [ $connection, new Grammar($connection), - m::mock(Processor::class), + new Processor, ]); $builder->expects('where')->with(['email' => 'foo'])->andReturn(m::self()); @@ -5878,11 +5878,11 @@ public function __invoke(bool $exists): array public function testUpdateOrInsertMethodWorksWithEmptyUpdateValues(): void { - $connection = m::mock(Connection::class); + $connection = new PdoConnection(new PDO('sqlite::memory:')); $builder = m::spy(Builder::class . '[where,exists,update]', [ $connection, new Grammar($connection), - m::mock(Processor::class), + new Processor, ]); $builder->expects('where')->with(['email' => 'foo'])->andReturn(m::self()); @@ -6187,7 +6187,7 @@ public function testMySqlUpdateWrappingJson(): void { $connection = $this->createMock(Connection::class); $grammar = new MySqlGrammar($connection); - $processor = m::mock(Processor::class); + $processor = new Processor; $connection->expects($this->once()) ->method('update') @@ -6205,7 +6205,7 @@ public function testMySqlUpdateWrappingNestedJson(): void { $connection = $this->createMock(Connection::class); $grammar = new MySqlGrammar($connection); - $processor = m::mock(Processor::class); + $processor = new Processor; $connection->expects($this->once()) ->method('update') @@ -6223,7 +6223,7 @@ public function testMySqlUpdateWrappingJsonArray(): void { $connection = $this->createMock(Connection::class); $grammar = new MySqlGrammar($connection); - $processor = m::mock(Processor::class); + $processor = new Processor; $connection->expects($this->once()) ->method('update') @@ -6250,7 +6250,7 @@ public function testMySqlUpdateWrappingJsonPathArrayIndex(): void { $connection = $this->createMock(Connection::class); $grammar = new MySqlGrammar($connection); - $processor = m::mock(Processor::class); + $processor = new Processor; $connection->expects($this->once()) ->method('update') @@ -6273,7 +6273,7 @@ public function testMySqlUpdateWithJsonPreparesBindingsCorrectly(): void { $connection = $this->getConnection(connectionClass: MySqlConnection::class); $grammar = new MySqlGrammar($connection); - $processor = m::mock(Processor::class); + $processor = new Processor; $connection->expects('update') ->with( @@ -7008,14 +7008,13 @@ public function testChunkWithLastChunkComplete(): void $builder->expects('limit')->times(3)->with(2)->andReturnSelf(); $builder->expects('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk3); + $seen = []; - $builder->chunk(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunk(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }); + + $this->assertSame([$chunk1, $chunk2], $seen); } public function testChunkWithLastChunkPartial(): void @@ -7033,13 +7032,13 @@ public function testChunkWithLastChunkPartial(): void $builder->expects('limit')->times(2)->with(2)->andReturnSelf(); $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); + $seen = []; - $builder->chunk(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunk(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }); + + $this->assertSame([$chunk1, $chunk2], $seen); } public function testChunkCanBeStoppedByReturningFalse(): void @@ -7055,15 +7054,15 @@ public function testChunkCanBeStoppedByReturningFalse(): void $builder->expects('limit')->with(2)->andReturnSelf(); $builder->expects('get')->andReturn($chunk1); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk2); + $seen = []; - $builder->chunk(2, function (Collection $results) use ($callbackAssertor): false { - $callbackAssertor->doSomething($results); + $builder->chunk(2, function (Collection $results) use (&$seen): false { + $seen[] = $results; return false; }); + + $this->assertSame([$chunk1], $seen); } public function testChunkWithCountZero(): void @@ -7102,14 +7101,13 @@ public function testChunkByIdOnArrays(): void $builder->expects('forPageAfterId')->with(2, 11, 'someIdField')->andReturnSelf(); $builder->expects('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk3); + $seen = []; - $builder->chunkById(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunkById(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }, 'someIdField'); + + $this->assertSame([$chunk1, $chunk2], $seen); } public function testChunkPaginatesUsingIdWithLastChunkComplete(): void @@ -7125,14 +7123,13 @@ public function testChunkPaginatesUsingIdWithLastChunkComplete(): void $builder->expects('forPageAfterId')->with(2, 11, 'someIdField')->andReturnSelf(); $builder->expects('get')->times(3)->andReturn($chunk1, $chunk2, $chunk3); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk3); + $seen = []; - $builder->chunkById(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunkById(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }, 'someIdField'); + + $this->assertSame([$chunk1, $chunk2], $seen); } public function testChunkPaginatesUsingIdWithLastChunkPartial(): void @@ -7146,13 +7143,13 @@ public function testChunkPaginatesUsingIdWithLastChunkPartial(): void $builder->expects('forPageAfterId')->with(2, 2, 'someIdField')->andReturnSelf(); $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->expects('doSomething')->with($chunk2); + $seen = []; - $builder->chunkById(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunkById(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }, 'someIdField'); + + $this->assertSame([$chunk1, $chunk2], $seen); } public function testChunkPaginatesUsingIdWithCountZero(): void @@ -7188,13 +7185,13 @@ public function testChunkPaginatesUsingIdWithAlias(): void $builder->expects('forPageAfterId')->with(2, 10, 'table.id')->andReturnSelf(); $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk2); + $seen = []; - $builder->chunkById(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunkById(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }, 'table.id', 'table_id'); + + $this->assertSame([$chunk1], $seen); } public function testOrderedChunkByIdAcceptsAscendingSortDirection(): void @@ -7226,13 +7223,13 @@ public function testChunkPaginatesUsingIdDesc(): void $builder->expects('forPageBeforeId')->with(2, 1, 'someIdField')->andReturnSelf(); $builder->expects('get')->times(2)->andReturn($chunk1, $chunk2); - $callbackAssertor = m::mock(stdClass::class); - $callbackAssertor->expects('doSomething')->with($chunk1); - $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk2); + $seen = []; - $builder->chunkByIdDesc(2, function (Collection $results) use ($callbackAssertor): void { - $callbackAssertor->doSomething($results); + $builder->chunkByIdDesc(2, function (Collection $results) use (&$seen): void { + $seen[] = $results; }, 'someIdField'); + + $this->assertSame([$chunk1], $seen); } public function testOrderedLazyByIdAcceptsAscendingSortDirection(): void @@ -8778,7 +8775,7 @@ public function testToRawSql(): void $grammar->expects('substituteBindingsIntoRawSql') ->with('select * from "users" where "email" = ?', ['foo']) ->andReturn('select * from "users" where "email" = \'foo\''); - $builder = new Builder($connection, $grammar, m::mock(Processor::class)); + $builder = new Builder($connection, $grammar, new Processor); $builder->select('*')->from('users')->where('email', 'foo'); $this->assertSame('select * from "users" where "email" = \'foo\'', $builder->toRawSql()); @@ -8990,7 +8987,7 @@ protected function getMockQueryBuilder(array $methods = []): Builder&m\MockInter return m::mock(Builder::class . ($methods ? '[' . implode(',', $methods) . ']' : ''), [ $connection = $this->getConnection(), new Grammar($connection), - m::mock(Processor::class), + new Processor, ])->makePartial(); } } diff --git a/tests/Database/DatabaseQueryGrammarTest.php b/tests/Database/DatabaseQueryGrammarTest.php index f6cd4b4518..d239071b10 100644 --- a/tests/Database/DatabaseQueryGrammarTest.php +++ b/tests/Database/DatabaseQueryGrammarTest.php @@ -6,6 +6,7 @@ use Hypervel\Database\BinaryParameter; use Hypervel\Database\Connection; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder; use Hypervel\Database\Query\Expression; use Hypervel\Database\Query\Grammars\Grammar; @@ -52,8 +53,9 @@ public function testQualifiedColumnsPrefixOnlyTheTableSegment(): void public function testWhereRawReturnsStringWhenExpressionPassed(): void { - $builder = m::mock(Builder::class); - $grammar = new Grammar(m::mock(Connection::class)); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $grammar = new Grammar($connection); + $builder = new Builder($connection, $grammar); $reflection = new ReflectionClass($grammar); $method = $reflection->getMethod('whereRaw'); $expressionArray = ['sql' => new Expression('select * from "users"')]; @@ -65,8 +67,9 @@ public function testWhereRawReturnsStringWhenExpressionPassed(): void public function testWhereRawReturnsStringWhenStringPassed(): void { - $builder = m::mock(Builder::class); - $grammar = new Grammar(m::mock(Connection::class)); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $grammar = new Grammar($connection); + $builder = new Builder($connection, $grammar); $reflection = new ReflectionClass($grammar); $method = $reflection->getMethod('whereRaw'); $stringArray = ['sql' => 'select * from "users"']; @@ -78,11 +81,9 @@ public function testWhereRawReturnsStringWhenStringPassed(): void public function testCompileOrdersAcceptsExpression(): void { - $builder = m::mock(Builder::class); - $grammar = new Grammar(m::mock(Connection::class)); - - // compileOrders() calls $query->getGrammar() → return our $grammar - $builder->expects('getGrammar')->andReturn($grammar); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $grammar = new Grammar($connection); + $builder = new Builder($connection, $grammar); $orders = [ ['sql' => new Expression('length("name") desc')], // mimics orderByRaw(DB::raw(...)) @@ -97,9 +98,9 @@ public function testCompileOrdersAcceptsExpression(): void public function testCompileOrdersAcceptsExpressionWithPlaceholders(): void { - $builder = m::mock(Builder::class); - $grammar = new Grammar(m::mock(Connection::class)); - $builder->expects('getGrammar')->andReturn($grammar); + $connection = new PdoConnection(new PDO('sqlite::memory:')); + $grammar = new Grammar($connection); + $builder = new Builder($connection, $grammar); $orders = [ ['sql' => new Expression('field(status, ?, ?) asc')], diff --git a/tests/Database/DatabaseSeederTest.php b/tests/Database/DatabaseSeederTest.php index 37500243d9..e3624b715a 100755 --- a/tests/Database/DatabaseSeederTest.php +++ b/tests/Database/DatabaseSeederTest.php @@ -167,14 +167,14 @@ public function testCallResolveTheClassAndCallsRun(): void public function testSetContainer(): void { $seeder = new TestSeeder; - $container = m::mock(Container::class); + $container = new ContainerImplementation; $this->assertEquals($seeder->setContainer($container), $seeder); } public function testSetCommand(): void { $seeder = new TestSeeder; - $command = m::mock(Command::class); + $command = new Command; $this->assertEquals($seeder->setCommand($command), $seeder); } diff --git a/tests/Database/DatabaseSoftDeletingScopeTest.php b/tests/Database/DatabaseSoftDeletingScopeTest.php index 59017cdc3c..14b5a57497 100644 --- a/tests/Database/DatabaseSoftDeletingScopeTest.php +++ b/tests/Database/DatabaseSoftDeletingScopeTest.php @@ -4,56 +4,47 @@ namespace Hypervel\Tests\Database; -use Hypervel\Database\ConnectionInterface; use Hypervel\Database\Eloquent\Builder as EloquentBuilder; use Hypervel\Database\Eloquent\Model; +use Hypervel\Database\Eloquent\SoftDeletes; use Hypervel\Database\Eloquent\SoftDeletingScope; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as BaseBuilder; use Hypervel\Database\Query\Grammars\Grammar; use Hypervel\Database\Query\Processors\Processor; +use Hypervel\Database\SQLiteConnection; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; class DatabaseSoftDeletingScopeTest extends TestCase { public function testApplyingScopeToABuilder(): void { - $scope = m::mock(SoftDeletingScope::class . '[extend]'); - $builder = m::mock(EloquentBuilder::class); - $model = m::mock(Model::class); - $model->expects('getQualifiedDeletedAtColumn')->andReturn('table.deleted_at'); - $builder->expects('qualifyColumn')->with('table.deleted_at')->andReturn('table.deleted_at'); - $builder->expects('whereNull')->with('table.deleted_at'); + $builder = $this->newBuilder(scoped: false); + + (new SoftDeletingScope)->apply($builder, $builder->getModel()); - $scope->apply($builder, $model); + $this->assertSame('select * from "users" where "users"."deleted_at" is null', $builder->toSql()); } public function testRestoreExtension(): void { - $builder = new EloquentBuilder(new BaseBuilder( - m::mock(ConnectionInterface::class), - m::mock(Grammar::class), - m::mock(Processor::class) - )); - $scope = new SoftDeletingScope; - $scope->extend($builder); - $callback = $builder->getMacro('restore'); - $givenBuilder = m::mock(EloquentBuilder::class); - $givenBuilder->expects('withTrashed'); - $model = m::mock(Model::class); - $givenBuilder->expects('getModel')->andReturn($model); - $model->expects('getDeletedAtColumn')->andReturn('deleted_at'); - $givenBuilder->expects('update')->with(['deleted_at' => null]); + $connection = $this->newConnection(); + $builder = $this->newBuilder($connection); + + $builder->restore(); - $callback($givenBuilder); + $this->assertSame(0, $connection->table('users')->whereNotNull('deleted_at')->count()); } public function testRestoreOrCreateExtension(): void { + $connection = new PdoConnection(new PDO('sqlite::memory:')); $builder = new EloquentBuilder(new BaseBuilder( - m::mock(ConnectionInterface::class), - m::mock(Grammar::class), - m::mock(Processor::class) + $connection, + new Grammar($connection), + new Processor )); $scope = new SoftDeletingScope; @@ -73,10 +64,11 @@ public function testRestoreOrCreateExtension(): void public function testCreateOrRestoreExtension(): void { + $connection = new PdoConnection(new PDO('sqlite::memory:')); $builder = new EloquentBuilder(new BaseBuilder( - m::mock(ConnectionInterface::class), - m::mock(Grammar::class), - m::mock(Processor::class) + $connection, + new Grammar($connection), + new Processor )); $scope = new SoftDeletingScope; @@ -96,62 +88,63 @@ public function testCreateOrRestoreExtension(): void public function testWithTrashedExtension(): void { - $builder = new EloquentBuilder(new BaseBuilder( - m::mock(ConnectionInterface::class), - m::mock(Grammar::class), - m::mock(Processor::class) - )); - $scope = m::mock(SoftDeletingScope::class . '[remove]'); - $scope->extend($builder); - $callback = $builder->getMacro('withTrashed'); - $givenBuilder = m::mock(EloquentBuilder::class); - $givenBuilder->expects('withoutGlobalScope')->with($scope)->andReturn($givenBuilder); - $result = $callback($givenBuilder); + $builder = $this->newBuilder(); - $this->assertEquals($givenBuilder, $result); + // Hypervel always groups the constraints added by a global scope. + $this->assertSame('select * from "users" where ("users"."deleted_at" is null)', $builder->toSql()); + $this->assertSame('select * from "users"', $builder->withTrashed()->toSql()); } public function testOnlyTrashedExtension(): void { - $builder = new EloquentBuilder(new BaseBuilder( - m::mock(ConnectionInterface::class), - m::mock(Grammar::class), - m::mock(Processor::class) - )); - $model = m::mock(Model::class)->makePartial(); - $scope = m::mock(SoftDeletingScope::class . '[remove]'); - $scope->extend($builder); - $callback = $builder->getMacro('onlyTrashed'); - $givenBuilder = m::mock(EloquentBuilder::class); - $givenBuilder->expects('getModel')->andReturn($model); - $givenBuilder->expects('withoutGlobalScope')->with($scope)->andReturn($givenBuilder); - $model->expects('getQualifiedDeletedAtColumn')->andReturn('table.deleted_at'); - $givenBuilder->expects('qualifyColumn')->with('table.deleted_at')->andReturn('table.deleted_at'); - $givenBuilder->expects('whereNotNull')->with('table.deleted_at'); - $result = $callback($givenBuilder); - - $this->assertEquals($givenBuilder, $result); + $builder = $this->newBuilder(); + + $this->assertSame('select * from "users" where "users"."deleted_at" is not null', $builder->onlyTrashed()->toSql()); } public function testWithoutTrashedExtension(): void { - $builder = new EloquentBuilder(new BaseBuilder( - m::mock(ConnectionInterface::class), - m::mock(Grammar::class), - m::mock(Processor::class) - )); - $model = m::mock(Model::class)->makePartial(); - $scope = m::mock(SoftDeletingScope::class . '[remove]'); - $scope->extend($builder); - $callback = $builder->getMacro('withoutTrashed'); - $givenBuilder = m::mock(EloquentBuilder::class); - $givenBuilder->expects('getModel')->andReturn($model); - $givenBuilder->expects('withoutGlobalScope')->with($scope)->andReturn($givenBuilder); - $model->expects('getQualifiedDeletedAtColumn')->andReturn('table.deleted_at'); - $givenBuilder->expects('qualifyColumn')->with('table.deleted_at')->andReturn('table.deleted_at'); - $givenBuilder->expects('whereNull')->with('table.deleted_at'); - $result = $callback($givenBuilder); - - $this->assertEquals($givenBuilder, $result); + $builder = $this->newBuilder(); + + $this->assertSame('select * from "users" where "users"."deleted_at" is null', $builder->withoutTrashed()->toSql()); } + + /** + * Create an in-memory connection with one trashed and one active user. + */ + protected function newConnection(): SQLiteConnection + { + $pdo = new PDO('sqlite::memory:'); + $pdo->exec('create table "users" ("id" integer primary key, "deleted_at" text, "updated_at" text)'); + $pdo->exec("insert into \"users\" values (1, '2023-01-01 00:00:00', null)"); + $pdo->exec('insert into "users" values (2, null, null)'); + + return new SQLiteConnection($pdo); + } + + /** + * Create a soft deleting model builder, optionally with the scope and its extensions applied. + */ + protected function newBuilder(?SQLiteConnection $connection = null, bool $scoped = true): EloquentBuilder + { + $builder = (new EloquentBuilder(($connection ?? new SQLiteConnection(new PDO('sqlite::memory:')))->query())) + ->setModel(new SoftDeletingScopeModelStub); + + if ($scoped) { + $scope = new SoftDeletingScope; + $builder->withGlobalScope(SoftDeletingScope::class, $scope); + $scope->extend($builder); + } + + return $builder; + } +} + +class SoftDeletingScopeModelStub extends Model +{ + use SoftDeletes; + + protected ?string $table = 'users'; + + protected ?string $dateFormat = 'Y-m-d H:i:s'; } diff --git a/tests/Database/EloquentHasOneOrManyDeprecationTest.php b/tests/Database/EloquentHasOneOrManyDeprecationTest.php index f06bf3f66f..c32d624855 100644 --- a/tests/Database/EloquentHasOneOrManyDeprecationTest.php +++ b/tests/Database/EloquentHasOneOrManyDeprecationTest.php @@ -9,9 +9,12 @@ use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Eloquent\Relations\HasMany; use Hypervel\Database\Eloquent\Relations\HasOne; +use Hypervel\Database\PdoConnection; use Hypervel\Database\Query\Builder as QueryBuilder; +use Hypervel\Database\Query\Grammars\Grammar; +use Hypervel\Database\Query\Processors\Processor; use Hypervel\Tests\TestCase; -use Mockery as m; +use PDO; class EloquentHasOneOrManyDeprecationTest extends TestCase { @@ -30,10 +33,6 @@ public function testHasManyMatchWithNullLocalKey(): void $model2 = new HasOneOrManyDeprecationModelStub; $model2->id = null; - $relation->getRelated()->expects('newCollection')->andReturnUsing(function (array $array): Collection { - return new Collection($array); - }); - $models = $relation->match([$model1, $model2], new Collection([$result1, $result2]), 'foo'); $this->assertCount(1, $models[0]->foo); @@ -74,37 +73,40 @@ public function testHasManyMatchWithNullForeignKey(): void } /** - * Create a has-many relation with mocked query constraints. + * Create a has-many relation. */ protected function getHasManyRelation(): HasMany { - $queryBuilder = m::mock(QueryBuilder::class); - $builder = m::mock(Builder::class, [$queryBuilder]); - $builder->expects('whereNotNull')->with('table.foreign_key'); - $builder->expects('where')->with('table.foreign_key', '=', 1); - $related = m::mock(Model::class); - $builder->expects('getModel')->andReturn($related); - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('id')->andReturn(1); - - return new HasMany($builder, $parent, 'table.foreign_key', 'id'); + return new HasMany($this->newBuilder(), $this->newParent(), 'table.foreign_key', 'id'); } /** - * Create a has-one relation with mocked query constraints. + * Create a has-one relation. */ protected function getHasOneRelation(): HasOne { - $queryBuilder = m::mock(QueryBuilder::class); - $builder = m::mock(Builder::class, [$queryBuilder]); - $builder->expects('whereNotNull')->with('table.foreign_key'); - $builder->expects('where')->with('table.foreign_key', '=', 1); - $related = m::mock(Model::class); - $builder->expects('getModel')->andReturn($related); - $parent = m::mock(Model::class); - $parent->expects('getAttribute')->with('id')->andReturn(1); - - return new HasOne($builder, $parent, 'table.foreign_key', 'id'); + return new HasOne($this->newBuilder(), $this->newParent(), 'table.foreign_key', 'id'); + } + + /** + * Create an Eloquent builder for the related stub model. + */ + protected function newBuilder(): Builder + { + $connection = new PdoConnection(new PDO('sqlite::memory:')); + + return (new Builder(new QueryBuilder($connection, new Grammar($connection), new Processor)))->setModel(new HasOneOrManyDeprecationModelStub); + } + + /** + * Create the parent model. + */ + protected function newParent(): Model + { + $parent = new HasOneOrManyDeprecationModelStub; + $parent->id = 1; + + return $parent; } } diff --git a/tests/Database/SeedCommandTest.php b/tests/Database/SeedCommandTest.php index 346e9ea7e7..cd14affdba 100644 --- a/tests/Database/SeedCommandTest.php +++ b/tests/Database/SeedCommandTest.php @@ -7,13 +7,13 @@ use Hypervel\Console\Command; use Hypervel\Console\CommandMutex; use Hypervel\Context\CoroutineContext; -use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Database\ConnectionResolver; use Hypervel\Database\ConnectionResolverInterface; use Hypervel\Database\Console\Seeds\SeedCommand; use Hypervel\Database\Console\Seeds\WithoutModelEvents; use Hypervel\Database\Eloquent\Model; use Hypervel\Database\Seeder; +use Hypervel\Events\Dispatcher; use Hypervel\Events\NullDispatcher; use Hypervel\Foundation\Application; use Hypervel\Testing\Assert; @@ -225,7 +225,7 @@ public function testWithoutModelEvents(): void UserWithoutModelEventsSeeder::class => $seeder, ]); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new Dispatcher; Model::setEventDispatcher($dispatcher); $command = new SeedCommand($resolver); diff --git a/tests/Events/BroadcastedEventsTest.php b/tests/Events/BroadcastedEventsTest.php index b5aa48bf06..b56061de7b 100644 --- a/tests/Events/BroadcastedEventsTest.php +++ b/tests/Events/BroadcastedEventsTest.php @@ -16,9 +16,25 @@ class BroadcastedEventsTest extends TestCase { + /** + * Create a dispatcher that exposes the protected broadcast check. + */ + protected function exposedDispatcher(): Dispatcher + { + return new class extends Dispatcher { + /** + * Determine if the payload has a broadcastable event. + */ + public function shouldBroadcast(array $payload): bool + { + return parent::shouldBroadcast($payload); + } + }; + } + public function testShouldBroadcastSuccess(): void { - $d = m::mock(Dispatcher::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $d = $this->exposedDispatcher(); $event = new BroadcastEvent; @@ -56,7 +72,7 @@ public function testHasListenersReturnsFalseForBroadcastEventsWithoutNormalListe public function testShouldBroadcastFail(): void { - $d = m::mock(Dispatcher::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $d = $this->exposedDispatcher(); $event = new BroadcastFalseCondition; diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index 10683a3515..331e51405c 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -17,7 +17,6 @@ use Hypervel\Contracts\Cache\Repository as Cache; use Hypervel\Contracts\Cache\Store as CacheStore; use Hypervel\Contracts\Queue\Factory as QueueFactory; -use Hypervel\Contracts\Queue\Job; use Hypervel\Contracts\Queue\Queue; use Hypervel\Contracts\Queue\ShouldBeUnique; use Hypervel\Contracts\Queue\ShouldBeUniqueUntilProcessing; @@ -29,6 +28,7 @@ use Hypervel\Queue\Attributes\Delay; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\QueueManager; use Hypervel\Queue\QueueRoutes; use Hypervel\Support\CarbonImmutable; @@ -43,18 +43,17 @@ class QueuedEventsTest extends TestCase public function testQueuedEventHandlersAreQueued(): void { $d = new Dispatcher; - $factory = m::mock(QueueFactory::class); - $queue = m::mock(Queue::class); - $factory->expects('connection')->with(null)->andReturn($queue); - $queue->expects('pushOn')->with(null, m::type(CallQueuedListener::class)); + $fakeQueue = new QueueFake(new Container); - $d->setQueueResolver(function () use ($factory) { - return $factory; + $d->setQueueResolver(function () use ($fakeQueue): QueueFake { + return $fakeQueue; }); $d->listen('some.event', TestDispatcherQueuedHandler::class . '@someMethod'); $d->dispatch('some.event', ['foo', 'bar']); + + $fakeQueue->assertPushedOn(null, CallQueuedListener::class); } public function testQueuedEnumEventsRemainSerializable(): void @@ -703,14 +702,12 @@ public function testUniqueLockIsReleasedOnProcessingWithListenerClassName(): voi ->andReturn($lock); $lock->expects('forceRelease'); - $job = m::mock(Job::class); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $handler = new CallQueuedHandler(new BusDispatcher($container), $container); $handler->call($job, ['command' => serialize($listener)]); + + $this->assertTrue($job->isDeleted()); } public function testUniqueUntilProcessingLockIsReleasedBeforeHandling(): void @@ -733,16 +730,12 @@ public function testUniqueUntilProcessingLockIsReleasedBeforeHandling(): void $this->assertTrue($cache->lock($expectedKey, 10)->get()); - $job = m::mock(Job::class); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('attempts')->andReturn(1); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $handler = new CallQueuedHandler(new BusDispatcher($container), $container); $handler->call($job, ['command' => serialize($listener)]); + $this->assertTrue($job->isDeleted()); $this->assertTrue(TestDispatcherShouldBeUniqueUntilProcessing::$lockReleasedBeforeHandling); // A replacement dispatch's lock must survive the first listener's cleanup. diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index eff1ffcbfe..9ea3721d3f 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -16,6 +16,7 @@ use Hypervel\Filesystem\FilesystemManager; use Hypervel\Filesystem\LocalFilesystemAdapter as HypervelLocalFilesystemAdapter; use Hypervel\Foundation\Application; +use Hypervel\Foundation\Exceptions\Handler; use Hypervel\Http\IterableStreamedResponse; use Hypervel\Http\Request; use Hypervel\Http\Response; @@ -24,6 +25,7 @@ use Hypervel\Image\ImageException; use Hypervel\Support\CarbonImmutable; use Hypervel\Support\Json; +use Hypervel\Support\Testing\Fakes\ExceptionHandlerFake; use Hypervel\Testbench\TestCase; use Hypervel\Testing\ParallelTesting; use InvalidArgumentException; @@ -1331,15 +1333,7 @@ public function testReportExceptionsForGet(): void { $container = Container::getInstance(); - $exceptionHandler = m::mock(ExceptionHandler::class); - - $exceptionHandler->expects('report') - ->andReturnUsing(function (UnableToReadFile $exception): void { - $this->assertStringContainsString( - 'Unable to read file from location: foo.txt.', - $exception->getMessage(), - ); - }); + $exceptionHandler = new ExceptionHandlerFake(new Handler($container)); $container->bind(ExceptionHandler::class, function () use ($exceptionHandler): ExceptionHandler { return $exceptionHandler; @@ -1352,21 +1346,15 @@ public function testReportExceptionsForGet(): void } catch (UnableToReadFile) { $this->fail('Exception was thrown.'); } + + $exceptionHandler->assertReported(fn (UnableToReadFile $exception): bool => str_contains($exception->getMessage(), 'Unable to read file from location: foo.txt.')); } public function testReportExceptionsForReadStream(): void { $container = Container::getInstance(); - $exceptionHandler = m::mock(ExceptionHandler::class); - - $exceptionHandler->expects('report') - ->andReturnUsing(function (UnableToReadFile $exception): void { - $this->assertStringContainsString( - 'Unable to read file from location: foo.txt.', - $exception->getMessage(), - ); - }); + $exceptionHandler = new ExceptionHandlerFake(new Handler($container)); $container->bind(ExceptionHandler::class, function () use ($exceptionHandler): ExceptionHandler { return $exceptionHandler; @@ -1379,6 +1367,8 @@ public function testReportExceptionsForReadStream(): void } catch (UnableToReadFile) { $this->fail('Exception was thrown.'); } + + $exceptionHandler->assertReported(fn (UnableToReadFile $exception): bool => str_contains($exception->getMessage(), 'Unable to read file from location: foo.txt.')); } public function testReportExceptionsForPut(): void @@ -1394,15 +1384,7 @@ public function testReportExceptionsForPut(): void $container = Container::getInstance(); - $exceptionHandler = m::mock(ExceptionHandler::class); - - $exceptionHandler->expects('report') - ->andReturnUsing(function (UnableToWriteFile $exception): void { - $this->assertStringContainsString( - 'Unable to write file at location: foo.txt.', - $exception->getMessage(), - ); - }); + $exceptionHandler = new ExceptionHandlerFake(new Handler($container)); $container->bind(ExceptionHandler::class, function () use ($exceptionHandler): ExceptionHandler { return $exceptionHandler; @@ -1416,21 +1398,15 @@ public function testReportExceptionsForPut(): void } finally { chmod($this->tempDir . '/foo.txt', 0600); } + + $exceptionHandler->assertReported(fn (UnableToWriteFile $exception): bool => str_contains($exception->getMessage(), 'Unable to write file at location: foo.txt.')); } public function testReportExceptionsForMimeType(): void { $container = Container::getInstance(); - $exceptionHandler = m::mock(ExceptionHandler::class); - - $exceptionHandler->expects('report') - ->andReturnUsing(function (UnableToRetrieveMetadata $exception): void { - $this->assertStringContainsString( - 'Unable to retrieve the mime_type for file at location: unknown.mime-type.', - $exception->getMessage(), - ); - }); + $exceptionHandler = new ExceptionHandlerFake(new Handler($container)); $container->bind(ExceptionHandler::class, function () use ($exceptionHandler): ExceptionHandler { return $exceptionHandler; @@ -1445,6 +1421,8 @@ public function testReportExceptionsForMimeType(): void } catch (UnableToRetrieveMetadata) { $this->fail('Exception was thrown.'); } + + $exceptionHandler->assertReported(fn (UnableToRetrieveMetadata $exception): bool => str_contains($exception->getMessage(), 'Unable to retrieve the mime_type for file at location: unknown.mime-type.')); } public function testGetAllFiles(): void diff --git a/tests/Foundation/Configuration/MiddlewareTest.php b/tests/Foundation/Configuration/MiddlewareTest.php index 4937997e2a..3bf9c04078 100644 --- a/tests/Foundation/Configuration/MiddlewareTest.php +++ b/tests/Foundation/Configuration/MiddlewareTest.php @@ -7,9 +7,10 @@ use Hypervel\Auth\AuthenticationException; use Hypervel\Auth\Middleware\Authenticate; use Hypervel\Auth\Middleware\RedirectIfAuthenticated; -use Hypervel\Contracts\Encryption\Encrypter; use Hypervel\Contracts\Foundation\Application; use Hypervel\Cookie\Middleware\EncryptCookies; +use Hypervel\Encryption\Encrypter; +use Hypervel\Foundation\Application as FoundationApplication; use Hypervel\Foundation\Configuration\Middleware; use Hypervel\Foundation\Http\Middleware\ConvertEmptyStringsToNull; use Hypervel\Foundation\Http\Middleware\PreventRequestForgery; @@ -191,7 +192,7 @@ public function testTrustHeaders(): void public function testTrustHosts(): void { - $app = m::mock(Application::class); + $app = new FoundationApplication; $configuration = new Middleware; $middleware = new class($app) extends TrustHosts { protected function allSubdomainsOfApplicationUrl(): ?string @@ -233,7 +234,7 @@ protected function allSubdomainsOfApplicationUrl(): ?string public function testEncryptCookies(): void { $configuration = new Middleware; - $encrypter = m::mock(Encrypter::class); + $encrypter = new Encrypter(str_repeat('a', 16)); $middleware = new EncryptCookies($encrypter); $this->assertFalse($middleware->isDisabled('aaa')); @@ -297,7 +298,7 @@ public function testPreventRequestsDuringMaintenance(): void { $configuration = new Middleware; - $app = m::mock(Application::class); + $app = new FoundationApplication; $middleware = new PreventRequestsDuringMaintenance($app); $reflection = new ReflectionClass($middleware); @@ -320,8 +321,8 @@ public function testPreventRequestForgery(): void { $configuration = new Middleware; $middleware = new PreventRequestForgery( - m::mock(Application::class), - m::mock(Encrypter::class) + new FoundationApplication, + new Encrypter(str_repeat('a', 16)) ); $this->assertSame([], $middleware->getExcludedPaths()); diff --git a/tests/Foundation/Exceptions/Renderer/ListenerTest.php b/tests/Foundation/Exceptions/Renderer/ListenerTest.php index 9a7b75987a..46ff2cf783 100644 --- a/tests/Foundation/Exceptions/Renderer/ListenerTest.php +++ b/tests/Foundation/Exceptions/Renderer/ListenerTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Foundation\Exceptions\Renderer; +use Hypervel\Database\Capsule\Manager; use Hypervel\Database\Connection; use Hypervel\Database\Events\QueryExecuted; use Hypervel\Foundation\Exceptions\Renderer\Listener; @@ -12,12 +13,20 @@ class ListenerTest extends TestCase { - public function testQueriesReturnsExpectedShapeAfterQueryExecuted(): void + /** + * Create a real in-memory SQLite connection. + */ + protected function getRealConnection(): Connection { - $connection = m::mock(Connection::class); + $capsule = new Manager; + $capsule->addConnection(['driver' => 'sqlite', 'database' => ':memory:'], 'testing'); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->with(['foo'])->andReturn(['foo']); + return $capsule->getConnection('testing'); + } + + public function testQueriesReturnsExpectedShapeAfterQueryExecuted(): void + { + $connection = $this->getRealConnection(); $event = new QueryExecuted('select * from users where id = ?', ['foo'], 5.2, $connection); @@ -66,9 +75,7 @@ public function testLargeSqlIsTruncated(): void { $listener = new Listener; - $connection = m::mock(Connection::class); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->andReturnUsing(fn (array $bindings): array => $bindings); + $connection = $this->getRealConnection(); $largeSql = str_repeat('x', 5000); $listener->onQueryExecuted( @@ -82,9 +89,7 @@ public function testBindingsMatchPlaceholderCountInTruncatedSql(): void { $listener = new Listener; - $connection = m::mock(Connection::class); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->andReturnUsing(fn (array $bindings): array => $bindings); + $connection = $this->getRealConnection(); // Build SQL with 1000 placeholders so truncation to 2000 bytes removes // some placeholders and their corresponding bindings. @@ -107,9 +112,7 @@ public function testExcessBindingsAreTrimmedToMatchPlaceholders(): void { $listener = new Listener; - $connection = m::mock(Connection::class); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->andReturnUsing(fn (array $bindings): array => $bindings); + $connection = $this->getRealConnection(); // 1 placeholder but 1000 bindings — only 1 binding should be kept $listener->onQueryExecuted( @@ -123,9 +126,7 @@ public function testShortSqlAndBindingsAreNotModified(): void { $listener = new Listener; - $connection = m::mock(Connection::class); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->andReturnUsing(fn (array $bindings): array => $bindings); + $connection = $this->getRealConnection(); $sql = 'select * from users where name = ?'; $listener->onQueryExecuted( @@ -140,9 +141,7 @@ public function testQueryWithNoBindingsIsUnchanged(): void { $listener = new Listener; - $connection = m::mock(Connection::class); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->andReturnUsing(fn (array $bindings): array => $bindings); + $connection = $this->getRealConnection(); $listener->onQueryExecuted( new QueryExecuted('select count(*) from users', [], 1.0, $connection) @@ -156,9 +155,7 @@ public function testNormalQuerySkipsTruncation(): void { $listener = new Listener; - $connection = m::mock(Connection::class); - $connection->expects('getName')->andReturn('testing'); - $connection->expects('prepareBindings')->andReturnUsing(fn (array $bindings): array => $bindings); + $connection = $this->getRealConnection(); $sql = 'select * from users where id = ? and name = ? and email = ?'; $bindings = [1, 'John', 'john@example.com']; diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php index e870b5969e..d055df195c 100644 --- a/tests/Foundation/FoundationApplicationTest.php +++ b/tests/Foundation/FoundationApplicationTest.php @@ -21,6 +21,7 @@ use Hypervel\Log\LogManager; use Hypervel\Support\Facades\Log; use Hypervel\Support\ServiceProvider; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Testing\ParallelTesting; use Hypervel\Tests\TestCase; use JsonException; @@ -54,14 +55,12 @@ protected function tearDown(): void public function testSetLocaleSetsLocaleAndFiresLocaleChangedEvent(): void { + $events = new EventFake(new EventDispatcher); $translator = m::mock(TranslatorContract::class); - $translator->expects('getLocale')->andReturn('bar')->globally()->ordered(); - $translator->expects('setLocale')->with('foo')->globally()->ordered(); - $events = m::mock(DispatcherContract::class); - $events->expects('hasListeners')->with(LocaleUpdated::class)->andReturn(true)->globally()->ordered(); - $events->expects('dispatch')->with(m::on(function (LocaleUpdated $event): bool { - return $event->locale === 'foo' && $event->previousLocale === 'bar'; - }))->globally()->ordered(); + $translator->expects('getLocale')->andReturn('bar')->ordered(); + $translator->expects('setLocale')->with('foo')->ordered()->andReturnUsing(function () use ($events): void { + $events->assertNothingDispatched(); + }); $config = new Repository(['app' => ['locale' => 'en']]); $app = new Application; @@ -73,6 +72,11 @@ public function testSetLocaleSetsLocaleAndFiresLocaleChangedEvent(): void // REMOVED: Effective current locale is request-local and does not mutate worker-shared config. $this->assertSame('en', $config->string('app.locale')); + + $events->assertDispatchedOnce(LocaleUpdated::class); + $events->assertDispatched(LocaleUpdated::class, function (LocaleUpdated $event): bool { + return $event->locale === 'foo' && $event->previousLocale === 'bar'; + }); } public function testSetLocaleDoesNotDispatchWhenLocaleEventHasNoListeners(): void @@ -155,14 +159,11 @@ public function testGetFallbackLocaleReadsFromTranslator(): void public function testServiceProvidersAreCorrectlyRegistered(): void { - $provider = m::mock(ApplicationBasicServiceProviderStub::class); - $class = get_class($provider); - $provider->shouldReceive('isEnabled')->andReturn(true); - $provider->expects('register'); $app = new Application; + $provider = new ApplicationBasicServiceProviderStub($app); $app->register($provider); - $this->assertArrayHasKey($class, $app->getLoadedProviders()); + $this->assertArrayHasKey(get_class($provider), $app->getLoadedProviders()); } public function testClassesAreBoundWhenServiceProviderIsRegistered(): void @@ -207,26 +208,20 @@ public function testSingletonsAreCreatedWhenServiceProviderIsRegistered(): void public function testServiceProvidersAreCorrectlyRegisteredWhenRegisterMethodIsNotFilled(): void { - $provider = m::mock(ServiceProvider::class); - $class = get_class($provider); - $provider->shouldReceive('isEnabled')->andReturn(true); - $provider->expects('register'); $app = new Application; + $provider = new class($app) extends ServiceProvider {}; $app->register($provider); - $this->assertArrayHasKey($class, $app->getLoadedProviders()); + $this->assertArrayHasKey(get_class($provider), $app->getLoadedProviders()); } public function testServiceProvidersCouldBeLoaded(): void { - $provider = m::mock(ServiceProvider::class); - $class = get_class($provider); - $provider->shouldReceive('isEnabled')->andReturn(true); - $provider->expects('register'); $app = new Application; + $provider = new class($app) extends ServiceProvider {}; $app->register($provider); - $this->assertTrue($app->providerIsLoaded($class)); + $this->assertTrue($app->providerIsLoaded(get_class($provider))); $this->assertFalse($app->providerIsLoaded(ApplicationBasicServiceProviderStub::class)); } diff --git a/tests/Foundation/FoundationAuthenticationTest.php b/tests/Foundation/FoundationAuthenticationTest.php index e30197ac97..16ce22a53a 100644 --- a/tests/Foundation/FoundationAuthenticationTest.php +++ b/tests/Foundation/FoundationAuthenticationTest.php @@ -5,6 +5,8 @@ namespace Hypervel\Tests\Foundation; use Hypervel\Auth\AuthManager; +use Hypervel\Auth\GenericUser; +use Hypervel\Config\Repository as ConfigRepository; use Hypervel\Contracts\Auth\Authenticatable; use Hypervel\Contracts\Auth\Guard; use Hypervel\Contracts\Auth\UserProvider; @@ -43,37 +45,112 @@ protected function mockGuard(): Guard return $guard; } + /** + * Register a real guard for the given user as the application's default guard. + */ + protected function getRealGuard(?Authenticatable $user = null): Guard + { + $guard = new class($user) implements Guard { + /** + * Create a new guard instance. + */ + public function __construct(protected ?Authenticatable $user) + { + } + + /** + * Determine if the current user is authenticated. + */ + public function check(): bool + { + return ! is_null($this->user); + } + + /** + * Determine if the current user is a guest. + */ + public function guest(): bool + { + return is_null($this->user); + } + + /** + * Get the currently authenticated user. + */ + public function user(): ?Authenticatable + { + return $this->user; + } + + /** + * Get the ID for the currently authenticated user. + */ + public function id(): int|string|null + { + return $this->user?->getAuthIdentifier(); + } + + /** + * Validate a user's credentials. + */ + public function validate(array $credentials = []): bool + { + return false; + } + + /** + * Determine if the guard has a user instance. + */ + public function hasUser(): bool + { + return ! is_null($this->user); + } + + /** + * Set the current user. + */ + public function setUser(Authenticatable $user): static + { + $this->user = $user; + + return $this; + } + }; + + $this->app = new Application; + $this->app->instance('config', new ConfigRepository([ + 'auth' => [ + 'defaults' => ['guard' => 'web'], + 'guards' => ['web' => ['driver' => 'test']], + ], + ])); + + $auth = new AuthManager($this->app); + $auth->extend('test', fn (): Guard => $guard); + $this->app->instance('auth', $auth); + + return $guard; + } + public function testAssertAuthenticated(): void { - $this->mockGuard() - ->expects('check') - ->andReturn(true); + $this->getRealGuard(new GenericUser(['id' => 1])); $this->assertAuthenticated(); } public function testAssertGuest(): void { - $this->mockGuard() - ->expects('check') - ->andReturn(false); + $this->getRealGuard(); $this->assertGuest(); } public function testAssertAuthenticatedAs(): void { - $expected = m::mock(Authenticatable::class); - $expected->expects('getAuthIdentifier') - ->andReturn('1'); - - $this->mockGuard() - ->expects('user') - ->andReturn($expected); + $this->getRealGuard(new GenericUser(['id' => 1])); - $user = m::mock(Authenticatable::class); - $user->expects('getAuthIdentifier') - ->andReturn('1'); + $user = new GenericUser(['id' => 1]); $this->assertAuthenticatedAs($user); } @@ -83,7 +160,7 @@ public function testAssertAuthenticatedAs(): void */ protected function setupProvider(array $credentials): void { - $user = m::mock(Authenticatable::class); + $user = new GenericUser([]); $provider = m::mock(UserProvider::class); diff --git a/tests/Foundation/Testing/DatabaseTruncationTest.php b/tests/Foundation/Testing/DatabaseTruncationTest.php index a55089187a..ff648fb6a0 100644 --- a/tests/Foundation/Testing/DatabaseTruncationTest.php +++ b/tests/Foundation/Testing/DatabaseTruncationTest.php @@ -7,12 +7,13 @@ use Hypervel\Config\Repository; use Hypervel\Container\Container; use Hypervel\Contracts\Console\Kernel as KernelContract; -use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Contracts\Events\Dispatcher as DispatcherContract; use Hypervel\Database\Connection; use Hypervel\Database\DatabaseManager; use Hypervel\Database\PdoConnection; use Hypervel\Database\Schema\Builder; use Hypervel\Database\Schema\PostgresBuilder; +use Hypervel\Events\Dispatcher; use Hypervel\Foundation\Testing\Concerns\InteractsWithConsole; use Hypervel\Foundation\Testing\DatabaseMigrations; use Hypervel\Foundation\Testing\DatabaseTransactions; @@ -268,7 +269,7 @@ public function testCachesAndRestoresConfiguredInMemoryConnections(): void $this->cacheInMemoryDatabases(); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = m::mock(DispatcherContract::class); $restoredDefault = m::mock(PdoConnection::class); $restoredNamed = m::mock(PdoConnection::class); $restoredDefault->expects('setPdo')->with($defaultPdo)->andReturnSelf(); @@ -282,7 +283,7 @@ public function testCachesAndRestoresConfiguredInMemoryConnections(): void $restoredDatabase->shouldNotReceive('connection')->with('file'); $this->app->instance('db', $restoredDatabase); - $this->app->instance(Dispatcher::class, $dispatcher); + $this->app->instance(DispatcherContract::class, $dispatcher); $this->restoreInMemoryDatabases(); @@ -547,7 +548,7 @@ function (array $tables) use (&$actual, &$withoutPrefix, $failure): void { $connection = m::mock(Connection::class); $connection->shouldReceive('getTablePrefix')->andReturn($prefix); - $dispatcher = m::mock(Dispatcher::class); + $dispatcher = new Dispatcher; $connection->expects('getEventDispatcher')->andReturn($dispatcher); $connection->expects('unsetEventDispatcher'); $connection->expects('setEventDispatcher')->with($dispatcher); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index a0b261ac9b..18ad45a212 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -31,6 +31,7 @@ use Hypervel\Contracts\Container\Container as ContainerContract; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Support\Arrayable; +use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Filesystem\Filesystem; use Hypervel\Http\Client\ConnectionException; use Hypervel\Http\Client\Events\ConnectionFailed as ConnectionFailedEvent; @@ -52,6 +53,7 @@ use Hypervel\Support\Sleep; use Hypervel\Support\Str; use Hypervel\Support\Stringable; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Support\Uri; use Hypervel\Testing\ParallelTesting; use Hypervel\Tests\Http\Fixtures\LoopbackHttpServer; @@ -4379,11 +4381,7 @@ public function testGlobalConfigurationSuppressionIsScopedToTheFactoryAndCorouti public function testTheRequestSendingAndResponseReceivedEventsAreFiredWhenARequestIsSent(): void { - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->times(5)->with(RequestSending::class)->andReturn(true); - $events->expects('hasListeners')->times(5)->with(ResponseReceived::class)->andReturn(true); - $events->expects('dispatch')->times(5)->with(m::type(RequestSending::class)); - $events->expects('dispatch')->times(5)->with(m::type(ResponseReceived::class)); + $events = new EventFake(new EventDispatcher); $factory = new Factory($events); $factory->fake(); @@ -4393,15 +4391,13 @@ public function testTheRequestSendingAndResponseReceivedEventsAreFiredWhenAReque $factory->post('https://example.com'); $factory->patch('https://example.com'); $factory->delete('https://example.com'); + $events->assertDispatchedTimes(RequestSending::class, 5); + $events->assertDispatchedTimes(ResponseReceived::class, 5); } public function testTheRequestSendingAndResponseReceivedEventsAreFiredWhenARequestIsSentAsync(): void { - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->times(5)->with(RequestSending::class)->andReturn(true); - $events->expects('hasListeners')->times(5)->with(ResponseReceived::class)->andReturn(true); - $events->expects('dispatch')->times(5)->with(m::type(RequestSending::class)); - $events->expects('dispatch')->times(5)->with(m::type(ResponseReceived::class)); + $events = new EventFake(new EventDispatcher); $factory = new Factory($events); $factory->fake(); @@ -4411,16 +4407,14 @@ public function testTheRequestSendingAndResponseReceivedEventsAreFiredWhenAReque $factory->async()->post('https://example.com')->wait(); $factory->async()->patch('https://example.com')->wait(); $factory->async()->delete('https://example.com')->wait(); + $events->assertDispatchedTimes(RequestSending::class, 5); + $events->assertDispatchedTimes(ResponseReceived::class, 5); } public function testTheRequestSendingAndResponseReceivedEventsAreFiredForEveryRetry(): void { Sleep::fake(); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->times(2)->with(RequestSending::class)->andReturn(true); - $events->expects('hasListeners')->times(2)->with(ResponseReceived::class)->andReturn(true); - $events->expects('dispatch')->times(2)->with(m::type(RequestSending::class)); - $events->expects('dispatch')->times(2)->with(m::type(ResponseReceived::class)); + $events = new EventFake(new EventDispatcher); $factory = new Factory($events); $factory->fake([ @@ -4432,6 +4426,8 @@ public function testTheRequestSendingAndResponseReceivedEventsAreFiredForEveryRe $this->assertTrue($response->failed()); $factory->assertSentCount(2); + $events->assertDispatchedTimes(RequestSending::class, 2); + $events->assertDispatchedTimes(ResponseReceived::class, 2); } public function testTheTransferStatsAreCalledSafelyWhenFakingTheRequest(): void @@ -4466,11 +4462,7 @@ public function testTransferStatsArePresentWhenFakingAnAsyncRequestUsingAPromise public function testClonedClientsWorkSuccessfullyWithTheRequestObject(): void { - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(RequestSending::class)->andReturn(true); - $events->expects('hasListeners')->with(ResponseReceived::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(RequestSending::class)); - $events->expects('dispatch')->with(m::type(ResponseReceived::class)); + $events = new EventFake(new EventDispatcher); $factory = new Factory($events); $factory->fake(['example.com' => $factory::response('foo', 200)]); @@ -4479,6 +4471,21 @@ public function testClonedClientsWorkSuccessfullyWithTheRequestObject(): void $clonedClient = clone $client; $clonedClient->get('https://example.com'); + $events->assertDispatchedTimes(RequestSending::class, 1); + $events->assertDispatchedTimes(ResponseReceived::class, 1); + } + + public function testRequestEventsAreNotDispatchedWithoutListeners(): void + { + $events = m::mock(Dispatcher::class); + $events->expects('hasListeners')->with(RequestSending::class)->andReturn(false); + $events->expects('hasListeners')->with(ResponseReceived::class)->andReturn(false); + $events->shouldNotReceive('dispatch'); + + $factory = new Factory($events); + $factory->fake(); + + $factory->get('https://example.com'); } public function testTheConnectionFailedEventIsFiredWhenARequestFailsToConnect(): void diff --git a/tests/Http/HttpRedirectResponseTest.php b/tests/Http/HttpRedirectResponseTest.php index 2043850d91..9034ee3f63 100755 --- a/tests/Http/HttpRedirectResponseTest.php +++ b/tests/Http/HttpRedirectResponseTest.php @@ -8,6 +8,7 @@ use Hypervel\Contracts\Support\MessageProvider; use Hypervel\Http\RedirectResponse; use Hypervel\Http\Request; +use Hypervel\Session\NullSessionHandler; use Hypervel\Session\Store; use Hypervel\Support\MessageBag; use Hypervel\Support\ViewErrorBag; @@ -78,10 +79,12 @@ public function testInputOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor', 'age' => 26]); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->withInput(); + + $this->assertSame(['name' => 'Taylor', 'age' => 26], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testWithCookies(): void @@ -100,33 +103,40 @@ public function testOnlyInputOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor']); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->onlyInput('name'); + + $this->assertSame(['name' => 'Taylor'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testExceptInputOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor']); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->exceptInput('age'); + + $this->assertSame(['name' => 'Taylor'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testFlashingErrorsOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('get')->with('errors', m::type(ViewErrorBag::class))->andReturn(new ViewErrorBag); - $session->expects('flash')->with('errors', m::type(ViewErrorBag::class)); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $provider = m::mock(MessageProvider::class); - $provider->expects('getMessageBag')->andReturn(new MessageBag); + $provider->expects('getMessageBag')->andReturn(new MessageBag(['name' => ['required']])); $response->withErrors($provider); + + $this->assertContains('errors', $session->get('_flash.new', [])); + $errors = $session->get('errors'); + $this->assertInstanceOf(ViewErrorBag::class, $errors); + $this->assertSame(['required'], $errors->getBag('default')->get('name')); } public function testCanEnforceSameOriginWhenSameOrigin(): void @@ -199,7 +209,7 @@ public function testSettersGettersOnRequest(): void $this->assertNull($response->getSession()); $request = Request::create('/', 'GET'); - $session = m::mock(Store::class); + $session = new Store('test', new NullSessionHandler); $response->setRequest($request); $response->setSession($session); $this->assertSame($request, $response->getRequest()); @@ -210,22 +220,27 @@ public function testRedirectWithErrorsArrayConvertsToMessageBag(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('get')->with('errors', m::type(ViewErrorBag::class))->andReturn(new ViewErrorBag); - $session->expects('flash')->with('errors', m::type(ViewErrorBag::class)); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $provider = ['foo' => 'bar']; $response->withErrors($provider); + + $this->assertContains('errors', $session->get('_flash.new', [])); + $errors = $session->get('errors'); + $this->assertInstanceOf(ViewErrorBag::class, $errors); + $this->assertSame(['bar'], $errors->getBag('default')->get('foo')); } public function testMagicCall(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flash')->with('foo', 'bar'); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->withFoo('bar'); + + $this->assertSame('bar', $session->get('foo')); + $this->assertContains('foo', $session->get('_flash.new', [])); } public function testMagicCallException(): void diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 6d3933e312..f6e63c4f5f 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -10,6 +10,7 @@ use Hypervel\Http\UploadedFile; use Hypervel\Image\Image; use Hypervel\Routing\Route; +use Hypervel\Session\NullSessionHandler; use Hypervel\Session\Store; use Hypervel\Support\CarbonImmutable as Carbon; use Hypervel\Support\Collection; @@ -1612,7 +1613,9 @@ public function testInputWithEmptyFilename(): void $baseRequest = SymfonyRequest::create('/?boom=breeze', 'GET', ['foo' => ['bar' => 'baz']], [], $invalidFiles); - Request::createFromBase($baseRequest); + $request = Request::createFromBase($baseRequest); + + $this->assertNull($request->file('file')); } public function testMultipleFileUploadWithEmptyValue(): void @@ -1637,8 +1640,8 @@ public function testMultipleFileUploadWithEmptyValue(): void public function testOldMethodCallsSession(): void { $request = Request::create('/'); - $session = m::mock(Store::class); - $session->expects('getOldInput')->with('foo', 'bar')->andReturn('boom'); + $session = new Store('test', new NullSessionHandler); + $session->flashInput(['foo' => 'boom']); $request->setHypervelSession($session); $this->assertSame('boom', $request->old('foo', 'bar')); } @@ -1646,8 +1649,7 @@ public function testOldMethodCallsSession(): void public function testOldMethodCallsSessionWhenDefaultIsArray(): void { $request = Request::create('/'); - $session = m::mock(Store::class); - $session->expects('getOldInput')->with('foo', ['bar'])->andReturn(['bar']); + $session = new Store('test', new NullSessionHandler); $request->setHypervelSession($session); $this->assertSame(['bar'], $request->old('foo', ['bar'])); } @@ -1655,10 +1657,9 @@ public function testOldMethodCallsSessionWhenDefaultIsArray(): void public function testOldMethodCanGetDefaultValueFromModelByKey(): void { $request = Request::create('/'); - $model = m::mock(Price::class); - $model->expects('getAttribute')->with('name')->andReturn('foobar'); - $session = m::mock(Store::class); - $session->expects('getOldInput')->with('name', 'foobar')->andReturn('foobar'); + $model = new Price; + $model->name = 'foobar'; + $session = new Store('test', new NullSessionHandler); $request->setHypervelSession($session); $this->assertSame('foobar', $request->old('name', $model)); } @@ -1680,10 +1681,13 @@ public function testOldMethodReturnsNonStringInputAndDefaults(): void public function testFlushMethodCallsSession(): void { $request = Request::create('/'); - $session = m::mock(Store::class); - $session->expects('flashInput'); + $session = new Store('test', new NullSessionHandler); + $session->flashInput(['foo' => 'bar']); + $session->ageFlashData(); $request->setHypervelSession($session); $request->flush(); + $this->assertSame([], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testExpectsJson(): void @@ -1967,7 +1971,7 @@ public function testHasSessionMethod(): void $this->assertFalse($request->hasSession()); - $session = m::mock(Store::class); + $session = new Store('test', new NullSessionHandler); $request->setHypervelSession($session); $this->assertTrue($request->hasSession()); @@ -1977,14 +1981,14 @@ public function testGetSessionMethodWithHypervelSession(): void { $request = Request::create('/'); - $hypervelSession = m::mock(Store::class); + $hypervelSession = new Store('test', new NullSessionHandler); $request->setHypervelSession($hypervelSession); $session = $request->getSession(); $this->assertInstanceOf(SessionInterface::class, $session); - $hypervelSession->expects('start')->andReturn(true); - $session->start(); + $this->assertTrue($session->start()); + $this->assertTrue($hypervelSession->isStarted()); } public function testGetSessionMethodWithoutHypervelSession(): void @@ -2175,29 +2179,32 @@ public function testMagicMethodsMergeNestedInputAndFilesWhilePreferringInput(): public function testHttpRequestFlashCallsSessionFlashInputWithInputData(): void { - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor', 'email' => 'foo']); + $session = new Store('test', new NullSessionHandler); $request = Request::create('/', 'GET', ['name' => 'Taylor', 'email' => 'foo']); $request->setHypervelSession($session); $request->flash(); + $this->assertSame(['name' => 'Taylor', 'email' => 'foo'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testHttpRequestFlashOnlyCallsFlashWithProperParameters(): void { - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor']); + $session = new Store('test', new NullSessionHandler); $request = Request::create('/', 'GET', ['name' => 'Taylor', 'email' => 'foo']); $request->setHypervelSession($session); $request->flashOnly(['name']); + $this->assertSame(['name' => 'Taylor'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testHttpRequestFlashExceptCallsFlashWithProperParameters(): void { - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor']); + $session = new Store('test', new NullSessionHandler); $request = Request::create('/', 'GET', ['name' => 'Taylor', 'email' => 'foo']); $request->setHypervelSession($session); $request->flashExcept(['email']); + $this->assertSame(['name' => 'Taylor'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testGeneratingJsonRequestFromParentRequestUsesCorrectType(): void diff --git a/tests/Http/HttpResponseTest.php b/tests/Http/HttpResponseTest.php index 70de282433..4f76c38053 100644 --- a/tests/Http/HttpResponseTest.php +++ b/tests/Http/HttpResponseTest.php @@ -15,6 +15,7 @@ use Hypervel\Http\RedirectResponse; use Hypervel\Http\Request; use Hypervel\Http\Response; +use Hypervel\Session\NullSessionHandler; use Hypervel\Session\Store; use Hypervel\Support\MessageBag; use Hypervel\Support\ViewErrorBag; @@ -85,9 +86,16 @@ public function testJsonResponsesAreConvertedAndHeadersAreSet(): void public function testRenderablesAreRendered(): void { - $mock = m::mock(Renderable::class); - $mock->expects('render')->andReturn('foo'); - $response = new Response($mock); + $renderable = new class implements Renderable { + /** + * Get the evaluated contents of the object. + */ + public function render(): string + { + return 'foo'; + } + }; + $response = new Response($renderable); $this->assertSame('foo', $response->getContent()); } @@ -262,33 +270,40 @@ public function testOnlyInputOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor']); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->onlyInput('name'); + + $this->assertSame(['name' => 'Taylor'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testExceptInputOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flashInput')->with(['name' => 'Taylor']); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->exceptInput('age'); + + $this->assertSame(['name' => 'Taylor'], $session->getOldInput()); + $this->assertContains('_old_input', $session->get('_flash.new', [])); } public function testFlashingErrorsOnRedirect(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('get')->with('errors', m::type(ViewErrorBag::class))->andReturn(new ViewErrorBag); - $session->expects('flash')->with('errors', m::type(ViewErrorBag::class)); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $provider = m::mock(MessageProvider::class); - $provider->expects('getMessageBag')->andReturn(new MessageBag); + $provider->expects('getMessageBag')->andReturn(new MessageBag(['name' => ['required']])); $response->withErrors($provider); + + $this->assertContains('errors', $session->get('_flash.new', [])); + $errors = $session->get('errors'); + $this->assertInstanceOf(ViewErrorBag::class, $errors); + $this->assertSame(['required'], $errors->getBag('default')->get('name')); } public function testSettersGettersOnRequest(): void @@ -298,7 +313,7 @@ public function testSettersGettersOnRequest(): void $this->assertNull($response->getSession()); $request = Request::create('/', 'GET'); - $session = m::mock(Store::class); + $session = new Store('test', new NullSessionHandler); $response->setRequest($request); $response->setSession($session); $this->assertSame($request, $response->getRequest()); @@ -309,12 +324,15 @@ public function testRedirectWithErrorsArrayConvertsToMessageBag(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('get')->with('errors', m::type(ViewErrorBag::class))->andReturn(new ViewErrorBag); - $session->expects('flash')->with('errors', m::type(ViewErrorBag::class)); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $provider = ['foo' => 'bar']; $response->withErrors($provider); + + $this->assertContains('errors', $session->get('_flash.new', [])); + $errors = $session->get('errors'); + $this->assertInstanceOf(ViewErrorBag::class, $errors); + $this->assertSame(['bar'], $errors->getBag('default')->get('foo')); } public function testWithHeaders(): void @@ -361,10 +379,12 @@ public function testMagicCall(): void { $response = new RedirectResponse('foo.bar'); $response->setRequest(Request::create('/', 'GET', ['name' => 'Taylor', 'age' => 26])); - $session = m::mock(Store::class); - $session->expects('flash')->with('foo', 'bar'); + $session = new Store('test', new NullSessionHandler); $response->setSession($session); $response->withFoo('bar'); + + $this->assertSame('bar', $session->get('foo')); + $this->assertContains('foo', $session->get('_flash.new', [])); } public function testMagicCallException(): void diff --git a/tests/Http/JsonResourceTest.php b/tests/Http/JsonResourceTest.php index 98d4f297cc..5761e12ff7 100644 --- a/tests/Http/JsonResourceTest.php +++ b/tests/Http/JsonResourceTest.php @@ -4,7 +4,9 @@ namespace Hypervel\Tests\Http; +use Hypervel\Container\Container; use Hypervel\Database\Eloquent\Model; +use Hypervel\Http\Request; use Hypervel\Http\Resources\Json\JsonResource; use Hypervel\Http\Resources\MissingValue; use Hypervel\Tests\TestCase; @@ -34,13 +36,9 @@ public function testJsonResourceNullAttributes(): void public function testJsonResourceToJsonSucceedsWithPriorErrors(): void { - $model = new class extends Model { - }; + Container::getInstance()->instance('request', Request::create('/')); - $resource = m::mock(JsonResource::class, ['resource' => $model]) - ->makePartial() - ->expects('jsonSerialize')->andReturn(['foo' => 'bar']) - ->getMock(); + $resource = new JsonResource(['foo' => 'bar']); // Simulate a JSON error json_decode('{'); @@ -51,10 +49,7 @@ public function testJsonResourceToJsonSucceedsWithPriorErrors(): void public function testJsonResourceToPrettyPrint(): void { - $model = new class extends Model { - }; - - $resource = m::mock(JsonResource::class, ['resource' => $model]) + $resource = m::mock(JsonResource::class) ->makePartial() ->expects('jsonSerialize')->times(3)->andReturn(['foo' => 'bar', 'bar' => 'foo', 'number' => 123]) ->getMock(); diff --git a/tests/Http/Middleware/PreventRequestForgeryTest.php b/tests/Http/Middleware/PreventRequestForgeryTest.php index ea242fcc9f..1245169320 100644 --- a/tests/Http/Middleware/PreventRequestForgeryTest.php +++ b/tests/Http/Middleware/PreventRequestForgeryTest.php @@ -4,13 +4,14 @@ namespace Hypervel\Tests\Http\Middleware; -use Hypervel\Contracts\Encryption\Encrypter; use Hypervel\Contracts\Foundation\Application; -use Hypervel\Contracts\Session\Session; +use Hypervel\Encryption\Encrypter; use Hypervel\Foundation\Http\Middleware\PreventRequestForgery; use Hypervel\Http\Exceptions\OriginMismatchException; use Hypervel\Http\Request; use Hypervel\Http\Response; +use Hypervel\Session\ArraySessionHandler; +use Hypervel\Session\Store; use Hypervel\Session\TokenMismatchException; use Hypervel\Tests\TestCase; use Mockery as m; @@ -208,8 +209,8 @@ protected function createRequest(array $server = [], ?string $token = null, stri $server ); - $session = m::mock(Session::class); - $session->shouldReceive('token')->andReturn('test-token'); + $session = new Store('test', new ArraySessionHandler(10)); + $session->put('_token', 'test-token'); $request->setHypervelSession($session); return $request; @@ -238,7 +239,7 @@ protected function createMiddleware(): PreventRequestForgeryTestStub { return new PreventRequestForgeryTestStub( m::mock(Application::class), - m::mock(Encrypter::class) + new Encrypter(str_repeat('a', 16)) ); } } diff --git a/tests/Image/ImageManagerTest.php b/tests/Image/ImageManagerTest.php index 8a084632c4..415cf0e903 100644 --- a/tests/Image/ImageManagerTest.php +++ b/tests/Image/ImageManagerTest.php @@ -43,7 +43,7 @@ public function testExtendRegistersCustomDriver(): void { $app = $this->makeApp(['images.default' => 'custom']); - $mockDriver = m::mock(Driver::class); + $mockDriver = new ImageManagerTestDriverStub; $manager = new ImageManager($app); $manager->extend('custom', function ($app) use ($mockDriver) { @@ -57,7 +57,7 @@ public function testDriverCachesResolvedInstances(): void { $app = $this->makeApp([]); - $mockDriver = m::mock(Driver::class); + $mockDriver = new ImageManagerTestDriverStub; $manager = new ImageManager($app); $manager->extend('custom', function () use ($mockDriver) { @@ -208,15 +208,10 @@ public function testFromPathReturnsImageFromFilePath(): void $file = UploadedFile::fake()->image('test.jpg', 100, 100); $path = $file->getRealPath(); - $filesystem = m::mock(Filesystem::class); - $filesystem->expects('get') - ->with($path) - ->andReturn(file_get_contents($path)); - $app = $this->makeApp([]); $app->expects('make') ->with(Filesystem::class) - ->andReturn($filesystem); + ->andReturn(new Filesystem); $manager = new ImageManager($app); $image = $manager->fromPath($path); @@ -617,8 +612,8 @@ public function testExtendOverwritesPreviousRegistration(): void { $app = $this->makeApp([]); - $firstDriver = m::mock(Driver::class); - $secondDriver = m::mock(Driver::class); + $firstDriver = new ImageManagerTestDriverStub; + $secondDriver = new ImageManagerTestDriverStub; $manager = new ImageManager($app); $manager->extend('custom', fn () => $firstDriver); @@ -631,8 +626,8 @@ public function testDriverCachesSeparatelyByName(): void { $app = $this->makeApp([]); - $driver1 = m::mock(Driver::class); - $driver2 = m::mock(Driver::class); + $driver1 = new ImageManagerTestDriverStub; + $driver2 = new ImageManagerTestDriverStub; $manager = new ImageManager($app); $manager->extend('one', fn () => $driver1); @@ -775,3 +770,38 @@ enum ImageUnitDiskStub { case public; } + +class ImageManagerTestDriverStub implements Driver +{ + /** + * Process the image contents. + */ + public function process(string $contents, ImagePipeline $pipeline): string + { + return $contents; + } + + /** + * Get the dominant image color. + */ + public function dominantColor(string $contents): string + { + return '#000000'; + } + + /** + * Get the image dimensions. + */ + public function dimensions(string $contents): array + { + return [0, 0]; + } + + /** + * Register a custom transformation handler. + */ + public function transformUsing(string $transformation, callable $callback): static + { + return $this; + } +} diff --git a/tests/Integration/Database/EloquentMassPrunableTest.php b/tests/Integration/Database/EloquentMassPrunableTest.php index 776590610e..e3cb824ea8 100644 --- a/tests/Integration/Database/EloquentMassPrunableTest.php +++ b/tests/Integration/Database/EloquentMassPrunableTest.php @@ -57,9 +57,8 @@ public function testPrunesRecords(): void $count = (new MassPrunableTestModel)->pruneAll(); $this->assertEquals(1500, $count); + Event::assertDispatchedTimes(ModelsPruned::class, 2); $this->assertEquals(3500, MassPrunableTestModel::count()); - - Event::assertDispatched(ModelsPruned::class, 2); } public function testPassiveObserverDoesNotCausePrunedEventToDispatch(): void @@ -92,10 +91,9 @@ public function testPrunesSoftDeletedRecords(): void $count = (new MassPrunableSoftDeleteTestModel)->pruneAll(); $this->assertEquals(3000, $count); + Event::assertDispatchedTimes(ModelsPruned::class, 3); $this->assertEquals(0, MassPrunableSoftDeleteTestModel::count()); $this->assertEquals(2000, MassPrunableSoftDeleteTestModel::withTrashed()->count()); - - Event::assertDispatched(ModelsPruned::class, 3); } public function testPrunesActiveAndSoftDeletedRecords(): void diff --git a/tests/Integration/Foundation/Exceptions/RendererTest.php b/tests/Integration/Foundation/Exceptions/RendererTest.php index d6e961d959..b0015e2cfc 100644 --- a/tests/Integration/Foundation/Exceptions/RendererTest.php +++ b/tests/Integration/Foundation/Exceptions/RendererTest.php @@ -4,16 +4,14 @@ namespace Hypervel\Tests\Integration\Foundation\Exceptions; -use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Foundation\ExceptionRenderer; -use Hypervel\Foundation\Exceptions\Renderer\Listener; +use Hypervel\Database\Events\QueryExecuted; +use Hypervel\Events\Dispatcher; 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; use RuntimeException; use Throwable; @@ -112,14 +110,13 @@ public function testItDoesNotRegisterListenersWhenDebugDisabled(): void $this->app->forgetInstance(ExceptionRenderer::class); $this->assertFalse($this->app->bound(ExceptionRenderer::class)); - $listener = m::mock(Listener::class); - $listener->shouldReceive('registerListeners')->never(); - - $this->app->instance(Listener::class, $listener); - Event::swap(m::mock(Dispatcher::class, ['listen' => null])); + $events = new Dispatcher; + $this->app->instance('events', $events); $provider = $this->app->getProvider(FoundationServiceProvider::class); $provider->boot(); + + $this->assertFalse($events->hasListeners(QueryExecuted::class)); } #[WithConfig('app.debug', true)] @@ -139,14 +136,13 @@ public function render(Throwable $throwable): string $this->assertTrue($this->app->bound(ExceptionRenderer::class)); - $listener = m::mock(Listener::class); - $listener->shouldReceive('registerListeners')->never(); - - $this->app->instance(Listener::class, $listener); - Event::swap(m::mock(Dispatcher::class, ['listen' => null])); + $events = new Dispatcher; + $this->app->instance('events', $events); $provider = $this->app->getProvider(FoundationServiceProvider::class); $provider->boot(); + + $this->assertFalse($events->hasListeners(QueryExecuted::class)); } #[WithConfig('app.debug', true)] @@ -155,14 +151,13 @@ public function testItRegistersListenersWhenRendererNotBound(): void $this->app->forgetInstance(ExceptionRenderer::class); $this->assertFalse($this->app->bound(ExceptionRenderer::class)); - $listener = m::mock(Listener::class); - $listener->expects('registerListeners'); - - $this->app->instance(Listener::class, $listener); - Event::swap(m::mock(Dispatcher::class, ['listen' => null])); + $events = new Dispatcher; + $this->app->instance('events', $events); $provider = $this->app->getProvider(FoundationServiceProvider::class); $provider->boot(); + + $this->assertTrue($events->hasListeners(QueryExecuted::class)); } #[WithConfig('app.debug', true)] diff --git a/tests/Integration/Queue/CallQueuedHandlerTest.php b/tests/Integration/Queue/CallQueuedHandlerTest.php index 412a04ceba..7aff027206 100644 --- a/tests/Integration/Queue/CallQueuedHandlerTest.php +++ b/tests/Integration/Queue/CallQueuedHandlerTest.php @@ -23,6 +23,7 @@ use Hypervel\Queue\Events\JobDebounced; use Hypervel\Queue\Events\JobFailed; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Support\Facades\Event; use Hypervel\Testbench\TestCase; use Mockery as m; @@ -36,16 +37,14 @@ public function testJobCanBeDispatched(): void $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize(new CallQueuedHandlerTestJob), ]); + $this->assertTrue($job->isDeleted()); + $this->assertTrue(CallQueuedHandlerTestJob::$handled); } @@ -56,16 +55,14 @@ public function testJobCanBeDispatchedThroughMiddleware(): void $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new CallQueuedHandlerTestJobWithMiddleware), ]); + $this->assertTrue($job->isDeleted()); + $this->assertInstanceOf(CallQueuedHandlerTestJobWithMiddleware::class, CallQueuedHandlerTestJobWithMiddleware::$middlewareCommand); $this->assertTrue(CallQueuedHandlerTestJobWithMiddleware::$handled); } @@ -78,11 +75,7 @@ public function testJobCanBeDispatchedThroughMiddlewareOnDispatch(): void $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $command = new CallQueuedHandlerTestJobWithMiddleware; $command->through([new TestJobMiddleware]); @@ -92,6 +85,8 @@ public function testJobCanBeDispatchedThroughMiddlewareOnDispatch(): void 'command' => serialize($command), ]); + $this->assertTrue($job->isDeleted()); + $this->assertInstanceOf(CallQueuedHandlerTestJobWithMiddleware::class, CallQueuedHandlerTestJobWithMiddleware::$middlewareCommand); $this->assertTrue(CallQueuedHandlerTestJobWithMiddleware::$handled); $this->assertTrue($_SERVER['__test.dispatchMiddleware']); @@ -104,13 +99,15 @@ public function testJobIsMarkedAsFailedIfModelNotFoundExceptionIsThrown(): void { $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - $job->expects('payload')->andReturn(['deleteWhenMissingModels' => false]); - $job->expects('fail'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize(new CallQueuedHandlerExceptionThrowerWithoutDelete), ]); + + $this->assertTrue($job->hasFailed()); + $this->assertInstanceOf(ModelNotFoundException::class, $job->failedWith); + $this->assertFalse($job->isDeleted()); } public function testJobIsDeletedIfHasDeleteProperty(): void @@ -124,7 +121,7 @@ public function testJobIsDeletedIfHasDeleteProperty(): void $job->expects('resolveQueuedJobClass')->andReturn(CallQueuedHandlerExceptionThrower::class); $job->shouldReceive('markAsFailed')->never(); $job->expects('delete'); - $job->shouldReceive('failed')->never(); + $job->shouldReceive('fail')->never(); $instance->call($job, [ 'command' => serialize(new CallQueuedHandlerExceptionThrower), @@ -144,7 +141,7 @@ public function testJobIsDeletedIfHasDeleteAttribute(): void $job->expects('resolveQueuedJobClass')->andReturn(CallQueuedHandlerAttributeExceptionThrower::class); $job->shouldReceive('markAsFailed')->never(); $job->expects('delete'); - $job->shouldReceive('failed')->never(); + $job->shouldReceive('fail')->never(); $instance->call($job, [ 'command' => serialize(new CallQueuedHandlerAttributeExceptionThrower), @@ -172,7 +169,7 @@ public function testBatchJobIsRecordedWhenDeletedDueToMissingModel(): void $job->expects('resolveQueuedJobClass')->andReturn(CallQueuedHandlerBatchableExceptionThrower::class); $job->shouldReceive('markAsFailed')->never(); $job->expects('delete'); - $job->shouldReceive('failed')->never(); + $job->shouldReceive('fail')->never(); $job->expects('uuid')->times(3)->andReturn('job-uuid'); $job->expects('payload')->times(2)->andReturn([ 'deleteWhenMissingModels' => true, diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index 9909839fea..e4cb573401 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -6,9 +6,9 @@ use Hypervel\Bus\Dispatcher; use Hypervel\Bus\Queueable; -use Hypervel\Contracts\Queue\Job; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\RateLimited; use Hypervel\RateLimiter\Limit; use Hypervel\RateLimiter\RateLimiter; @@ -16,7 +16,6 @@ use Hypervel\RateLimiter\Unlimited; use Hypervel\Support\CarbonImmutable; use Hypervel\Testbench\TestCase; -use Mockery as m; class RateLimitedTest extends TestCase { @@ -222,17 +221,14 @@ protected function assertJobRanSuccessfully(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isDeleted()); + $this->assertTrue($class::$handled); } @@ -246,17 +242,14 @@ protected function assertJobWasReleased(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release'); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isReleased()); + $this->assertFalse($class::$handled); } @@ -270,17 +263,15 @@ protected function assertJobWasReleasedAfter(string $class, int $releaseAfter): $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release')->withArgs([$releaseAfter]); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isReleased()); + $this->assertSame($releaseAfter, $job->releaseDelay); + $this->assertFalse($class::$handled); } @@ -294,17 +285,14 @@ protected function assertJobWasSkipped(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isDeleted()); + $this->assertFalse($class::$handled); } diff --git a/tests/Integration/Queue/Redis/RateLimitedRedisStoreTest.php b/tests/Integration/Queue/Redis/RateLimitedRedisStoreTest.php index 9bcc1d63c9..4e706c0c00 100644 --- a/tests/Integration/Queue/Redis/RateLimitedRedisStoreTest.php +++ b/tests/Integration/Queue/Redis/RateLimitedRedisStoreTest.php @@ -10,6 +10,7 @@ use Hypervel\Foundation\Testing\Concerns\InteractsWithRedis; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\RateLimited; use Hypervel\RateLimiter\Limit; use Hypervel\RateLimiter\RateLimiter; @@ -193,17 +194,14 @@ protected function assertJobRanSuccessfully(RedisRateLimitedTestJob $testJob): v $testJob::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($testJob), ]); + $this->assertTrue($job->isDeleted()); + $this->assertTrue($testJob::$handled); } @@ -215,17 +213,14 @@ protected function assertJobWasReleased(RedisRateLimitedTestJob $testJob): void $testJob::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release'); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($testJob), ]); + $this->assertTrue($job->isReleased()); + $this->assertFalse($testJob::$handled); } @@ -259,17 +254,14 @@ protected function assertJobWasSkipped(RedisRateLimitedTestJob $testJob): void $testJob::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($testJob), ]); + $this->assertTrue($job->isDeleted()); + $this->assertFalse($testJob::$handled); } } diff --git a/tests/Integration/Queue/Redis/ThrottlesExceptionsRedisStoreTest.php b/tests/Integration/Queue/Redis/ThrottlesExceptionsRedisStoreTest.php index 8a505e5211..aa8c9e67a6 100644 --- a/tests/Integration/Queue/Redis/ThrottlesExceptionsRedisStoreTest.php +++ b/tests/Integration/Queue/Redis/ThrottlesExceptionsRedisStoreTest.php @@ -8,10 +8,10 @@ use Hypervel\Bus\Dispatcher; use Hypervel\Bus\Queueable; use Hypervel\Contracts\Debug\ExceptionHandler; -use Hypervel\Contracts\Queue\Job; use Hypervel\Foundation\Testing\Concerns\InteractsWithRedis; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\ThrottlesExceptions; use Hypervel\Support\Str; use Hypervel\Testbench\TestCase; @@ -58,17 +58,15 @@ protected function assertJobWasReleasedImmediately(string $class, string $key): $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release')->with(0); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class($key)), ]); + $this->assertTrue($job->isReleased()); + $this->assertSame(0, $job->releaseDelay); + $this->assertTrue($class::$handled); } @@ -82,21 +80,16 @@ protected function assertJobWasReleasedWithDelay(string $class, string $key): vo $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release')->withArgs(function (int $delay): bool { - // The delay is the remainder of the decay window, less wall clock - // seconds elapsed since the first exception opened the circuit. - return $delay >= 590 && $delay <= 610; - }); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class($key)), ]); + $this->assertTrue($job->isReleased()); + $this->assertGreaterThanOrEqual(590, $job->releaseDelay); + $this->assertLessThanOrEqual(610, $job->releaseDelay); + $this->assertFalse($class::$handled); } @@ -110,17 +103,14 @@ protected function assertJobRanSuccessfully(string $class, string $key): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class($key)), ]); + $this->assertTrue($job->isDeleted()); + $this->assertTrue($class::$handled); } diff --git a/tests/Integration/Queue/ReleaseMiddlewareTest.php b/tests/Integration/Queue/ReleaseMiddlewareTest.php index 691bd45553..945067ec24 100644 --- a/tests/Integration/Queue/ReleaseMiddlewareTest.php +++ b/tests/Integration/Queue/ReleaseMiddlewareTest.php @@ -6,13 +6,12 @@ use Hypervel\Bus\Dispatcher; use Hypervel\Bus\Queueable; -use Hypervel\Contracts\Queue\Job; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\Release; use Hypervel\Testbench\TestCase; use Laravel\SerializableClosure\SerializableClosure; -use Mockery as m; class ReleaseMiddlewareTest extends TestCase { @@ -87,18 +86,15 @@ protected function assertJobRanSuccessfully(ReleaseTestJob $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); - $job->shouldReceive('release')->never(); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($class), ]); + $this->assertTrue($job->isDeleted()); + $this->assertFalse($job->isReleased()); + $this->assertTrue($class::$handled); } @@ -110,18 +106,16 @@ protected function assertJobWasReleased(ReleaseTestJob $class, int $releaseAfter $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); - $job->expects('release')->with($releaseAfter); - $job->shouldReceive('delete')->never(); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($class), ]); + $this->assertTrue($job->isReleased()); + $this->assertSame($releaseAfter, $job->releaseDelay); + $this->assertFalse($job->isDeleted()); + $this->assertFalse($class::$handled); } } diff --git a/tests/Integration/Queue/SkipMiddlewareTest.php b/tests/Integration/Queue/SkipMiddlewareTest.php index 7858a50f4c..49ca909f93 100644 --- a/tests/Integration/Queue/SkipMiddlewareTest.php +++ b/tests/Integration/Queue/SkipMiddlewareTest.php @@ -6,13 +6,12 @@ use Hypervel\Bus\Dispatcher; use Hypervel\Bus\Queueable; -use Hypervel\Contracts\Queue\Job; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\Skip; use Hypervel\Testbench\TestCase; use Laravel\SerializableClosure\SerializableClosure; -use Mockery as m; class SkipMiddlewareTest extends TestCase { @@ -96,17 +95,14 @@ protected function assertJobHandled(SkipTestJob $class, bool $expectedHandledVal $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->shouldReceive('isReleased')->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($class), ]); + $this->assertTrue($job->isDeleted()); + $this->assertEquals($expectedHandledValue, $class::$handled); } } diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index 586fc86071..e344c4deb4 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -8,11 +8,11 @@ use Hypervel\Bus\Dispatcher; use Hypervel\Bus\Queueable; use Hypervel\Contracts\Debug\ExceptionHandler; -use Hypervel\Contracts\Queue\Job; use Hypervel\Engine\Channel; use Hypervel\Engine\Coroutine as EngineCoroutine; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\ThrottlesExceptions; use Hypervel\RateLimiter\Limit; use Hypervel\RateLimiter\Limiter; @@ -69,17 +69,15 @@ protected function assertJobWasReleasedImmediately(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release')->with(0); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isReleased()); + $this->assertSame(0, $job->releaseDelay); + $this->assertTrue($class::$handled); } @@ -93,21 +91,16 @@ protected function assertJobWasReleasedWithDelay(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('release')->withArgs(function (int $delay): bool { - // The delay is the remainder of the decay window, less wall clock - // seconds elapsed since the first exception opened the circuit. - return $delay >= 590 && $delay <= 610; - }); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isReleased()); + $this->assertGreaterThanOrEqual(590, $job->releaseDelay); + $this->assertLessThanOrEqual(610, $job->releaseDelay); + $this->assertFalse($class::$handled); } @@ -121,17 +114,14 @@ protected function assertJobWasDeleted(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('delete'); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isDeleted()); + $this->assertTrue($class::$handled); } @@ -145,17 +135,15 @@ protected function assertJobWasFailed(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(true); - $job->expects('fail'); - $job->expects('isReleased')->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->hasFailed()); + $this->assertFalse($job->isReleased()); + $this->assertTrue($class::$handled); } @@ -169,17 +157,14 @@ protected function assertJobRanSuccessfully(string $class): void $class::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new $class), ]); + $this->assertTrue($job->isDeleted()); + $this->assertTrue($class::$handled); } diff --git a/tests/Integration/Queue/WithoutOverlappingJobsTest.php b/tests/Integration/Queue/WithoutOverlappingJobsTest.php index 1059c1a391..43663a2dfb 100644 --- a/tests/Integration/Queue/WithoutOverlappingJobsTest.php +++ b/tests/Integration/Queue/WithoutOverlappingJobsTest.php @@ -12,6 +12,7 @@ use Hypervel\Contracts\Queue\Job; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Queue\Jobs\FakeJob; use Hypervel\Queue\Middleware\WithoutOverlapping; use Hypervel\Testbench\Attributes\WithConfig; use Hypervel\Tests\Integration\Queue\QueueTestCase; @@ -24,17 +25,14 @@ public function testNonOverlappingJobsAreExecuted(): void OverlappingTestJob::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command = new OverlappingTestJob), ]); + $this->assertTrue($job->isDeleted()); + $lockKey = (new WithoutOverlapping)->getLockKey($command); $this->assertTrue(OverlappingTestJob::$handled); @@ -46,7 +44,7 @@ public function testLockIsReleasedOnJobExceptions(): void FailedOverlappingTestJob::$handled = false; $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); - $job = m::mock(Job::class); + $job = new FakeJob; $this->expectException(Exception::class); @@ -70,17 +68,14 @@ public function testOverlappingJobsAreReleased(): void $lockKey = (new WithoutOverlapping)->getLockKey($command = new OverlappingTestJob); $this->app->get(Cache::class)->lock($lockKey, 10)->acquire(); - $job = m::mock(Job::class); - - $job->expects('release'); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command), ]); + $this->assertTrue($job->isReleased()); + $this->assertFalse(OverlappingTestJob::$handled); } @@ -92,17 +87,14 @@ public function testOverlappingJobsCanBeSkipped(): void $lockKey = (new WithoutOverlapping)->getLockKey($command = new SkipOverlappingTestJob); $this->app->get(Cache::class)->lock($lockKey, 10)->acquire(); - $job = m::mock(Job::class); - - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(false); - $job->expects('isDeletedOrReleased')->andReturn(false); - $job->expects('delete'); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize($command), ]); + $this->assertTrue($job->isDeleted()); + $this->assertFalse(SkipOverlappingTestJob::$handled); } @@ -114,17 +106,14 @@ public function testCanShareKeyAcrossJobs(): void $lockKey = (new WithoutOverlapping)->shared()->getLockKey(new OverlappingTestJobWithSharedKeyTwo); $this->app->get(Cache::class)->lock($lockKey, 10)->acquire(); - $job = m::mock(Job::class); - - $job->expects('release'); - $job->expects('hasFailed')->andReturn(false); - $job->expects('isReleased')->times(2)->andReturn(true); - $job->expects('isDeletedOrReleased')->andReturn(true); + $job = new FakeJob; $instance->call($job, [ 'command' => serialize(new OverlappingTestJobWithSharedKeyOne), ]); + $this->assertTrue($job->isReleased()); + $this->assertFalse(OverlappingTestJobWithSharedKeyOne::$handled); } diff --git a/tests/Log/LogLoggerTest.php b/tests/Log/LogLoggerTest.php index 58b28e8cb6..4e2b3cf651 100644 --- a/tests/Log/LogLoggerTest.php +++ b/tests/Log/LogLoggerTest.php @@ -30,24 +30,26 @@ class LogLoggerTest extends TestCase { public function testMethodsPassErrorAdditionsToMonolog(): void { - $monolog = $this->mockMonolog(); - $monolog->expects('isHandling')->with('error')->andReturn(true); - $monolog->expects('error')->with('foo', []); + $monolog = new Monolog('test'); + $monolog->pushHandler($handler = new TestHandler); $writer = new Logger($monolog); $writer->error('foo'); + + $this->assertTrue($handler->hasErrorThatContains('foo')); + $this->assertSame([], $handler->getRecords()[0]->context); } public function testContextIsAddedToAllSubsequentLogs(): void { - $monolog = $this->mockMonolog(); + $monolog = new Monolog('test'); + $monolog->pushHandler($handler = new TestHandler); $writer = new Logger($monolog); $writer->withContext(['bar' => 'baz']); - $monolog->expects('isHandling')->with('error')->andReturn(true); - $monolog->expects('error')->with('foo', ['bar' => 'baz']); - $writer->error('foo'); + + $this->assertSame(['bar' => 'baz'], $handler->getRecords()[0]->context); } public function testNumericContextKeysArePreservedAndOverridden(): void @@ -67,35 +69,34 @@ public function testNumericContextKeysArePreservedAndOverridden(): void public function testContextIsFlushed(): void { - $monolog = $this->mockMonolog(); + $monolog = new Monolog('test'); + $monolog->pushHandler($handler = new TestHandler); $writer = new Logger($monolog); $writer->withContext(['bar' => 'baz']); $writer->withoutContext(); - $monolog->expects('isHandling')->with('error')->andReturn(true); - $monolog->expects('error')->with('foo', []); - $writer->error('foo'); + + $this->assertSame([], $handler->getRecords()[0]->context); } public function testContextKeysCanBeRemovedForSubsequentLogs(): void { - $monolog = $this->mockMonolog(); + $monolog = new Monolog('test'); + $monolog->pushHandler($handler = new TestHandler); $writer = new Logger($monolog); $writer->withContext(['bar' => 'baz', 'forget' => 'me']); $writer->withoutContext(['forget']); - $monolog->expects('isHandling')->with('error')->andReturn(true); - $monolog->expects('error')->with('foo', ['bar' => 'baz']); - $writer->error('foo'); + + $this->assertSame(['bar' => 'baz'], $handler->getRecords()[0]->context); } public function testLoggerFiresEventsDispatcher(): void { - $monolog = $this->mockMonolog(); - $monolog->expects('isHandling')->with('error')->andReturn(true); - $monolog->expects('error')->with('foo', []); + $monolog = new Monolog('test'); + $monolog->pushHandler(new TestHandler); $writer = new Logger($monolog, $events = new Dispatcher); $context = []; @@ -119,41 +120,45 @@ public function testListenShortcutFailsWithNoDispatcher(): void { $this->expectExceptionObject(new RuntimeException('Events dispatcher has not been set.')); - $writer = new Logger($this->mockMonolog()); + $writer = new Logger(new Monolog('test')); $writer->listen(function (): void { }); } public function testListenShortcut(): void { - $events = m::mock(DispatcherContract::class); - $writer = new Logger($this->mockMonolog(), $events); + $events = new Dispatcher; + $writer = new Logger(new Monolog('test'), $events); - $callback = function (): string { - return 'success'; - }; - $events->expects('listen')->with(MessageLogged::class, $callback); + $called = false; + $writer->listen(function () use (&$called): void { + $called = true; + }); + + $this->assertTrue($events->hasListeners(MessageLogged::class)); + + $events->dispatch(new MessageLogged('info', 'foo', [])); - $writer->listen($callback); + $this->assertTrue($called); } public function testComplexContextManipulation(): void { - $monolog = $this->mockMonolog(); + $monolog = new Monolog('test'); + $monolog->pushHandler($handler = new TestHandler); $writer = new Logger($monolog); $writer->withContext(['user_id' => 123, 'action' => 'login']); $writer->withContext(['ip' => '127.0.0.1', 'timestamp' => '1986-10-29']); $writer->withoutContext(['timestamp']); - $monolog->expects('isHandling')->with('info')->andReturn(true); - $monolog->expects('info')->with('User action', [ + $writer->info('User action'); + + $this->assertSame([ 'user_id' => 123, 'action' => 'login', 'ip' => '127.0.0.1', - ]); - - $writer->info('User action'); + ], $handler->getRecords()[0]->context); } public function testSkipsSerializationWhenLogLevelNotHandled(): void diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index c687233481..38081f48b2 100644 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -9,6 +9,7 @@ use Hypervel\Contracts\Queue\ShouldQueue; use Hypervel\Contracts\View\Factory as ViewFactory; use Hypervel\Contracts\View\View as ViewContract; +use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Mail\Events\MessageSending; use Hypervel\Mail\Events\MessageSent; use Hypervel\Mail\Mailable; @@ -17,6 +18,7 @@ use Hypervel\Mail\SendQueuedMailable; use Hypervel\Mail\Transport\ArrayTransport; use Hypervel\Support\HtmlString; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Support\Testing\Fakes\QueueFake; use Hypervel\Testbench\TestCase; use InvalidArgumentException; @@ -28,7 +30,7 @@ class MailMailerTest extends TestCase { public function testMailerSendSendsMessageWithProperViewContent(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); @@ -41,7 +43,7 @@ public function testMailerSendSendsMessageWithProperViewContent(): void public function testMailerCallbackCanInspectAndReplaceRenderedContent(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $renderedContent = null; @@ -59,7 +61,7 @@ public function testMailerCallbackCanInspectAndReplaceRenderedContent(): void public function testMailerSendSendsMessageWithCcAndBccRecipients(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); @@ -148,14 +150,7 @@ public function testMailerSendSendsMessageWithProperViewContentUsingHtmlMethod() public function testMailerSendSendsMessageWithProperPlainViewContent(): void { - $viewInterface = m::mock(ViewContract::class); - $viewInterface->expects('render') - ->andReturn('rendered.view'); - $viewInterface->expects('render') - ->andReturn('rendered.plain'); - - $view = m::mock(ViewFactory::class); - $view->expects('make')->times(2)->andReturn($viewInterface); + $view = $this->viewFactory('rendered.view', 'rendered.plain'); $mailer = new Mailer('array', $view, new ArrayTransport); @@ -184,14 +179,7 @@ public function testMailerSendSendsMessageWithProperPlainViewContent(): void public function testMailerSendSendsMessageWithProperPlainViewContentWhenExplicit(): void { - $viewInterface = m::mock(ViewContract::class); - $viewInterface->expects('render') - ->andReturn('rendered.view'); - $viewInterface->expects('render') - ->andReturn('rendered.plain'); - - $view = m::mock(ViewFactory::class); - $view->expects('make')->times(2)->andReturn($viewInterface); + $view = $this->viewFactory('rendered.view', 'rendered.plain'); $mailer = new Mailer('array', $view, new ArrayTransport); @@ -220,7 +208,7 @@ public function testMailerSendSendsMessageWithProperPlainViewContentWhenExplicit public function testToAllowsEmailAndName(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $sentMessage = $mailer->to('taylor@hypervel.org', 'Taylor Otwell')->send(new TestMail); @@ -233,10 +221,7 @@ public function testToAllowsEmailAndName(): void public function testMailerRejectsAddressesContainingLineBreaks(): void { - $renderedView = m::mock(ViewContract::class); - $renderedView->expects('render')->andReturn('rendered.view'); - $view = m::mock(ViewFactory::class); - $view->expects('make')->andReturn($renderedView); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $this->expectExceptionObject(new InvalidArgumentException('Email addresses may not contain line break characters.')); @@ -248,10 +233,7 @@ public function testMailerRejectsAddressesContainingLineBreaks(): void public function testMailerRejectsSymfonyAddressesContainingLineBreaks(): void { - $renderedView = m::mock(ViewContract::class); - $renderedView->expects('render')->andReturn('rendered.view'); - $view = m::mock(ViewFactory::class); - $view->expects('make')->andReturn($renderedView); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $this->expectExceptionObject(new MimeInvalidArgumentException('Email address contains control characters.')); @@ -263,7 +245,7 @@ public function testMailerRejectsSymfonyAddressesContainingLineBreaks(): void public function testGlobalFromIsRespectedOnAllMessages(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $mailer->alwaysFrom('hello@hypervel.org'); @@ -277,7 +259,7 @@ public function testGlobalFromIsRespectedOnAllMessages(): void public function testGlobalReplyToIsRespectedOnAllMessages(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $mailer->alwaysReplyTo('taylor@hypervel.org', 'Taylor Otwell'); @@ -291,7 +273,7 @@ public function testGlobalReplyToIsRespectedOnAllMessages(): void public function testGlobalToIsRespectedOnAllMessages(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $mailer->alwaysTo('taylor@hypervel.org', 'Taylor Otwell'); @@ -319,7 +301,7 @@ public function testGlobalToIsRespectedOnAllMessages(): void public function testGlobalReturnPathIsRespectedOnAllMessages(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $mailer = new Mailer('array', $view, new ArrayTransport); $mailer->alwaysReturnPath('taylorotwell@gmail.com'); @@ -333,24 +315,23 @@ public function testGlobalReturnPathIsRespectedOnAllMessages(): void public function testEventsAreDispatched(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); - $events = m::mock(Dispatcher::class); - $events->expects('hasListeners')->with(MessageSending::class)->andReturn(true); - $events->expects('until')->with(m::type(MessageSending::class)); - $events->expects('hasListeners')->with(MessageSent::class)->andReturn(true); - $events->expects('dispatch')->with(m::type(MessageSent::class)); + $events = new EventFake(new EventDispatcher); $mailer = new Mailer('array', $view, new ArrayTransport, $events); $mailer->send('foo', ['data'], function (Message $message): void { $message->to('taylor@hypervel.org')->from('hello@hypervel.org'); }); + + $events->assertDispatchedOnce(MessageSending::class); + $events->assertDispatchedOnce(MessageSent::class); } public function testEventsAreSkippedWhenNoListenersAreRegistered(): void { - $view = $this->mockView(); + $view = $this->viewFactory('rendered.view'); $events = m::mock(Dispatcher::class); $events->expects('hasListeners')->with(MessageSending::class)->andReturn(false); @@ -398,18 +379,19 @@ public function testSendQueuedMailableReturnsNull(): void } /** - * Create a view factory expecting one rendered view. + * Create a view factory that renders the given contents in order. */ - protected function mockView(): ViewFactory + protected function viewFactory(string ...$rendered): ViewFactory { - $viewInterface = m::mock(ViewContract::class); - $viewInterface->expects('render') - ->andReturn('rendered.view'); + $factory = m::mock(ViewFactory::class); - $view = m::mock(ViewFactory::class); - $view->expects('make')->andReturn($viewInterface); + foreach ($rendered as $contents) { + $view = m::mock(ViewContract::class); + $view->expects('render')->andReturn($contents); + $factory->expects('make')->andReturn($view); + } - return $view; + return $factory; } } diff --git a/tests/Mail/MailSesV2TransportTest.php b/tests/Mail/MailSesV2TransportTest.php index e9aff24f4c..873454d81a 100644 --- a/tests/Mail/MailSesV2TransportTest.php +++ b/tests/Mail/MailSesV2TransportTest.php @@ -69,10 +69,7 @@ public function testSend(): void $message->getHeaders()->addTextHeader('X-SES-LIST-MANAGEMENT-OPTIONS', 'contactListName=TestList;topicName=TestTopic'); $client = m::mock(SesV2Client::class); - $sesResult = m::mock(); - $sesResult->expects('get') - ->with('MessageId') - ->andReturn('ses-message-id'); + $sesResult = new Result(['MessageId' => 'ses-message-id']); $client->expects('sendEmail') ->with(m::on(function (array $arg): bool { return $arg['Source'] === 'myself@example.com' @@ -119,10 +116,7 @@ public function testSendWithTenantName(): void $message->getHeaders()->addTextHeader('X-SES-TENANT-NAME', 'my-tenant'); $client = m::mock(SesV2Client::class); - $sesResult = m::mock(); - $sesResult->expects('get') - ->with('MessageId') - ->andReturn('ses-message-id'); + $sesResult = new Result(['MessageId' => 'ses-message-id']); $client->expects('sendEmail') ->with(m::on(function (array $arg): bool { return $arg['TenantName'] === 'my-tenant'; @@ -164,10 +158,7 @@ public function testSendWithoutTenantNameDoesNotSetTheOption(): void $message->to('me@example.com'); $client = m::mock(SesV2Client::class); - $sesResult = m::mock(); - $sesResult->expects('get') - ->with('MessageId') - ->andReturn('ses-message-id'); + $sesResult = new Result(['MessageId' => 'ses-message-id']); $client->expects('sendEmail') ->with(m::on(function (array $arg): bool { return ! array_key_exists('TenantName', $arg); diff --git a/tests/Notifications/Fixtures/ChannelSpy.php b/tests/Notifications/Fixtures/ChannelSpy.php new file mode 100644 index 0000000000..296f7e475a --- /dev/null +++ b/tests/Notifications/Fixtures/ChannelSpy.php @@ -0,0 +1,30 @@ +sent[] = [$notifiable, $notification]; + + if ($this->exception) { + throw $this->exception; + } + + return $this->response; + } +} diff --git a/tests/Notifications/NotificationBroadcastChannelTest.php b/tests/Notifications/NotificationBroadcastChannelTest.php index a47259a6c6..7f7472a326 100644 --- a/tests/Notifications/NotificationBroadcastChannelTest.php +++ b/tests/Notifications/NotificationBroadcastChannelTest.php @@ -14,6 +14,7 @@ use Hypervel\Tests\TestCase; use LogicException; use Mockery as m; +use stdClass; class NotificationBroadcastChannelTest extends TestCase { @@ -21,7 +22,7 @@ public function testDatabaseChannelCreatesDatabaseRecordWithProperData(): void { $notification = new NotificationBroadcastChannelTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = new stdClass; $events = m::mock(Dispatcher::class); $events->expects('dispatch')->with(m::type(BroadcastNotificationCreated::class)); @@ -97,7 +98,7 @@ public function testNotificationIsBroadcastedWithCustomEventName(): void { $notification = new CustomEventNameTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = new stdClass; $event = new BroadcastNotificationCreated( $notifiable, @@ -114,7 +115,7 @@ public function testNotificationIsBroadcastedWithCustomDataType(): void { $notification = new CustomEventNameTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = new stdClass; $event = new BroadcastNotificationCreated( $notifiable, @@ -131,7 +132,7 @@ public function testNotificationIsBroadcastedNow(): void { $notification = new TestNotificationBroadCastedNow; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = new stdClass; $events = m::mock(Dispatcher::class); $events->expects('dispatch')->with(m::on(function (BroadcastNotificationCreated $event): bool { @@ -145,7 +146,7 @@ public function testNotificationIsBroadcastedWithCustomAdditionalPayload(): void { $notification = new CustomBroadcastWithTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = new stdClass; $event = new BroadcastNotificationCreated( $notifiable, diff --git a/tests/Notifications/NotificationChannelManagerTest.php b/tests/Notifications/NotificationChannelManagerTest.php index f05af31b1f..6f26433876 100644 --- a/tests/Notifications/NotificationChannelManagerTest.php +++ b/tests/Notifications/NotificationChannelManagerTest.php @@ -6,6 +6,7 @@ use Closure; use Exception; +use Hypervel\Bus\Dispatcher as BusDispatcher; use Hypervel\Bus\Queueable; use Hypervel\Config\Repository as ConfigRepository; use Hypervel\Container\Container; @@ -15,6 +16,7 @@ use Hypervel\Contracts\Notifications\Dispatcher as NotificationDispatcherContract; use Hypervel\Contracts\Notifications\Factory as NotificationFactoryContract; use Hypervel\Contracts\Queue\ShouldQueue; +use Hypervel\Events\Dispatcher as EventsDispatcher; use Hypervel\Foundation\Application; use Hypervel\Notifications\ChannelManager; use Hypervel\Notifications\Channels\MailChannel; @@ -30,12 +32,16 @@ use Hypervel\Queue\InteractsWithQueue; use Hypervel\Queue\QueueRoutes; use Hypervel\Queue\SerializesModels; +use Hypervel\Support\Testing\Fakes\BusFake; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Support\Testing\Fakes\NotificationFake; +use Hypervel\Tests\Notifications\Fixtures\ChannelSpy; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Laravel\SerializableClosure\SerializableClosure; use Mockery as m; use Mockery\MockInterface; +use stdClass; class NotificationChannelManagerTest extends TestCase { @@ -100,17 +106,21 @@ public function testNotificationCanBeDispatchedToDriver(): void { $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventFake(new EventsDispatcher); + $container->instance(Dispatcher::class, $events); $manager = m::mock(ChannelManager::class . '[driver]', [$container]); - $driver = m::mock(); + $driver = new ChannelSpy; $manager->expects('driver')->andReturn($driver); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $driver->expects('send'); - $events->expects('dispatch')->with(m::type(NotificationDelivered::class)); - $events->expects('dispatch')->with(m::type(NotificationSent::class)); - $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + $manager->send($notifiable = new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + + $this->assertCount(1, $driver->sent); + $this->assertSame($notifiable, $driver->sent[0][0]); + $this->assertInstanceOf(NotificationChannelManagerTestNotification::class, $driver->sent[0][1]); + $events->assertDispatchedOnce(NotificationSending::class); + $events->assertDispatchedOnce(NotificationDelivered::class); + $events->assertDispatchedOnce(NotificationSent::class); } public function testChannelCanBeResolvedUsingBackedEnum(): void @@ -136,72 +146,98 @@ public function testNotificationNotSentOnHalt(): void { $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventsDispatcher; + $container->instance(Dispatcher::class, $events); + $calls = 0; + $events->listen(NotificationSending::class, function () use (&$calls): bool { + return $calls++ > 0; + }); + $skipped = []; + $events->listen(NotificationSkipped::class, function (NotificationSkipped $event) use (&$skipped): void { + $skipped[] = $event; + }); + $delivered = []; + $events->listen(NotificationDelivered::class, function (NotificationDelivered $event) use (&$delivered): void { + $delivered[] = $event; + }); + $sent = []; + $events->listen(NotificationSent::class, function (NotificationSent $event) use (&$sent): void { + $sent[] = $event; + }); $manager = m::mock(ChannelManager::class . '[driver]', [$container]); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(false); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $driver = m::mock(); + $driver = new ChannelSpy; $manager->expects('driver')->andReturn($driver); - $driver->expects('send'); - $events->expects('dispatch')->with(m::type(NotificationSkipped::class)); - $events->expects('dispatch')->with(m::type(NotificationDelivered::class)); - $events->expects('dispatch')->with(m::type(NotificationSent::class)); $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestNotificationWithTwoChannels); + + $this->assertCount(1, $driver->sent); + $this->assertCount(1, $skipped); + $this->assertCount(1, $delivered); + $this->assertCount(1, $sent); } public function testNotificationNotSentWhenCancelled(): void { $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventFake(new EventsDispatcher); + $container->instance(Dispatcher::class, $events); $manager = m::mock(ChannelManager::class . '[driver]', [$container]); $manager->shouldNotReceive('driver'); - $events->expects('dispatch')->with(m::type(NotificationSkipped::class)); - $events->shouldNotReceive('dispatch')->with(m::type(NotificationSent::class)); $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestCancelledNotification); + + $events->assertDispatchedOnce(NotificationSkipped::class); + $events->assertNotDispatched(NotificationSent::class); } public function testNotificationSentWhenNotCancelled(): void { $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventFake(new EventsDispatcher); + $container->instance(Dispatcher::class, $events); $manager = m::mock(ChannelManager::class . '[driver]', [$container]); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $driver = m::mock(); + $driver = new ChannelSpy; $manager->expects('driver')->andReturn($driver); - $driver->expects('send'); - $events->expects('dispatch')->with(m::type(NotificationDelivered::class)); - $events->expects('dispatch')->with(m::type(NotificationSent::class)); $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestNotCancelledNotification); + + $this->assertCount(1, $driver->sent); + $events->assertDispatchedOnce(NotificationSending::class); + $events->assertDispatchedOnce(NotificationDelivered::class); + $events->assertDispatchedOnce(NotificationSent::class); } public function testNotificationNotSentWhenFailed(): void { - $this->expectException(Exception::class); - $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventFake(new EventsDispatcher); + $container->instance(Dispatcher::class, $events); $manager = m::mock(ChannelManager::class . '[driver]', [$container]); - $driver = m::mock(); + $driver = new ChannelSpy; + $driver->exception = $exception = new Exception; $manager->expects('driver')->andReturn($driver); - $driver->expects('send')->andThrow(new Exception); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $events->expects('dispatch')->with(m::type(NotificationFailed::class)); - $events->shouldReceive('dispatch')->never()->with(m::type(NotificationDelivered::class)); - $events->shouldReceive('dispatch')->never()->with(m::type(NotificationSent::class)); - $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + try { + $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + $this->fail('The channel exception was not thrown.'); + } catch (Exception $caught) { + $this->assertSame($exception, $caught); + } + + $events->assertDispatchedOnce(NotificationSending::class); + $events->assertDispatchedOnce(NotificationFailed::class); + $events->assertNotDispatched(NotificationDelivered::class); + $events->assertNotDispatched(NotificationSent::class); } public function testNotificationFailedDispatchedOnlyOnceWhenMultipleFailed(): void { $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventsDispatcher; + $container->instance(Dispatcher::class, $events); $manager = new ChannelManager($container); $manager->extend('test', function (): object { return new class { @@ -221,53 +257,62 @@ public function send(mixed $notifiable, Notification $notification): void }; }); - // The provider owns the listener; sending must not register additional listeners. - // NotificationFailedEventTest covers channel-owned failure deduplication through the real provider. - $events->shouldNotReceive('listen'); - $events->expects('until')->times(3)->with(m::type(NotificationSending::class))->andReturn(true); - $events->expects('dispatch')->with(m::type(NotificationFailed::class)); - $events->expects('dispatch')->times(2)->with(m::type(NotificationDelivered::class)); - $events->expects('dispatch')->times(2)->with(m::type(NotificationSent::class)); - - $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); - $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + $failed = 0; + $events->listen(NotificationFailed::class, function () use (&$failed): void { + ++$failed; + }); + $delivered = 0; + $events->listen(NotificationDelivered::class, function () use (&$delivered): void { + ++$delivered; + }); + $sent = 0; + $events->listen(NotificationSent::class, function () use (&$sent): void { + ++$sent; + }); $this->expectExceptionObject(new Exception('Channel failed.')); - $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + try { + $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + } finally { + $this->assertSame(2, $sent); + $this->assertSame(2, $delivered); + $this->assertSame(1, $failed); + // The provider owns the failure listener; sending must not register additional listeners. + // NotificationFailedEventTest covers channel-owned failure deduplication through the real provider. + $this->assertCount(1, $events->getListeners(NotificationFailed::class)); + } } public function testNotificationCanBeQueued(): void { $container = $this->getContainer(); - $container->instance(QueueRoutes::class, $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->andReturn(null); - $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance(QueueRoutes::class, $queueRoutes = new QueueRoutes); $container->instance('queue.routes', $queueRoutes); - $container->make(BusDispatcherContract::class) - ->expects('dispatch') - ->with(m::type(SendQueuedNotifications::class)); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestQueuedNotification); + + $bus->assertDispatched(SendQueuedNotifications::class); } public function testSendQueuedNotificationsCanBeOverrideViaContainer(): void { $container = $this->getContainer(); - $container->instance(QueueRoutes::class, $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->andReturn(null); - $queueRoutes->expects('getConnection')->andReturn(null); + $container->instance(QueueRoutes::class, $queueRoutes = new QueueRoutes); $container->instance('queue.routes', $queueRoutes); - $container->make(BusDispatcherContract::class) - ->expects('dispatch') - ->with(m::type(TestSendQueuedNotifications::class)); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); $container->bind(SendQueuedNotifications::class, TestSendQueuedNotifications::class); - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestQueuedNotification); + + $bus->assertDispatched(TestSendQueuedNotifications::class); } public function testQueuedNotificationForwardsMessageGroupFromMethodToQueueJob(): void @@ -278,20 +323,19 @@ public function testQueuedNotificationForwardsMessageGroupFromMethodToQueueJob() $notification->expects($this->exactly(2))->method('messageGroup')->willReturn($mockedMessageGroupId); $container = $this->getContainer(); - $container->instance(QueueRoutes::class, $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); + $container->instance(QueueRoutes::class, $queueRoutes = new QueueRoutes); $container->instance('queue.routes', $queueRoutes); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job) use ($mockedMessageGroupId): bool { - $this->assertEquals($mockedMessageGroupId, $job->messageGroup); - - return true; - }); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job) use ($mockedMessageGroupId): void { + $this->assertEquals($mockedMessageGroupId, $job->messageGroup); + }); } public function testQueuedNotificationForwardsMessageGroupFromPropertyOverridingMethodToQueueJob(): void @@ -304,20 +348,19 @@ public function testQueuedNotificationForwardsMessageGroupFromPropertyOverriding $notification->onGroup($mockedMessageGroupId); $container = $this->getContainer(); - $container->instance(QueueRoutes::class, $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); + $container->instance(QueueRoutes::class, $queueRoutes = new QueueRoutes); $container->instance('queue.routes', $queueRoutes); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job) use ($mockedMessageGroupId): bool { - $this->assertEquals($mockedMessageGroupId, $job->messageGroup); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - return true; - }); - - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job) use ($mockedMessageGroupId): void { + $this->assertEquals($mockedMessageGroupId, $job->messageGroup); + }); } public function testQueuedNotificationForwardsMessageGroupSetToQueueJob(): void @@ -328,21 +371,20 @@ public function testQueuedNotificationForwardsMessageGroupSetToQueueJob(): void ]; $container = $this->getContainer(); - $container->instance(QueueRoutes::class, $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); + $container->instance(QueueRoutes::class, $queueRoutes = new QueueRoutes); $container->instance('queue.routes', $queueRoutes); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job) use ($mockedMessageGroupSet): bool { - $this->assertEquals($mockedMessageGroupSet[$job->channels[0]], $job->messageGroup); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - return true; - }); - - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $notification = (new NotificationChannelManagerTestQueuedNotificationWithTwoChannels)->onGroup($mockedMessageGroupSet); $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job) use ($mockedMessageGroupSet): void { + $this->assertEquals($mockedMessageGroupSet[$job->channels[0]], $job->messageGroup); + }); } public function testQueuedNotificationForwardsMessageGroupSetFromClassToQueueJob(): void @@ -353,20 +395,19 @@ public function testQueuedNotificationForwardsMessageGroupSetFromClassToQueueJob ]; $container = $this->getContainer(); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job) use ($mockedMessageGroupSet): bool { - $this->assertEquals($mockedMessageGroupSet[$job->channels[0]], $job->messageGroup); + $container->instance('queue.routes', new QueueRoutes); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - return true; - }); - - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $notification = new NotificationChannelManagerTestQueuedNotificationWithMessageGroups; $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job) use ($mockedMessageGroupSet): void { + $this->assertEquals($mockedMessageGroupSet[$job->channels[0]], $job->messageGroup); + }); } public function testQueuedNotificationForwardsDeduplicatorToQueueJob(): void @@ -374,21 +415,17 @@ public function testQueuedNotificationForwardsDeduplicatorToQueueJob(): void $mockedDeduplicator = fn (string $payload, string $queue): string => 'deduplication-id-1'; $container = $this->getContainer(); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->andReturn(null); - $queueRoutes->expects('getConnection')->andReturn(null); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->withArgs(function (SendQueuedNotifications $job) use ($mockedDeduplicator): bool { - $this->assertInstanceOf(SerializableClosure::class, $job->deduplicator); - $this->assertEquals($mockedDeduplicator, $job->deduplicator->getClosure()); + $container->instance('queue.routes', new QueueRoutes); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - return true; - }); - - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $notification = (new NotificationChannelManagerTestQueuedNotification)->withDeduplicator($mockedDeduplicator); $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $job = $bus->dispatched(SendQueuedNotifications::class)->sole(); + $this->assertInstanceOf(SerializableClosure::class, $job->deduplicator); + $this->assertEquals($mockedDeduplicator, $job->deduplicator->getClosure()); } public function testQueuedNotificationForwardsArrayCallableDeduplicatorToQueueJob(): void @@ -423,92 +460,91 @@ public function testQueuedNotificationForwardsDeduplicatorSetToQueueJob(): void ]; $container = $this->getContainer(); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job) use ($mockedDeduplicatorSet): bool { - $this->assertInstanceOf(SerializableClosure::class, $job->deduplicator); - $this->assertEquals($mockedDeduplicatorSet[$job->channels[0]], $job->deduplicator->getClosure()); - - $restored = unserialize(serialize($job)); - $this->assertSame( - $mockedDeduplicatorSet[$job->channels[0]]('payload', 'queue'), - ($restored->deduplicator)('payload', 'queue') - ); + $container->instance('queue.routes', new QueueRoutes); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - return true; - }); - - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $notification = (new NotificationChannelManagerTestQueuedNotificationWithTwoChannels)->withDeduplicator($mockedDeduplicatorSet); $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job) use ($mockedDeduplicatorSet): void { + $this->assertInstanceOf(SerializableClosure::class, $job->deduplicator); + $this->assertEquals($mockedDeduplicatorSet[$job->channels[0]], $job->deduplicator->getClosure()); + + $restored = unserialize(serialize($job)); + $this->assertSame( + $mockedDeduplicatorSet[$job->channels[0]]('payload', 'queue'), + ($restored->deduplicator)('payload', 'queue') + ); + }); } public function testQueuedNotificationForwardsDeduplicatorSetFromClassToQueueJob(): void { $container = $this->getContainer(); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job): bool { - $this->assertEquals($job->notification->deduplicatorResults[$job->channels[0]], call_user_func($job->deduplicator, 'payload', 'queue')); - - return true; - }); + $container->instance('queue.routes', new QueueRoutes); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $notification = new NotificationChannelManagerTestQueuedNotificationWithDeduplicators; $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job): void { + $this->assertEquals( + $job->notification->deduplicatorResults[$job->channels[0]], + call_user_func($job->deduplicator, 'payload', 'queue') + ); + }); } public function testQueuedNotificationForwardsDeduplicationIdMethodToQueueJob(): void { $container = $this->getContainer(); - $container->instance('queue.routes', $queueRoutes = m::mock(QueueRoutes::class)); - $queueRoutes->expects('getQueue')->times(2)->andReturn(null); - $queueRoutes->expects('getConnection')->times(2)->andReturn(null); - $container->make(BusDispatcherContract::class) - ->expects('dispatch')->times(2)->withArgs(function (SendQueuedNotifications $job): bool { - $this->assertInstanceOf(SerializableClosure::class, $job->deduplicator); - - $deduplicator = $job->deduplicator->getClosure(); - $this->assertSame( - $job->notification->deduplicationId('payload', 'queue'), - $deduplicator('payload', 'queue') - ); + $container->instance('queue.routes', new QueueRoutes); + $container->instance(BusDispatcherContract::class, $bus = new BusFake(new BusDispatcher(new Container))); - return true; - }); - - $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $manager = new ChannelManager($container); $notification = new NotificationChannelManagerTestQueuedNotificationWithDeduplicationId; $manager->send([new NotificationChannelManagerTestNotifiable], $notification); + + $jobs = $bus->dispatched(SendQueuedNotifications::class); + $this->assertCount(2, $jobs); + $jobs->each(function (SendQueuedNotifications $job): void { + $this->assertInstanceOf(SerializableClosure::class, $job->deduplicator); + $this->assertSame( + $job->notification->deduplicationId('payload', 'queue'), + ($job->deduplicator->getClosure())('payload', 'queue') + ); + }); } public function testAfterSendingMethodAfterSendingNotification(): void { $container = $this->getContainer(); - $events = $container->make(Dispatcher::class); + $events = new EventFake(new EventsDispatcher); + $container->instance(Dispatcher::class, $events); $manager = m::mock(ChannelManager::class . '[driver]', [$container]); - $driver = m::mock(); + $driver = new ChannelSpy; $manager->expects('driver')->andReturn($driver); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $response = m::mock(); - $driver->expects('send')->andReturn($response); - $events->expects('dispatch')->with(m::type(NotificationDelivered::class)); - $events->expects('dispatch')->with(m::type(NotificationSent::class)); + $driver->response = $response = new stdClass; $manager->send($notifiable = new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerWithAfterSendingMethodNotification); $this->assertSame($notifiable, NotificationChannelManagerWithAfterSendingMethodNotification::$afterSendingNotifiable); $this->assertSame('test', NotificationChannelManagerWithAfterSendingMethodNotification::$afterSendingChannel); $this->assertSame($response, NotificationChannelManagerWithAfterSendingMethodNotification::$afterSendingResponse); + + $events->assertDispatchedOnce(NotificationSending::class); + $events->assertDispatchedOnce(NotificationDelivered::class); + $events->assertDispatchedOnce(NotificationSent::class); } /** @@ -812,4 +848,11 @@ enum NotificationChannelManagerTestChannelEnum: string class NotificationChannelManagerTestCustomChannel { + /** + * Send the given notification. + */ + public function send(mixed $notifiable, Notification $notification): mixed + { + return null; + } } diff --git a/tests/Notifications/NotificationDatabaseChannelTest.php b/tests/Notifications/NotificationDatabaseChannelTest.php index 6aafd50b39..f1143c3bd8 100644 --- a/tests/Notifications/NotificationDatabaseChannelTest.php +++ b/tests/Notifications/NotificationDatabaseChannelTest.php @@ -5,6 +5,7 @@ namespace Hypervel\Tests\Notifications; use Hypervel\Database\Eloquent\Model; +use Hypervel\Notifications\AnonymousNotifiable; use Hypervel\Notifications\Channels\DatabaseChannel; use Hypervel\Notifications\Messages\DatabaseMessage; use Hypervel\Notifications\Notification; @@ -18,7 +19,7 @@ public function testDatabaseChannelCreatesDatabaseRecordWithProperData(): void { $notification = new NotificationDatabaseChannelTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = m::mock(AnonymousNotifiable::class); $notifiable->expects('routeNotificationFor->create')->with([ 'id' => '1', @@ -35,7 +36,7 @@ public function testCorrectPayloadIsSentToDatabase(): void { $notification = new NotificationDatabaseChannelTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = m::mock(AnonymousNotifiable::class); $notifiable->expects('routeNotificationFor->create')->with([ 'id' => '1', @@ -55,7 +56,7 @@ public function testCustomizeTypeIsSentToDatabase(): void $notification = new NotificationDatabaseChannelCustomizeTypeTestNotification; $notification->id = '1'; - $notifiable = m::mock(); + $notifiable = m::mock(AnonymousNotifiable::class); $notifiable->expects('routeNotificationFor->create')->with([ 'id' => '1', diff --git a/tests/Notifications/NotificationSenderTest.php b/tests/Notifications/NotificationSenderTest.php index fe10e49222..3fae1ba6fe 100644 --- a/tests/Notifications/NotificationSenderTest.php +++ b/tests/Notifications/NotificationSenderTest.php @@ -5,12 +5,14 @@ namespace Hypervel\Tests\Notifications; use Closure; +use Hypervel\Bus\Dispatcher as BusDispatcher; use Hypervel\Bus\Queueable; use Hypervel\Config\Repository as Config; use Hypervel\Container\Container; use Hypervel\Contracts\Bus\Dispatcher as BusDispatcherContract; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Contracts\Queue\ShouldQueue; +use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Notifications\AnonymousNotifiable; use Hypervel\Notifications\ChannelManager; use Hypervel\Notifications\Events\NotificationDelivered; @@ -25,6 +27,9 @@ use Hypervel\Queue\Attributes\Delay; use Hypervel\Queue\Attributes\Queue; use Hypervel\Queue\QueueRoutes; +use Hypervel\Support\Testing\Fakes\BusFake; +use Hypervel\Support\Testing\Fakes\EventFake; +use Hypervel\Tests\Notifications\Fixtures\ChannelSpy; use Hypervel\Tests\TestCase; use Mockery as m; use PHPUnit\Framework\Attributes\TestWith; @@ -218,179 +223,168 @@ public function testThrowingAfterSendingCallbackIsNotRelabeledAsDeliveryFailure( public function testItCanSendQueuedNotificationsWithAStringVia(): void { - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->andReturn(app()); - $manager->expects('resolveQueueFromQueueRoute')->andReturn(null); - $manager->expects('resolveConnectionFromQueueRoute')->andReturn(null); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch'); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); + + $bus->assertDispatched(SendQueuedNotifications::class); } public function testItCanSendQueuedNotificationsWithAnArrayVia(): void { - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->times(2)->andReturn(app()); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->queue === 'dummy' && $job->channels === ['database'] && $job->connection === 'redis'; - }); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->queue === 'dummy' && $job->channels === ['mail'] && $job->connection === 'redis'; - }); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyQueuedNotificationWithArrayVia); + + $bus->assertDispatchedTimes(SendQueuedNotifications::class, 2); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->queue === 'dummy' && $job->channels === ['database'] && $job->connection === 'redis'; + }); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->queue === 'dummy' && $job->channels === ['mail'] && $job->connection === 'redis'; + }); } public function testItCanSendNotificationsWithAnEmptyStringVia(): void { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); - $bus = m::mock(BusDispatcherContract::class); - $bus->shouldNotReceive('dispatch'); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $events->shouldNotReceive('dispatch'); $sender = new NotificationSender($manager, $bus, $events); $sender->sendNow($notifiable, new DummyNotificationWithEmptyStringVia); + + $bus->assertNothingDispatched(); } public function testItCannotSendNotificationsViaDatabaseForAnonymousNotifiables(): void { $notifiable = new AnonymousNotifiable; $manager = m::mock(ChannelManager::class); - $bus = m::mock(BusDispatcherContract::class); - $bus->shouldNotReceive('dispatch'); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $events->shouldNotReceive('dispatch'); $sender = new NotificationSender($manager, $bus, $events); $sender->sendNow($notifiable, new DummyNotificationWithDatabaseVia); + + $bus->assertNothingDispatched(); } public function testItCanSendQueuedNotificationsThroughMiddleware(): void { - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->andReturn(app()); - $manager->expects('resolveQueueFromQueueRoute')->andReturn(null); - $manager->expects('resolveConnectionFromQueueRoute')->andReturn(null); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->middleware[0] instanceof TestNotificationMiddleware; - }); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyNotificationWithMiddleware); + + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return ($job->middleware[0] ?? null) instanceof TestNotificationMiddleware; + }); } public function testItCanSendQueuedMultiChannelNotificationsThroughDifferentMiddleware(): void { - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->times(3)->andReturn(app()); - $manager->expects('resolveQueueFromQueueRoute')->times(3)->andReturn(null); - $manager->expects('resolveConnectionFromQueueRoute')->times(3)->andReturn(null); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->middleware[0] instanceof TestMailNotificationMiddleware; - }); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->middleware[0] instanceof TestDatabaseNotificationMiddleware; - }); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return empty($job->middleware); - }); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyMultiChannelNotificationWithConditionalMiddleware); + + $bus->assertDispatchedTimes(SendQueuedNotifications::class, 3); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return ($job->middleware[0] ?? null) instanceof TestMailNotificationMiddleware; + }); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return ($job->middleware[0] ?? null) instanceof TestDatabaseNotificationMiddleware; + }); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return empty($job->middleware); + }); } public function testItCanSendQueuedWithViaConnectionsNotifications(): void { $notifiable = new AnonymousNotifiable; - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->times(2)->andReturn(app()); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->connection === 'sync' && $job->channels === ['database'] && $job->queue === 'dummy'; - }); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->connection === 'redis' && $job->channels === ['mail'] && $job->queue === 'dummy'; - }); + $manager = $this->getManager(); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyNotificationWithViaConnections); + + $bus->assertDispatchedTimes(SendQueuedNotifications::class, 2); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->connection === 'sync' && $job->channels === ['database'] && $job->queue === 'dummy'; + }); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->connection === 'redis' && $job->channels === ['mail'] && $job->queue === 'dummy'; + }); } public function testItCanSendQueuedWithViaQueuesNotifications(): void { $notifiable = new AnonymousNotifiable; - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->times(2)->andReturn(app()); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->queue === 'dummy' && $job->channels === ['database'] && $job->connection === 'redis'; - }); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->queue === 'admin_notifications' && $job->channels === ['mail'] && $job->connection === 'redis'; - }); + $manager = $this->getManager(); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyNotificationWithViaQueues); + + $bus->assertDispatchedTimes(SendQueuedNotifications::class, 2); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->queue === 'dummy' && $job->channels === ['database'] && $job->connection === 'redis'; + }); + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->queue === 'admin_notifications' && $job->channels === ['mail'] && $job->connection === 'redis'; + }); } public function testItCanSendQueuedNotificationsWithQueueRoute(): void { $notifiable = new AnonymousNotifiable; - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->andReturn(app()); - $manager->expects('resolveQueueFromQueueRoute')->andReturn('notification-queue'); - $manager->expects('resolveConnectionFromQueueRoute')->andReturn('notification-connection'); + $manager = $this->getManager([ + DummyQueuedNotificationWithStringVia::class => ['notification-queue', 'notification-connection'], + ]); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(function (SendQueuedNotifications $job): bool { - return $job->queue === 'notification-queue' && $job->channels === ['mail'] && $job->connection === 'notification-connection'; - }); + $bus = new BusFake(new BusDispatcher(new Container)); $events = m::mock(Dispatcher::class); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); + + $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { + return $job->queue === 'notification-queue' && $job->channels === ['mail'] && $job->connection === 'notification-connection'; + }); } #[TestWith([null, 'cloud'])] @@ -527,17 +521,19 @@ public function testItCanSendQueuedNotificationsWithDelayProperty(): void public function testNotificationFailedSentWithoutHttpTransportException(): void { $notifiable = new AnonymousNotifiable; - $manager = m::mock(ChannelManager::class); - $manager->expects('driver')->andReturn($driver = m::mock()); + $container = new Container; + $container->instance('config', new Config(['app' => ['name' => 'Name', 'logo' => 'Logo']])); + $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $driver = new ChannelSpy; $response = m::mock(ResponseInterface::class); - $exception = new HttpTransportException('Transport error', $response); - $driver->expects('send')->andThrow($exception); + $driver->exception = $exception = new HttpTransportException('Transport error', $response); + $manager->expects('driver')->andReturn($driver); $bus = m::mock(BusDispatcherContract::class); - $events = $this->mockEventDispatcher(); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $events->expects('dispatch')->withArgs(function (object $event): bool { - return $event instanceof NotificationFailed && $event->data['exception']::class === TransportException::class; + $events = new EventDispatcher; + $failed = null; + $events->listen(NotificationFailed::class, function (NotificationFailed $event) use (&$failed): void { + $failed = $event; }); $sender = new NotificationSender($manager, $bus, $events); @@ -549,26 +545,30 @@ public function testNotificationFailedSentWithoutHttpTransportException(): void } catch (TransportException $caught) { $this->assertSame($exception, $caught); } + + $this->assertSame(TransportException::class, $failed->data['exception']::class); } public function testItPreservesNotificationStateMutatedInViaMethod(): void { $notifiable = new AnonymousNotifiable; - $manager = m::mock(ChannelManager::class); - $manager->expects('driver')->andReturn($driver = m::mock()); - $driver->expects('send')->withArgs(function (AnonymousNotifiable $notifiable, DummyNotificationWithViaMutation $notification): bool { - return $notification->channelData === 'default'; - }); + $container = new Container; + $container->instance('config', new Config(['app' => ['name' => 'Name', 'logo' => 'Logo']])); + $manager = m::mock(ChannelManager::class . '[driver]', [$container]); + $driver = new ChannelSpy; + $manager->expects('driver')->andReturn($driver); $bus = m::mock(BusDispatcherContract::class); - $events = $this->mockEventDispatcher(); - $events->expects('until')->with(m::type(NotificationSending::class))->andReturn(true); - $events->expects('dispatch')->with(m::type(NotificationDelivered::class)); - $events->expects('dispatch')->with(m::type(NotificationSent::class)); + $events = new EventFake(new EventDispatcher); $sender = new NotificationSender($manager, $bus, $events); $sender->sendNow($notifiable, new DummyNotificationWithViaMutation); + + $this->assertSame('default', $driver->sent[0][1]->channelData); + $events->assertDispatched(NotificationSending::class); + $events->assertDispatched(NotificationDelivered::class); + $events->assertDispatched(NotificationSent::class); } public function testItQueueOverridesQueueAttribute(): void @@ -587,20 +587,18 @@ public function via(mixed $notifiable): string $notification->onQueue('manual-queue'); - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->andReturn(app()); - $manager->expects('resolveConnectionFromQueueRoute')->andReturn(null); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); $events = m::mock(Dispatcher::class); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(fn (SendQueuedNotifications $job): bool => $job->queue === 'manual-queue'); + $bus = new BusFake(new BusDispatcher(new Container)); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, $notification); + + $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'manual-queue'); } public function testItQueueAttributeIsUsedWhenOnQueueIsNotCalled(): void @@ -617,20 +615,18 @@ public function via(mixed $notifiable): string } }; - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->andReturn(app()); - $manager->expects('resolveConnectionFromQueueRoute')->andReturn(null); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); $events = m::mock(Dispatcher::class); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(fn (SendQueuedNotifications $job): bool => $job->queue === 'attribute-queue'); + $bus = new BusFake(new BusDispatcher(new Container)); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, $notification); + + $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'attribute-queue'); } public function testItConstructorOverrideTakesPrecedenceOverQueueAttribute(): void @@ -655,20 +651,18 @@ public function via(mixed $notifiable): string } }; - $notifiable = m::mock(Notifiable::class); - $manager = m::mock(ChannelManager::class); - $manager->expects('getContainer')->andReturn(app()); - $manager->expects('resolveConnectionFromQueueRoute')->andReturn(null); + $notifiable = new DummyNotifiable; + $manager = $this->getManager(); $events = m::mock(Dispatcher::class); - $bus = m::mock(BusDispatcherContract::class); - $bus->expects('dispatch') - ->withArgs(fn (SendQueuedNotifications $job): bool => $job->queue === 'constructor-override-queue'); + $bus = new BusFake(new BusDispatcher(new Container)); $sender = new NotificationSender($manager, $bus, $events); $sender->send($notifiable, $notification); + + $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'constructor-override-queue'); } public function testNotificationEventsAreSkippedWhenNoListenersAreRegistered(): void @@ -719,6 +713,31 @@ public function testNotificationFailedIsSkippedWithoutListeners(): void } } + /** + * Create a channel manager with the given notification queue routes. + * + * @param array $queueRoutes + */ + protected function getManager(array $queueRoutes = []): ChannelManager + { + $container = new Container; + $container->instance('config', new Config(['app' => ['name' => 'Name', 'logo' => 'Logo']])); + + if ($queueRoutes) { + $routes = new QueueRoutes; + + foreach ($queueRoutes as $class => [$queue, $connection]) { + $routes->set($class, $queue, $connection); + } + + $container->instance('queue.routes', $routes); + } + + Container::setInstance($container); + + return new ChannelManager($container); + } + /** * Create an event dispatcher that reports listeners by default. */ @@ -986,3 +1005,8 @@ public function via(mixed $notifiable): string return 'mail'; } } + +class DummyNotifiable +{ + use Notifiable; +} diff --git a/tests/Queue/FailoverQueueTest.php b/tests/Queue/FailoverQueueTest.php index 0e187ce2b1..95e6ad2ab3 100644 --- a/tests/Queue/FailoverQueueTest.php +++ b/tests/Queue/FailoverQueueTest.php @@ -91,7 +91,11 @@ public static function forwardedQueueOperations(): array public function testPushFailsOverOnException(UnitEnum|string|null $connection, ?string $expectedName): void { $queue = m::mock(QueueManager::class); - $events = m::mock(DispatcherContract::class); + $events = new EventDispatcher; + $failedOver = []; + $events->listen(QueueFailedOver::class, function (QueueFailedOver $event) use (&$failedOver): void { + $failedOver[] = $event; + }); $failover = new FailoverQueue($queue, $events, [ $connection, 'sync', @@ -103,11 +107,6 @@ public function testPushFailsOverOnException(UnitEnum|string|null $connection, ? $sync = m::mock(SyncQueue::class); $queue->expects('connection')->with('sync')->andReturn($sync); - $events->expects('hasListeners')->with(QueueFailedOver::class)->andReturnTrue(); - $events->expects('dispatch')->with(m::on( - fn (QueueFailedOver $event): bool => $event->connectionName === $expectedName, - )); - $redis->expects('push')->andReturnUsing( fn (): never => throw new Exception('error') ); @@ -115,6 +114,11 @@ public function testPushFailsOverOnException(UnitEnum|string|null $connection, ? $sync->expects('push'); $failover->push('some-job'); + + $this->assertCount(1, $failedOver); + $this->assertSame($expectedName, $failedOver[0]->connectionName); + $this->assertSame('some-job', $failedOver[0]->command); + $this->assertSame('error', $failedOver[0]->exception->getMessage()); } /** diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 9e4b0e5b6f..9c85f7a1d4 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -375,7 +375,7 @@ public function testFailureToCreatePayloadFromObject(): void $job = new stdClass; $job->invalid = "\xc3\x28"; - $queue = m::mock(Queue::class)->makePartial(); + $queue = new DatabaseQueue(m::mock(ConnectionResolverInterface::class), null, 'table', 'default'); $class = new ReflectionClass(Queue::class); $createPayload = $class->getMethod('createPayload'); @@ -389,7 +389,7 @@ public function testFailureToCreatePayloadFromArray(): void { $this->expectException(InvalidArgumentException::class); - $queue = m::mock(Queue::class)->makePartial(); + $queue = new DatabaseQueue(m::mock(ConnectionResolverInterface::class), null, 'table', 'default'); $class = new ReflectionClass(Queue::class); $createPayload = $class->getMethod('createPayload'); @@ -903,8 +903,9 @@ public function testBulkDefersAfterCommitJobsUntilTheTransactionCommits(): void public function testBuildDatabaseRecordWithPayloadAtTheEnd(): void { - $queue = m::mock(DatabaseQueue::class); - $record = $queue->buildDatabaseRecord('queue', 'any_payload', 0); + $queue = new DatabaseQueue(m::mock(ConnectionResolverInterface::class), null, 'table', 'default'); + $class = new ReflectionClass(DatabaseQueue::class); + $record = $class->getMethod('buildDatabaseRecord')->invoke($queue, 'queue', 'any_payload', 0); $this->assertArrayHasKey('payload', $record); $this->assertArrayHasKey('payload', array_slice($record, -1, 1, true)); } @@ -1058,7 +1059,7 @@ public function testTotalSize(): void $database = m::mock(ConnectionInterface::class); $resolver->expects('connection')->with(null)->andReturn($database); $queue = new TestDatabaseQueue($resolver, null, 'table', 'default', 1732502704); - $queue->setContainer(m::spy(Container::class)); + $queue->setContainer(new Container); $query = m::mock(Builder::class); $database->expects('table')->with('table')->andReturn($query); @@ -1073,7 +1074,7 @@ public function testTotalPendingSize(): void $database = m::mock(ConnectionInterface::class); $resolver->expects('connection')->with(null)->andReturn($database); $queue = new TestDatabaseQueue($resolver, null, 'table', 'default', 1732502704); - $queue->setContainer(m::spy(Container::class)); + $queue->setContainer(new Container); $query = m::mock(Builder::class); $database->expects('table')->with('table')->andReturn($query); @@ -1090,7 +1091,7 @@ public function testTotalDelayedSize(): void $database = m::mock(ConnectionInterface::class); $resolver->expects('connection')->with(null)->andReturn($database); $queue = new TestDatabaseQueue($resolver, null, 'table', 'default', 1732502704); - $queue->setContainer(m::spy(Container::class)); + $queue->setContainer(new Container); $query = m::mock(Builder::class); $database->expects('table')->with('table')->andReturn($query); @@ -1107,7 +1108,7 @@ public function testTotalReservedSize(): void $database = m::mock(ConnectionInterface::class); $resolver->expects('connection')->with(null)->andReturn($database); $queue = new TestDatabaseQueue($resolver, null, 'table', 'default', 1732502704); - $queue->setContainer(m::spy(Container::class)); + $queue->setContainer(new Container); $query = m::mock(Builder::class); $database->expects('table')->with('table')->andReturn($query); diff --git a/tests/Queue/QueueManagerTest.php b/tests/Queue/QueueManagerTest.php index 6c681e9e9f..96eac97d23 100644 --- a/tests/Queue/QueueManagerTest.php +++ b/tests/Queue/QueueManagerTest.php @@ -7,20 +7,23 @@ use Hypervel\Config\Repository as ConfigRepository; use Hypervel\Container\Container; use Hypervel\Contracts\Container\Container as ContainerContract; -use Hypervel\Contracts\Encryption\Encrypter; +use Hypervel\Contracts\Encryption\Encrypter as EncrypterContract; use Hypervel\Contracts\ObjectPool\Factory as PoolFactory; use Hypervel\Contracts\Queue\ClearableQueue; use Hypervel\Contracts\Queue\Queue; +use Hypervel\Encryption\Encrypter; use Hypervel\Events\Dispatcher; use Hypervel\ObjectPool\PoolManager; use Hypervel\Queue\ClearableQueuePoolProxy; use Hypervel\Queue\Connectors\ConnectorInterface; use Hypervel\Queue\Connectors\FailoverConnector; use Hypervel\Queue\Connectors\NullConnector; +use Hypervel\Queue\Connectors\SyncConnector; use Hypervel\Queue\Jobs\Job; use Hypervel\Queue\NullQueue; use Hypervel\Queue\QueueManager; use Hypervel\Queue\QueuePoolProxy; +use Hypervel\Queue\SyncQueue; use Hypervel\Tests\TestCase; use InvalidArgumentException; use Mockery as m; @@ -71,17 +74,14 @@ public function testDefaultConnectionCanBeResolved(): void $config->set('queue.connections.sync', ['driver' => 'sync']); $manager = new QueueManager($container); - $connector = m::mock(ConnectorInterface::class); - $queue = m::mock(Queue::class); - $queue->expects('setConnectionName')->with('sync')->andReturnSelf(); - $queue->expects('setConfig')->andReturnSelf(); - $queue->expects('setContainer')->with($container)->andReturnSelf(); - $connector->expects('connect')->with(['driver' => 'sync'])->andReturn($queue); - $manager->addConnector('sync', function () use ($connector): ConnectorInterface { - return $connector; - }); + $manager->addConnector('sync', fn (): ConnectorInterface => new SyncConnector); - $this->assertSame($queue, $manager->connection('sync')); + $queue = $manager->connection('sync'); + + $this->assertInstanceOf(SyncQueue::class, $queue); + $this->assertSame('sync', $queue->getConnectionName()); + $this->assertSame($container, $queue->getContainer()); + $this->assertSame(['driver' => 'sync'], $queue->getConfig()); } public function testOtherConnectionCanBeResolved(): void @@ -92,17 +92,14 @@ public function testOtherConnectionCanBeResolved(): void $config->set('queue.connections.foo', ['driver' => 'bar']); $manager = new QueueManager($container); - $connector = m::mock(ConnectorInterface::class); - $queue = m::mock(Queue::class); - $queue->expects('setConnectionName')->with('foo')->andReturnSelf(); - $queue->expects('setConfig')->andReturnSelf(); - $connector->expects('connect')->with(['driver' => 'bar'])->andReturn($queue); - $manager->addConnector('bar', function () use ($connector): ConnectorInterface { - return $connector; - }); - $queue->expects('setContainer')->with($container)->andReturnSelf(); + $manager->addConnector('bar', fn (): ConnectorInterface => new SyncConnector); - $this->assertSame($queue, $manager->connection('foo')); + $queue = $manager->connection('foo'); + + $this->assertInstanceOf(SyncQueue::class, $queue); + $this->assertSame('foo', $queue->getConnectionName()); + $this->assertSame($container, $queue->getContainer()); + $this->assertSame(['driver' => 'bar'], $queue->getConfig()); } public function testNullConnectionCanBeResolved(): void @@ -112,17 +109,14 @@ public function testNullConnectionCanBeResolved(): void $config->set('queue.default', 'null'); $manager = new QueueManager($container); - $connector = m::mock(ConnectorInterface::class); - $queue = m::mock(Queue::class); - $queue->expects('setConnectionName')->with('null')->andReturnSelf(); - $queue->expects('setConfig')->andReturnSelf(); - $connector->expects('connect')->with(['driver' => 'null'])->andReturn($queue); - $manager->addConnector('null', function () use ($connector): ConnectorInterface { - return $connector; - }); - $queue->expects('setContainer')->with($container)->andReturnSelf(); + $manager->addConnector('null', fn (): ConnectorInterface => new SyncConnector); + + $queue = $manager->connection('null'); - $this->assertSame($queue, $manager->connection('null')); + $this->assertInstanceOf(SyncQueue::class, $queue); + $this->assertSame('null', $queue->getConnectionName()); + $this->assertSame($container, $queue->getContainer()); + $this->assertSame(['driver' => 'null'], $queue->getConfig()); } public function testEnumConnectionCanBeResolved(): void @@ -133,17 +127,14 @@ public function testEnumConnectionCanBeResolved(): void $config->set('queue.connections.sync', ['driver' => 'sync']); $manager = new QueueManager($container); - $connector = m::mock(ConnectorInterface::class); - $queue = m::mock(Queue::class); - $queue->expects('setConnectionName')->with('sync')->andReturnSelf(); - $queue->expects('setConfig')->andReturnSelf(); - $connector->expects('connect')->with(['driver' => 'sync'])->andReturn($queue); - $manager->addConnector('sync', function () use ($connector): ConnectorInterface { - return $connector; - }); - $queue->expects('setContainer')->with($container)->andReturnSelf(); + $manager->addConnector('sync', fn (): ConnectorInterface => new SyncConnector); + + $queue = $manager->connection(QueueConnectionName::Sync); - $this->assertSame($queue, $manager->connection(QueueConnectionName::Sync)); + $this->assertInstanceOf(SyncQueue::class, $queue); + $this->assertSame('sync', $queue->getConnectionName()); + $this->assertSame($container, $queue->getContainer()); + $this->assertSame(['driver' => 'sync'], $queue->getConfig()); } public function testEnumConnectionCanBeChecked(): void @@ -154,15 +145,7 @@ public function testEnumConnectionCanBeChecked(): void $config->set('queue.connections.sync', ['driver' => 'sync']); $manager = new QueueManager($container); - $connector = m::mock(ConnectorInterface::class); - $queue = m::mock(Queue::class); - $queue->expects('setConnectionName')->with('sync')->andReturnSelf(); - $queue->expects('setConfig')->andReturnSelf(); - $connector->expects('connect')->with(['driver' => 'sync'])->andReturn($queue); - $manager->addConnector('sync', function () use ($connector): ConnectorInterface { - return $connector; - }); - $queue->expects('setContainer')->with($container)->andReturnSelf(); + $manager->addConnector('sync', fn (): ConnectorInterface => new SyncConnector); $this->assertFalse($manager->connected(QueueConnectionName::Sync)); $manager->connection(QueueConnectionName::Sync); @@ -542,7 +525,7 @@ protected function getContainer(): Container $container = new Container; $container->instance(ContainerContract::class, $container); $container->instance('config', new ConfigRepository([])); - $container->instance(Encrypter::class, m::mock(Encrypter::class)); + $container->instance(EncrypterContract::class, new Encrypter(str_repeat('a', 16))); $container->singleton(PoolFactory::class, PoolManager::class); Container::setInstance($container); diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 8b05940d16..42030a6712 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -796,7 +796,7 @@ public function testPushPassesUnchangedQueueToCreatePayload(): void ->setConstructorArgs([$redis, 'default', 'default']) ->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); - $queue->setContainer(m::spy(Container::class)->makePartial()); + $queue->setContainer(new Container); $queue->setConnectionName('default'); $redisProxy = m::mock(RedisProxy::class); diff --git a/tests/Queue/QueueRetryCommandTest.php b/tests/Queue/QueueRetryCommandTest.php index c71af515d8..8a0dfca668 100644 --- a/tests/Queue/QueueRetryCommandTest.php +++ b/tests/Queue/QueueRetryCommandTest.php @@ -11,6 +11,7 @@ use Hypervel\Queue\Console\RetryCommand; use Hypervel\Queue\Events\JobRetryRequested; use Hypervel\Queue\Failed\FailedJobProviderInterface; +use Hypervel\Queue\Failed\NullFailedJobProvider; use Hypervel\Queue\QueueManager; use Hypervel\Queue\SqsQueue; use Hypervel\Support\Collection; @@ -136,7 +137,7 @@ public function testRetriesJobsWithinTheGivenIdRange(): void public function testDisplaysInfoWhenThereAreNoJobsToRetry(): void { - $failer = m::mock(FailedJobProviderInterface::class); + $failer = new NullFailedJobProvider; $output = $this->runRetryCommand(['id' => []], $failer, []); diff --git a/tests/Queue/QueueSqsQueueTest.php b/tests/Queue/QueueSqsQueueTest.php index 2aba55b875..1e54c14e22 100644 --- a/tests/Queue/QueueSqsQueueTest.php +++ b/tests/Queue/QueueSqsQueueTest.php @@ -9,6 +9,8 @@ use Aws\Sqs\SqsClient; use Hypervel\Bus\Dispatcher as BusDispatcher; use Hypervel\Bus\DispatchLockContext; +use Hypervel\Cache\ArrayStore; +use Hypervel\Cache\Repository; use Hypervel\Container\Container; use Hypervel\Contracts\Bus\Dispatcher as DispatcherContract; use Hypervel\Contracts\Cache\Factory as CacheFactory; @@ -166,7 +168,7 @@ protected function createSpyContainer(): Container public function testPopProperlyPopsJobOffOfSqs(): void { $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); - $queue->setContainer(m::mock(ContainerContract::class)); + $queue->setContainer(new Container); $queue->setConnectionName('sqs'); $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); $this->sqs->expects('receiveMessage')->with(['QueueUrl' => $this->queueUrl, 'MessageSystemAttributeNames' => ['ApproximateReceiveCount']])->andReturn($this->mockedReceiveMessageResponseModel); @@ -177,7 +179,7 @@ public function testPopProperlyPopsJobOffOfSqs(): void public function testPopProperlyHandlesEmptyMessage(): void { $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['getQueue'])->setConstructorArgs([$this->sqs, $this->queueName, $this->account])->getMock(); - $queue->setContainer(m::mock(ContainerContract::class)); + $queue->setContainer(new Container); $queue->expects($this->once())->method('getQueue')->with($this->queueName)->willReturn($this->queueUrl); $this->sqs->expects('receiveMessage')->with(['QueueUrl' => $this->queueUrl, 'MessageSystemAttributeNames' => ['ApproximateReceiveCount']])->andReturn($this->mockedReceiveEmptyMessageResponseModel); $result = $queue->pop($this->queueName); @@ -1341,14 +1343,11 @@ public function testPushRawRetainsOverflowPayloadWhenSqsDeliveryIsCanceled(): vo public function testClearFlushesOverflowStoreWhenFlushOnClearEnabled(): void { - $store = m::mock(CacheStore::class); - $store->expects('flush')->andReturnTrue(); - - $repository = m::mock(CacheRepository::class); - $repository->expects('getStore')->andReturn($store); + $store = new Repository(new ArrayStore); + $store->put('foo', 'bar'); $cache = m::mock(CacheFactory::class); - $cache->expects('store')->with('database')->andReturn($repository); + $cache->expects('store')->with('database')->andReturn($store); $container = m::mock(ContainerContract::class); $container->expects('make')->with('cache')->andReturn($cache); @@ -1371,6 +1370,8 @@ public function testClearFlushesOverflowStoreWhenFlushOnClearEnabled(): void $this->sqs->expects('purgeQueue')->with(['QueueUrl' => $this->queueUrl]); $this->assertSame(5, $queue->clear($this->queueName)); + + $this->assertNull($store->get('foo')); } public function testClearFailsWhenTheOverflowStoreCannotBeFlushed(): void @@ -1462,14 +1463,11 @@ public function testClearDoesNotFlushOverflowStoreWhenOverflowDisabled(): void public function testClearForwardsConfiguredStoreNameToFactory(): void { - $store = m::mock(CacheStore::class); - $store->expects('flush')->andReturnTrue(); - - $repository = m::mock(CacheRepository::class); - $repository->expects('getStore')->andReturn($store); + $store = new Repository(new ArrayStore); + $store->put('foo', 'bar'); $cache = m::mock(CacheFactory::class); - $cache->expects('store')->with('redis')->andReturn($repository); + $cache->expects('store')->with('redis')->andReturn($store); $container = m::mock(ContainerContract::class); $container->expects('make')->with('cache')->andReturn($cache); @@ -1492,6 +1490,8 @@ public function testClearForwardsConfiguredStoreNameToFactory(): void $this->sqs->expects('purgeQueue')->with(['QueueUrl' => $this->queueUrl]); $this->assertSame(5, $queue->clear($this->queueName)); + + $this->assertNull($store->get('foo')); } public function testBulkRejectsDelayAttributeOnFifoBeforePayloadCreation(): void diff --git a/tests/Queue/QueueWorkerTest.php b/tests/Queue/QueueWorkerTest.php index ca7e607d67..3246761498 100644 --- a/tests/Queue/QueueWorkerTest.php +++ b/tests/Queue/QueueWorkerTest.php @@ -31,6 +31,8 @@ use Hypervel\Database\QueryException; use Hypervel\Engine\Channel; use Hypervel\Engine\Coroutine as EngineCoroutine; +use Hypervel\Events\Dispatcher as EventsDispatcher; +use Hypervel\Foundation\Exceptions\Handler; use Hypervel\Http\Request; use Hypervel\Queue\CallQueuedHandler; use Hypervel\Queue\Events\JobAttempted; @@ -60,6 +62,8 @@ use Hypervel\Queue\WorkerOptions; use Hypervel\Queue\WorkerStopReason; use Hypervel\Support\CarbonImmutable; +use Hypervel\Support\Testing\Fakes\EventFake; +use Hypervel\Support\Testing\Fakes\ExceptionHandlerFake; use Hypervel\Tests\TestCase; use Mockery as m; use PDOException; @@ -89,14 +93,14 @@ protected function setUp(): void { parent::setUp(); - $this->events = m::spy(EventDispatcher::class); - $this->events->shouldReceive('hasListeners')->byDefault()->andReturn(true); - $this->exceptionHandler = m::spy(ExceptionHandlerContract::class); - $this->container = new Container; + $this->events = new EventFake(new EventsDispatcher); + + Container::setInstance($this->container = new Container); + + $this->exceptionHandler = new ExceptionHandlerFake(new Handler($this->container)); + $this->container->instance(EventDispatcher::class, $this->events); $this->container->instance(ExceptionHandlerContract::class, $this->exceptionHandler); - - Container::setInstance($this->container); } public function testJobCanBeFired(): void @@ -104,16 +108,16 @@ public function testJobCanBeFired(): void $worker = $this->getWorker('default', ['queue' => [$job = new WorkerFakeJob]]); $worker->runNextJob('default', 'queue', new WorkerOptions); $this->assertTrue($job->fired); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobPopping::class))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobPopped::class))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessing::class))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) { - return $event instanceof JobProcessed && is_float($event->duration); - }))->once(); + $this->events->assertDispatchedOnce(JobPopping::class); + $this->events->assertDispatchedOnce(JobPopped::class); + $this->events->assertDispatchedOnce(JobProcessing::class); + $this->events->assertDispatchedOnce(JobProcessed::class); + $this->events->assertDispatched(fn (JobProcessed $event): bool => is_float($event->duration)); } public function testJobLifecycleEventsAreNotDispatchedWithoutListeners(): void { + $this->spyEvents(); $this->events->shouldReceive('hasListeners')->andReturnFalse(); $job = new WorkerFakeJob(static function (WorkerFakeJob $job): void { $job->release(10); @@ -131,6 +135,7 @@ public function testJobLifecycleEventsAreNotDispatchedWithoutListeners(): void public function testProcessingAndAttemptedEventsSurroundSuccessfulJobExecution(): void { + $this->spyEvents(); $order = []; $attemptedEvent = null; $this->events->shouldReceive('dispatch')->times(3)->andReturnUsing( @@ -160,6 +165,7 @@ function (object $event) use (&$order, &$attemptedEvent): void { public function testAttemptedEventRunsAfterThrownJobExecution(): void { + $this->spyEvents(); $order = []; $attemptedEvent = null; $exception = new RuntimeException('Job failed.'); @@ -219,14 +225,15 @@ public function testCancellationDuringJobExecutionEscapesWithoutFailureOrComplet $this->assertSame([], $worker->runningJobsForTest()); } - $this->exceptionHandler->shouldNotHaveReceived('report'); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobExceptionOccurred::class)]); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobAttempted::class)]); + $this->exceptionHandler->assertNothingReported(); + $this->events->assertNotDispatched(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); + $this->events->assertNotDispatched(JobAttempted::class); } public function testCancellationDuringFailureObservationStopsReleaseAndCompletion(): void { + $this->spyEvents(); $gate = $this->armCurrentCoroutineCancellation(); $this->events->shouldReceive('dispatch') ->with(m::type(JobExceptionOccurred::class)) @@ -251,13 +258,15 @@ public function testCancellationDuringFailureObservationStopsReleaseAndCompletio $this->assertSame([], $worker->runningJobsForTest()); } - $this->exceptionHandler->shouldNotHaveReceived('report'); + $this->exceptionHandler->assertNothingReported(); $this->events->shouldNotHaveReceived('dispatch', [m::type(JobReleasedAfterException::class)]); $this->events->shouldNotHaveReceived('dispatch', [m::type(JobAttempted::class)]); } public function testInvalidPayloadTerminatesBeforeProcessingAndIsReportedOnce(): void { + $this->spyEvents(); + $this->exceptionHandler = m::spy(ExceptionHandlerContract::class); $order = []; $attemptedEvent = null; $exception = new InvalidPayloadException('Invalid queue payload.', '{invalid'); @@ -306,13 +315,14 @@ public function testInvalidPayloadIsNotReportedWhenJobExceptionReportingIsDisabl $worker = $this->getWorker('default', ['queue' => [$job]]); $worker->runNextJob('default', 'queue', new WorkerOptions); - $this->exceptionHandler->shouldNotHaveReceived('report'); + $this->exceptionHandler->assertNothingReported(); $this->assertTrue($job->hasFailed()); $this->assertTrue($job->isDeleted()); } public function testWorkerOptionsCoroutineContextIsScopedToJob(): void { + $this->spyEvents(); CoroutineContext::set('queue.worker.test.previous', 'previous'); $seen = []; @@ -354,6 +364,7 @@ public function testWorkerOptionsCoroutineContextIsScopedToJob(): void public function testDaemonLifecycleCallbacksRunInOwnedCoroutinesWithConfiguredContext(): void { + $this->spyEvents(); $ownerCoroutineId = EngineCoroutine::id(); $seen = []; $options = new WorkerOptions(stopWhenEmpty: true, memory: 1024); @@ -438,15 +449,16 @@ public function testJobPoppingEvent(): void $worker->runNextJob('default', 'queue', new WorkerOptions); $this->assertTrue($job->fired); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) { - return $event instanceof JobPopping - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(JobPopping::class); + $this->events->assertDispatched(JobPopping::class, function (JobPopping $event): bool { + return $event->connectionName === 'default' && $event->queue === 'queue'; - }))->once(); + }); } public function testJobPopEventsAreSkippedWhenNoListenersAreRegistered(): void { + $this->spyEvents(); $this->events->shouldReceive('hasListeners')->with(JobPopping::class)->andReturn(false); $this->events->shouldReceive('hasListeners')->with(JobPopped::class)->andReturn(false); @@ -481,7 +493,7 @@ public function testWorkerCanMonitorTimeoutJobs(): void $this->assertSame(0, $status); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessing::class))->once(); + $this->events->assertDispatchedOnce(JobProcessing::class); } public function testDefaultPopWaiterIsUnbounded(): void @@ -545,24 +557,27 @@ public function testTimeoutMonitorUsesTheDefaultErrorExitAndTimedOutReason(?int $this->assertSame(Worker::EXIT_ERROR, $exception->status); } - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerStopping - && $event->reason === WorkerStopReason::TimedOut + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched( + WorkerStopping::class, + static fn (WorkerStopping $event): bool => $event->reason === WorkerStopReason::TimedOut && $event->connectionName === 'default' && $event->queue === 'queue' && $event->terminatesImmediately - ))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof JobTimedOut - && $event->job === $job + ); + $this->events->assertDispatchedOnce(JobTimedOut::class); + $this->events->assertDispatched( + JobTimedOut::class, + static fn (JobTimedOut $event): bool => $event->job === $job && $event->timeout === $expectedTimeout - ))->once(); + ); } #[TestWith([false])] #[TestWith([true])] public function testNonKillingTimeoutUnwindsAndRetriesOnlyTheExpiredJob(bool $listenerThrows): void { + $this->spyEvents(); Worker::$killOnTimeout = false; Worker::$pausable = false; $timer = new QueueWorkerTimer; @@ -635,7 +650,8 @@ public function testNonKillingTimeoutUnwindsAndRetriesOnlyTheExpiredJob(bool $li static fn (object $event): bool => $event instanceof JobProcessed && $event->job === $other ))->once(); $this->events->shouldNotHaveReceived('dispatch', [m::type(WorkerStopping::class)]); - $this->exceptionHandler->shouldHaveReceived('report')->with(m::type(TimeoutExceededException::class))->once(); + $this->exceptionHandler->assertReportedCount(1); + $this->exceptionHandler->assertReported(TimeoutExceededException::class); } finally { $expiredGate->close(); $otherGate->close(); @@ -645,6 +661,7 @@ public function testNonKillingTimeoutUnwindsAndRetriesOnlyTheExpiredJob(bool $li public function testTimeoutScanDoesNotCancelCompletedCleanupOrTimeoutAnotherFinishedJob(): void { + $this->spyEvents(); Worker::$killOnTimeout = false; $timer = new QueueWorkerTimer; $firstGate = new Channel(1); @@ -702,7 +719,7 @@ static function () use ($firstGate, $secondGate): void { static fn (object $event): bool => $event instanceof JobAttempted && $event->job === $second && $event->successful() ))->once(); - $this->exceptionHandler->shouldNotHaveReceived('report'); + $this->exceptionHandler->assertNothingReported(); } finally { $firstGate->close(); $secondGate->close(); @@ -713,6 +730,7 @@ static function () use ($firstGate, $secondGate): void { public function testTimeoutMonitorRunsInAnOwnedCoroutineWithWorkerContextAndStoppingDetails(): void { + $this->spyEvents(); $ownerCoroutineId = EngineCoroutine::id(); $callbackCoroutineIds = []; $contextValues = []; @@ -762,6 +780,7 @@ function (object $event) use (&$callbackCoroutineIds, &$contextValues): void { public function testTimeoutLifecycleEventsAreNotDispatchedWithoutListeners(): void { + $this->spyEvents(); $this->events->shouldReceive('hasListeners')->andReturnFalse(); $timer = new QueueWorkerTimer; $worker = new KillTestWorker(...$this->workerDependencies(timer: $timer)); @@ -854,10 +873,10 @@ public function testDaemonFailureCancelsAdmittedJobsWithoutFalseOutcomes(): void $blocker->close(); } - $this->exceptionHandler->shouldNotHaveReceived('report'); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobExceptionOccurred::class)]); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobAttempted::class)]); + $this->exceptionHandler->assertNothingReported(); + $this->events->assertNotDispatched(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); + $this->events->assertNotDispatched(JobAttempted::class); } public function testCancellationDuringGracefulShutdownCancelsRunningJobs(): void @@ -887,7 +906,7 @@ public function testCancellationDuringGracefulShutdownCancelsRunningJobs(): void $blocker->close(); } - $this->events->shouldNotHaveReceived('dispatch', [m::type(WorkerStopping::class)]); + $this->events->assertNotDispatched(WorkerStopping::class); } public function testTimeoutMonitorUnlocksWhenScanningThrows(): void @@ -924,10 +943,11 @@ public function testKillDoesNotWaitForUnrelatedActiveJobs(): void } $this->assertNull($worker->sleptFor); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerStopping - && $event->terminatesImmediately - ))->once(); + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched( + WorkerStopping::class, + static fn (WorkerStopping $event): bool => $event->terminatesImmediately + ); } public function testWorkerCanWorkUntilQueueIsEmpty(): void @@ -947,9 +967,9 @@ public function testWorkerCanWorkUntilQueueIsEmpty(): void $this->assertSame(0, $status); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessing::class))->twice(); + $this->events->assertDispatchedTimes(JobProcessing::class, 2); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessed::class))->twice(); + $this->events->assertDispatchedTimes(JobProcessed::class, 2); } public function testWorkerStopsWhenQueueIsEmptyForConfiguredSeconds(): void @@ -963,13 +983,13 @@ public function testWorkerStopsWhenQueueIsEmptyForConfiguredSeconds(): void $this->assertSame(Worker::EXIT_SUCCESS, $status); $this->assertSame(6.0, $worker->currentTime); - $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerIdle::class))->twice(); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - fn (object $event): bool => $event instanceof WorkerStopping - && $event->status === Worker::EXIT_SUCCESS + $this->events->assertDispatchedTimes(WorkerIdle::class, 2); + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched(WorkerStopping::class, function (WorkerStopping $event) use ($workerOptions): bool { + return $event->status === Worker::EXIT_SUCCESS && $event->workerOptions === $workerOptions - && $event->reason === WorkerStopReason::QueueEmptyFor - ))->once(); + && $event->reason === WorkerStopReason::QueueEmptyFor; + }); } public function testWorkerResetsQueueEmptyTimerAfterProcessingJob(): void @@ -987,13 +1007,13 @@ public function testWorkerResetsQueueEmptyTimerAfterProcessingJob(): void $this->assertSame(Worker::EXIT_SUCCESS, $status); $this->assertTrue($job->fired); $this->assertSame(16.0, $worker->currentTime); - $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerIdle::class))->twice(); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - fn (object $event): bool => $event instanceof WorkerStopping - && $event->status === Worker::EXIT_SUCCESS + $this->events->assertDispatchedTimes(WorkerIdle::class, 2); + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched(WorkerStopping::class, function (WorkerStopping $event) use ($workerOptions): bool { + return $event->status === Worker::EXIT_SUCCESS && $event->workerOptions === $workerOptions - && $event->reason === WorkerStopReason::QueueEmptyFor - ))->once(); + && $event->reason === WorkerStopReason::QueueEmptyFor; + }); } public function testWorkerDoesNotStopForAnEmptyQueueWhileAJobIsRunning(): void @@ -1020,12 +1040,6 @@ public function testWorkerDoesNotStopForAnEmptyQueueWhileAJobIsRunning(): void public function testWorkerWaitsForConcurrencySlotWithoutPollingDelay(): void { $jobCompleted = false; - $reported = null; - $this->exceptionHandler->shouldReceive('report')->andReturnUsing( - function (Throwable $exception) use (&$reported): void { - $reported = $exception; - }, - ); $job = new WorkerFakeJob(function () use (&$jobCompleted): void { Coroutine::sleep(0.001); $jobCompleted = true; @@ -1040,7 +1054,7 @@ function (Throwable $exception) use (&$reported): void { $this->assertSame(Worker::EXIT_SUCCESS, $status); $this->assertTrue($job->fired); - $this->assertNull($reported, $reported?->getMessage() ?? 'Unexpected job failure.'); + $this->exceptionHandler->assertNothingReported(); $this->assertTrue($jobCompleted); $this->assertNull($worker->sleptFor); } @@ -1092,10 +1106,11 @@ public function testPausedWorkerUsesItsActualStartTimeForMaxTime(): void $this->assertSame(Worker::EXIT_SUCCESS, $status); $this->assertSame(16.0, $worker->currentTime); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerStopping - && $event->reason === WorkerStopReason::MaxTimeExceeded - ))->once(); + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched( + WorkerStopping::class, + static fn (WorkerStopping $event): bool => $event->reason === WorkerStopReason::MaxTimeExceeded + ); } public function testWorkerStopsWhenMemoryExceeded(): void @@ -1114,9 +1129,9 @@ public function testWorkerStopsWhenMemoryExceeded(): void $this->assertFalse($secondJob->fired); $this->assertSame(12, $status); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessing::class))->once(); + $this->events->assertDispatchedOnce(JobProcessing::class); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessed::class))->once(); + $this->events->assertDispatchedOnce(JobProcessed::class); } public function testWorkerMemoryExceededWhenMemoryIsZero(): void @@ -1192,6 +1207,7 @@ public function testForcedWorkerDoesNotCheckMaintenanceMode(): void public function testQueuePauseEventsTrackOnlyTheCurrentSelection(): void { + $this->spyEvents(); $paused = ['emails']; $manager = m::mock(QueueManager::class); $manager->shouldReceive('connection')->with('first')->andReturn( @@ -1241,6 +1257,7 @@ static function (array $queues, string $connection) use (&$paused): array { public function testPauseHistoryIsRetainedWithoutEventListeners(): void { + $this->spyEvents(); $manager = m::mock(QueueManager::class); $manager->shouldReceive('connection')->with('default')->andReturn( new WorkerFakeConnection('default', ['queue' => []]), @@ -1284,12 +1301,12 @@ public function testEachDaemonRunReportsInitiallyPausedQueues(): void $this->assertSame(Worker::EXIT_SUCCESS, $worker->daemon('default', 'queue', $options)); $this->assertSame(Worker::EXIT_SUCCESS, $worker->daemon('default', 'queue', $options)); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerQueuePaused - && $event->connectionName === 'default' + $this->assertCount(2, $this->events->dispatched( + WorkerQueuePaused::class, + static fn (WorkerQueuePaused $event): bool => $event->connectionName === 'default' && $event->queue === 'queue', - ))->twice(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(WorkerQueueResumed::class)]); + )); + $this->events->assertNotDispatched(WorkerQueueResumed::class); } public function testJobCanBeFiredBasedOnPriority(): void @@ -1346,7 +1363,7 @@ function () { $worker->runNextJob('default', 'queue', $this->workerOptions()); - $this->exceptionHandler->shouldHaveReceived('report')->with($e); + $this->exceptionHandler->assertReported(fn (RuntimeException $reported): bool => $reported === $e); } public function testCancellationDuringJobPopEscapesWithoutReportingOrSleeping(): void @@ -1369,8 +1386,8 @@ public function testCancellationDuringJobPopEscapesWithoutReportingOrSleeping(): Worker::popUsing('cancel-pop', null); } - $this->exceptionHandler->shouldNotHaveReceived('report'); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobPopped::class)]); + $this->exceptionHandler->assertNothingReported(); + $this->events->assertNotDispatched(JobPopped::class); } public function testWorkerSleepsWhenQueueIsEmpty(): void @@ -1393,9 +1410,9 @@ public function testJobIsReleasedOnException(): void $this->assertEquals(10, $job->releaseAfter); $this->assertFalse($job->deleted); - $this->exceptionHandler->shouldHaveReceived('report')->with($e); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); + $this->exceptionHandler->assertReported(fn (RuntimeException $reported): bool => $reported === $e); + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); } public function testJobAttemptedEventIsDispatched(): void @@ -1409,15 +1426,14 @@ public function testJobAttemptedEventIsDispatched(): void $worker = $this->getWorker('default', ['queue' => [$job]]); $worker->runNextJob('default', 'queue', $this->workerOptions()); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobAttempted::class))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($job, $e) { - return $event instanceof JobAttempted - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(JobAttempted::class); + $this->events->assertDispatched(JobAttempted::class, function (JobAttempted $event) use ($job, $e): bool { + return $event->connectionName === 'default' && $event->job === $job && $event->exception === $e && ! $event->successful() && is_float($event->duration); - }))->once(); + }); } public function testJobIsFailedIfExceptionHandlerSaysItShouldntRetry(): void @@ -1434,13 +1450,14 @@ public function testJobIsFailedIfExceptionHandlerSaysItShouldntRetry(): void $this->assertNull($job->releaseAfter); $this->assertTrue($job->deleted); $this->assertSame($exception, $job->failedWith); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof JobExceptionOccurred - && $event->job === $job + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); + $this->events->assertDispatched( + JobExceptionOccurred::class, + static fn (JobExceptionOccurred $event): bool => $event->job === $job && $event->exception === $exception && $event->job->hasFailed(), - ))->once(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobReleasedAfterException::class)]); + ); + $this->events->assertNotDispatched(JobReleasedAfterException::class); } public function testExceptionIsNotReportedIfReportJobExceptionsIsDisabled(): void @@ -1455,8 +1472,8 @@ public function testExceptionIsNotReportedIfReportJobExceptionsIsDisabled(): voi $worker = $this->getWorker('default', ['queue' => [$job]]); $worker->runNextJob('default', 'queue', $this->workerOptions(['backoff' => 10])); - $this->exceptionHandler->shouldNotHaveReceived('report'); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); + $this->exceptionHandler->assertNothingReported(); + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); } public function testJobIsNotReleasedIfItHasExceededMaxAttempts(): void @@ -1478,9 +1495,9 @@ public function testJobIsNotReleasedIfItHasExceededMaxAttempts(): void $this->assertNull($job->releaseAfter); $this->assertTrue($job->deleted); $this->assertEquals($e, $job->failedWith); - $this->exceptionHandler->shouldHaveReceived('report')->with($e); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); + $this->exceptionHandler->assertReported(fn (RuntimeException $reported): bool => $reported === $e); + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); } public function testJobIsNotReleasedIfItHasExpired(): void @@ -1508,9 +1525,9 @@ public function testJobIsNotReleasedIfItHasExpired(): void $this->assertNull($job->releaseAfter); $this->assertTrue($job->deleted); $this->assertEquals($e, $job->failedWith); - $this->exceptionHandler->shouldHaveReceived('report')->with($e); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); + $this->exceptionHandler->assertReported(fn (RuntimeException $reported): bool => $reported === $e); + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); } public function testJobIsFailedIfItHasAlreadyExceededMaxAttempts(): void @@ -1527,9 +1544,9 @@ public function testJobIsFailedIfItHasAlreadyExceededMaxAttempts(): void $this->assertNull($job->releaseAfter); $this->assertTrue($job->deleted); $this->assertInstanceOf(MaxAttemptsExceededException::class, $job->failedWith); - $this->exceptionHandler->shouldHaveReceived('report')->with(m::type(MaxAttemptsExceededException::class)); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); + $this->exceptionHandler->assertReported(MaxAttemptsExceededException::class); + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); } public function testJobIsFailedIfItHasAlreadyExpired(): void @@ -1552,9 +1569,9 @@ public function testJobIsFailedIfItHasAlreadyExpired(): void $this->assertNull($job->releaseAfter); $this->assertTrue($job->deleted); $this->assertInstanceOf(MaxAttemptsExceededException::class, $job->failedWith); - $this->exceptionHandler->shouldHaveReceived('report')->with(m::type(MaxAttemptsExceededException::class)); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobExceptionOccurred::class))->once(); - $this->events->shouldNotHaveReceived('dispatch', [m::type(JobProcessed::class)]); + $this->exceptionHandler->assertReported(MaxAttemptsExceededException::class); + $this->events->assertDispatchedOnce(JobExceptionOccurred::class); + $this->events->assertNotDispatched(JobProcessed::class); } public function testJobIsFailedWhenTheMaxExceptionsCounterReachesTheLimit(): void @@ -1668,7 +1685,7 @@ public function testJobDoesNotFireIfDeleted(): void $job->delete(); $worker->runNextJob('default', 'queue', $this->workerOptions()); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobProcessed::class))->once(); + $this->events->assertDispatchedOnce(JobProcessed::class); $this->assertFalse($job->hasFailed()); $this->assertFalse($job->isReleased()); $this->assertTrue($job->isDeleted()); @@ -1685,11 +1702,12 @@ public function testJobReleasedEventIsRaisedWhenJobReleasesItself(): void $this->assertTrue($job->isReleased()); $this->assertFalse($job->isDeleted()); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof JobReleased - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(JobReleased::class); + $this->events->assertDispatched( + JobReleased::class, + static fn (JobReleased $event): bool => $event->connectionName === 'default' && $event->job === $job, - ))->once(); + ); } public function testWorkerPicksJobUsingCustomCallbacks(): void @@ -1722,8 +1740,8 @@ public function testWorkerPicksJobUsingCustomCallbacks(): void $this->assertFalse($defaultJob->fired); $this->assertTrue($customJob->fired); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobPopped::class))->twice(); - $this->exceptionHandler->shouldNotHaveReceived('report'); + $this->events->assertDispatchedTimes(JobPopped::class, 2); + $this->exceptionHandler->assertNothingReported(); Worker::popUsing('myworker', null); } @@ -1781,11 +1799,11 @@ public function testWorkerCanBeKilledUsingCustomCallback(): void } finally { Worker::killUsing(null); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event): bool { - return $event instanceof WorkerStopping - && $event->status === 124 + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched(WorkerStopping::class, function (WorkerStopping $event): bool { + return $event->status === 124 && $event->reason === WorkerStopReason::TimedOut; - }))->once(); + }); } } @@ -1804,7 +1822,7 @@ public function testWorkerStartingIsDispatched(): void $this->assertTrue($firstJob->fired); $this->assertTrue($secondJob->fired); - $this->events->shouldHaveReceived('dispatch')->with(m::type(WorkerStarting::class))->once(); + $this->events->assertDispatchedOnce(WorkerStarting::class); } public function testWorkerIdleIsDispatched(): void @@ -1814,12 +1832,12 @@ public function testWorkerIdleIsDispatched(): void $worker->daemon('default', 'queue', $workerOptions); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerIdle - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(WorkerIdle::class); + $this->events->assertDispatched(WorkerIdle::class, function (WorkerIdle $event) use ($workerOptions): bool { + return $event->connectionName === 'default' && $event->queue === 'queue' - && $event->workerOptions === $workerOptions - ))->once(); + && $event->workerOptions === $workerOptions; + }); } public function testWorkerStoppingIsDispatched(): void @@ -1837,9 +1855,9 @@ public function testWorkerStoppingIsDispatched(): void $this->assertTrue($firstJob->fired); $this->assertTrue($secondJob->fired); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { - return $event instanceof WorkerStopping - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched(WorkerStopping::class, function (WorkerStopping $event) use ($workerOptions): bool { + return $event->connectionName === 'default' && $event->queue === 'queue' && $event->status === 0 && $event->workerOptions === $workerOptions @@ -1848,7 +1866,7 @@ public function testWorkerStoppingIsDispatched(): void && $event->lastJobProcessedAt !== null && $event->memoryUsage > 0 && ! $event->terminatesImmediately; - }))->once(); + }); } #[DataProvider('lostDatabaseConnections')] @@ -1864,12 +1882,12 @@ public function testWorkerStopsWithLostConnectionReason(Throwable $failure): voi $this->assertSame(Worker::EXIT_SUCCESS, $status); $this->assertTrue($job->fired); $this->assertTrue($worker->lostConnection); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerStopping - && $event->status === 0 + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched(WorkerStopping::class, function (WorkerStopping $event) use ($workerOptions): bool { + return $event->status === 0 && $event->workerOptions === $workerOptions - && $event->reason === WorkerStopReason::LostConnection - ))->once(); + && $event->reason === WorkerStopReason::LostConnection; + }); } /** @@ -1931,11 +1949,12 @@ public function testWorkerKeepsRunningAfterAFailureThatOnlyReadsLikeALostConnect $this->assertTrue($failing->fired); $this->assertTrue($next->fired); $this->assertFalse($worker->lostConnection); - $this->exceptionHandler->shouldHaveReceived('report')->with($failure); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerStopping - && $event->reason === WorkerStopReason::QueueEmpty - ))->once(); + $this->exceptionHandler->assertReported(fn (QueryException $reported): bool => $reported === $failure); + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched( + WorkerStopping::class, + static fn (WorkerStopping $event): bool => $event->reason === WorkerStopReason::QueueEmpty + ); } /** @@ -1977,12 +1996,12 @@ public function testWorkerDoesNotStopOnLostConnectionWhenDisabled(): void $this->assertSame(Worker::EXIT_SUCCESS, $status); $this->assertTrue($job->fired); $this->assertFalse($worker->lostConnection); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof WorkerStopping - && $event->status === Worker::EXIT_SUCCESS + $this->events->assertDispatchedOnce(WorkerStopping::class); + $this->events->assertDispatched(WorkerStopping::class, function (WorkerStopping $event) use ($workerOptions): bool { + return $event->status === Worker::EXIT_SUCCESS && $event->workerOptions === $workerOptions - && $event->reason === WorkerStopReason::QueueEmpty - ))->once(); + && $event->reason === WorkerStopReason::QueueEmpty; + }); } public function testWorkerInterruptionSignalDispatchesEventAndNotifiesRunningJobs(): void @@ -2009,28 +2028,30 @@ public function interrupted(int $signal): void $this->assertTrue($worker->shouldQuit); $this->assertSame([], $interruptible->signals); - $this->events->shouldNotHaveReceived('dispatch', [m::type(WorkerInterrupted::class)]); + $this->events->assertNotDispatched(WorkerInterrupted::class); $worker->drainPendingSignalsForTest(); $this->assertSame([SIGTERM], $interruptible->signals); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { - return $event instanceof WorkerInterrupted - && $event->signal === SIGTERM + $this->events->assertDispatchedOnce(WorkerInterrupted::class); + $this->events->assertDispatched(WorkerInterrupted::class, function (WorkerInterrupted $event) use ($workerOptions): bool { + return $event->signal === SIGTERM && $event->connectionName === 'default' && $event->queue === 'queue' && $event->workerOptions === $workerOptions; - }))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof JobInterrupted - && $event->connectionName === '' + }); + $this->events->assertDispatchedOnce(JobInterrupted::class); + $this->events->assertDispatched( + JobInterrupted::class, + static fn (JobInterrupted $event): bool => $event->connectionName === '' && $event->job === $job && $event->signal === SIGTERM - ))->once(); + ); } public function testGracefulShutdownDrainsInterruptionBeforeWaitingForRunningJob(): void { + $this->spyEvents(); $order = new ArrayObject; $releaseJob = new Channel(1); $options = new WorkerOptions(maxJobs: 1); @@ -2087,6 +2108,7 @@ function (object $event) use ($order): void { public function testSignalAndStopEventsAreNotDispatchedWithoutListeners(): void { + $this->spyEvents(); $this->events->shouldReceive('hasListeners')->andReturnFalse(); $workerOptions = new WorkerOptions; $worker = $this->getWorker('default', ['queue' => []]); @@ -2121,7 +2143,7 @@ public function testInterruptibleJobIsNotifiedOnSignal(): void $this->assertSame([SIGINT], $firstInterruptible->signals); $this->assertSame([SIGINT], $secondInterruptible->signals); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobInterrupted::class))->twice(); + $this->events->assertDispatchedTimes(JobInterrupted::class, 2); } public function testJobInterruptedEventIsDispatchedForInterruptibleJobs(): void @@ -2133,12 +2155,12 @@ public function testJobInterruptedEventIsDispatchedForInterruptibleJobs(): void $worker->notifyJobsOfSignalForTest(SIGTERM); - $this->events->shouldHaveReceived('dispatch')->with(m::on( - static fn (object $event): bool => $event instanceof JobInterrupted - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(JobInterrupted::class); + $this->events->assertDispatched(JobInterrupted::class, function (JobInterrupted $event) use ($job): bool { + return $event->connectionName === 'default' && $event->job === $job - && $event->signal === SIGTERM, - ))->once(); + && $event->signal === SIGTERM; + }); } public function testInterruptibleJobIsNotifiedWhenTheJobTimesOut(): void @@ -2175,12 +2197,15 @@ public function interrupted(int $signal): void $worker = $this->getWorker('default', ['queue' => []]); $this->timeOutJob($worker, $this->workerJobWithRunningCommand($interruptible)); - $this->exceptionHandler->shouldHaveReceived('report')->with($exception)->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::type(JobTimedOut::class))->once(); + $this->exceptionHandler->assertReportedCount(1); + $this->exceptionHandler->assertReported(fn (Exception $reported): bool => $reported === $exception); + + $this->events->assertDispatchedOnce(JobTimedOut::class); } public function testJobInterruptedEventIsSkippedWithoutListenersButTheJobIsStillNotified(): void { + $this->spyEvents(); $this->events->shouldReceive('hasListeners')->andReturnFalse(); $interruptible = new WorkerInterruptibleJob; $worker = $this->getWorker('default', ['queue' => []]); @@ -2239,23 +2264,23 @@ public function testWorkerPauseAndResumeSignalEventsCarryWorkerContext(): void $worker->handleResumeSignalForTest('default', 'queue', $workerOptions); $this->assertFalse($worker->paused); - $this->events->shouldNotHaveReceived('dispatch', [m::type(WorkerPausing::class)]); - $this->events->shouldNotHaveReceived('dispatch', [m::type(WorkerResuming::class)]); + $this->events->assertNotDispatched(WorkerPausing::class); + $this->events->assertNotDispatched(WorkerResuming::class); $worker->drainPendingSignalsForTest(); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { - return $event instanceof WorkerPausing - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(WorkerPausing::class); + $this->events->assertDispatched(WorkerPausing::class, function (WorkerPausing $event) use ($workerOptions): bool { + return $event->connectionName === 'default' && $event->queue === 'queue' && $event->workerOptions === $workerOptions; - }))->once(); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($workerOptions) { - return $event instanceof WorkerResuming - && $event->connectionName === 'default' + }); + $this->events->assertDispatchedOnce(WorkerResuming::class); + $this->events->assertDispatched(WorkerResuming::class, function (WorkerResuming $event) use ($workerOptions): bool { + return $event->connectionName === 'default' && $event->queue === 'queue' && $event->workerOptions === $workerOptions; - }))->once(); + }); } public function testJobReleasedEvent(): void @@ -2269,17 +2294,18 @@ public function testJobReleasedEvent(): void $worker = $this->getWorker('default', ['queue' => [$job]]); $worker->runNextJob('default', 'queue', $this->workerOptions(['backoff' => 10])); - $this->events->shouldHaveReceived('dispatch')->with(m::on(function ($event) use ($job, $e) { - return $event instanceof JobReleasedAfterException - && $event->connectionName === 'default' + $this->events->assertDispatchedOnce(JobReleasedAfterException::class); + $this->events->assertDispatched(JobReleasedAfterException::class, function (JobReleasedAfterException $event) use ($job, $e): bool { + return $event->connectionName === 'default' && $event->job === $job && $event->backoff === 10 && $event->exception === $e; - }))->once(); + }); } public function testFailureLifecycleEventsAreNotDispatchedWithoutListeners(): void { + $this->spyEvents(); $this->events->shouldReceive('hasListeners')->andReturnFalse(); $exception = new RuntimeException; $job = new WorkerFakeJob(static function () use ($exception): never { @@ -2330,6 +2356,15 @@ private function workerDependencies( ]; } + /** + * Replace the event fake with a spy for tests that intercept dispatcher calls. + */ + private function spyEvents(): void + { + $this->events = m::spy(EventDispatcher::class); + $this->events->shouldReceive('hasListeners')->byDefault()->andReturn(true); + } + /** * Create the query exception a connection throws for a driver error. */ diff --git a/tests/Redis/RedisEventsTest.php b/tests/Redis/RedisEventsTest.php index 070f8dbba8..9f13ffde23 100644 --- a/tests/Redis/RedisEventsTest.php +++ b/tests/Redis/RedisEventsTest.php @@ -9,6 +9,7 @@ use Hypervel\ConnectionPool\PoolOptions; use Hypervel\Container\Container; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Events\Dispatcher as EventsDispatcher; use Hypervel\Redis\Events\CommandExecuted; use Hypervel\Redis\Events\CommandFailed; use Hypervel\Redis\PhpRedisConnection; @@ -17,6 +18,7 @@ use Hypervel\Redis\RedisConnection; use Hypervel\Redis\RedisProxy; use Hypervel\Redis\RedisSentinelFactory; +use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Tests\TestCase; use Mockery as m; use Redis as PhpRedis; @@ -28,48 +30,35 @@ public function testCommandFailedEventIsDispatched(): void { $exception = new Exception('Test exception'); - $mockEventDispatcher = m::mock(Dispatcher::class); - $mockEventDispatcher->shouldReceive('hasListeners') - ->with(CommandFailed::class) - ->andReturn(true); - $mockEventDispatcher->shouldReceive('hasListeners') - ->with(CommandExecuted::class) - ->andReturn(false); - $mockEventDispatcher->expects('dispatch') - ->with(m::on(function (mixed $event) use ($exception): bool { - return $event instanceof CommandFailed - && $event->command === 'get' - && $event->parameters === ['key'] - && $event->exception === $exception; - })); + $events = new EventFake(new EventsDispatcher); - $connection = $this->createMockRedisConnection('get', null, $exception, $mockEventDispatcher); + $connection = $this->createMockRedisConnection('get', null, $exception, $events); $connection->expects('release'); $redis = $this->createRedis($connection); - $this->expectExceptionObject($exception); + try { + $redis->get('key'); + $this->fail('Expected the Redis command to fail.'); + } catch (Exception $thrown) { + $this->assertSame($exception, $thrown); + } - $redis->get('key'); + $events->assertDispatchedOnce(CommandFailed::class); + $events->assertDispatched(CommandFailed::class, function (CommandFailed $event) use ($exception): bool { + return $event->command === 'get' + && $event->parameters === ['key'] + && $event->exception === $exception; + }); } public function testCommandExecutedEventIsNotDispatchedWhenCommandFails(): void { $exception = new Exception('Test exception'); - $mockEventDispatcher = m::mock(Dispatcher::class); - $mockEventDispatcher->shouldReceive('hasListeners') - ->with(CommandFailed::class) - ->andReturn(true); - $mockEventDispatcher->shouldReceive('hasListeners') - ->with(CommandExecuted::class) - ->andReturn(true); - $mockEventDispatcher->expects('dispatch') - ->with(m::type(CommandFailed::class)); - $mockEventDispatcher->shouldNotReceive('dispatch') - ->with(m::type(CommandExecuted::class)); + $events = new EventFake(new EventsDispatcher); - $connection = $this->createMockRedisConnection('get', null, $exception, $mockEventDispatcher); + $connection = $this->createMockRedisConnection('get', null, $exception, $events); $connection->expects('release'); $redis = $this->createRedis($connection); @@ -79,26 +68,18 @@ public function testCommandExecutedEventIsNotDispatchedWhenCommandFails(): void } catch (Exception) { // Expected } + + $events->assertDispatchedOnce(CommandFailed::class); + $events->assertNotDispatched(CommandExecuted::class); } public function testCommandFailedEventContainsConnectionName(): void { $exception = new Exception('Test exception'); - $mockEventDispatcher = m::mock(Dispatcher::class); - $mockEventDispatcher->shouldReceive('hasListeners') - ->with(CommandFailed::class) - ->andReturn(true); - $mockEventDispatcher->shouldReceive('hasListeners') - ->with(CommandExecuted::class) - ->andReturn(false); - $mockEventDispatcher->expects('dispatch') - ->with(m::on(function (mixed $event): bool { - return $event instanceof CommandFailed - && $event->connectionName === 'default'; - })); + $events = new EventFake(new EventsDispatcher); - $connection = $this->createMockRedisConnection('get', null, $exception, $mockEventDispatcher); + $connection = $this->createMockRedisConnection('get', null, $exception, $events); $connection->expects('release'); $redis = $this->createRedis($connection); @@ -108,6 +89,10 @@ public function testCommandFailedEventContainsConnectionName(): void } catch (Exception) { // Expected } + + $events->assertDispatched(CommandFailed::class, function (CommandFailed $event): bool { + return $event->connectionName === 'default'; + }); } public function testCommandFailedEventContainsTime(): void @@ -158,18 +143,18 @@ public function testListenRegistersCallback(): void public function testListenForFailuresRegistersCallback(): void { - $mockEventDispatcher = m::mock(Dispatcher::class); - $mockEventDispatcher->expects('listen') - ->with(CommandFailed::class, m::type(Closure::class)); + $events = new EventsDispatcher; $container = Container::getInstance(); - $container->instance('events', $mockEventDispatcher); + $container->instance('events', $events); $redis = $this->createRedis($this->createMockRedisConnection()); $redis->listenForFailures(function (): void { // callback }); + + $this->assertTrue($events->hasListeners(CommandFailed::class)); } public function testCommandExecutedNotDispatchedWhenNoListeners(): void diff --git a/tests/Routing/RoutingRedirectorTest.php b/tests/Routing/RoutingRedirectorTest.php index abe90cee7a..753c37e4b8 100644 --- a/tests/Routing/RoutingRedirectorTest.php +++ b/tests/Routing/RoutingRedirectorTest.php @@ -9,6 +9,7 @@ use Hypervel\Http\Request; use Hypervel\Routing\Redirector; use Hypervel\Routing\UrlGenerator; +use Hypervel\Session\NullSessionHandler; use Hypervel\Session\Store; use Mockery as m; use Symfony\Component\HttpFoundation\HeaderBag; @@ -29,7 +30,7 @@ protected function setUp(): void { parent::setUp(); - $this->headers = m::mock(HeaderBag::class); + $this->headers = new HeaderBag; $this->request = m::mock(Request::class); $this->request->shouldReceive('isMethod')->andReturn(true)->byDefault(); @@ -48,7 +49,7 @@ protected function setUp(): void $this->url->shouldReceive('to')->with('/', [], null)->andReturn('http://foo.com/'); $this->url->shouldReceive('to')->with('http://foo.com/bar?signature=secret', [], null)->andReturn('http://foo.com/bar?signature=secret'); - $this->session = m::mock(Store::class); + $this->session = new Store('test', new NullSessionHandler); $this->redirect = new Redirector($this->url); $this->redirect->setSession($this->session); @@ -77,42 +78,41 @@ public function testComplexRedirectTo(): void public function testGuestPutCurrentUrlInSession(): void { $this->url->expects('full')->andReturn('http://foo.com/bar'); - $this->session->expects('put')->with('url.intended', 'http://foo.com/bar'); $response = $this->redirect->guest('login'); $this->assertSame('http://foo.com/login', $response->getTargetUrl()); + $this->assertSame('http://foo.com/bar', $this->session->get('url.intended')); } public function testGuestPutPreviousUrlInSession(): void { $this->request->expects('isMethod')->with('GET')->andReturn(false); - $this->session->expects('put')->with('url.intended', 'http://foo.com/bar'); $this->url->expects('previous')->andReturn('http://foo.com/bar'); $response = $this->redirect->guest('login'); $this->assertSame('http://foo.com/login', $response->getTargetUrl()); + $this->assertSame('http://foo.com/bar', $this->session->get('url.intended')); } public function testIntendedRedirectToIntendedUrlInSession(): void { - $this->session->expects('pull')->with('url.intended', '/')->andReturn('http://foo.com/bar'); + $this->session->put('url.intended', 'http://foo.com/bar'); $response = $this->redirect->intended(); $this->assertSame('http://foo.com/bar', $response->getTargetUrl()); + $this->assertNull($this->session->get('url.intended')); } public function testIntendedWithoutIntendedUrlInSession(): void { // without fallback url - $this->session->expects('pull')->with('url.intended', '/')->andReturn('/'); $response = $this->redirect->intended(); $this->assertSame('http://foo.com/', $response->getTargetUrl()); // with a fallback url - $this->session->expects('pull')->with('url.intended', 'bar')->andReturn('bar'); $response = $this->redirect->intended('bar'); $this->assertSame('http://foo.com/bar', $response->getTargetUrl()); } @@ -187,9 +187,6 @@ public function testTemporarySignedRoute(): void public function testItSetsAndGetsValidIntendedUrl(): void { - $this->session->expects('put')->with('url.intended', 'http://foo.com/bar'); - $this->session->expects('get')->andReturn('http://foo.com/bar'); - $result = $this->redirect->setIntendedUrl('http://foo.com/bar'); $this->assertInstanceOf(Redirector::class, $result); diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 2c3b98439f..25c1e16c51 100644 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -23,7 +23,6 @@ use Hypervel\Tests\TestCase; use IteratorAggregate; use LogicException; -use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use ReflectionClass; use RuntimeException; @@ -65,8 +64,22 @@ public function testE(): void $str = 'A \'quote\' is bold'; $this->assertSame('A 'quote' is <b>bold</b>', e($str)); - $html = m::mock(Htmlable::class); - $html->expects('toHtml')->andReturn($str); + $html = new class($str) implements Htmlable { + /** + * Create a new HTML string instance. + */ + public function __construct(protected string $html) + { + } + + /** + * Get content as a string of HTML. + */ + public function toHtml(): string + { + return $this->html; + } + }; $this->assertEquals($str, e($html)); } @@ -852,9 +865,8 @@ public function testTap(): void $object->id = 2; })->id); - $mock = m::mock(); - $mock->expects('foo')->andReturn('bar'); - $this->assertEquals($mock, tap($mock)->foo()); + $collection = collect(); + $this->assertSame($collection, tap($collection)->all()); } public function testThrow(): void diff --git a/tests/Support/SupportTestingEventFakeTest.php b/tests/Support/SupportTestingEventFakeTest.php index d02261ea43..038b41170b 100644 --- a/tests/Support/SupportTestingEventFakeTest.php +++ b/tests/Support/SupportTestingEventFakeTest.php @@ -5,6 +5,7 @@ namespace Hypervel\Tests\Support; use Hypervel\Contracts\Events\Dispatcher; +use Hypervel\Events\Dispatcher as EventDispatcher; use Hypervel\Support\Testing\Fakes\EventFake; use Hypervel\Tests\TestCase; use Mockery as m; @@ -46,12 +47,8 @@ public function testAssertDispatchedWithClosure(): void public function testAssertListening(): void { - $listener = ListenerStub::class; - - $dispatcher = m::mock(Dispatcher::class); - $dispatcher->expects('getListeners')->andReturn([function ($event, $payload) use ($listener) { - return $listener(...array_values($payload)); - }]); + $dispatcher = new EventDispatcher; + $dispatcher->listen(EventStub::class, ListenerStub::class); $fake = new EventFake($dispatcher); @@ -133,8 +130,11 @@ public function testAssertNotDispatchedWithClosure(): void public function testAssertDispatchedWithIgnore(): void { - $dispatcher = m::mock(Dispatcher::class); - $dispatcher->expects('dispatch'); + $dispatcher = new EventDispatcher; + $passedThrough = []; + $dispatcher->listen('*', function (string $event, array $payload) use (&$passedThrough): void { + $passedThrough[] = $event; + }); $fake = new EventFake($dispatcher, [ 'Foo', @@ -150,6 +150,7 @@ function ($event, $payload) { $fake->assertDispatched('Foo'); $fake->assertDispatched('Bar'); $fake->assertNotDispatched('Baz'); + $this->assertSame(['Baz'], $passedThrough); } public function testAssertNothingDispatched(): void diff --git a/tests/Validation/ValidationFactoryTest.php b/tests/Validation/ValidationFactoryTest.php index 8cffd0456a..371f42d490 100755 --- a/tests/Validation/ValidationFactoryTest.php +++ b/tests/Validation/ValidationFactoryTest.php @@ -7,6 +7,8 @@ use Hypervel\Contracts\Container\Container; use Hypervel\Contracts\Translation\Translator as TranslatorInterface; use Hypervel\Tests\TestCase; +use Hypervel\Translation\ArrayLoader; +use Hypervel\Translation\Translator; use Hypervel\Validation\Factory; use Hypervel\Validation\PresenceVerifierInterface; use Hypervel\Validation\Validator; @@ -17,14 +19,14 @@ class ValidationFactoryTest extends TestCase { public function testMakeMethodCreatesValidValidator(): void { - $translator = m::mock(TranslatorInterface::class); + $translator = new Translator(new ArrayLoader, 'en'); $factory = new Factory($translator); $validator = $factory->make(['foo' => 'bar'], ['baz' => 'boom']); $this->assertEquals($translator, $validator->getTranslator()); $this->assertEquals(['foo' => 'bar'], $validator->getData()); $this->assertEquals(['baz' => ['boom']], $validator->getRules()); - $presence = m::mock(PresenceVerifierInterface::class); + $presence = new FakePresenceVerifier; $noop1 = function (): void { }; $noop2 = function (): void { @@ -41,7 +43,7 @@ public function testMakeMethodCreatesValidValidator(): void $this->assertEquals(['replacer' => $noop3], $validator->replacers); $this->assertEquals($presence, $validator->getPresenceVerifier()); - $presence = m::mock(PresenceVerifierInterface::class); + $presence = new FakePresenceVerifier; $factory->extend('foo', $noop1, 'foo!'); $factory->extendImplicit('implicit', $noop2, 'implicit!'); $factory->extendImplicit('dependent', $noop3, 'dependent!'); @@ -54,15 +56,8 @@ public function testMakeMethodCreatesValidValidator(): void public function testValidateCallsValidateOnTheValidator(): void { - $validator = m::mock(Validator::class); - $translator = m::mock(TranslatorInterface::class); - $factory = m::mock(Factory::class . '[make]', [$translator]); - - $factory->expects('make') - ->with(['foo' => 'bar', 'baz' => 'boom'], ['foo' => 'required'], [], []) - ->andReturn($validator); - - $validator->expects('validate')->andReturn(['foo' => 'bar']); + $translator = new Translator(new ArrayLoader, 'en'); + $factory = new Factory($translator); $validated = $factory->validate( ['foo' => 'bar', 'baz' => 'boom'], @@ -75,7 +70,7 @@ public function testValidateCallsValidateOnTheValidator(): void public function testCustomResolverIsCalled(): void { unset($_SERVER['__validator.factory']); - $translator = m::mock(TranslatorInterface::class); + $translator = new Translator(new ArrayLoader, 'en'); $factory = new Factory($translator); $factory->resolver(function (TranslatorInterface $translator, array $data, array $rules): Validator { $_SERVER['__validator.factory'] = true; @@ -93,7 +88,7 @@ public function testCustomResolverIsCalled(): void public function testValidateMethodCanBeCalledPublicly(): void { - $translator = m::mock(TranslatorInterface::class); + $translator = new Translator(new ArrayLoader, 'en'); $factory = new Factory($translator); $factory->extend('foo', function (string $attribute, mixed $value, array $parameters, Validator $validator): bool { return $validator->validateArray($attribute, $value); @@ -105,7 +100,7 @@ public function testValidateMethodCanBeCalledPublicly(): void public function testExcludeAndIncludeUnvalidatedArrayKeys(): void { - $translator = m::mock(TranslatorInterface::class); + $translator = new Translator(new ArrayLoader, 'en'); $factory = new Factory($translator); // check the default behaviour. @@ -138,7 +133,7 @@ public function testExcludeAndIncludeUnvalidatedArrayKeys(): void public function testSetContainer(): void { - $translator = m::mock(TranslatorInterface::class); + $translator = new Translator(new ArrayLoader, 'en'); $container = m::mock(Container::class); $factory = new Factory($translator); @@ -156,7 +151,7 @@ public function testPresenceVerifierIsNullableWhenFactoryIsConstructedDirectly() public function testFakeDnsLookupsDelegatesToTheValidator(): void { - $factory = new Factory(m::mock(TranslatorInterface::class)); + $factory = new Factory(new Translator(new ArrayLoader, 'en')); $property = new ReflectionProperty(Validator::class, 'fakeDnsLookups'); $factory->fakeDnsLookups(); @@ -166,3 +161,22 @@ public function testFakeDnsLookupsDelegatesToTheValidator(): void $this->assertFalse($property->getValue()); } } + +class FakePresenceVerifier implements PresenceVerifierInterface +{ + /** + * Count the number of objects in a collection having the given value. + */ + public function getCount(string $collection, string $column, mixed $value, int|string|null $excludeId = null, ?string $idColumn = null, array $extra = []): int + { + return 0; + } + + /** + * Count the number of objects in a collection with the given values. + */ + public function getMultiCount(string $collection, string $column, array $values, array $extra = []): int + { + return 0; + } +} diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php index 0126092898..405d02cec1 100644 --- a/tests/Validation/ValidationNotPwnedVerifierTest.php +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -5,13 +5,15 @@ namespace Hypervel\Tests\Validation; use Generator; +use GuzzleHttp\Promise\PromiseInterface; use Hypervel\Contracts\Debug\ExceptionHandler; +use Hypervel\Foundation\Exceptions\Handler; use Hypervel\Http\Client\ConnectionException; use Hypervel\Http\Client\Factory as HttpFactory; -use Hypervel\Http\Client\Response; +use Hypervel\Http\Client\Request; +use Hypervel\Support\Testing\Fakes\ExceptionHandlerFake; use Hypervel\Testbench\TestCase; use Hypervel\Validation\NotPwnedVerifier; -use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; class ValidationNotPwnedVerifierTest extends TestCase @@ -19,13 +21,16 @@ class ValidationNotPwnedVerifierTest extends TestCase #[DataProvider('dataProviderEmptyValues')] public function testEmptyValues(string|bool|int $password): void { - $httpFactory = m::mock(HttpFactory::class); - $verifier = new NotPwnedVerifier($httpFactory); + $http = new HttpFactory; + $http->fake(); + $verifier = new NotPwnedVerifier($http); $this->assertFalse($verifier->verify([ 'value' => $password, 'threshold' => 0, ])); + + $http->assertNothingSent(); } /** @@ -40,63 +45,30 @@ public static function dataProviderEmptyValues(): Generator public function testApiResponseGoesWrong(): void { - $httpFactory = m::mock(HttpFactory::class); - $response = m::mock(Response::class); - - $httpFactory - ->expects('withHeaders') - ->with(['Add-Padding' => 'true']) - ->andReturn($httpFactory); - - $httpFactory - ->expects('timeout') - ->with(30) - ->andReturn($httpFactory); - - $httpFactory->expects('get') - ->andReturn($response); - - $response->expects('successful') - ->andReturn(true); - - $response->expects('body') - ->andReturn(''); - - $verifier = new NotPwnedVerifier($httpFactory); + $http = new HttpFactory; + $http->fake(['api.pwnedpasswords.com/*' => HttpFactory::response('', 200)]); + $verifier = new NotPwnedVerifier($http); $this->assertTrue($verifier->verify([ 'value' => 123123123, 'threshold' => 0, ])); + + $http->assertSentCount(1); } public function testApiGoesDown(): void { - $httpFactory = m::mock(HttpFactory::class); - $response = m::mock(Response::class); - - $httpFactory - ->expects('withHeaders') - ->with(['Add-Padding' => 'true']) - ->andReturn($httpFactory); - - $httpFactory - ->expects('timeout') - ->with(30) - ->andReturn($httpFactory); - - $httpFactory->expects('get') - ->andReturn($response); - - $response->expects('successful') - ->andReturn(false); - - $verifier = new NotPwnedVerifier($httpFactory); + $http = new HttpFactory; + $http->fake(['api.pwnedpasswords.com/*' => HttpFactory::response('', 500)]); + $verifier = new NotPwnedVerifier($http); $this->assertTrue($verifier->verify([ 'value' => 123123123, 'threshold' => 0, ])); + + $http->assertSentCount(1); } public function testMagicHashDoesNotCauseFalsePositive(): void @@ -110,67 +82,42 @@ public function testMagicHashDoesNotCauseFalsePositive(): void $differentSuffix = '00000000000000000000000000000000000'; - $httpFactory = m::mock(HttpFactory::class); - $response = m::mock(Response::class); - - $httpFactory - ->expects('withHeaders') - ->with(['Add-Padding' => 'true']) - ->andReturn($httpFactory); - - $httpFactory - ->expects('timeout') - ->with(30) - ->andReturn($httpFactory); - - $httpFactory->expects('get') - ->with('https://api.pwnedpasswords.com/range/' . $hashPrefix) - ->andReturn($response); - - $response->expects('successful') - ->andReturn(true); - - $response->expects('body') - ->andReturn($differentSuffix . ':5'); + $timeout = null; + $http = new HttpFactory; + // Capture the request options, since recorded requests do not expose the timeout. + $http->fake(['api.pwnedpasswords.com/*' => function (Request $request, array $options) use ($differentSuffix, &$timeout): PromiseInterface { + $timeout = $options['timeout']; - $verifier = new NotPwnedVerifier($httpFactory); + return HttpFactory::response($differentSuffix . ':5', 200); + }]); + $verifier = new NotPwnedVerifier($http); $this->assertTrue($verifier->verify([ 'value' => $password, 'threshold' => 0, ])); + + $http->assertSent(fn (Request $request): bool => $request->url() === 'https://api.pwnedpasswords.com/range/' . $hashPrefix + && $request->hasHeader('Add-Padding', 'true')); + $this->assertSame(30, $timeout); } public function testDnsDown(): void { - $exception = new ConnectionException; - - $exceptionHandler = m::mock(ExceptionHandler::class); - $exceptionHandler->expects('report')->with($exception, []); + $exceptionHandler = new ExceptionHandlerFake(new Handler($this->app)); $this->app->singleton(ExceptionHandler::class, function () use ($exceptionHandler): ExceptionHandler { return $exceptionHandler; }); - $httpFactory = m::mock(HttpFactory::class); - - $httpFactory - ->expects('withHeaders') - ->with(['Add-Padding' => 'true']) - ->andReturn($httpFactory); + $http = new HttpFactory; + $http->fake(HttpFactory::failedConnection()); + $verifier = new NotPwnedVerifier($http); - $httpFactory - ->expects('timeout') - ->with(30) - ->andReturn($httpFactory); - - $httpFactory - ->expects('get') - ->andThrow($exception); - - $verifier = new NotPwnedVerifier($httpFactory); $this->assertTrue($verifier->verify([ 'value' => 123123123, 'threshold' => 0, ])); + + $exceptionHandler->assertReported(ConnectionException::class); } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index f638e96aad..cf853e7ba6 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -11,6 +11,7 @@ use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; use Hypervel\Container\Container; use Hypervel\Contracts\Auth\Authenticatable; +use Hypervel\Contracts\Auth\Factory as AuthFactory; use Hypervel\Contracts\Auth\Guard; use Hypervel\Contracts\Container\Container as ContainerContract; use Hypervel\Contracts\Hashing\Hasher; @@ -486,12 +487,8 @@ public function testClassBasedCustomReplacers(): void $trans = $this->getArrayTranslator(); $trans->addLines(['validation.foo' => 'foo!'], 'en'); $v = new Validator($trans, [], ['name' => 'required']); - $container = m::mock(ContainerContract::class); - $v->setContainer($container); - $v->addReplacer('required', 'Foo@bar'); - $foo = m::mock(stdClass::class); - $container->expects('make')->with('Foo')->andReturn($foo); - $foo->expects('bar')->andReturn('replaced!'); + $v->setContainer(new Container); + $v->addReplacer('required', ClassBasedValidatorStub::class . '@replace'); $v->passes(); $v->messages()->setFormat(':message'); $this->assertSame('replaced!', $v->messages()->first('name')); @@ -1514,9 +1511,10 @@ public function testArrayValidationAcceptsLiteralKeysWhenCalledDirectly(): void public function testValidateCurrentPassword(): void { // Fails when user is not logged in. - $auth = m::mock(Guard::class); - $auth->expects('guard')->andReturn($auth); - $auth->expects('guest')->andReturn(true); + $guard = m::mock(Guard::class); + $guard->expects('guest')->andReturn(true); + $auth = m::mock(AuthFactory::class); + $auth->expects('guard')->andReturn($guard); $hasher = m::mock(Hasher::class); @@ -1537,10 +1535,11 @@ public function testValidateCurrentPassword(): void $user = m::mock(Authenticatable::class); $user->expects('getAuthPassword'); - $auth = m::mock(Guard::class); - $auth->expects('guard')->andReturn($auth); - $auth->expects('guest')->andReturn(false); - $auth->expects('user')->andReturn($user); + $guard = m::mock(Guard::class); + $guard->expects('guest')->andReturn(false); + $guard->expects('user')->andReturn($user); + $auth = m::mock(AuthFactory::class); + $auth->expects('guard')->andReturn($guard); $hasher = m::mock(Hasher::class); $hasher->expects('check')->andReturn(false); @@ -1562,10 +1561,11 @@ public function testValidateCurrentPassword(): void $user = m::mock(Authenticatable::class); $user->expects('getAuthPassword'); - $auth = m::mock(Guard::class); - $auth->expects('guard')->andReturn($auth); - $auth->expects('guest')->andReturn(false); - $auth->expects('user')->andReturn($user); + $guard = m::mock(Guard::class); + $guard->expects('guest')->andReturn(false); + $guard->expects('user')->andReturn($user); + $auth = m::mock(AuthFactory::class); + $auth->expects('guard')->andReturn($guard); $hasher = m::mock(Hasher::class); $hasher->expects('check')->andReturn(true); @@ -1586,10 +1586,11 @@ public function testValidateCurrentPassword(): void $user = m::mock(Authenticatable::class); $user->expects('getAuthPassword'); - $auth = m::mock(Guard::class); - $auth->expects('guard')->with('custom')->andReturn($auth); - $auth->expects('guest')->andReturn(false); - $auth->expects('user')->andReturn($user); + $guard = m::mock(Guard::class); + $guard->expects('guest')->andReturn(false); + $guard->expects('user')->andReturn($user); + $auth = m::mock(AuthFactory::class); + $auth->expects('guard')->with('custom')->andReturn($guard); $hasher = m::mock(Hasher::class); $hasher->expects('check')->andReturn(true); @@ -5335,11 +5336,8 @@ public function testValidateEmailWithFilterUnicodeCheck(): void public function testValidateEmailWithCustomClassCheck(): void { - $container = m::mock(ContainerContract::class); - $container->expects('make')->with(NoRFCWarningsValidation::class)->andReturn(new NoRFCWarningsValidation); - $v = new Validator($this->getArrayTranslator(), ['x' => 'foo@bar '], ['x' => 'email:' . NoRFCWarningsValidation::class]); - $v->setContainer($container); + $v->setContainer(new Container); $this->assertFalse($v->passes()); } @@ -7784,12 +7782,8 @@ public function testClassBasedCustomValidators(): void $trans = $this->getArrayTranslator(); $trans->addLines(['validation.foo' => 'foo!'], 'en'); $v = new Validator($trans, ['name' => 'taylor'], ['name' => 'foo']); - $container = m::mock(ContainerContract::class); - $v->setContainer($container); - $v->addExtension('foo', 'Foo@bar'); - $foo = m::mock(stdClass::class); - $container->expects('make')->with('Foo')->andReturn($foo); - $foo->expects('bar')->andReturn(false); + $v->setContainer(new Container); + $v->addExtension('foo', ClassBasedValidatorStub::class . '@bar'); $this->assertFalse($v->passes()); $v->messages()->setFormat(':message'); $this->assertSame('foo!', $v->messages()->first('name')); @@ -7800,12 +7794,8 @@ public function testClassBasedCustomValidatorsUsingConventionalMethod(): void $trans = $this->getArrayTranslator(); $trans->addLines(['validation.foo' => 'foo!'], 'en'); $v = new Validator($trans, ['name' => 'taylor'], ['name' => 'foo']); - $container = m::mock(ContainerContract::class); - $v->setContainer($container); - $v->addExtension('foo', 'Foo'); - $foo = m::mock(stdClass::class); - $container->expects('make')->with('Foo')->andReturn($foo); - $foo->expects('validate')->andReturn(false); + $v->setContainer(new Container); + $v->addExtension('foo', ConventionalValidatorStub::class); $this->assertFalse($v->passes()); $v->messages()->setFormat(':message'); $this->assertSame('foo!', $v->messages()->first('name')); @@ -11326,6 +11316,36 @@ protected function mockContainer() } } +class ClassBasedValidatorStub +{ + /** + * Replace the validation message. + */ + public function replace(): string + { + return 'replaced!'; + } + + /** + * Validate the attribute. + */ + public function bar(): bool + { + return false; + } +} + +class ConventionalValidatorStub +{ + /** + * Validate the attribute. + */ + public function validate(): bool + { + return false; + } +} + class ImplicitTableModel extends Model { protected array $guarded = []; diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index 399fbd5132..1ef4be7b36 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -32,7 +32,6 @@ use Mockery as m; use ReflectionFunction; use RuntimeException; -use stdClass; class ViewFactoryTest extends TestCase { @@ -530,30 +529,22 @@ public function testClassCallbacks(): void { $factory = $this->getFactory(); $factory->getDispatcher()->expects('listen')->with('composing: foo', m::type(Closure::class)); - $container = m::mock(Container::class); - $factory->setContainer($container); - $composer = m::mock(stdClass::class); - $container->expects('make')->with('FooComposer')->andReturn($composer); - $composer->expects('compose')->with('view')->andReturn('composed'); - $callback = $factory->composer('foo', 'FooComposer'); + $factory->setContainer(new ContainerInstance); + $callback = $factory->composer('foo', ViewComposerStub::class); $callback = $callback[0]; - $this->assertSame('composed', $callback('view')); + $this->assertSame('view composed', $callback('view')); } public function testClassCallbacksWithMethods(): void { $factory = $this->getFactory(); $factory->getDispatcher()->expects('listen')->with('composing: foo', m::type(Closure::class)); - $container = m::mock(Container::class); - $factory->setContainer($container); - $composer = m::mock(stdClass::class); - $container->expects('make')->with('FooComposer')->andReturn($composer); - $composer->expects('doComposer')->with('view')->andReturn('composed'); - $callback = $factory->composer('foo', 'FooComposer@doComposer'); + $factory->setContainer(new ContainerInstance); + $callback = $factory->composer('foo', ViewComposerStub::class . '@doComposer'); $callback = $callback[0]; - $this->assertSame('composed', $callback('view')); + $this->assertSame('view do-composed', $callback('view')); } public function testCallComposerCallsProperEvent(): void @@ -1243,3 +1234,22 @@ protected function getFactoryArgs(): array ]; } } + +class ViewComposerStub +{ + /** + * Compose the view. + */ + public function compose(string $view): string + { + return $view . ' composed'; + } + + /** + * Compose the view through a custom method. + */ + public function doComposer(string $view): string + { + return $view . ' do-composed'; + } +} From fff208dad3055dd3a3cde1831b07ee5242990c94 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:48:45 +0000 Subject: [PATCH 4/5] Compare FakeJob release delays in seconds in assertReleased FakeJob now stores every delay its release() method accepts, but InteractsWithQueue::assertReleased() still compared the stored value directly with the expected number of seconds. When a test released the fake job itself with a date or interval, building the failure message threw an Error, because those objects cannot be converted to a string. The assertion now converts the stored delay to seconds before comparing it. Add a regression test for a fake job released directly with an interval. The interval tests in InteractsWithQueueTest now freeze the clock, since converting an interval reads the current time twice and could return 59 instead of 60 if the second changed between the reads. Verified with php-cs-fixer, PHPStan and the test file. --- src/queue/src/InteractsWithQueue.php | 7 +++++-- tests/Queue/InteractsWithQueueTest.php | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/queue/src/InteractsWithQueue.php b/src/queue/src/InteractsWithQueue.php index 8cb20f4c90..050b3c893b 100644 --- a/src/queue/src/InteractsWithQueue.php +++ b/src/queue/src/InteractsWithQueue.php @@ -193,10 +193,13 @@ public function assertReleased(DateInterval|DateTimeInterface|int|null $delay = ); if (! is_null($delay)) { + // A fake job released directly can hold a date or interval instead of seconds. + $actual = $this->secondsUntil($this->job->releaseDelay); // @phpstan-ignore property.notFound (FakeJob test-double property) + PHPUnit::assertSame( $delay, - $this->job->releaseDelay, // @phpstan-ignore property.notFound (FakeJob test-double property) - "Expected job to be released with delay of [{$delay}] seconds, but was released with delay of [{$this->job->releaseDelay}] seconds." // @phpstan-ignore property.notFound + $actual, + "Expected job to be released with delay of [{$delay}] seconds, but was released with delay of [{$actual}] seconds." ); } diff --git a/tests/Queue/InteractsWithQueueTest.php b/tests/Queue/InteractsWithQueueTest.php index 026dba205f..7c3c1a0f66 100644 --- a/tests/Queue/InteractsWithQueueTest.php +++ b/tests/Queue/InteractsWithQueueTest.php @@ -8,6 +8,7 @@ use Exception; use Hypervel\Contracts\Queue\Job; use Hypervel\Queue\InteractsWithQueue; +use Hypervel\Support\CarbonImmutable; use Hypervel\Tests\TestCase; use Mockery as m; @@ -34,6 +35,8 @@ public function testCreatesAnExceptionFromString(): void public function testReleasesUsingDateInterval(): void { + CarbonImmutable::setTestNow(CarbonImmutable::now()); + $queueJob = m::mock(Job::class); $queueJob->expects('release')->with(60); @@ -47,6 +50,8 @@ public function testReleasesUsingDateInterval(): void public function testAssertsReleaseUsingDateInterval(): void { + CarbonImmutable::setTestNow(CarbonImmutable::now()); + $job = new class { use InteractsWithQueue; }; @@ -56,4 +61,18 @@ public function testAssertsReleaseUsingDateInterval(): void $job->assertReleased(new DateInterval('PT1M')); } + + public function testAssertsReleaseOfFakeJobReleasedDirectlyWithDateInterval(): void + { + CarbonImmutable::setTestNow(CarbonImmutable::now()); + + $job = new class { + use InteractsWithQueue; + }; + + $job->withFakeQueueInteractions(); + $job->job->release(new DateInterval('PT1M')); + + $job->assertReleased(60); + } } From f6cf615513ddccda3b681e82843bb595e02cda3b Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:48:45 +0000 Subject: [PATCH 5/5] Keep exact dispatch counts and the verifier timeout check in converted tests Several tests converted from mocks to event, bus and queue fakes lost their exact-count checks. Their Mockery expectations had required a single dispatch or push, but the converted assertDispatched(), assertPushed() and assertPushedOn() calls only check that at least one happened, so a duplicate dispatch would no longer fail them. Restore assertDispatchedOnce() and assertPushedOnce() in the affected tests in BusDispatcherTest, DatabaseConnectionTest, QueuedEventsTest, NotificationChannelManagerTest, NotificationSenderTest and RedisEventsTest. The password verifier's magic-hash test checked for a 30-second timeout, which is also the HTTP client's default, so it would pass even if the verifier stopped sending its configured timeout. It now uses a 5-second timeout. Verified with php-cs-fixer, PHPStan and the changed test files. --- tests/Bus/BusDispatcherTest.php | 2 ++ tests/Database/DatabaseConnectionTest.php | 12 ++++++------ tests/Events/QueuedEventsTest.php | 1 + .../NotificationChannelManagerTest.php | 4 ++-- tests/Notifications/NotificationSenderTest.php | 13 +++++++++---- tests/Redis/RedisEventsTest.php | 1 + tests/Validation/ValidationNotPwnedVerifierTest.php | 5 +++-- 7 files changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/Bus/BusDispatcherTest.php b/tests/Bus/BusDispatcherTest.php index 90d198c025..c3e6adb7a8 100644 --- a/tests/Bus/BusDispatcherTest.php +++ b/tests/Bus/BusDispatcherTest.php @@ -108,6 +108,7 @@ public function testCommandsAreDispatchedWithQueueRoute(): void $dispatcher->dispatch(new BusDispatcherQueueable); + $queue->assertPushedOnce(BusDispatcherQueueable::class); $queue->assertPushedOn('high-priority', BusDispatcherQueueable::class); } @@ -130,6 +131,7 @@ public function testCommandsAreForwardedToConnectionByQueueName(): void $dispatcher->dispatch((new BusDispatcherQueueable)->onQueue('reports')); $this->assertSame('cloud', $usedConnection); + $queue->assertPushedOnce(BusDispatcherQueueable::class); $queue->assertPushedOn('reports', BusDispatcherQueueable::class); } diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index 16d6aaa02e..3c3eea5b03 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -585,7 +585,7 @@ public function testBeganTransactionFiresEventsIfSet(): void $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->beginTransaction(); - $events->assertDispatched(TransactionBeginning::class); + $events->assertDispatchedOnce(TransactionBeginning::class); } public function testCommittedFiresEventsIfSet(): void @@ -595,7 +595,7 @@ public function testCommittedFiresEventsIfSet(): void $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->commit(); - $events->assertDispatched(TransactionCommitted::class); + $events->assertDispatchedOnce(TransactionCommitted::class); } public function testCommittingFiresEventsIfSet(): void @@ -606,8 +606,8 @@ public function testCommittingFiresEventsIfSet(): void $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->commit(); - $events->assertDispatched(TransactionCommitting::class); - $events->assertDispatched(TransactionCommitted::class); + $events->assertDispatchedOnce(TransactionCommitting::class); + $events->assertDispatchedOnce(TransactionCommitted::class); } public function testRollBackedFiresEventsIfSet(): void @@ -618,7 +618,7 @@ public function testRollBackedFiresEventsIfSet(): void $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->rollBack(); - $events->assertDispatched(TransactionRolledBack::class); + $events->assertDispatchedOnce(TransactionRolledBack::class); } public function testBeganTransactionSkipsDispatchWhenNoListenersAreRegistered(): void @@ -1537,7 +1537,7 @@ public function testLogQueryFiresEventsIfSet(): void $events = new EventFake(new EventsDispatcher); $connection->setEventDispatcher($events); $connection->logQuery('foo', [], null); - $events->assertDispatched(QueryExecuted::class); + $events->assertDispatchedOnce(QueryExecuted::class); } public function testLogQuerySkipsDispatchWhenNoListenersAreRegistered(): void diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index 331e51405c..6524860902 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -53,6 +53,7 @@ public function testQueuedEventHandlersAreQueued(): void $d->listen('some.event', TestDispatcherQueuedHandler::class . '@someMethod'); $d->dispatch('some.event', ['foo', 'bar']); + $fakeQueue->assertPushedOnce(CallQueuedListener::class); $fakeQueue->assertPushedOn(null, CallQueuedListener::class); } diff --git a/tests/Notifications/NotificationChannelManagerTest.php b/tests/Notifications/NotificationChannelManagerTest.php index 6f26433876..054acdaf29 100644 --- a/tests/Notifications/NotificationChannelManagerTest.php +++ b/tests/Notifications/NotificationChannelManagerTest.php @@ -297,7 +297,7 @@ public function testNotificationCanBeQueued(): void $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestQueuedNotification); - $bus->assertDispatched(SendQueuedNotifications::class); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); } public function testSendQueuedNotificationsCanBeOverrideViaContainer(): void @@ -312,7 +312,7 @@ public function testSendQueuedNotificationsCanBeOverrideViaContainer(): void $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestQueuedNotification); - $bus->assertDispatched(TestSendQueuedNotifications::class); + $bus->assertDispatchedOnce(TestSendQueuedNotifications::class); } public function testQueuedNotificationForwardsMessageGroupFromMethodToQueueJob(): void diff --git a/tests/Notifications/NotificationSenderTest.php b/tests/Notifications/NotificationSenderTest.php index 3fae1ba6fe..83f3680759 100644 --- a/tests/Notifications/NotificationSenderTest.php +++ b/tests/Notifications/NotificationSenderTest.php @@ -232,7 +232,7 @@ public function testItCanSendQueuedNotificationsWithAStringVia(): void $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); - $bus->assertDispatched(SendQueuedNotifications::class); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); } public function testItCanSendQueuedNotificationsWithAnArrayVia(): void @@ -297,6 +297,7 @@ public function testItCanSendQueuedNotificationsThroughMiddleware(): void $sender->send($notifiable, new DummyNotificationWithMiddleware); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { return ($job->middleware[0] ?? null) instanceof TestNotificationMiddleware; }); @@ -382,6 +383,7 @@ public function testItCanSendQueuedNotificationsWithQueueRoute(): void $sender->send($notifiable, new DummyQueuedNotificationWithStringVia); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); $bus->assertDispatched(SendQueuedNotifications::class, function (SendQueuedNotifications $job): bool { return $job->queue === 'notification-queue' && $job->channels === ['mail'] && $job->connection === 'notification-connection'; }); @@ -566,9 +568,9 @@ public function testItPreservesNotificationStateMutatedInViaMethod(): void $sender->sendNow($notifiable, new DummyNotificationWithViaMutation); $this->assertSame('default', $driver->sent[0][1]->channelData); - $events->assertDispatched(NotificationSending::class); - $events->assertDispatched(NotificationDelivered::class); - $events->assertDispatched(NotificationSent::class); + $events->assertDispatchedOnce(NotificationSending::class); + $events->assertDispatchedOnce(NotificationDelivered::class); + $events->assertDispatchedOnce(NotificationSent::class); } public function testItQueueOverridesQueueAttribute(): void @@ -598,6 +600,7 @@ public function via(mixed $notifiable): string $sender->send($notifiable, $notification); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'manual-queue'); } @@ -626,6 +629,7 @@ public function via(mixed $notifiable): string $sender->send($notifiable, $notification); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'attribute-queue'); } @@ -662,6 +666,7 @@ public function via(mixed $notifiable): string $sender->send($notifiable, $notification); + $bus->assertDispatchedOnce(SendQueuedNotifications::class); $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'constructor-override-queue'); } diff --git a/tests/Redis/RedisEventsTest.php b/tests/Redis/RedisEventsTest.php index 9f13ffde23..1cc85f5076 100644 --- a/tests/Redis/RedisEventsTest.php +++ b/tests/Redis/RedisEventsTest.php @@ -90,6 +90,7 @@ public function testCommandFailedEventContainsConnectionName(): void // Expected } + $events->assertDispatchedOnce(CommandFailed::class); $events->assertDispatched(CommandFailed::class, function (CommandFailed $event): bool { return $event->connectionName === 'default'; }); diff --git a/tests/Validation/ValidationNotPwnedVerifierTest.php b/tests/Validation/ValidationNotPwnedVerifierTest.php index 405d02cec1..6011b3bff6 100644 --- a/tests/Validation/ValidationNotPwnedVerifierTest.php +++ b/tests/Validation/ValidationNotPwnedVerifierTest.php @@ -90,7 +90,8 @@ public function testMagicHashDoesNotCauseFalsePositive(): void return HttpFactory::response($differentSuffix . ':5', 200); }]); - $verifier = new NotPwnedVerifier($http); + // Use a timeout that differs from the HTTP client's 30-second default. + $verifier = new NotPwnedVerifier($http, 5); $this->assertTrue($verifier->verify([ 'value' => $password, @@ -99,7 +100,7 @@ public function testMagicHashDoesNotCauseFalsePositive(): void $http->assertSent(fn (Request $request): bool => $request->url() === 'https://api.pwnedpasswords.com/range/' . $hashPrefix && $request->hasHeader('Add-Padding', 'true')); - $this->assertSame(30, $timeout); + $this->assertSame(5, $timeout); } public function testDnsDown(): void