diff --git a/src/queue/src/InteractsWithQueue.php b/src/queue/src/InteractsWithQueue.php index 221d716987..050b3c893b 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; } } @@ -195,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/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/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..c3e6adb7a8 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,17 @@ 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->assertPushedOnce(BusDispatcherQueueable::class); + $queue->assertPushedOn('high-priority', BusDispatcherQueueable::class); } public function testCommandsAreForwardedToConnectionByQueueName(): void @@ -131,20 +119,20 @@ 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->assertPushedOnce(BusDispatcherQueueable::class); + $queue->assertPushedOn('reports', BusDispatcherQueueable::class); } public function testExplicitConnectionWinsOverForwardedQueue(): void @@ -173,28 +161,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 +212,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..3c3eea5b03 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->assertDispatchedOnce(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->assertDispatchedOnce(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->assertDispatchedOnce(TransactionCommitting::class); + $events->assertDispatchedOnce(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->assertDispatchedOnce(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->assertDispatchedOnce(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..6524860902 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,18 @@ 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->assertPushedOnce(CallQueuedListener::class); + $fakeQueue->assertPushedOn(null, CallQueuedListener::class); } public function testQueuedEnumEventsRemainSerializable(): void @@ -703,14 +703,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 +731,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..054acdaf29 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->assertDispatchedOnce(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->assertDispatchedOnce(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..83f3680759 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,170 @@ 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->assertDispatchedOnce(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->assertDispatchedOnce(SendQueuedNotifications::class); + $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->assertDispatchedOnce(SendQueuedNotifications::class); + $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 +523,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 +547,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->assertDispatchedOnce(NotificationSending::class); + $events->assertDispatchedOnce(NotificationDelivered::class); + $events->assertDispatchedOnce(NotificationSent::class); } public function testItQueueOverridesQueueAttribute(): void @@ -587,20 +589,19 @@ 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->assertDispatchedOnce(SendQueuedNotifications::class); + $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'manual-queue'); } public function testItQueueAttributeIsUsedWhenOnQueueIsNotCalled(): void @@ -617,20 +618,19 @@ 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->assertDispatchedOnce(SendQueuedNotifications::class); + $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'attribute-queue'); } public function testItConstructorOverrideTakesPrecedenceOverQueueAttribute(): void @@ -655,20 +655,19 @@ 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->assertDispatchedOnce(SendQueuedNotifications::class); + $bus->assertDispatched(SendQueuedNotifications::class, fn (SendQueuedNotifications $job): bool => $job->queue === 'constructor-override-queue'); } public function testNotificationEventsAreSkippedWhenNoListenersAreRegistered(): void @@ -719,6 +718,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 +1010,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/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')], + ]; + } } 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); + } } 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..1cc85f5076 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,11 @@ public function testCommandFailedEventContainsConnectionName(): void } catch (Exception) { // Expected } + + $events->assertDispatchedOnce(CommandFailed::class); + $events->assertDispatched(CommandFailed::class, function (CommandFailed $event): bool { + return $event->connectionName === 'default'; + }); } public function testCommandFailedEventContainsTime(): void @@ -158,18 +144,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..6011b3bff6 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,43 @@ 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); + }]); + // Use a timeout that differs from the HTTP client's 30-second default. + $verifier = new NotPwnedVerifier($http, 5); $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(5, $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'; + } +}