diff --git a/infection.json.dist b/infection.json.dist index 7e8b0ee4..801e71ae 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -13,6 +13,6 @@ "mutators": { "@default": true }, - "minMsi": 59, - "minCoveredMsi": 83 + "minMsi": 51, + "minCoveredMsi": 90 } diff --git a/tests/Integration/BasicImplementation/BasicIntegrationTest.php b/tests/Integration/BasicImplementation/BasicIntegrationTest.php index 40683626..e1a54b1a 100644 --- a/tests/Integration/BasicImplementation/BasicIntegrationTest.php +++ b/tests/Integration/BasicImplementation/BasicIntegrationTest.php @@ -24,6 +24,9 @@ use function file_exists; use function unlink; +/** + * @coversNothing + */ final class BasicIntegrationTest extends TestCase { private Connection $connection; diff --git a/tests/Integration/Pipeline/PipelineChangeStoreTest.php b/tests/Integration/Pipeline/PipelineChangeStoreTest.php index 27600673..c748c1a4 100644 --- a/tests/Integration/Pipeline/PipelineChangeStoreTest.php +++ b/tests/Integration/Pipeline/PipelineChangeStoreTest.php @@ -27,6 +27,9 @@ use function file_exists; use function unlink; +/** + * @coversNothing + */ final class PipelineChangeStoreTest extends TestCase { private Connection $connectionOld; diff --git a/tests/Unit/Aggregate/AggregateChangedTest.php b/tests/Unit/Aggregate/AggregateChangedTest.php index 4fc3ed35..3bccdc50 100644 --- a/tests/Unit/Aggregate/AggregateChangedTest.php +++ b/tests/Unit/Aggregate/AggregateChangedTest.php @@ -6,6 +6,7 @@ use DateTimeImmutable; use Error; +use Patchlevel\EventSourcing\Aggregate\AggregateException; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId; @@ -95,6 +96,18 @@ public function testSerialize(): void ); } + public function testSerializeNotRecorded(): void + { + $id = ProfileId::fromString('1'); + $email = Email::fromString('d.a.badura@gmail.com'); + + $event = ProfileCreated::raise($id, $email); + + $this->expectException(AggregateException::class); + $this->expectExceptionMessage('The change was not recorded.'); + $event->serialize(); + } + public function testDeserialize(): void { $id = ProfileId::fromString('1'); diff --git a/tests/Unit/Aggregate/SnapshotableAggregateRootTest.php b/tests/Unit/Aggregate/SnapshotableAggregateRootTest.php new file mode 100644 index 00000000..bbf621f0 --- /dev/null +++ b/tests/Unit/Aggregate/SnapshotableAggregateRootTest.php @@ -0,0 +1,153 @@ +aggregateRootId()); + self::assertEquals(0, $profile->playhead()); + self::assertEquals($id, $profile->id()); + self::assertEquals($email, $profile->email()); + + $events = $profile->releaseEvents(); + + self::assertCount(1, $events); + $event = $events[0]; + self::assertEquals(0, $event->playhead()); + } + + public function testExecuteMethod(): void + { + $profileId = ProfileId::fromString('1'); + $email = Email::fromString('d.a.badura@gmail.com'); + + $messageId = MessageId::fromString('2'); + + $profile = ProfileWithSnapshot::createProfile($profileId, $email); + + $events = $profile->releaseEvents(); + + self::assertCount(1, $events); + self::assertEquals(0, $profile->playhead()); + $event = $events[0]; + self::assertEquals(0, $event->playhead()); + + $profile->publishMessage( + Message::create( + $messageId, + 'foo' + ) + ); + + self::assertEquals('1', $profile->aggregateRootId()); + self::assertEquals(1, $profile->playhead()); + self::assertEquals($profileId, $profile->id()); + self::assertEquals($email, $profile->email()); + + $events = $profile->releaseEvents(); + + self::assertCount(1, $events); + $event = $events[0]; + self::assertEquals(1, $event->playhead()); + } + + public function testEventWithoutApplyMethod(): void + { + $visitorProfile = ProfileWithSnapshot::createProfile( + ProfileId::fromString('1'), + Email::fromString('visitor@test.com') + ); + + $events = $visitorProfile->releaseEvents(); + self::assertCount(1, $events); + self::assertEquals(0, $visitorProfile->playhead()); + $event = $events[0]; + self::assertEquals(0, $event->playhead()); + + $visitedProfile = ProfileWithSnapshot::createProfile( + ProfileId::fromString('2'), + Email::fromString('visited@test.com') + ); + + $events = $visitedProfile->releaseEvents(); + self::assertCount(1, $events); + self::assertEquals(0, $visitedProfile->playhead()); + $event = $events[0]; + self::assertEquals(0, $event->playhead()); + + $visitorProfile->visitProfile($visitedProfile->id()); + + $events = $visitedProfile->releaseEvents(); + self::assertCount(0, $events); + self::assertEquals(0, $visitedProfile->playhead()); + } + + public function testInitliazingState(): void + { + $eventStream = [ + ProfileCreated::raise( + ProfileId::fromString('1'), + Email::fromString('profile@test.com') + ), + MessagePublished::raise( + ProfileId::fromString('1'), + Message::create( + MessageId::fromString('2'), + 'message value' + ) + ), + ]; + + $profile = ProfileWithSnapshot::createFromEventStream($eventStream); + + self::assertEquals('1', $profile->id()->toString()); + self::assertCount(1, $profile->messages()); + } + + public function testCreateFromSnapshot(): void + { + $eventStream = [ + MessagePublished::raise( + ProfileId::fromString('1'), + Message::create( + MessageId::fromString('2'), + 'message value' + ) + ), + ]; + + $snapshot = new Snapshot( + ProfileWithSnapshot::class, + '1', + 1, + [ + 'id' => '1', + 'email' => 'profile@test.com', + ] + ); + + $profile = ProfileWithSnapshot::createFromSnapshot($snapshot, $eventStream); + + self::assertEquals('1', $profile->id()->toString()); + self::assertCount(1, $profile->messages()); + } +} diff --git a/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php index 166b70a2..903e1e0c 100644 --- a/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php +++ b/tests/Unit/Pipeline/Middleware/RecalculatePlayheadMiddlewareTest.php @@ -35,4 +35,26 @@ public function testReculatePlayhead(): void self::assertEquals(0, $event->playhead()); } + + public function testReculatePlayheadWithSamePlayhead(): void + { + $middleware = new RecalculatePlayheadMiddleware(); + + $bucket = new EventBucket( + Profile::class, + ProfileCreated::raise( + ProfileId::fromString('1'), + Email::fromString('d.a.badura@gmail.com') + )->recordNow(0) + ); + + $result = $middleware($bucket); + + self::assertCount(1, $result); + self::assertEquals(Profile::class, $result[0]->aggregateClass()); + + $event = $result[0]->event(); + + self::assertEquals(0, $event->playhead()); + } } diff --git a/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php b/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php index 71f6eefd..fa890f4f 100644 --- a/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php +++ b/tests/Unit/Pipeline/Middleware/ReplaceEventMiddlewareTest.php @@ -7,6 +7,7 @@ use Patchlevel\EventSourcing\Pipeline\EventBucket; use Patchlevel\EventSourcing\Pipeline\Middleware\ReplaceEventMiddleware; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email; +use Patchlevel\EventSourcing\Tests\Unit\Fixture\MessagePublished; use Patchlevel\EventSourcing\Tests\Unit\Fixture\Profile; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId; @@ -45,4 +46,35 @@ static function (ProfileCreated $event) { self::assertInstanceOf(ProfileVisited::class, $event); self::assertEquals(5, $event->playhead()); } + + public function testReplaceInvalidClass(): void + { + $middleware = new ReplaceEventMiddleware( + MessagePublished::class, + static function (ProfileCreated $event) { + return ProfileVisited::raise( + $event->profileId(), + $event->profileId() + ); + } + ); + + $bucket = new EventBucket( + Profile::class, + ProfileCreated::raise( + ProfileId::fromString('1'), + Email::fromString('d.a.badura@gmail.com') + )->recordNow(5) + ); + + $result = $middleware($bucket); + + self::assertCount(1, $result); + self::assertEquals(Profile::class, $result[0]->aggregateClass()); + + $event = $result[0]->event(); + + self::assertInstanceOf(ProfileCreated::class, $event); + self::assertEquals(5, $event->playhead()); + } } diff --git a/tests/Unit/Pipeline/Target/InMemoryTargetTest.php b/tests/Unit/Pipeline/Target/InMemoryTargetTest.php new file mode 100644 index 00000000..5ff5930b --- /dev/null +++ b/tests/Unit/Pipeline/Target/InMemoryTargetTest.php @@ -0,0 +1,31 @@ +save($bucket); + + $buckets = $inMemoryTarget->buckets(); + self::assertCount(1, $buckets); + self::assertEquals($bucket, $buckets[0]); + } +} diff --git a/tests/Unit/Pipeline/Target/ProjectionRepositoryTargetTest.php b/tests/Unit/Pipeline/Target/ProjectionRepositoryTargetTest.php new file mode 100644 index 00000000..4851190b --- /dev/null +++ b/tests/Unit/Pipeline/Target/ProjectionRepositoryTargetTest.php @@ -0,0 +1,35 @@ +prophesize(ProjectionRepository::class); + $projectionRepository->handle($bucket->event())->shouldBeCalledOnce(); + + $projectionRepositoryTarget = new ProjectionRepositoryTarget($projectionRepository->reveal()); + + $projectionRepositoryTarget->save($bucket); + } +} diff --git a/tests/Unit/Pipeline/Target/ProjectionTargetTest.php b/tests/Unit/Pipeline/Target/ProjectionTargetTest.php new file mode 100644 index 00000000..6e4584f7 --- /dev/null +++ b/tests/Unit/Pipeline/Target/ProjectionTargetTest.php @@ -0,0 +1,37 @@ +markTestIncomplete('Testing not finished, needs discussion'); + + $bucket = new EventBucket( + Profile::class, + ProfileCreated::raise(ProfileId::fromString('1'), Email::fromString('foo@test.com')) + ); + + $projectionRepository = $this->prophesize(Projection::class); + $projectionRepository->handledEvents()->will(static fn () => yield ProfileCreated::class => 'applyProfileCreated'); + + $projectionTarget = new ProjectionTarget($projectionRepository->reveal()); + + $projectionTarget->save($bucket); + } +} diff --git a/tests/Unit/Pipeline/Target/StoreTargetTest.php b/tests/Unit/Pipeline/Target/StoreTargetTest.php new file mode 100644 index 00000000..843ff386 --- /dev/null +++ b/tests/Unit/Pipeline/Target/StoreTargetTest.php @@ -0,0 +1,35 @@ +prophesize(PipelineStore::class); + $pipelineStore->saveEventBucket($bucket)->shouldBeCalled(); + + $storeTarget = new StoreTarget($pipelineStore->reveal()); + + $storeTarget->save($bucket); + } +} diff --git a/tests/Unit/Repository/RepositoryTest.php b/tests/Unit/Repository/RepositoryTest.php index 2e8ca009..9ad76672 100644 --- a/tests/Unit/Repository/RepositoryTest.php +++ b/tests/Unit/Repository/RepositoryTest.php @@ -4,6 +4,7 @@ namespace Patchlevel\EventSourcing\Tests\Unit\Repository; +use InvalidArgumentException; use Patchlevel\EventSourcing\Aggregate\AggregateChanged; use Patchlevel\EventSourcing\EventBus\EventBus; use Patchlevel\EventSourcing\Repository\AggregateNotFoundException; @@ -19,11 +20,42 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use stdClass; class RepositoryTest extends TestCase { use ProphecyTrait; + public function testInstantiateWithWrongClass(): void + { + $store = $this->prophesize(Store::class); + $eventBus = $this->prophesize(EventBus::class); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Class \'stdClass\' is not an AggregateRoot.'); + new Repository( + $store->reveal(), + $eventBus->reveal(), + stdClass::class, + ); + } + + public function testInstantiateWithNonSnapshotAggregateButWithSnapshotStore(): void + { + $store = $this->prophesize(Store::class); + $eventBus = $this->prophesize(EventBus::class); + $snapshotStore = $this->prophesize(SnapshotStore::class); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Class \'Patchlevel\EventSourcing\Tests\Unit\Fixture\Profile\' do not extends SnapshotableAggregateRoot.'); + new Repository( + $store->reveal(), + $eventBus->reveal(), + Profile::class, + $snapshotStore->reveal() + ); + } + public function testSaveAggregate(): void { $store = $this->prophesize(Store::class); diff --git a/tests/Unit/Schema/DoctrineSchemaManagerTest.php b/tests/Unit/Schema/DoctrineSchemaManagerTest.php new file mode 100644 index 00000000..7094b821 --- /dev/null +++ b/tests/Unit/Schema/DoctrineSchemaManagerTest.php @@ -0,0 +1,239 @@ +prophesize(DoctrineStore::class); + $connection = $this->prophesize(Connection::class); + $schema = $this->prophesize(Schema::class); + $platform = $this->prophesize(AbstractPlatform::class); + + $connection->getDatabasePlatform()->willReturn($platform->reveal()); + $schema->toSql(Argument::type(AbstractPlatform::class))->willReturn(['this is sql!']); + $store->schema()->willReturn($schema->reveal()); + + $connection->executeStatement('this is sql!')->shouldBeCalledOnce(); + $store->connection()->willReturn($connection->reveal()); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + $doctrineSchemaManager->create($store->reveal()); + } + + public function testCreateNotSupportedStore(): void + { + $store = $this->prophesize(Store::class); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + + $this->expectException(StoreNotSupported::class); + $doctrineSchemaManager->create($store->reveal()); + } + + public function testDryRunCreate(): void + { + $store = $this->prophesize(DoctrineStore::class); + $connection = $this->prophesize(Connection::class); + $schema = $this->prophesize(Schema::class); + $platform = $this->prophesize(AbstractPlatform::class); + + $connection->getDatabasePlatform()->willReturn($platform->reveal()); + $schema->toSql(Argument::type(AbstractPlatform::class))->willReturn(['this is sql!']); + $store->schema()->willReturn($schema->reveal()); + $store->connection()->willReturn($connection->reveal()); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + $sqlStatements = $doctrineSchemaManager->dryRunCreate($store->reveal()); + + self::assertEquals(['this is sql!'], $sqlStatements); + } + + public function testDryRunCreateNotSupportedStore(): void + { + $store = $this->prophesize(Store::class); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + + $this->expectException(StoreNotSupported::class); + $doctrineSchemaManager->dryRunCreate($store->reveal()); + } + + public function testUpdate(): void + { + $store = $this->prophesize(DoctrineStore::class); + $connection = $this->prophesize(Connection::class); + $fromSchema = $this->prophesize(Schema::class); + $toSchema = $this->prophesize(Schema::class); + $schemaManager = $this->prophesize(AbstractSchemaManager::class); + $platform = $this->prophesize(AbstractPlatform::class); + + $table = new Table('foo'); + + $toSchema->getNamespaces()->willReturn([]); + $toSchema->getTables()->willReturn([$table]); + $toSchema->getTable('foo')->willReturn($table); + $toSchema->getSequences()->willReturn([]); + $toSchema->getName()->willReturn('toSchema'); + + $fromSchema->getNamespaces()->willReturn([]); + $fromSchema->getTables()->willReturn([]); + $fromSchema->getSequences()->willReturn([]); + $fromSchema->hasTable('foo')->willReturn(false); + + $platform->supportsSchemas()->willReturn(false); + $platform->supportsForeignKeyConstraints()->willReturn(false); + $platform->supportsSequences()->willReturn(false); + $platform->supportsForeignKeyConstraints()->willReturn(false); + + $platform->getCreateTableSQL($table, AbstractPlatform::CREATE_INDEXES)->willReturn(['CREATE TABLE foo;']); + + $schemaManager->createSchema()->willReturn($fromSchema->reveal()); + + $connection->getSchemaManager()->willReturn($schemaManager->reveal()); + $connection->getDatabasePlatform()->willReturn($platform->reveal()); + $store->schema()->willReturn($toSchema->reveal()); + + $connection->executeStatement('CREATE TABLE foo;')->shouldBeCalled(); + $store->connection()->willReturn($connection->reveal()); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + $doctrineSchemaManager->update($store->reveal()); + } + + public function testUpdateNotSupportedStore(): void + { + $store = $this->prophesize(Store::class); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + + $this->expectException(StoreNotSupported::class); + $doctrineSchemaManager->update($store->reveal()); + } + + public function testDryRunUpdate(): void + { + $store = $this->prophesize(DoctrineStore::class); + $connection = $this->prophesize(Connection::class); + $fromSchema = $this->prophesize(Schema::class); + $toSchema = $this->prophesize(Schema::class); + $schemaManager = $this->prophesize(AbstractSchemaManager::class); + $platform = $this->prophesize(AbstractPlatform::class); + + $fromSchema->getNamespaces()->willReturn([]); + $fromSchema->getTables()->willReturn([]); + $fromSchema->getSequences()->willReturn([]); + $toSchema->getNamespaces()->willReturn([]); + $toSchema->getTables()->willReturn([]); + $toSchema->getSequences()->willReturn([]); + + $schemaManager->createSchema()->willReturn($fromSchema->reveal()); + + $connection->getSchemaManager()->willReturn($schemaManager->reveal()); + $connection->getDatabasePlatform()->willReturn($platform->reveal()); + $store->schema()->willReturn($toSchema->reveal()); + $store->connection()->willReturn($connection->reveal()); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + $sqlStatements = $doctrineSchemaManager->dryRunUpdate($store->reveal()); + + self::assertEquals([], $sqlStatements); + } + + public function testDryRunUpdateNotSupportedStore(): void + { + $store = $this->prophesize(Store::class); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + + $this->expectException(StoreNotSupported::class); + $doctrineSchemaManager->dryRunUpdate($store->reveal()); + } + + public function testDrop(): void + { + $store = $this->prophesize(DoctrineStore::class); + $connection = $this->prophesize(Connection::class); + $currentSchema = $this->prophesize(Schema::class); + $toSchema = $this->prophesize(Schema::class); + $schemaManager = $this->prophesize(AbstractSchemaManager::class); + + $toSchema->getTableNames()->willReturn(['foo', 'bar']); + + $currentSchema->hasTable('foo')->willReturn(true); + $currentSchema->hasTable('bar')->willReturn(false); + + $schemaManager->createSchema()->willReturn($currentSchema->reveal()); + $connection->getSchemaManager()->willReturn($schemaManager->reveal()); + $store->schema()->willReturn($toSchema->reveal()); + + $connection->executeStatement('DROP TABLE foo;')->shouldBeCalled(); + $connection->executeStatement('DROP TABLE bar;')->shouldNotBeCalled(); + $store->connection()->willReturn($connection->reveal()); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + $doctrineSchemaManager->drop($store->reveal()); + } + + public function testDropNotSupported(): void + { + $store = $this->prophesize(Store::class); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + + $this->expectException(StoreNotSupported::class); + $doctrineSchemaManager->drop($store->reveal()); + } + + public function testdDryRunDrop(): void + { + $store = $this->prophesize(DoctrineStore::class); + $connection = $this->prophesize(Connection::class); + $currentSchema = $this->prophesize(Schema::class); + $toSchema = $this->prophesize(Schema::class); + $schemaManager = $this->prophesize(AbstractSchemaManager::class); + + $toSchema->getTableNames()->willReturn(['foo', 'bar']); + + $currentSchema->hasTable('foo')->willReturn(true); + $currentSchema->hasTable('bar')->willReturn(false); + + $schemaManager->createSchema()->willReturn($currentSchema->reveal()); + $connection->getSchemaManager()->willReturn($schemaManager->reveal()); + $store->schema()->willReturn($toSchema->reveal()); + + $store->connection()->willReturn($connection->reveal()); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + $queries = $doctrineSchemaManager->dryRunDrop($store->reveal()); + self::assertEquals(['DROP TABLE foo;'], $queries); + } + + public function testDryRunDropNotSupported(): void + { + $store = $this->prophesize(Store::class); + + $doctrineSchemaManager = new DoctrineSchemaManager(); + + $this->expectException(StoreNotSupported::class); + $doctrineSchemaManager->dryRunDrop($store->reveal()); + } +}