diff --git a/README.md b/README.md index a9f4f9b..ce48d01 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,14 @@ The httplug event store is an implementation that uses httplug to communicate wi This example uses Guzzle6 httplug adapter ```php +$messageFactory = new \Prooph\Common\Messaging\FQCNMessageFactory(); +$messageConverter = new \Prooph\Common\Messaging\NoOpMessageConverter(); $httplug = new \Http\Adapter\Guzzle6\Client(); -$eventStore = new \Prooph\EventStore\Httplug($httpPlug, $options); +$uri = new \GuzzleHttp\Psr7\Uri('http:/localhost:8080'); -$streamEvents =$eventStore->load(new StreamName('test-stream')); +$eventStore = new \Prooph\EventStore\Httplug\HttplugEventStore($messageFactory, $messageConverter, $httpPlug, $uri); + +$streamEvents = $eventStore->load(new StreamName('test-stream')); ``` ## Support diff --git a/composer.json b/composer.json index 07a7e8a..0e1c464 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,11 @@ ], "require": { "php": "^7.1", - "prooph/event-store" : "^7.2" + "prooph/event-store": "^7.2", + "php-http/httplug": "^1.1.0", + "psr/http-message": "^1.0.1", + "php-http/message-factory": "^1.0.2", + "php-http/discovery": "^1.1.1" }, "require-dev": { "phpunit/phpunit": "^6.0", @@ -43,13 +47,13 @@ }, "autoload": { "psr-4": { - "Prooph\\EventStore\\Httplug\\": "src/" + "Prooph\\EventStore\\Httplug\\": "src/", + "ProophTest\\EventStore\\": "vendor/prooph/event-store/tests/" } }, "autoload-dev": { "psr-4": { - "ProophTest\\EventStore\\Httplug\\": "tests/", - "ProophTest\\EventStore\\": "vendor/prooph/event-store/tests/" + "ProophTest\\EventStore\\Httplug\\": "tests/" } }, "config": { diff --git a/src/Container/HttplugEventStoreFactory.php b/src/Container/HttplugEventStoreFactory.php new file mode 100644 index 0000000..a7893f5 --- /dev/null +++ b/src/Container/HttplugEventStoreFactory.php @@ -0,0 +1,106 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Httplug\Container; + +use Interop\Config\ConfigurationTrait; +use Interop\Config\ProvidesDefaultOptions; +use Interop\Config\RequiresConfigId; +use Interop\Config\RequiresMandatoryOptions; +use Prooph\Common\Messaging\FQCNMessageFactory; +use Prooph\Common\Messaging\NoOpMessageConverter; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Httplug\HttplugEventStore; +use Psr\Container\ContainerInterface; + +final class HttplugEventStoreFactory implements + ProvidesDefaultOptions, + RequiresConfigId, + RequiresMandatoryOptions +{ + use ConfigurationTrait; + + /** + * @var string + */ + private $configId; + + /** + * Creates a new instance from a specified config, specifically meant to be used as static factory. + * + * In case you want to use another config key than provided by the factories, you can add the following factory to + * your config: + * + * + * [HttplugEventStoreFactory::class, 'service_name'], + * ]; + * + * + * @throws InvalidArgumentException + */ + public static function __callStatic(string $name, array $arguments): HttplugEventStore + { + if (! isset($arguments[0]) || ! $arguments[0] instanceof ContainerInterface) { + throw new InvalidArgumentException( + sprintf('The first argument must be of type %s', ContainerInterface::class) + ); + } + + return (new static($name))->__invoke($arguments[0]); + } + + public function __construct(string $configId = 'default') + { + $this->configId = $configId; + } + + public function __invoke(ContainerInterface $container): HttplugEventStore + { + $config = $container->get('config'); + $config = $this->options($config, $this->configId); + + $requestFactory = null; + + if (isset($config['request_factory'])) { + $requestFactory = $container->get($config['request_factory']); + } + + return new HttplugEventStore( + $container->get($config['message_factory']), + $container->get($config['message_converter']), + $container->get($config['http_client']), + $requestFactory + ); + } + + public function dimensions(): iterable + { + return ['prooph', 'event_store']; + } + + public function defaultOptions(): iterable + { + return [ + 'message_factory' => FQCNMessageFactory::class, + 'message_converter' => NoOpMessageConverter::class, + ]; + } + + public function mandatoryOptions(): iterable + { + return [ + 'http_client', + ]; + } +} diff --git a/src/Container/Projection/HttplugProjectionManagerFactory.php b/src/Container/Projection/HttplugProjectionManagerFactory.php new file mode 100644 index 0000000..e2b2766 --- /dev/null +++ b/src/Container/Projection/HttplugProjectionManagerFactory.php @@ -0,0 +1,92 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Httplug\Container\Projection; + +use Interop\Config\ConfigurationTrait; +use Interop\Config\RequiresConfigId; +use Interop\Config\RequiresMandatoryOptions; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Httplug\Projection\HttplugProjectionManager; +use Psr\Container\ContainerInterface; + +final class HttplugProjectionManagerFactory implements + RequiresConfigId, + RequiresMandatoryOptions +{ + use ConfigurationTrait; + + /** + * @var string + */ + private $configId; + + /** + * Creates a new instance from a specified config, specifically meant to be used as static factory. + * + * In case you want to use another config key than provided by the factories, you can add the following factory to + * your config: + * + * + * [HttplugProjectionManagerFactory::class, 'service_name'], + * ]; + * + * + * @throws InvalidArgumentException + */ + public static function __callStatic(string $name, array $arguments): HttplugProjectionManager + { + if (! isset($arguments[0]) || ! $arguments[0] instanceof ContainerInterface) { + throw new InvalidArgumentException( + sprintf('The first argument must be of type %s', ContainerInterface::class) + ); + } + + return (new static($name))->__invoke($arguments[0]); + } + + public function __construct(string $configId = 'default') + { + $this->configId = $configId; + } + + public function __invoke(ContainerInterface $container): HttplugProjectionManager + { + $config = $container->get('config'); + $config = $this->options($config, $this->configId); + + $requestFactory = null; + + if (isset($config['request_factory'])) { + $requestFactory = $container->get($config['request_factory']); + } + + return new HttplugProjectionManager( + $container->get($config['http_client']), + $requestFactory + ); + } + + public function dimensions(): iterable + { + return ['prooph', 'projection_manager']; + } + + public function mandatoryOptions(): iterable + { + return [ + 'http_client', + ]; + } +} diff --git a/src/Exception/NotAllowed.php b/src/Exception/NotAllowed.php new file mode 100644 index 0000000..78963a4 --- /dev/null +++ b/src/Exception/NotAllowed.php @@ -0,0 +1,20 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Httplug\Exception; + +use Prooph\EventStore\Exception\RuntimeException; + +class NotAllowed extends RuntimeException +{ + protected $message = 'You are not allowed to access this resource'; +} diff --git a/src/HttplugEventStore.php b/src/HttplugEventStore.php index b3d9bbc..784415d 100644 --- a/src/HttplugEventStore.php +++ b/src/HttplugEventStore.php @@ -1 +1,515 @@ + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Httplug; + +use DateTimeImmutable; +use DateTimeZone; +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\RequestFactory; +use Iterator; +use Prooph\Common\Messaging\MessageConverter; +use Prooph\Common\Messaging\MessageFactory; +use Prooph\EventStore\EventStore; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Exception\RuntimeException; +use Prooph\EventStore\Exception\StreamNotFound; +use Prooph\EventStore\Httplug\Exception\NotAllowed; +use Prooph\EventStore\Metadata\FieldType; +use Prooph\EventStore\Metadata\MetadataMatcher; +use Prooph\EventStore\Stream; +use Prooph\EventStore\StreamName; +use Psr\Http\Message\ResponseInterface; + +final class HttplugEventStore implements EventStore +{ + /** + * @var MessageFactory + */ + private $messageFactory; + + /** + * @var MessageConverter + */ + private $messageConverter; + + /** + * @var HttpClient + */ + private $httpClient; + + /** + * @var RequestFactory + */ + private $requestFactory; + + public function __construct( + MessageFactory $messageFactory, + MessageConverter $messageConverter, + HttpClient $httpClient, + RequestFactory $requestFactory = null + ) { + $this->messageFactory = $messageFactory; + $this->messageConverter = $messageConverter; + $this->httpClient = $httpClient; + $this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find(); + } + + public function updateStreamMetadata(StreamName $streamName, array $newMetadata): void + { + $body = json_encode($newMetadata); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new InvalidArgumentException('Metadata could not be json encoded'); + } + + $request = $this->requestFactory->createRequest( + 'POST', + 'streammetadata/' . urlencode($streamName->toString()), + [ + 'Content-Type' => 'application/json', + ], + $body + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 204: + break; + case 404: + throw StreamNotFound::with($streamName); + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function create(Stream $stream): void + { + $messages = []; + + foreach ($stream->streamEvents() as $event) { + $message = $this->messageConverter->convertToArray($event); + $message['created_at'] = $message['created_at']->format('Y-m-d\TH:i:s.u'); + + $messages[] = $message; + } + + $body = json_encode($messages); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new InvalidArgumentException('Events could not be json encoded'); + } + + $streamName = $stream->streamName(); + + $request = $this->requestFactory->createRequest( + 'POST', + 'stream/' . urlencode($streamName->toString()), + [ + 'Content-Type' => 'application/vnd.eventstore.atom+json', + ], + $body + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 204: + if (! empty($stream->metadata())) { + $this->updateStreamMetadata($streamName, $stream->metadata()); + } + break; + case 400: + throw new RuntimeException($response->getReasonPhrase()); + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function appendTo(StreamName $streamName, Iterator $streamEvents): void + { + $stream = new Stream($streamName, $streamEvents); + + $this->create($stream); + } + + public function delete(StreamName $streamName): void + { + $request = $this->requestFactory->createRequest( + 'POST', + 'delete/' . urlencode($streamName->toString()) + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 204: + break; + case 404: + throw StreamNotFound::with($streamName); + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchStreamMetadata(StreamName $streamName): array + { + $request = $this->requestFactory->createRequest( + 'GET', + 'streammetadata/' . urlencode($streamName->toString()), + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 200: + $metadata = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $metadata; + case 404: + throw StreamNotFound::with($streamName); + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function hasStream(StreamName $streamName): bool + { + $request = $this->requestFactory->createRequest( + 'GET', + 'has-stream/' . urlencode($streamName->toString()) + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 200: + return true; + case 404: + return false; + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function load( + StreamName $streamName, + int $fromNumber = 1, + int $count = null, + MetadataMatcher $metadataMatcher = null + ): Iterator { + if (null === $count) { + $count = PHP_INT_MAX; + } + + $uri = 'stream/' . urlencode($streamName->toString()) . '/' . $fromNumber . '/forward/' . $count + . '?' . $this->buildQueryFromMetadataMatcher($metadataMatcher); + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 404: + throw StreamNotFound::with($streamName); + case 400: + throw new InvalidArgumentException($response->getReasonPhrase()); + case 200: + return $this->createIteratorFromResponse($response); + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function loadReverse( + StreamName $streamName, + int $fromNumber = null, + int $count = null, + MetadataMatcher $metadataMatcher = null + ): Iterator { + if (null === $fromNumber) { + $fromNumber = PHP_INT_MAX; + } + + if (null === $count) { + $count = PHP_INT_MAX; + } + + $uri = 'stream/' . urlencode($streamName->toString()) . '/' . $fromNumber . '/backward/' . $count + . '?' . $this->buildQueryFromMetadataMatcher($metadataMatcher); + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 404: + throw StreamNotFound::with($streamName); + case 400: + throw new InvalidArgumentException($response->getReasonPhrase()); + case 200: + return $this->createIteratorFromResponse($response); + case 403: + case 405: + throw new NotAllowed(); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchStreamNames( + ?string $filter, + ?MetadataMatcher $metadataMatcher, + int $limit = 20, + int $offset = 0 + ): array { + $limitPart = 'limit=' . $limit . '&offset=' . $offset; + + $query = $this->buildQueryFromMetadataMatcher($metadataMatcher); + + if ($query === '') { + $query = $limitPart; + } else { + $query .= '&' . $limitPart; + } + + if (null !== $filter) { + $uri = 'streams/' . urlencode($filter) . '?' . $query; + } else { + $uri = 'streams?' . $query; + } + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 200: + $streamNames = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $streamNames; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchStreamNamesRegex( + string $filter, + ?MetadataMatcher $metadataMatcher, + int $limit = 20, + int $offset = 0 + ): array { + $limitPart = 'limit=' . $limit . '&offset=' . $offset; + + $query = $this->buildQueryFromMetadataMatcher($metadataMatcher); + + if ($query === '') { + $query = $limitPart; + } else { + $query .= '&' . $limitPart; + } + + $uri = 'streams-regex/' . urlencode($filter) . '?' . $query; + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 200: + $streamNames = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $streamNames; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchCategoryNames(?string $filter, int $limit = 20, int $offset = 0): array + { + $query = 'limit=' . $limit . '&offset=' . $offset; + + if (null !== $filter) { + $uri = 'categories/' . urlencode($filter) . '?' . $query; + } else { + $uri = 'categories?' . $query; + } + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 200: + $categories = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $categories; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchCategoryNamesRegex(string $filter, int $limit = 20, int $offset = 0): array + { + $uri = 'categories-regex/' . urlencode($filter) . '?limit=' . $limit . '&offset=' . $offset; + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 200: + $categories = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $categories; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + private function buildQueryFromMetadataMatcher(MetadataMatcher $metadataMatcher = null): string + { + if (null === $metadataMatcher) { + return ''; + } + + $params = []; + + foreach ($metadataMatcher->data() as $key => $match) { + if (FieldType::METADATA()->is($match['fieldType'])) { + $prefix = 'meta_' . $key . '_'; + } else { + $prefix = 'property_' . $key . '_'; + } + + $params[] = $prefix . 'field=' . $match['field']; + $params[] = $prefix . 'operator=' . $match['operator']->getName(); + $params[] = $prefix . 'value=' . $match['value']; + } + + return implode('&', $params); + } + + private function createIteratorFromResponse(ResponseInterface $response): Iterator + { + $data = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response from event store'); + } + + foreach ($data['entries'] as $entry) { + $entry['created_at'] = DateTimeImmutable::createFromFormat( + 'Y-m-d\TH:i:s.u', + $entry['created_at'], + new DateTimeZone('UTC') + ); + + if (! $entry['created_at'] instanceof DateTimeImmutable) { + throw new RuntimeException('Could not create DateTimeImmutable object from event data'); + } + + yield $this->messageFactory->createMessageFromArray($entry['message_name'], $entry); + } + } +} diff --git a/src/Projection/HttplugProjectionManager.php b/src/Projection/HttplugProjectionManager.php new file mode 100644 index 0000000..20cea55 --- /dev/null +++ b/src/Projection/HttplugProjectionManager.php @@ -0,0 +1,294 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Prooph\EventStore\Httplug\Projection; + +use Http\Client\HttpClient; +use Http\Discovery\MessageFactoryDiscovery; +use Http\Message\RequestFactory; +use Prooph\EventStore\Exception\ProjectionNotFound; +use Prooph\EventStore\Exception\RuntimeException; +use Prooph\EventStore\Httplug\Exception\NotAllowed; +use Prooph\EventStore\Projection\ProjectionManager; +use Prooph\EventStore\Projection\ProjectionStatus; +use Prooph\EventStore\Projection\Projector; +use Prooph\EventStore\Projection\Query; +use Prooph\EventStore\Projection\ReadModel; +use Prooph\EventStore\Projection\ReadModelProjector; + +final class HttplugProjectionManager implements ProjectionManager +{ + /** + * @var HttpClient + */ + private $httpClient; + + /** + * @var RequestFactory + */ + private $requestFactory; + + public function __construct( + HttpClient $httpClient, + RequestFactory $requestFactory = null + ) { + $this->httpClient = $httpClient; + $this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find(); + } + + public function createQuery(): Query + { + throw new \BadMethodCallException(__METHOD__ . ' not implemented'); + } + + public function createProjection( + string $name, + array $options = [] + ): Projector { + throw new \BadMethodCallException(__METHOD__ . ' not implemented'); + } + + public function createReadModelProjection( + string $name, + ReadModel $readModel, + array $options = [] + ): ReadModelProjector { + throw new \BadMethodCallException(__METHOD__ . ' not implemented'); + } + + public function deleteProjection(string $name, bool $deleteEmittedEvents): void + { + if ($deleteEmittedEvents) { + $deleteEmittedEvents = 'true'; + } else { + $deleteEmittedEvents = 'false'; + } + + $request = $this->requestFactory->createRequest( + 'POST', + 'projection/delete/' . urlencode($name) . '/' . $deleteEmittedEvents + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 404: + throw ProjectionNotFound::withName($name); + case 204: + break; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function resetProjection(string $name): void + { + $request = $this->requestFactory->createRequest( + 'POST', + 'projection/reset/' . urlencode($name) + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 404: + throw ProjectionNotFound::withName($name); + case 204: + break; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function stopProjection(string $name): void + { + $request = $this->requestFactory->createRequest( + 'POST', + 'projection/stop/' . urlencode($name) + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 404: + throw ProjectionNotFound::withName($name); + case 204: + break; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchProjectionNames(?string $filter, int $limit = 20, int $offset = 0): array + { + $query = 'limit=' . $limit . '&offset=' . $offset; + + if (null !== $filter) { + $uri = 'projections/' . urlencode($filter) . '?' . $query; + } else { + $uri = 'projections?' . $query; + } + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 200: + $projectionNames = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $projectionNames; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchProjectionNamesRegex(string $regex, int $limit = 20, int $offset = 0): array + { + $uri = 'projections-regex/' . urlencode($regex) . '?limit=' . $limit . '&offset=' . $offset; + + $request = $this->requestFactory->createRequest( + 'GET', + $uri, + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 200: + $projectionNames = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $projectionNames; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchProjectionStatus(string $name): ProjectionStatus + { + $request = $this->requestFactory->createRequest( + 'GET', + 'projection/status/' . urlencode($name), + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 404: + throw ProjectionNotFound::withName($name); + case 200: + return ProjectionStatus::byName($response->getReasonPhrase()); + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchProjectionStreamPositions(string $name): array + { + $request = $this->requestFactory->createRequest( + 'GET', + 'projection/stream-positions/' . urlencode($name), + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 404: + throw ProjectionNotFound::withName($name); + case 200: + $streamPositions = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $streamPositions; + default: + throw new RuntimeException('Unknown error occurred'); + } + } + + public function fetchProjectionState(string $name): array + { + $request = $this->requestFactory->createRequest( + 'GET', + 'projection/state/' . urlencode($name), + [ + 'Accept' => 'application/json', + ] + ); + + $response = $this->httpClient->sendRequest($request); + + switch ($response->getStatusCode()) { + case 403: + case 405: + throw new NotAllowed(); + case 404: + throw ProjectionNotFound::withName($name); + case 200: + $state = json_decode($response->getBody()->getContents(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not json decode response'); + } + + return $state; + default: + throw new RuntimeException('Unknown error occurred'); + } + } +} diff --git a/tests/Container/HttplugEventStoreFactoryTest.php b/tests/Container/HttplugEventStoreFactoryTest.php new file mode 100644 index 0000000..a2a7d59 --- /dev/null +++ b/tests/Container/HttplugEventStoreFactoryTest.php @@ -0,0 +1,97 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophTest\HttplugEventStore\Container; + +use Http\Client\HttpClient; +use Http\Message\RequestFactory; +use PHPUnit\Framework\TestCase; +use Prooph\Common\Messaging\FQCNMessageFactory; +use Prooph\Common\Messaging\NoOpMessageConverter; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Httplug\Container\HttplugEventStoreFactory; +use Prooph\EventStore\Httplug\HttplugEventStore; +use Psr\Container\ContainerInterface; + +class HttplugEventStoreFactoryTest extends TestCase +{ + /** + * @test + */ + public function it_creates_httplug_event_store(): void + { + $config = [ + 'prooph' => [ + 'event_store' => [ + 'default' => [ + 'http_client' => 'client', + 'request_factory' => 'requestFactory', + ], + ], + ], + ]; + + $container = $this->prophesize(ContainerInterface::class); + $container->get('config')->willReturn($config)->shouldBeCalled(); + $container->get(FQCNMessageFactory::class)->willReturn(new FQCNMessageFactory())->shouldBeCalled(); + $container->get(NoOpMessageConverter::class)->willReturn(new NoOpMessageConverter())->shouldBeCalled(); + $container->get('client')->willReturn($this->prophesize(HttpClient::class)->reveal())->shouldBeCalled(); + $container->get('requestFactory')->willReturn($this->prophesize(RequestFactory::class)->reveal())->shouldBeCalled(); + + $factory = new HttplugEventStoreFactory(); + $eventStore = $factory($container->reveal()); + + $this->assertInstanceOf(HttplugEventStore::class, $eventStore); + } + + /** + * @test + */ + public function it_creates_httplug_event_store_using_callstatic(): void + { + $config = [ + 'prooph' => [ + 'event_store' => [ + 'default' => [ + 'http_client' => 'client', + 'request_factory' => 'requestFactory', + ], + ], + ], + ]; + + $container = $this->prophesize(ContainerInterface::class); + $container->get('config')->willReturn($config)->shouldBeCalled(); + $container->get(FQCNMessageFactory::class)->willReturn(new FQCNMessageFactory())->shouldBeCalled(); + $container->get(NoOpMessageConverter::class)->willReturn(new NoOpMessageConverter())->shouldBeCalled(); + $container->get('client')->willReturn($this->prophesize(HttpClient::class)->reveal())->shouldBeCalled(); + $container->get('requestFactory')->willReturn($this->prophesize(RequestFactory::class)->reveal())->shouldBeCalled(); + + $name = 'default'; + + $eventStore = HttplugEventStoreFactory::$name($container->reveal()); + + $this->assertInstanceOf(HttplugEventStore::class, $eventStore); + } + + /** + * @test + */ + public function it_throws_invalid_argument_exception_when_invalid_container_given(): void + { + $this->expectException(InvalidArgumentException::class); + + $name = 'default'; + + HttplugEventStoreFactory::$name('invalid'); + } +} diff --git a/tests/Container/Projection/HttplugProjectionManagerFactoryTest.php b/tests/Container/Projection/HttplugProjectionManagerFactoryTest.php new file mode 100644 index 0000000..8f672bb --- /dev/null +++ b/tests/Container/Projection/HttplugProjectionManagerFactoryTest.php @@ -0,0 +1,91 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophTest\HttplugEventStore\Container\Projection; + +use Http\Client\HttpClient; +use Http\Message\RequestFactory; +use PHPUnit\Framework\TestCase; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Httplug\Container\Projection\HttplugProjectionManagerFactory; +use Prooph\EventStore\Httplug\Projection\HttplugProjectionManager; +use Psr\Container\ContainerInterface; + +class HttplugProjectionManagerFactoryTest extends TestCase +{ + /** + * @test + */ + public function it_creates_httplug_projection_manager(): void + { + $config = [ + 'prooph' => [ + 'projection_manager' => [ + 'default' => [ + 'http_client' => 'client', + 'request_factory' => 'requestFactory', + ], + ], + ], + ]; + + $container = $this->prophesize(ContainerInterface::class); + $container->get('config')->willReturn($config)->shouldBeCalled(); + $container->get('client')->willReturn($this->prophesize(HttpClient::class)->reveal())->shouldBeCalled(); + $container->get('requestFactory')->willReturn($this->prophesize(RequestFactory::class)->reveal())->shouldBeCalled(); + + $factory = new HttplugProjectionManagerFactory(); + $eventStore = $factory($container->reveal()); + + $this->assertInstanceOf(HttplugProjectionManager::class, $eventStore); + } + + /** + * @test + */ + public function it_creates_httplug_projection_manager_using_callstatic(): void + { + $config = [ + 'prooph' => [ + 'projection_manager' => [ + 'default' => [ + 'http_client' => 'client', + 'request_factory' => 'requestFactory', + ], + ], + ], + ]; + + $container = $this->prophesize(ContainerInterface::class); + $container->get('config')->willReturn($config)->shouldBeCalled(); + $container->get('client')->willReturn($this->prophesize(HttpClient::class)->reveal())->shouldBeCalled(); + $container->get('requestFactory')->willReturn($this->prophesize(RequestFactory::class)->reveal())->shouldBeCalled(); + + $name = 'default'; + + $projectionManager = HttplugProjectionManagerFactory::$name($container->reveal()); + + $this->assertInstanceOf(HttplugProjectionManager::class, $projectionManager); + } + + /** + * @test + */ + public function it_throws_invalid_argument_exception_when_invalid_container_given(): void + { + $this->expectException(InvalidArgumentException::class); + + $name = 'default'; + + HttplugProjectionManagerFactory::$name('invalid'); + } +} diff --git a/tests/HttplugEventStoreTest.php b/tests/HttplugEventStoreTest.php new file mode 100644 index 0000000..345bd75 --- /dev/null +++ b/tests/HttplugEventStoreTest.php @@ -0,0 +1,2171 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophTest\HttplugEventStore; + +use Http\Client\HttpClient; +use Http\Message\RequestFactory; +use PHPUnit\Framework\TestCase; +use Prooph\Common\Messaging\FQCNMessageFactory; +use Prooph\Common\Messaging\MessageConverter; +use Prooph\Common\Messaging\MessageFactory; +use Prooph\Common\Messaging\NoOpMessageConverter; +use Prooph\EventStore\Exception\InvalidArgumentException; +use Prooph\EventStore\Exception\RuntimeException; +use Prooph\EventStore\Exception\StreamNotFound; +use Prooph\EventStore\Httplug\Exception\NotAllowed; +use Prooph\EventStore\Httplug\HttplugEventStore; +use Prooph\EventStore\Metadata\FieldType; +use Prooph\EventStore\Metadata\MetadataMatcher; +use Prooph\EventStore\Metadata\Operator; +use Prooph\EventStore\Stream; +use Prooph\EventStore\StreamName; +use ProophTest\EventStore\Mock\TestDomainEvent; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; + +class HttplugEventStoreTest extends TestCase +{ + // + + /** + * @test + */ + public function it_updates_stream_metadata(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'streammetadata/somename', + [ + 'Content-Type' => 'application/json', + ], + json_encode(['some' => 'value']) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->updateStreamMetadata(new StreamName('somename'), ['some' => 'value']); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_update_metadata(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'streammetadata/unknown', + [ + 'Content-Type' => 'application/json', + ], + json_encode(['some' => 'value']) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->updateStreamMetadata(new StreamName('unknown'), ['some' => 'value']); + } + + /** + * @test + */ + public function it_throws_exception_on_unknown_error_when_updating_metadata(): void + { + $this->expectException(RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'streammetadata/unknown', + [ + 'Content-Type' => 'application/json', + ], + json_encode(['some' => 'value']) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->updateStreamMetadata(new StreamName('unknown'), ['some' => 'value']); + } + + /** + * @test + */ + public function it_throws_exception_when_cannot_json_encode_metadata_for_update(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Metadata could not be json encoded'); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $this->prophesize(HttpClient::class)->reveal(), + $this->prophesize(RequestFactory::class)->reveal() + ); + + $eventStore->updateStreamMetadata(new StreamName('test'), ["\xB1\x31"]); + } + + /** + * @test + */ + public function it_throws_stream_not_found_when_trying_to_update_unknown_stream_metadata(): void + { + $this->expectException(StreamNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'streammetadata/unknown', + [ + 'Content-Type' => 'application/json', + ], + json_encode(['some' => 'value']) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->updateStreamMetadata(new StreamName('unknown'), ['some' => 'value']); + } + + // + + // + + /** + * @test + */ + public function it_creates_stream(): void + { + $messageConverter = new NoOpMessageConverter(); + + $message = TestDomainEvent::with(['foo' => 'bar'], 1); + $messageData = $messageConverter->convertToArray($message); + $messageData['created_at'] = $messageData['created_at']->format('Y-m-d\TH:i:s.u'); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $request2 = $this->prophesize(RequestInterface::class); + $request2 = $request2->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'stream/somename', + [ + 'Content-Type' => 'application/vnd.eventstore.atom+json', + ], + json_encode([$messageData]) + ) + ->willReturn($request); + + $requestFactory + ->createRequest( + 'POST', + 'streammetadata/somename', + [ + 'Content-Type' => 'application/json', + ], + json_encode(['some' => 'meta']) + ) + ->willReturn($request2); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $response2 = $this->prophesize(ResponseInterface::class); + $response2->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + $httpClient->sendRequest($request2)->willReturn($response2->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $messageConverter, + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->create(new Stream(new StreamName('somename'), new \ArrayIterator([$message]), ['some' => 'meta'])); + } + + /** + * @test + */ + public function it_creates_stream_and_throws_error_on_400(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('some message'); + + $messageConverter = new NoOpMessageConverter(); + + $message = TestDomainEvent::with(['foo' => 'bar'], 1); + $messageData = $messageConverter->convertToArray($message); + $messageData['created_at'] = $messageData['created_at']->format('Y-m-d\TH:i:s.u'); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'stream/somename', + [ + 'Content-Type' => 'application/vnd.eventstore.atom+json', + ], + json_encode([$messageData]) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(400)->shouldBeCalled(); + $response->getReasonPhrase()->willReturn('some message')->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $messageConverter, + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->create(new Stream(new StreamName('somename'), new \ArrayIterator([$message]), ['some' => 'meta'])); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_create_stream(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $messageConverter = new NoOpMessageConverter(); + + $message = TestDomainEvent::with(['foo' => 'bar'], 1); + $messageData = $messageConverter->convertToArray($message); + $messageData['created_at'] = $messageData['created_at']->format('Y-m-d\TH:i:s.u'); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'stream/somename', + [ + 'Content-Type' => 'application/vnd.eventstore.atom+json', + ], + json_encode([$messageData]) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $messageConverter, + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->create(new Stream(new StreamName('somename'), new \ArrayIterator([$message]), ['some' => 'meta'])); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_create(): void + { + $this->expectException(RuntimeException::class); + + $messageConverter = new NoOpMessageConverter(); + + $message = TestDomainEvent::with(['foo' => 'bar'], 1); + $messageData = $messageConverter->convertToArray($message); + $messageData['created_at'] = $messageData['created_at']->format('Y-m-d\TH:i:s.u'); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'stream/somename', + [ + 'Content-Type' => 'application/vnd.eventstore.atom+json', + ], + json_encode([$messageData]) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $messageConverter, + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->create(new Stream(new StreamName('somename'), new \ArrayIterator([$message]), ['some' => 'meta'])); + } + + // + + // + + /** + * @test + */ + public function it_appends_to_stream_and_creates_it_automatically(): void + { + $messageConverter = new NoOpMessageConverter(); + + $message = TestDomainEvent::with(['foo' => 'bar'], 1); + $messageData = $messageConverter->convertToArray($message); + $messageData['created_at'] = $messageData['created_at']->format('Y-m-d\TH:i:s.u'); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'stream/somename', + [ + 'Content-Type' => 'application/vnd.eventstore.atom+json', + ], + json_encode([$messageData]) + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $messageConverter, + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->appendTo(new StreamName('somename'), new \ArrayIterator([$message])); + } + + // + + // + + /** + * @test + */ + public function it_deletes_stream(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'delete/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->delete(new StreamName('somename')); + } + + /** + * @test + */ + public function it_cannot_delete_stream_when_not_found(): void + { + $this->expectException(StreamNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'delete/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->delete(new StreamName('somename')); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_delete(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'delete/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->delete(new StreamName('somename')); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_delete(): void + { + $this->expectException(RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'POST', + 'delete/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->delete(new StreamName('somename')); + } + + // + + // + + /** + * @test + */ + public function it_fetches_stream_metadata(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streammetadata/somename', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $body = $this->prophesize(StreamInterface::class); + $body->getContents()->willReturn(json_encode(['foo' => 'bar']))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($body->reveal())->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $streamMetadata = $eventStore->fetchStreamMetadata(new StreamName('somename')); + + $this->assertSame(['foo' => 'bar'], $streamMetadata); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_fetch_stream_metadata(int $forbidenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streammetadata/somename', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbidenStatusCode)->shouldBeCalled(); + $response->getBody()->shouldNotBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamMetadata(new StreamName('somename')); + } + + /** + * @test + */ + public function it_throws_stream_not_found_when_unknown_fetch_stream_metadata(): void + { + $this->expectException(StreamNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streammetadata/somename', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + $response->getBody()->shouldNotBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamMetadata(new StreamName('somename')); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_fetch_stream_metadata(): void + { + $this->expectException(RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streammetadata/somename', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamMetadata(new StreamName('somename')); + } + + // + + // + + /** + * @test + */ + public function it_returns_true_when_asking_for_existing_stream(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'has-stream/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $this->assertTrue($eventStore->hasStream(new StreamName('somename'))); + } + + /** + * @test + */ + public function it_returns_false_when_asking_for_non_existing_stream(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'has-stream/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $this->assertFalse($eventStore->hasStream(new StreamName('somename'))); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_call_has_stream(int $forbidenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'has-stream/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbidenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->hasStream(new StreamName('somename')); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_has_stream(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'has-stream/somename' + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->hasStream(new StreamName('somename')); + } + + // + + // + + /** + * @test + * @dataProvider getTestEvents + */ + public function it_loads_stream(array $testEvents): void + { + $testEvent1 = current($testEvents); + next($testEvents); + $testEvent2 = current($testEvents); + next($testEvents); + $testEvent3 = current($testEvents); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/foo/1/forward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $testEvent1Array = $testEvent1->toArray(); + $testEvent1Array['created_at'] = $testEvent1->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent2Array = $testEvent2->toArray(); + $testEvent2Array['created_at'] = $testEvent2->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent3Array = $testEvent3->toArray(); + $testEvent3Array['created_at'] = $testEvent3->createdAt()->format('Y-m-d\TH:i:s.u'); + + $content = [ + 'title' => 'Event Stream \'foo\'', + 'id' => 'http://localhost:8080/stream/foo', + 'streamName' => 'foo', + '_links' => [ + [ + 'uri' => 'http://localhost:8080/stream/foo', + 'relation' => 'self', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/1/forward/3', + 'relation' => 'first', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/head/backward/3', + 'relation' => 'last', + ], + ], + 'entries' => [ + $testEvent1Array, + $testEvent2Array, + $testEvent3Array, + ], + ]; + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode($content))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + new FQCNMessageFactory(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $events = $eventStore->load(new StreamName('foo')); + + $this->assertInstanceOf(\Iterator::class, $events); + + $this->assertTrue($testEvent1->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent1->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertTrue($testEvent2->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent2->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertTrue($testEvent3->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent3->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertNull($events->current()); + } + + /** + * @test + * @dataProvider getTestEvents + */ + public function it_loads_stream_with_metadata_matcher_limit_and_offset(array $testEvents): void + { + $testEvent1 = current($testEvents); + next($testEvents); + $testEvent2 = current($testEvents); + next($testEvents); + $testEvent3 = current($testEvents); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/foo/2/forward/3?meta_0_field=key&meta_0_operator=EQUALS&meta_0_value=value&property_1_field=uuid&property_1_operator=EQUALS&property_1_value=' . $testEvent3->uuid()->toString(), + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $testEvent1Array = $testEvent1->toArray(); + $testEvent1Array['created_at'] = $testEvent1->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent2Array = $testEvent2->toArray(); + $testEvent2Array['created_at'] = $testEvent2->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent3Array = $testEvent3->toArray(); + $testEvent3Array['created_at'] = $testEvent3->createdAt()->format('Y-m-d\TH:i:s.u'); + + $content = [ + 'title' => 'Event Stream \'foo\'', + 'id' => 'http://localhost:8080/stream/foo', + 'streamName' => 'foo', + '_links' => [ + [ + 'uri' => 'http://localhost:8080/stream/foo', + 'relation' => 'self', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/2/forward/3', + 'relation' => 'first', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/head/backward/3', + 'relation' => 'last', + ], + ], + 'entries' => [ + $testEvent3Array, + ], + ]; + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode($content))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + new FQCNMessageFactory(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $metadataMatcher = new MetadataMatcher(); + $metadataMatcher = $metadataMatcher->withMetadataMatch('key', Operator::EQUALS(), 'value'); + $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::EQUALS(), $testEvent3->uuid()->toString(), FieldType::MESSAGE_PROPERTY()); + + $events = $eventStore->load(new StreamName('foo'), 2, 3, $metadataMatcher); + + $this->assertInstanceOf(\Iterator::class, $events); + + $this->assertTrue($testEvent3->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent3->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertNull($events->current()); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_load(int $forbidenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/somename/1/forward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbidenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->load(new StreamName('somename')); + } + + /** + * @test + */ + public function it_throws_stream_not_found_on_load(): void + { + $this->expectException(StreamNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/unknown/1/forward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->load(new StreamName('unknown')); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_load(): void + { + $this->expectException(RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/somename/1/forward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->load(new StreamName('somename')); + } + + // + + // + + /** + * @test + * @dataProvider getTestEvents + */ + public function it_loads_stream_reverse(array $testEvents): void + { + $testEvent1 = current($testEvents); + next($testEvents); + $testEvent2 = current($testEvents); + next($testEvents); + $testEvent3 = current($testEvents); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/foo/' . PHP_INT_MAX . '/backward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $testEvent1Array = $testEvent1->toArray(); + $testEvent1Array['created_at'] = $testEvent1->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent2Array = $testEvent2->toArray(); + $testEvent2Array['created_at'] = $testEvent2->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent3Array = $testEvent3->toArray(); + $testEvent3Array['created_at'] = $testEvent3->createdAt()->format('Y-m-d\TH:i:s.u'); + + $content = [ + 'title' => 'Event Stream \'foo\'', + 'id' => 'http://localhost:8080/stream/foo', + 'streamName' => 'foo', + '_links' => [ + [ + 'uri' => 'http://localhost:8080/stream/foo', + 'relation' => 'self', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/1/forward/3', + 'relation' => 'first', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/head/backward/3', + 'relation' => 'last', + ], + ], + 'entries' => [ + $testEvent3Array, + $testEvent2Array, + $testEvent1Array, + ], + ]; + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode($content))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + new FQCNMessageFactory(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $events = $eventStore->loadReverse(new StreamName('foo')); + + $this->assertInstanceOf(\Iterator::class, $events); + + $this->assertTrue($testEvent3->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent3->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertTrue($testEvent2->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent2->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertTrue($testEvent1->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent1->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertNull($events->current()); + } + + /** + * @test + * @dataProvider getTestEvents + */ + public function it_loads_stream_reverse_with_metadata_matcher_limit_and_offset(array $testEvents): void + { + $testEvent1 = current($testEvents); + next($testEvents); + $testEvent2 = current($testEvents); + next($testEvents); + $testEvent3 = current($testEvents); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/foo/3/backward/3?meta_0_field=key&meta_0_operator=EQUALS&meta_0_value=value&property_1_field=uuid&property_1_operator=EQUALS&property_1_value=' . $testEvent3->uuid()->toString(), + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $testEvent1Array = $testEvent1->toArray(); + $testEvent1Array['created_at'] = $testEvent1->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent2Array = $testEvent2->toArray(); + $testEvent2Array['created_at'] = $testEvent2->createdAt()->format('Y-m-d\TH:i:s.u'); + + $testEvent3Array = $testEvent3->toArray(); + $testEvent3Array['created_at'] = $testEvent3->createdAt()->format('Y-m-d\TH:i:s.u'); + + $content = [ + 'title' => 'Event Stream \'foo\'', + 'id' => 'http://localhost:8080/stream/foo', + 'streamName' => 'foo', + '_links' => [ + [ + 'uri' => 'http://localhost:8080/stream/foo', + 'relation' => 'self', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/2/forward/3', + 'relation' => 'first', + ], + [ + 'uri' => 'http://localhost:8080/stream/foo/head/backward/3', + 'relation' => 'last', + ], + ], + 'entries' => [ + $testEvent3Array, + ], + ]; + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode($content))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + new FQCNMessageFactory(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $metadataMatcher = new MetadataMatcher(); + $metadataMatcher = $metadataMatcher->withMetadataMatch('key', Operator::EQUALS(), 'value'); + $metadataMatcher = $metadataMatcher->withMetadataMatch('uuid', Operator::EQUALS(), $testEvent3->uuid()->toString(), FieldType::MESSAGE_PROPERTY()); + + $events = $eventStore->loadReverse(new StreamName('foo'), 3, 3, $metadataMatcher); + + $this->assertInstanceOf(\Iterator::class, $events); + + $this->assertTrue($testEvent3->uuid()->equals($events->current()->uuid())); + $this->assertSame($testEvent3->payload(), $events->current()->payload()); + + $events->next(); + + $this->assertNull($events->current()); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_load_reverse(int $forbidenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/somename/' . PHP_INT_MAX . '/backward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbidenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->loadReverse(new StreamName('somename')); + } + + /** + * @test + */ + public function it_throws_stream_not_found_on_load_reverse(): void + { + $this->expectException(StreamNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/unknown/' . PHP_INT_MAX . '/backward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->loadReverse(new StreamName('unknown')); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_load_reverse(): void + { + $this->expectException(RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'stream/somename/' . PHP_INT_MAX . '/backward/' . PHP_INT_MAX . '?', + [ + 'Accept' => 'application/vnd.eventstore.atom+json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->loadReverse(new StreamName('somename')); + } + + // + + // + + /** + * @test + */ + public function it_fetches_stream_names(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams?meta_0_field=key&meta_0_operator=EQUALS&meta_0_value=value&limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo", "bar"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $metadataMatcher = new MetadataMatcher(); + $metadataMatcher = $metadataMatcher->withMetadataMatch('key', Operator::EQUALS(), 'value'); + + $streamNames = $eventStore->fetchStreamNames(null, $metadataMatcher); + + $this->assertSame(['foo', 'bar'], $streamNames); + } + + /** + * @test + */ + public function it_fetches_stream_names_using_filter(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $streamNames = $eventStore->fetchStreamNames('foo', null, 30, 40); + + $this->assertSame(['foo'], $streamNames); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_fetch_stream_names(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamNames('foo', null, 30, 40); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_fetch_stream_names(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamNames('foo', null, 30, 40); + } + + // + + // + + /** + * @test + */ + public function it_fetches_stream_names_regex(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams-regex/' . urlencode('^foo') . '?meta_0_field=key&meta_0_operator=EQUALS&meta_0_value=value&limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo", "foobar"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $metadataMatcher = new MetadataMatcher(); + $metadataMatcher = $metadataMatcher->withMetadataMatch('key', Operator::EQUALS(), 'value'); + + $streamNames = $eventStore->fetchStreamNamesRegex('^foo', $metadataMatcher); + + $this->assertSame(['foo', 'foobar'], $streamNames); + } + + /** + * @test + */ + public function it_fetches_stream_names_regex_using_limit_and_offset(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $streamNames = $eventStore->fetchStreamNamesRegex('^foo', null, 30, 40); + + $this->assertSame(['foo'], $streamNames); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_fetch_stream_names_regex(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamNamesRegex('^foo', null, 30, 40); + } + + /** + * @test + */ + public function it_handles_unknown_errors_on_fetch_stream_names_regex(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'streams-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchStreamNamesRegex('^foo', null, 30, 40); + } + + // + + // + + /** + * @test + */ + public function it_fetches_category_names(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo", "bar"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $categoryNames = $eventStore->fetchCategoryNames(null); + + $this->assertSame(['foo', 'bar'], $categoryNames); + } + + /** + * @test + */ + public function it_fetches_category_names_using_filter(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $categoryNames = $eventStore->fetchCategoryNames('foo', 30, 40); + + $this->assertSame(['foo'], $categoryNames); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_fetch_category_names(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchCategoryNames('foo', 30, 40); + } + + /** + * @test + */ + public function it_throws_exception_on_unknown_error_when_fetch_category_names(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchCategoryNames('foo', 30, 40); + } + + // + + // + + /** + * @test + */ + public function it_fetches_category_names_regex(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories-regex/' . urlencode('^foo') . '?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo", "foobar"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $streamNames = $eventStore->fetchCategoryNamesRegex('^foo'); + + $this->assertSame(['foo', 'foobar'], $streamNames); + } + + /** + * @test + */ + public function it_fetches_category_names_regex_using_limit_and_offset(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn('["foo"]')->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal()); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $categoryNames = $eventStore->fetchCategoryNamesRegex('^foo', 30, 40); + + $this->assertSame(['foo'], $categoryNames); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_throws_exception_when_forbidden_to_fetch_category_names_regex(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchCategoryNamesRegex('^foo', 30, 40); + } + + /** + * @test + */ + public function it_throws_exception_on_unknown_error_when_fetch_category_names_regex(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory + ->createRequest( + 'GET', + 'categories-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + ) + ->willReturn($request); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $eventStore = new HttplugEventStore( + $this->prophesize(MessageFactory::class)->reveal(), + $this->prophesize(MessageConverter::class)->reveal(), + $httpClient->reveal(), + $requestFactory->reveal() + ); + + $eventStore->fetchCategoryNamesRegex('^foo', 30, 40); + } + + // + + public function forbiddenStatusCodes(): array + { + return [ + [403], + [405], + ]; + } + + public function getTestEvents(): array + { + $event1 = TestDomainEvent::with(['foo' => 'bar'], 1); + $event2 = TestDomainEvent::with(['foo' => 'baz'], 2); + $event3 = TestDomainEvent::with(['foo' => 'bam'], 3); + $event3 = $event3->withAddedMetadata('key', 'value'); + + return [[[$event1, $event2, $event3]]]; + } +} diff --git a/tests/Projection/HttplugProjectionManagerTest.php b/tests/Projection/HttplugProjectionManagerTest.php new file mode 100644 index 0000000..2c30454 --- /dev/null +++ b/tests/Projection/HttplugProjectionManagerTest.php @@ -0,0 +1,1090 @@ + + * (c) 2017-2017 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ProophTest\HttplugEventStore\Projection; + +use Http\Client\HttpClient; +use Http\Message\RequestFactory; +use PHPUnit\Framework\TestCase; +use Prooph\EventStore\Exception\ProjectionNotFound; +use Prooph\EventStore\Httplug\Exception\NotAllowed; +use Prooph\EventStore\Httplug\Projection\HttplugProjectionManager; +use Prooph\EventStore\Projection\ProjectionStatus; +use Prooph\EventStore\Projection\ReadModel; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; + +class HttplugProjectionManagerTest extends TestCase +{ + /** + * @test + */ + public function it_cannot_create_query(): void + { + $this->expectException(\BadMethodCallException::class); + + $httpClient = $this->prophesize(HttpClient::class); + $requestFactory = $this->prophesize(RequestFactory::class); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->createQuery(); + } + + /** + * @test + */ + public function it_cannot_create_projection(): void + { + $this->expectException(\BadMethodCallException::class); + + $httpClient = $this->prophesize(HttpClient::class); + $requestFactory = $this->prophesize(RequestFactory::class); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->createProjection('test'); + } + + /** + * @test + */ + public function it_cannot_create_read_model_projection(): void + { + $this->expectException(\BadMethodCallException::class); + + $httpClient = $this->prophesize(HttpClient::class); + $requestFactory = $this->prophesize(RequestFactory::class); + $readModel = $this->prophesize(ReadModel::class); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->createReadModelProjection('test', $readModel->reveal()); + } + + // + + /** + * @test + */ + public function it_deletes_projection_without_emitted_events(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/delete/somename/false' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->deleteProjection('somename', false); + } + + /** + * @test + */ + public function it_deletes_projection_with_emitted_events(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/delete/somename/true' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->deleteProjection('somename', true); + } + + /** + * @test + */ + public function it_cannot_delete_non_existing_projection(): void + { + $this->expectException(ProjectionNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/delete/somename/true' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->deleteProjection('somename', true); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_delete_projection_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/delete/somename/true' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->deleteProjection('somename', true); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_delete_projection(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/delete/somename/true' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->deleteProjection('somename', true); + } + + // + + // + + /** + * @test + */ + public function it_resets_projection(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/reset/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->resetProjection('somename'); + } + + /** + * @test + */ + public function it_cannot_reset_non_existing_projection(): void + { + $this->expectException(ProjectionNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/reset/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->resetProjection('somename'); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_reset_projection_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/reset/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->resetProjection('somename'); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_reset_projection(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/reset/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->resetProjection('somename'); + } + + // + + // + + /** + * @test + */ + public function it_stops_projection(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(204)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/stop/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->stopProjection('somename'); + } + + /** + * @test + */ + public function it_cannot_stop_non_existing_projection(): void + { + $this->expectException(ProjectionNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/stop/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->stopProjection('somename'); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_stop_projection_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/stop/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->stopProjection('somename'); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_stop_projection(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'POST', + 'projection/stop/somename' + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->stopProjection('somename'); + } + + // + + // + + /** + * @test + */ + public function it_fetches_projection_names(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode(['foo', 'bar']))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionNames = $projectionManager->fetchProjectionNames(null); + + $this->assertSame(['foo', 'bar'], $projectionNames); + } + + /** + * @test + */ + public function it_fetches_projection_names_using_filter_offset_and_limit(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode(['foo', 'foobar']))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections/foo?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionNames = $projectionManager->fetchProjectionNames('foo', 30, 40); + + $this->assertSame(['foo', 'foobar'], $projectionNames); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_fetch_projection_names_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionNames(null); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_fetch_projection_names(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionNames(null); + } + + // + + // + + /** + * @test + */ + public function it_fetches_projection_names_regex(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode(['foo', 'foobar']))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections-regex/' . urlencode('^foo') . '?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionNames = $projectionManager->fetchProjectionNamesRegex('^foo'); + + $this->assertSame(['foo', 'foobar'], $projectionNames); + } + + /** + * @test + */ + public function it_fetches_projection_names_regex_offset_and_limit(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode(['foo', 'foobar']))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections-regex/' . urlencode('^foo') . '?limit=30&offset=40', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionNames = $projectionManager->fetchProjectionNamesRegex('^foo', 30, 40); + + $this->assertSame(['foo', 'foobar'], $projectionNames); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_fetch_projection_names_regex_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections-regex/' . urlencode('^foo') . '?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionNamesRegex('^foo'); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_fetch_projection_names_regex(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projections-regex/' . urlencode('^foo') . '?limit=20&offset=0', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionNamesRegex('^foo'); + } + + // + + // + + /** + * @test + */ + public function it_fetches_projection_status(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getReasonPhrase()->willReturn(ProjectionStatus::RUNNING()->getName())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/status/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $status = $projectionManager->fetchProjectionStatus('somename'); + + $this->assertTrue(ProjectionStatus::RUNNING()->is($status)); + } + + /** + * @test + */ + public function it_cannot_unknown_fetch_projection_status(): void + { + $this->expectException(ProjectionNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/status/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionStatus('somename'); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_fetch_projection_status_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/status/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionStatus('somename'); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_fetch_projection_status(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/status/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionStatus('somename'); + } + + // + + // + + /** + * @test + */ + public function it_fetches_projection_stream_positions(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode(['stream1' => 200, 'stream2' => 400]))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/stream-positions/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $streamPositions = $projectionManager->fetchProjectionStreamPositions('somename'); + + $this->assertSame(['stream1' => 200, 'stream2' => 400], $streamPositions); + } + + /** + * @test + */ + public function it_cannot_unknown_fetch_projection_stream_positions(): void + { + $this->expectException(ProjectionNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/stream-positions/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionStreamPositions('somename'); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_fetch_projection_stream_positions_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/stream-positions/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionStreamPositions('somename'); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_fetch_projection_stream_positions(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/stream-positions/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionStreamPositions('somename'); + } + + // + + // + + /** + * @test + */ + public function it_fetches_projection_state(): void + { + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn(json_encode(['foo' => 'bar']))->shouldBeCalled(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(200)->shouldBeCalled(); + $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/state/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $state = $projectionManager->fetchProjectionState('somename'); + + $this->assertSame(['foo' => 'bar'], $state); + } + + /** + * @test + */ + public function it_cannot_unknown_fetch_projection_state(): void + { + $this->expectException(ProjectionNotFound::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(404)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/state/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionState('somename'); + } + + /** + * @test + * @dataProvider forbiddenStatusCodes + */ + public function it_cannot_fetch_projection_state_when_forbidden(int $forbiddenStatusCode): void + { + $this->expectException(NotAllowed::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn($forbiddenStatusCode)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/state/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionState('somename'); + } + + /** + * @test + */ + public function it_handles_unknown_error_on_fetch_projection_state(): void + { + $this->expectException(\RuntimeException::class); + + $request = $this->prophesize(RequestInterface::class); + $request = $request->reveal(); + + $response = $this->prophesize(ResponseInterface::class); + $response->getStatusCode()->willReturn(500)->shouldBeCalled(); + + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->createRequest( + 'GET', + 'projection/state/somename', + [ + 'Accept' => 'application/json', + ] + )->willReturn($request); + + $httpClient = $this->prophesize(HttpClient::class); + $httpClient->sendRequest($request)->willReturn($response->reveal())->shouldBeCalled(); + + $projectionManager = new HttplugProjectionManager($httpClient->reveal(), $requestFactory->reveal()); + + $projectionManager->fetchProjectionState('somename'); + } + + // + + public function forbiddenStatusCodes(): array + { + return [ + [403], + [405], + ]; + } +}