From 0febbd00f4573d1b5deddb2b355f4ba264fee4de Mon Sep 17 00:00:00 2001 From: David Badura Date: Thu, 6 Oct 2022 15:33:42 +0200 Subject: [PATCH] remove logger from projectionist api and add tests --- baseline.xml | 16 +- .../Command/ProjectionistBootCommand.php | 5 +- src/Console/Command/ProjectionistCommand.php | 2 +- .../Command/ProjectionistRemoveCommand.php | 5 +- .../Command/ProjectionistRunCommand.php | 14 +- .../Command/ProjectionistTeardownCommand.php | 5 +- src/Projection/DefaultProjectionist.php | 68 +-- src/Projection/Projectionist.php | 31 +- .../Projection/DefaultProjectionistTest.php | 574 ++++++++++++++++++ tests/Unit/Projection/DummyStore.php | 62 ++ 10 files changed, 703 insertions(+), 79 deletions(-) create mode 100644 tests/Unit/Projection/DefaultProjectionistTest.php create mode 100644 tests/Unit/Projection/DummyStore.php diff --git a/baseline.xml b/baseline.xml index 97584f87c..423f2f787 100644 --- a/baseline.xml +++ b/baseline.xml @@ -29,7 +29,8 @@ - + + $messageLimit $sleep @@ -343,6 +344,19 @@ $this->prophesize(PipelineStore::class) + + + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + class implements Projector { + + class implements Projector { diff --git a/src/Console/Command/ProjectionistBootCommand.php b/src/Console/Command/ProjectionistBootCommand.php index 04eb339ac..e6ba33144 100644 --- a/src/Console/Command/ProjectionistBootCommand.php +++ b/src/Console/Command/ProjectionistBootCommand.php @@ -6,7 +6,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -17,10 +16,8 @@ final class ProjectionistBootCommand extends ProjectionistCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $logger = new ConsoleLogger($output); - $criteria = $this->projectorCriteria(); - $this->projectionist->boot($criteria, $logger); + $this->projectionist->boot($criteria); return 0; } diff --git a/src/Console/Command/ProjectionistCommand.php b/src/Console/Command/ProjectionistCommand.php index f0a266d37..5a095a97d 100644 --- a/src/Console/Command/ProjectionistCommand.php +++ b/src/Console/Command/ProjectionistCommand.php @@ -11,7 +11,7 @@ abstract class ProjectionistCommand extends Command { public function __construct( - protected readonly Projectionist $projectionist + protected readonly Projectionist $projectionist, ) { parent::__construct(); } diff --git a/src/Console/Command/ProjectionistRemoveCommand.php b/src/Console/Command/ProjectionistRemoveCommand.php index 0ad5eb252..61645c4c4 100644 --- a/src/Console/Command/ProjectionistRemoveCommand.php +++ b/src/Console/Command/ProjectionistRemoveCommand.php @@ -6,7 +6,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -17,10 +16,8 @@ final class ProjectionistRemoveCommand extends ProjectionistCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $logger = new ConsoleLogger($output); - $criteria = $this->projectorCriteria(); - $this->projectionist->remove($criteria, $logger); + $this->projectionist->remove($criteria); return 0; } diff --git a/src/Console/Command/ProjectionistRunCommand.php b/src/Console/Command/ProjectionistRunCommand.php index a3ea24c69..023e134d1 100644 --- a/src/Console/Command/ProjectionistRunCommand.php +++ b/src/Console/Command/ProjectionistRunCommand.php @@ -65,7 +65,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $runLimit = InputHelper::nullableInt($input->getOption('run-limit')); - $messageLimit = InputHelper::int($input->getOption('message-limit')); + $messageLimit = InputHelper::nullableInt($input->getOption('message-limit')); $memoryLimit = InputHelper::nullableString($input->getOption('memory-limit')); $timeLimit = InputHelper::nullableInt($input->getOption('time-limit')); $sleep = InputHelper::int($input->getOption('sleep')); @@ -85,7 +85,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($memoryLimit) { - $eventDispatcher->addSubscriber(new StopWorkerOnMemoryLimitListener(Bytes::parseFromString($memoryLimit), $logger)); + $eventDispatcher->addSubscriber( + new StopWorkerOnMemoryLimitListener(Bytes::parseFromString($memoryLimit), $logger) + ); } if ($timeLimit) { @@ -96,9 +98,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $eventDispatcher->addSubscriber(new StopWorkerOnTimeLimitListener($timeLimit, $logger)); } + if ($messageLimit !== null && $messageLimit <= 0) { + throw new InvalidArgumentGiven($messageLimit, 'null|positive-int'); + } + $worker = new DefaultWorker( - function () use ($criteria, $messageLimit, $logger): void { - $this->projectionist->run($criteria, $messageLimit, $logger); + function () use ($criteria, $messageLimit): void { + $this->projectionist->run($criteria, $messageLimit); }, $eventDispatcher, $logger diff --git a/src/Console/Command/ProjectionistTeardownCommand.php b/src/Console/Command/ProjectionistTeardownCommand.php index 8254a7d98..7d29f3e61 100644 --- a/src/Console/Command/ProjectionistTeardownCommand.php +++ b/src/Console/Command/ProjectionistTeardownCommand.php @@ -6,7 +6,6 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( @@ -17,10 +16,8 @@ final class ProjectionistTeardownCommand extends ProjectionistCommand { protected function execute(InputInterface $input, OutputInterface $output): int { - $logger = new ConsoleLogger($output); - $criteria = $this->projectorCriteria(); - $this->projectionist->teardown($criteria, $logger); + $this->projectionist->teardown($criteria); return 0; } diff --git a/src/Projection/DefaultProjectionist.php b/src/Projection/DefaultProjectionist.php index a6809b36c..6adadd379 100644 --- a/src/Projection/DefaultProjectionist.php +++ b/src/Projection/DefaultProjectionist.php @@ -21,13 +21,12 @@ public function __construct( private readonly ProjectorStore $projectorStore, private readonly ProjectorRepository $projectorRepository, private readonly ProjectorResolver $resolver = new MetadataProjectorResolver(), + private readonly ?LoggerInterface $logger = null ) { } - public function boot( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void { + public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void + { $projectorStates = $this->projectorStates() ->filterByProjectorStatus(ProjectorStatus::New) ->filterByCriteria($criteria); @@ -50,10 +49,10 @@ public function boot( try { $createMethod(); - $logger?->info(sprintf('%s created', $projectorState->id()->toString())); + $this->logger?->info(sprintf('%s created', $projectorState->id()->toString())); } catch (Throwable $e) { - $logger?->error(sprintf('%s create error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); + $this->logger?->error(sprintf('%s create error', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); $projectorState->error(); $this->projectorStore->saveProjectorState($projectorState); } @@ -63,21 +62,18 @@ public function boot( foreach ($stream as $message) { foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { - $this->handleMessage($message, $projectorState, $logger); + $this->handleMessage($message, $projectorState); } } - foreach ($projectorStates as $projectorState) { + foreach ($projectorStates->filterByProjectorStatus(ProjectorStatus::Booting) as $projectorState) { $projectorState->active(); $this->projectorStore->saveProjectorState($projectorState); } } - public function run( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?int $limit = null, - ?LoggerInterface $logger = null - ): void { + public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void + { $projectorStates = $this->projectorStates() ->filterByProjectorStatus(ProjectorStatus::Active) ->filterByCriteria($criteria); @@ -109,7 +105,7 @@ public function run( $currentPosition++; - $logger?->info(sprintf('position: %s', $currentPosition)); + $this->logger?->info(sprintf('position: %s', $currentPosition)); $messageCounter++; if ($messageCounter >= $limit) { @@ -118,17 +114,17 @@ public function run( } } - public function teardown( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void { + public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void + { $projectorStates = $this->projectorStates()->filterByProjectorStatus(ProjectorStatus::Outdated); foreach ($projectorStates as $projectorState) { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { - $logger?->warning(sprintf('projector with the id "%s" not found', $projectorState->id()->toString())); + $this->logger?->warning( + sprintf('projector with the id "%s" not found', $projectorState->id()->toString()) + ); continue; } @@ -137,13 +133,10 @@ public function teardown( if ($dropMethod) { try { $dropMethod(); - $logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); + $this->logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); } catch (Throwable $e) { - $logger?->error(sprintf('%s drop error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); - $projectorState->error(); - $this->projectorStore->saveProjectorState($projectorState); - + $this->logger?->error(sprintf('%s drop error', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); continue; } } @@ -152,10 +145,8 @@ public function teardown( } } - public function remove( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void { + public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void + { $projectorStates = $this->projectorStates(); foreach ($projectorStates as $projectorState) { @@ -167,10 +158,10 @@ public function remove( if ($dropMethod) { try { $dropMethod(); - $logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); + $this->logger?->info(sprintf('%s dropped', $projectorState->id()->toString())); } catch (Throwable $e) { - $logger?->warning(sprintf('%s drop error, skipped', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); + $this->logger?->warning(sprintf('%s drop error, skipped', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); } } } @@ -179,11 +170,8 @@ public function remove( } } - private function handleMessage( - Message $message, - ProjectorState $projectorState, - ?LoggerInterface $logger = null - ): void { + private function handleMessage(Message $message, ProjectorState $projectorState): void + { $projector = $this->projectorRepository->findByProjectorId($projectorState->id()); if (!$projector) { @@ -196,8 +184,8 @@ private function handleMessage( try { $handleMethod($message); } catch (Throwable $e) { - $logger?->error(sprintf('%s message error', $projectorState->id()->toString())); - $logger?->error($e->getMessage()); + $this->logger?->error(sprintf('%s message error', $projectorState->id()->toString())); + $this->logger?->error($e->getMessage()); $projectorState->error(); $this->projectorStore->saveProjectorState($projectorState); diff --git a/src/Projection/Projectionist.php b/src/Projection/Projectionist.php index 27d3cf1ae..4aa722425 100644 --- a/src/Projection/Projectionist.php +++ b/src/Projection/Projectionist.php @@ -5,30 +5,19 @@ namespace Patchlevel\EventSourcing\Projection; use Patchlevel\EventSourcing\Projection\ProjectorStore\ProjectorStateCollection; -use Psr\Log\LoggerInterface; interface Projectionist { - public function boot( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void; - - public function run( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?int $limit = null, - ?LoggerInterface $logger = null - ): void; - - public function teardown( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void; - - public function remove( - ProjectorCriteria $criteria = new ProjectorCriteria(), - ?LoggerInterface $logger = null - ): void; + public function boot(ProjectorCriteria $criteria = new ProjectorCriteria()): void; + + /** + * @param positive-int $limit + */ + public function run(ProjectorCriteria $criteria = new ProjectorCriteria(), ?int $limit = null): void; + + public function teardown(ProjectorCriteria $criteria = new ProjectorCriteria()): void; + + public function remove(ProjectorCriteria $criteria = new ProjectorCriteria()): void; public function projectorStates(): ProjectorStateCollection; } diff --git a/tests/Unit/Projection/DefaultProjectionistTest.php b/tests/Unit/Projection/DefaultProjectionistTest.php new file mode 100644 index 000000000..aa696c898 --- /dev/null +++ b/tests/Unit/Projection/DefaultProjectionistTest.php @@ -0,0 +1,574 @@ +prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorStore = $this->prophesize(ProjectorStore::class); + $projectorStore->getStateFromAllProjectors()->willReturn($projectorStateCollection)->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore->reveal(), + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + } + + public function testBootWithoutCreateMethod(): void + { + $projector = new class implements Projector { + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + }; + + $projectorStore = new DummyStore([ + new ProjectorState($projector->projectorId()), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting), + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting, 1), + new ProjectorState($projector->projectorId(), ProjectorStatus::Active, 1), + ], $projectorStore->savedStates); + } + + public function testBootWithMethods(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $created = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function create(): void + { + $this->created = true; + } + + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $projectorStore = new DummyStore(); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); + $projectorResolver->resolveHandleMethod($projector, $message)->willReturn($projector->handle(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting), + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting, 1), + new ProjectorState($projector->projectorId(), ProjectorStatus::Active, 1), + ], $projectorStore->savedStates); + + self::assertTrue($projector->created); + self::assertSame($message, $projector->message); + } + + public function testBootWithCreateError(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $created = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function create(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([ + new ProjectorState($projector->projectorId()), + ]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream()->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 1 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveCreateMethod($projector)->willReturn($projector->create(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->boot(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Booting), + new ProjectorState($projector->projectorId(), ProjectorStatus::Error), + ], $projectorStore->savedStates); + } + + public function testRunning(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function handle(Message $message): void + { + $this->message = $message; + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Active)]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream(0)->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveHandleMethod($projector, $message)->willReturn($projector->handle(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->run(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Active, 1), + ], $projectorStore->savedStates); + + self::assertSame($message, $projector->message); + } + + public function testRunningWithError(): void + { + $projector = new class implements Projector { + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function handle(Message $message): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Active)]); + + $message = new Message(new ProfileVisited(ProfileId::fromString('test'))); + + $generatorFactory = static function () use ($message): Generator { + yield $message; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream(0)->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes( + 2 + ); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveHandleMethod($projector, $message)->willReturn($projector->handle(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->run(); + + self::assertEquals([ + new ProjectorState($projector->projectorId(), ProjectorStatus::Error, 0), + ], $projectorStore->savedStates); + } + + public function testRunningMarkOutdated(): void + { + $projectorId = new ProjectorId('test', 1); + + $projectorStore = new DummyStore([new ProjectorState($projectorId, ProjectorStatus::Active)]); + + $generatorFactory = static function (): Generator { + yield from []; + }; + + $streamableStore = $this->prophesize(StreamableStore::class); + $streamableStore->stream(0)->willReturn($generatorFactory())->shouldBeCalledOnce(); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projectorId)->willReturn(null)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->run(); + + self::assertEquals([ + new ProjectorState($projectorId, ProjectorStatus::Outdated, 0), + ], $projectorStore->savedStates); + } + + public function testTeardownWithProjector(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + $this->dropped = true; + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->teardown(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projector->projectorId()], $projectorStore->removedIds); + self::assertTrue($projector->dropped); + } + + public function testTeardownWithProjectorAndError(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->teardown(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([], $projectorStore->removedIds); + } + + public function testTeardownWithoutProjector(): void + { + $projectorId = new ProjectorId('test', 1); + + $projectorStore = new DummyStore([new ProjectorState($projectorId, ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projectorId)->willReturn(null)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->teardown(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([], $projectorStore->removedIds); + } + + public function testRemoveWithProjector(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + $this->dropped = true; + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->remove(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projector->projectorId()], $projectorStore->removedIds); + self::assertTrue($projector->dropped); + } + + public function testRemoveWithProjectorAndError(): void + { + $projector = new class implements Projector { + public ?Message $message = null; + public bool $dropped = false; + + public function projectorId(): ProjectorId + { + return new ProjectorId('test', 1); + } + + public function drop(): void + { + throw new RuntimeException('ERROR'); + } + }; + + $projectorStore = new DummyStore([new ProjectorState($projector->projectorId(), ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([$projector])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projector->projectorId())->willReturn($projector)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + $projectorResolver->resolveDropMethod($projector)->willReturn($projector->drop(...)); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->remove(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projector->projectorId()], $projectorStore->removedIds); + } + + public function testRemoveWithoutProjector(): void + { + $projectorId = new ProjectorId('test', 1); + + $projectorStore = new DummyStore([new ProjectorState($projectorId, ProjectorStatus::Outdated)]); + + $streamableStore = $this->prophesize(StreamableStore::class); + + $projectorRepository = $this->prophesize(ProjectorRepository::class); + $projectorRepository->projectors()->willReturn([])->shouldBeCalledOnce(); + $projectorRepository->findByProjectorId($projectorId)->willReturn(null)->shouldBeCalledTimes(1); + + $projectorResolver = $this->prophesize(ProjectorResolver::class); + + $projectionist = new DefaultProjectionist( + $streamableStore->reveal(), + $projectorStore, + $projectorRepository->reveal(), + $projectorResolver->reveal(), + ); + + $projectionist->remove(); + + self::assertEquals([], $projectorStore->savedStates); + self::assertEquals([$projectorId], $projectorStore->removedIds); + } +} diff --git a/tests/Unit/Projection/DummyStore.php b/tests/Unit/Projection/DummyStore.php new file mode 100644 index 000000000..31b64d468 --- /dev/null +++ b/tests/Unit/Projection/DummyStore.php @@ -0,0 +1,62 @@ + */ + private array $store = []; + /** @var list */ + public array $savedStates = []; + /** @var list */ + public array $removedIds = []; + + /** + * @param list $store + */ + public function __construct(array $store = []) + { + foreach ($store as $state) { + $this->store[$state->id()->toString()] = $state; + } + } + + public function getProjectorState(ProjectorId $projectorId): ProjectorState + { + if (array_key_exists($projectorId->toString(), $this->store)) { + return $this->store[$projectorId->toString()]; + } + + throw new ProjectorStateNotFound($projectorId); + } + + public function getStateFromAllProjectors(): ProjectorStateCollection + { + return new ProjectorStateCollection(array_values($this->store)); + } + + public function saveProjectorState(ProjectorState ...$projectorStates): void + { + foreach ($projectorStates as $state) { + $this->store[$state->id()->toString()] = $state; + $this->savedStates[] = clone $state; + } + } + + public function removeProjectorState(ProjectorId $projectorId): void + { + $this->removedIds[] = $projectorId; + unset($this->store[$projectorId->toString()]); + } +}