diff --git a/README.md b/README.md index bc1c0b7..ad0ba51 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ In order to use unsupported serializer you should create a decorator which imple - [ ] Regexp matchers for body, query and path. - [x] Separate `UniversalMockException` into several exceptions (`PockClientException`, `PockNetworkException`, etc). - [x] Add methods for easier throwing of exceptions listed in previous entry. -- [ ] `replyCallback` - reply using specified callback. -- [ ] `replyFactory` - reply using specified response factory (provide corresponding interface). +- [x] `replyWithCallback` - reply using specified callback. +- [x] `replyWithFactory` - reply using specified response factory (provide corresponding interface). - [ ] Compare XML bodies using `DOMDocument`, fallback to text comparison in case of problems. - [ ] Document everything (with examples if it’s feasible). diff --git a/src/Client.php b/src/Client.php index b584530..bae2f72 100644 --- a/src/Client.php +++ b/src/Client.php @@ -81,6 +81,18 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient return new HttpFulfilledPromise($mock->getResponse()); } + if (null !== $mock->getReplyFactory()) { + $mock->registerHit(); + + try { + return new HttpFulfilledPromise( + $mock->getReplyFactory()->createReply($request, new PockResponseBuilder()) + ); + } catch (Throwable $throwable) { + return new HttpRejectedPromise($throwable); + } + } + $throwable = $mock->getThrowable($request); if (null !== $throwable) { @@ -94,13 +106,23 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient } if (null !== $this->fallbackClient) { - try { - return new HttpFulfilledPromise($this->fallbackClient->sendRequest($request)); - } catch (Throwable $throwable) { - return new HttpRejectedPromise($throwable); - } + return $this->replyWithFallbackClient($request); } throw new UnsupportedRequestException(); } + + /** + * @param \Psr\Http\Message\RequestInterface $request + * + * @return \Http\Promise\Promise + */ + protected function replyWithFallbackClient(RequestInterface $request): Promise + { + try { + return new HttpFulfilledPromise($this->fallbackClient->sendRequest($request)); // @phpstan-ignore-line + } catch (Throwable $throwable) { + return new HttpRejectedPromise($throwable); + } + } } diff --git a/src/Factory/CallbackReplyFactory.php b/src/Factory/CallbackReplyFactory.php new file mode 100644 index 0000000..0fc8e27 --- /dev/null +++ b/src/Factory/CallbackReplyFactory.php @@ -0,0 +1,44 @@ +callback = $callback; + } + + /** + * @inheritDoc + */ + public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface + { + return call_user_func($this->callback, $request, $responseBuilder); + } +} diff --git a/src/Factory/ReplyFactoryInterface.php b/src/Factory/ReplyFactoryInterface.php new file mode 100644 index 0000000..eacf194 --- /dev/null +++ b/src/Factory/ReplyFactoryInterface.php @@ -0,0 +1,38 @@ +matcher = $matcher; + $this->replyFactory = $replyFactory; $this->response = $response; $this->throwable = $throwable; $this->matchAt = $matchAt; @@ -128,6 +135,14 @@ class Mock implements MockInterface return $this->response; } + /** + * @inheritDoc + */ + public function getReplyFactory(): ?ReplyFactoryInterface + { + return $this->replyFactory; + } + /** * @inheritDoc */ diff --git a/src/MockInterface.php b/src/MockInterface.php index f985e7b..3f3aeb9 100644 --- a/src/MockInterface.php +++ b/src/MockInterface.php @@ -9,6 +9,7 @@ namespace Pock; +use Pock\Factory\ReplyFactoryInterface; use Pock\Matchers\RequestMatcherInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -53,6 +54,13 @@ interface MockInterface */ public function getResponse(): ?ResponseInterface; + /** + * Returns reply factory which should be used to form the mocked response. + * + * @return \Pock\Factory\ReplyFactoryInterface|null + */ + public function getReplyFactory(): ?ReplyFactoryInterface; + /** * Returns the throwable which will be thrown as mock data. * diff --git a/src/PockBuilder.php b/src/PockBuilder.php index 0ecf0c4..647179d 100644 --- a/src/PockBuilder.php +++ b/src/PockBuilder.php @@ -15,6 +15,8 @@ use Pock\Enum\RequestScheme; use Pock\Exception\PockClientException; use Pock\Exception\PockNetworkException; use Pock\Exception\PockRequestException; +use Pock\Factory\CallbackReplyFactory; +use Pock\Factory\ReplyFactoryInterface; use Pock\Matchers\AnyRequestMatcher; use Pock\Matchers\BodyMatcher; use Pock\Matchers\CallbackRequestMatcher; @@ -62,6 +64,9 @@ class PockBuilder /** @var \Pock\PockResponseBuilder|null */ private $responseBuilder; + /** @var ReplyFactoryInterface|null */ + private $replyFactory; + /** @var \Throwable|null */ private $throwable; @@ -454,6 +459,31 @@ class PockBuilder return $this->responseBuilder->withStatusCode($statusCode); } + /** + * Construct the response during request execution using provided ReplytFactoryInterface implementation. + * + * @param \Pock\Factory\ReplyFactoryInterface $factory + * @see ReplyFactoryInterface + */ + public function replyWithFactory(ReplyFactoryInterface $factory): void + { + $this->replyFactory = $factory; + } + + /** + * Construct the response during request execution using provided callback. + * + * Callback should receive the same parameters as in the `ReplyFactoryInterface::createReply` method. + * + * @see ReplyFactoryInterface::createReply() + * + * @param callable $callback + */ + public function replyWithCallback(callable $callback): void + { + $this->replyWithFactory(new CallbackReplyFactory($callback)); + } + /** * Resets the builder. * @@ -462,6 +492,7 @@ class PockBuilder public function reset(): self { $this->matcher = new MultipleMatcher(); + $this->replyFactory = null; $this->responseBuilder = null; $this->throwable = null; $this->maxHits = 1; @@ -495,7 +526,7 @@ class PockBuilder private function closePrevious(): void { - if (null !== $this->responseBuilder || null !== $this->throwable) { + if (null !== $this->responseBuilder || null !== $this->replyFactory || null !== $this->throwable) { if (0 === count($this->matcher)) { $this->matcher->addMatcher(new AnyRequestMatcher()); } @@ -508,12 +539,14 @@ class PockBuilder $this->mocks[] = new Mock( $this->matcher, + $this->replyFactory, $response, $this->throwable, $this->maxHits, $this->matchAt ); $this->matcher = new MultipleMatcher(); + $this->replyFactory = null; $this->responseBuilder = null; $this->throwable = null; $this->maxHits = 1; diff --git a/tests/src/PockBuilderTest.php b/tests/src/PockBuilderTest.php index 96800e8..23dd4df 100644 --- a/tests/src/PockBuilderTest.php +++ b/tests/src/PockBuilderTest.php @@ -12,13 +12,18 @@ namespace Pock\Tests; use Pock\Enum\RequestMethod; use Pock\Enum\RequestScheme; use Pock\Exception\UnsupportedRequestException; +use Pock\Factory\ReplyFactoryInterface; use Pock\PockBuilder; +use Pock\PockResponseBuilder; use Pock\TestUtils\PockTestCase; use Pock\TestUtils\SimpleObject; +use Pock\TestUtils\TestReplyFactory; use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use RuntimeException; /** * Class PockBuilderTest @@ -587,4 +592,61 @@ EOF; self::assertEquals(1 === $i ? 200 : (4 === $i ? 201 : 400), $response->getStatusCode()); } } + + public function testReplyWithFactory(): void + { + $builder = new PockBuilder(); + $builder->matchMethod(RequestMethod::GET) + ->matchUri(self::TEST_URI) + ->always() + ->replyWithFactory(new TestReplyFactory()); + + for ($i = 0; $i < 5; $i++) { + $response = $builder->getClient()->sendRequest( + self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI) + ); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals('Request #' . ($i + 1), $response->getBody()->getContents()); + } + } + + public function testReplyWithCallback(): void + { + $builder = new PockBuilder(); + $builder->matchMethod(RequestMethod::GET) + ->matchUri(self::TEST_URI) + ->always() + ->replyWithCallback(static function (RequestInterface $request, PockResponseBuilder $responseBuilder) { + return $responseBuilder->withStatusCode(200) + ->withBody(self::TEST_URI) + ->getResponse(); + }); + + $response = $builder->getClient()->sendRequest( + self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI) + ); + + self::assertEquals(200, $response->getStatusCode()); + self::assertEquals(self::TEST_URI, $response->getBody()->getContents()); + } + + public function testReplyWithCallbackException(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Exception from the callback'); + + $builder = new PockBuilder(); + $builder->matchMethod(RequestMethod::GET) + ->matchUri(self::TEST_URI) + ->always() + ->replyWithCallback(static function (RequestInterface $request, PockResponseBuilder $responseBuilder) { + throw new RuntimeException('Exception from the callback'); + }); + + $builder->getClient()->sendRequest(self::getPsr17Factory()->createRequest( + RequestMethod::GET, + self::TEST_URI + )); + } } diff --git a/tests/utils/TestReplyFactory.php b/tests/utils/TestReplyFactory.php new file mode 100644 index 0000000..32801d3 --- /dev/null +++ b/tests/utils/TestReplyFactory.php @@ -0,0 +1,37 @@ +withStatusCode(200) + ->withBody('Request #' . ++$this->requestNumber) + ->getResponse(); + } +}