diff --git a/src/AbstractApi.php b/src/AbstractApi.php index 0a7aeda..bd6b820 100644 --- a/src/AbstractApi.php +++ b/src/AbstractApi.php @@ -7,8 +7,6 @@ use Closure; use JustSteveKing\UriBuilder\Uri; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; -use WrkFlow\ApiSdkBuilder\Actions\SendRequestAction; use WrkFlow\ApiSdkBuilder\Contracts\ApiFactoryContract; use WrkFlow\ApiSdkBuilder\Endpoints\AbstractEndpoint; use WrkFlow\ApiSdkBuilder\Environments\AbstractEnvironment; @@ -17,8 +15,6 @@ use WrkFlow\ApiSdkBuilder\Exceptions\ServerFailedException; use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; use WrkFlow\ApiSdkBuilder\Interfaces\EnvironmentOverrideEndpointsInterface; -use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; -use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; abstract class AbstractApi implements ApiInterface { @@ -29,25 +25,17 @@ abstract class AbstractApi implements ApiInterface */ private array $cachedEndpoints = []; - private ?SendRequestAction $sendRequestAction = null; - /** * @var array> */ private readonly array $overrideEndpoints; - /** - * @param array> $overrideEndpoints - */ + public function __construct( private readonly AbstractEnvironment $environment, - private readonly ApiFactoryContract $factory, - array $overrideEndpoints = [] + private readonly ApiFactoryContract $factory ) { - $this->overrideEndpoints = array_merge( - $overrideEndpoints, - $environment instanceof EnvironmentOverrideEndpointsInterface ? $environment->endpoints() : [] - ); + $this->overrideEndpoints = $environment instanceof EnvironmentOverrideEndpointsInterface ? $environment->endpoints() : []; } final public function environment(): AbstractEnvironment @@ -67,124 +55,6 @@ final public function uri(): Uri return $this->environment->uri(); } - final public function get( - string $responseClass, - Uri $uri, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $request = $this->factory() - ->request() - ->createRequest('GET', $uri->toString()); - - return $this->sendRequestAction() - ->execute( - api: $this, - request: $request, - responseClass: $responseClass, - headers: $headers, - expectedResponseStatusCode: $expectedResponseStatusCode, - shouldIgnoreLoggersOnError: $shouldIgnoreLoggersOnError, - ); - } - - final public function post( - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $request = $this->factory() - ->request() - ->createRequest('POST', $uri->toString()); - - return $this->sendRequestAction() - ->execute( - api: $this, - request: $request, - responseClass: $responseClass, - body: $body, - headers: $headers, - expectedResponseStatusCode: $expectedResponseStatusCode, - shouldIgnoreLoggersOnError: $shouldIgnoreLoggersOnError, - ); - } - - final public function put( - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $request = $this->factory() - ->request() - ->createRequest('PUT', $uri->toString()); - - return $this->sendRequestAction() - ->execute( - api: $this, - request: $request, - responseClass: $responseClass, - body: $body, - headers: $headers, - expectedResponseStatusCode: $expectedResponseStatusCode, - shouldIgnoreLoggersOnError: $shouldIgnoreLoggersOnError, - ); - } - - final public function delete( - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $request = $this->factory() - ->request() - ->createRequest('DELETE', $uri->toString()); - - return $this->sendRequestAction() - ->execute( - api: $this, - request: $request, - responseClass: $responseClass, - body: $body, - headers: $headers, - expectedResponseStatusCode: $expectedResponseStatusCode, - shouldIgnoreLoggersOnError: $shouldIgnoreLoggersOnError, - ); - } - - public function fake( - ResponseInterface $response, - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - return $this->sendRequestAction() - ->execute( - api: $this, - request: $this->factory() - ->request() - ->createRequest('FAKE', $uri->toString()), - responseClass: $responseClass, - body: $body, - headers: $headers, - expectedResponseStatusCode: $expectedResponseStatusCode, - fakedResponse: $response, - shouldIgnoreLoggersOnError: $shouldIgnoreLoggersOnError, - ); - } - public function createFailedResponseException(int $statusCode, ResponseInterface $response): ResponseException { if ($statusCode >= 400 && $statusCode < 500) { @@ -194,6 +64,11 @@ public function createFailedResponseException(int $statusCode, ResponseInterface return new ServerFailedException($response); } + final public function shouldIgnoreLoggersOnException(): ?Closure + { + return null; + } + /** * @template T of AbstractEndpoint * @@ -247,15 +122,4 @@ private function getOverrideEndpointClassIfCan(string $endpoint): string return $endpoint; } - - private function sendRequestAction(): SendRequestAction - { - if ($this->sendRequestAction instanceof SendRequestAction === false) { - $this->sendRequestAction = $this->factory() - ->container() - ->make(SendRequestAction::class); - } - - return $this->sendRequestAction; - } } diff --git a/src/Actions/MakeApiFactory.php b/src/Actions/MakeApiFactory.php index 70d31f6..5556a27 100644 --- a/src/Actions/MakeApiFactory.php +++ b/src/Actions/MakeApiFactory.php @@ -61,7 +61,7 @@ interface: StreamFactoryInterface::class, } /** - * @template T + * @template T of object * * @param class-string $interface * @param Closure():T $create diff --git a/src/Actions/SendRequestAction.php b/src/Actions/SendRequestAction.php index b1fdcaa..03b3de5 100644 --- a/src/Actions/SendRequestAction.php +++ b/src/Actions/SendRequestAction.php @@ -10,23 +10,25 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; -use Throwable; -use WrkFlow\ApiSdkBuilder\AbstractApi; +use WrkFlow\ApiSdkBuilder\Contracts\SendRequestActionContract; use WrkFlow\ApiSdkBuilder\Environments\AbstractEnvironment; use WrkFlow\ApiSdkBuilder\Events\RequestConnectionFailedEvent; use WrkFlow\ApiSdkBuilder\Events\RequestFailedEvent; use WrkFlow\ApiSdkBuilder\Events\ResponseReceivedEvent; use WrkFlow\ApiSdkBuilder\Events\SendingRequestEvent; use WrkFlow\ApiSdkBuilder\Exceptions\ResponseException; +use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; use WrkFlow\ApiSdkBuilder\Interfaces\EnvironmentFakeResponseInterface; -use WrkFlow\ApiSdkBuilder\Interfaces\HeadersInterface; use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; use WrkFlow\ApiSdkBuilder\Log\Entities\LoggerConfigEntity; use WrkFlow\ApiSdkBuilder\Log\Entities\LoggerFailConfigEntity; use WrkFlow\ApiSdkBuilder\Log\Interfaces\ApiLoggerInterface; use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; -final class SendRequestAction +/** + * @phpstan-import-type IgnoreLoggersOnExceptionClosure from ApiInterface + */ +final class SendRequestAction implements SendRequestActionContract { public function __construct( private readonly BuildHeadersAction $buildHeadersAction, @@ -35,20 +37,8 @@ public function __construct( ) { } - /** - * @template TResponse of AbstractResponse - * - * @param array $headers - * @param class-string $responseClass - * @param int|null $expectedResponseStatusCode Will raise and failed - * exception if response - * status code is different - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError - * - * @return TResponse - */ public function execute( - AbstractApi $api, + ApiInterface $api, RequestInterface $request, string $responseClass, OptionsInterface|StreamInterface|string|null $body = null, @@ -68,7 +58,7 @@ public function execute( $loggerConfig = $api->factory() ->loggerConfig(); - $logger = $this->getLoggerAction->execute(config: $loggerConfig, host: $request->getUri() ->getHost()); + $logger = $this->getLoggerAction->execute(config: $loggerConfig, host: $request->getUri()->getHost()); $environment = $api->environment(); @@ -107,7 +97,7 @@ public function execute( */ private function sendRequest( AbstractEnvironment $environment, - AbstractApi $api, + ApiInterface $api, ?EventDispatcherInterface $dispatcher, ?ApiLoggerInterface $logger, LoggerConfigEntity $loggerConfig, @@ -149,7 +139,7 @@ private function sendRequest( } private function withBody( - AbstractApi $api, + ApiInterface $api, OptionsInterface|StreamInterface|string|null $body, RequestInterface $request ): RequestInterface { @@ -174,16 +164,16 @@ private function getRequestDuration(float $timeStart): float /** * @template TResponse of AbstractResponse * - * @param class-string $responseClass - * @param int|null $expectedResponseStatusCode Will raise and failed + * @param class-string $responseClass + * @param int|null $expectedResponseStatusCode Will raise and failed * exception if response * status code is different - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError + * @param IgnoreLoggersOnExceptionClosure $shouldIgnoreLoggersOnError * * @return TResponse */ private function handleResponse( - AbstractApi $api, + ApiInterface $api, ResponseInterface $response, ?int $expectedResponseStatusCode, string $responseClass, @@ -254,7 +244,7 @@ private function handleResponse( private function buildRequest( AbstractEnvironment $environment, - AbstractApi $api, + ApiInterface $api, array $headers, RequestInterface $request, StreamInterface|string|OptionsInterface|null $body diff --git a/src/Contracts/SDKContainerFactoryContract.php b/src/Contracts/SDKContainerFactoryContract.php index 292f25a..e946043 100644 --- a/src/Contracts/SDKContainerFactoryContract.php +++ b/src/Contracts/SDKContainerFactoryContract.php @@ -5,8 +5,8 @@ namespace WrkFlow\ApiSdkBuilder\Contracts; use Psr\Http\Message\ResponseInterface; -use WrkFlow\ApiSdkBuilder\AbstractApi; use WrkFlow\ApiSdkBuilder\Endpoints\AbstractEndpoint; +use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; use Wrkflow\GetValue\GetValue; @@ -19,12 +19,12 @@ interface SDKContainerFactoryContract * * @return T */ - public function makeEndpoint(AbstractApi $api, string $endpointClass): AbstractEndpoint; + public function makeEndpoint(ApiInterface $api, string $endpointClass): AbstractEndpoint; /** * Dynamically creates an instance of the given class. * - Some classes should be cached for performance (as singletons). - * @template T + * @template T of object * * @param class-string $class * diff --git a/src/Contracts/SendRequestActionContract.php b/src/Contracts/SendRequestActionContract.php new file mode 100644 index 0000000..2544068 --- /dev/null +++ b/src/Contracts/SendRequestActionContract.php @@ -0,0 +1,42 @@ + $headers + * @param class-string $responseClass + * @param int|null $expectedResponseStatusCode Will raise and failed + * exception if response + * status code is different + * @param IgnoreLoggersOnExceptionClosure $shouldIgnoreLoggersOnError + * @return TResponse + */ + public function execute( + ApiInterface $api, + RequestInterface $request, + string $responseClass, + OptionsInterface|StreamInterface|string|null $body = null, + array $headers = [], + ?int $expectedResponseStatusCode = null, + ?ResponseInterface $fakedResponse = null, + Closure $shouldIgnoreLoggersOnError = null + ): AbstractResponse; +} diff --git a/src/Endpoints/AbstractEndpoint.php b/src/Endpoints/AbstractEndpoint.php index 128e722..b5421fc 100644 --- a/src/Endpoints/AbstractEndpoint.php +++ b/src/Endpoints/AbstractEndpoint.php @@ -4,30 +4,276 @@ namespace WrkFlow\ApiSdkBuilder\Endpoints; +use Closure; use JustSteveKing\UriBuilder\Uri; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use Throwable; +use WrkFlow\ApiSdkBuilder\Contracts\ApiFactoryContract; +use WrkFlow\ApiSdkBuilder\Entities\EndpointDIEntity; use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; +use WrkFlow\ApiSdkBuilder\Interfaces\HeadersInterface; +use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; +use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; +/** + * Endpoint names that contains all API endpoint methods that sends a request. + * This class should be immutable because it is cached. + * + * @phpstan-import-type IgnoreLoggersOnExceptionClosure from ApiInterface + */ abstract class AbstractEndpoint { + /** + * @var IgnoreLoggersOnExceptionClosure + */ + private Closure|null $shouldIgnoreLoggersOnException = null; + public function __construct( - protected ApiInterface $api, + protected readonly EndpointDIEntity $di, ) { } + /** + * Returns a copy of endpoint with ability to prevent loggers from logging failed responses for given + * exception. + */ + final public function setShouldIgnoreLoggersForExceptions(Closure $closure): static + { + $cloned = clone $this; + + $cloned->shouldIgnoreLoggersOnException = $closure; + + return $cloned; + } + + final protected function shouldIgnoreLoggersOnException(): ?Closure + { + return function (Throwable $throwable): array { + $return = []; + $globalClosure = $this->di->api() + ->shouldIgnoreLoggersOnException(); + + if ($globalClosure !== null) { + $return += $globalClosure($throwable); + } + + $localClosure = $this->shouldIgnoreLoggersOnException; + if ($localClosure !== null) { + $return += $localClosure($throwable); + } + + return $return; + }; + } + /** * Appends to base path in uri. Must start with /. */ abstract protected function basePath(): string; - protected function uri(string $appendPath = ''): Uri + final protected function uri(string $appendPath = ''): Uri { - $uri = $this->api->uri(); + $uri = $this->di->api() + ->uri(); $basePath = $this->appendSlashIfNeeded($this->basePath()); $appendPath = $this->appendSlashIfNeeded($appendPath); return $uri->addPath($uri->path() . $basePath . $appendPath); } + + /** + * @template TResponse of AbstractResponse + * + * @param class-string $responseClass + * @param array $headers + * + * @return TResponse + */ + final protected function sendGet( + string $responseClass, + Uri $uri, + array $headers = [], + ?int $expectedResponseStatusCode = null, + ): AbstractResponse { + $request = $this->factory() + ->request() + ->createRequest('GET', $uri->toString()); + + return $this + ->di + ->sendRequestAction() + ->execute( + api: $this + ->di + ->api(), + request: $request, + responseClass: $responseClass, + headers: $headers, + expectedResponseStatusCode: $expectedResponseStatusCode, + shouldIgnoreLoggersOnError: $this->shouldIgnoreLoggersOnException(), + ); + } + + /** + * @template TResponse of AbstractResponse + * + * @param class-string $responseClass + * @param array $headers + * @param int|null $expectedResponseStatusCode Will raise and failed + * exception if response + * + * @return TResponse + */ + final protected function sendPost( + string $responseClass, + Uri $uri, + OptionsInterface|StreamInterface|string $body = null, + array $headers = [], + ?int $expectedResponseStatusCode = null, + ): AbstractResponse { + $request = $this->factory() + ->request() + ->createRequest('POST', $uri->toString()); + + return $this->di->sendRequestAction() + ->execute( + api: $this + ->di + ->api(), + request: $request, + responseClass: $responseClass, + body: $body, + headers: $headers, + expectedResponseStatusCode: $expectedResponseStatusCode, + shouldIgnoreLoggersOnError: $this->shouldIgnoreLoggersOnException(), + ); + } + + /** + * @template TResponse of AbstractResponse + * + * @param class-string $responseClass + * @param array $headers + * @param int|null $expectedResponseStatusCode Will raise and failed + * exception if response + * + * @return TResponse + */ + final protected function sendPut( + string $responseClass, + Uri $uri, + OptionsInterface|StreamInterface|string $body = null, + array $headers = [], + ?int $expectedResponseStatusCode = null, + Closure $shouldIgnoreLoggersOnError = null, + ): AbstractResponse { + $request = $this->factory() + ->request() + ->createRequest('PUT', $uri->toString()); + + return $this + ->di + ->sendRequestAction() + ->execute( + api: $this + ->di + ->api(), + request: $request, + responseClass: $responseClass, + body: $body, + headers: $headers, + expectedResponseStatusCode: $expectedResponseStatusCode, + shouldIgnoreLoggersOnError: $this->shouldIgnoreLoggersOnException(), + ); + } + + /** + * @template TResponse of AbstractResponse + * + * @param class-string $responseClass + * @param array $headers + * @param int|null $expectedResponseStatusCode Will raise and failed + * exception if response + * + * @return TResponse + */ + final protected function sendDelete( + string $responseClass, + Uri $uri, + OptionsInterface|StreamInterface|string $body = null, + array $headers = [], + ?int $expectedResponseStatusCode = null, + ): AbstractResponse { + $request = $this->factory() + ->request() + ->createRequest('DELETE', $uri->toString()); + + return $this + ->di + ->sendRequestAction() + ->execute( + api: $this + ->di + ->api(), + request: $request, + responseClass: $responseClass, + body: $body, + headers: $headers, + expectedResponseStatusCode: $expectedResponseStatusCode, + shouldIgnoreLoggersOnError: $this->shouldIgnoreLoggersOnException(), + ); + } + + /** + * Sends a fake request with fake response (will all events in place). + * + * @template TResponse of AbstractResponse + * + * @param class-string $responseClass + * @param array $headers + * @param int|null $expectedResponseStatusCode Will raise and failed + * exception if response + * + * @return TResponse + */ + final protected function sendFake( + ResponseInterface $response, + string $responseClass, + Uri $uri, + OptionsInterface|StreamInterface|string $body = null, + array $headers = [], + ?int $expectedResponseStatusCode = null, + ): AbstractResponse { + return $this + ->di + ->sendRequestAction() + ->execute( + api: $this + ->di + ->api(), + request: $this + ->factory() + ->request() + ->createRequest('FAKE', $uri->toString()), + responseClass: $responseClass, + body: $body, + headers: $headers, + expectedResponseStatusCode: $expectedResponseStatusCode, + fakedResponse: $response, + shouldIgnoreLoggersOnError: $this->shouldIgnoreLoggersOnException(), + ); + } + + final protected function factory(): ApiFactoryContract + { + return $this + ->di + ->api() + ->factory(); + } + private function appendSlashIfNeeded(string $path): string { if ($path !== '' && $path[0] !== '/') { diff --git a/src/Endpoints/AbstractFakeEndpoint.php b/src/Endpoints/AbstractFakeEndpoint.php index 218246b..54b691d 100644 --- a/src/Endpoints/AbstractFakeEndpoint.php +++ b/src/Endpoints/AbstractFakeEndpoint.php @@ -7,9 +7,9 @@ use Psr\Http\Message\StreamInterface; use SimpleXMLElement; use WrkFlow\ApiSdkBuilder\Actions\BuildHeadersAction; +use WrkFlow\ApiSdkBuilder\Entities\EndpointDIEntity; use WrkFlow\ApiSdkBuilder\Headers\JsonHeaders; use WrkFlow\ApiSdkBuilder\Headers\XMLHeaders; -use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; use Wrkflow\GetValue\GetValue; @@ -18,11 +18,11 @@ abstract class AbstractFakeEndpoint extends AbstractEndpoint { public function __construct( - ApiInterface $api, + EndpointDIEntity $di, protected readonly GetValueFactory $getValueFactory, protected readonly BuildHeadersAction $buildHeadersAction ) { - parent::__construct($api); + parent::__construct($di); } protected function basePath(): string @@ -37,7 +37,7 @@ protected function basePath(): string * * @return TResponse */ - protected function makeResponse( + final protected function makeResponse( string $responseClass, GetValue|StreamInterface|null $responseBody = null, OptionsInterface|StreamInterface|string $requestBody = null, @@ -45,7 +45,7 @@ protected function makeResponse( ?int $expectedResponseStatusCode = null ): AbstractResponse { $responseHeaders = []; - $response = $this->api->factory() + $response = $this->factory() ->response() ->createResponse(); @@ -59,13 +59,15 @@ protected function makeResponse( if ($rawBody instanceof SimpleXMLElement) { $responseHeaders[] = new XMLHeaders(); - $stream = $this->api->factory() + $stream = $this + ->factory() ->stream() ->createStream((string) $rawBody->asXML()); } elseif (is_array($rawBody)) { $responseHeaders[] = new JsonHeaders(); - $stream = $this->api->factory() + $stream = $this + ->factory() ->stream() ->createStream((string) json_encode($rawBody, JSON_THROW_ON_ERROR)); } @@ -79,13 +81,13 @@ protected function makeResponse( } } - return $this->api->fake( - $response, - $responseClass, - $this->uri(), - $requestBody, - array_merge($headers, $responseHeaders), - $expectedResponseStatusCode + return $this->sendFake( + response: $response, + responseClass: $responseClass, + uri: $this->uri(), + body: $requestBody, + headers: array_merge($headers, $responseHeaders), + expectedResponseStatusCode: $expectedResponseStatusCode ); } } diff --git a/src/Entities/EndpointDIEntity.php b/src/Entities/EndpointDIEntity.php new file mode 100644 index 0000000..c25cab8 --- /dev/null +++ b/src/Entities/EndpointDIEntity.php @@ -0,0 +1,27 @@ +api; + } + + public function sendRequestAction(): SendRequestActionContract + { + return $this->sendRequestAction; + } +} diff --git a/src/Interfaces/ApiInterface.php b/src/Interfaces/ApiInterface.php index 33c942f..01262ab 100644 --- a/src/Interfaces/ApiInterface.php +++ b/src/Interfaces/ApiInterface.php @@ -7,121 +7,27 @@ use Closure; use JustSteveKing\UriBuilder\Uri; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; use Throwable; use WrkFlow\ApiSdkBuilder\Contracts\ApiFactoryContract; use WrkFlow\ApiSdkBuilder\Environments\AbstractEnvironment; use WrkFlow\ApiSdkBuilder\Exceptions\ResponseException; use WrkFlow\ApiSdkBuilder\Log\Interfaces\ApiLoggerInterface; -use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; +/** + * @phpstan-type IgnoreLoggersOnExceptionClosure Closure(Throwable):(array>)|null + */ interface ApiInterface extends HeadersInterface { - public function environment(): AbstractEnvironment; - - public function factory(): ApiFactoryContract; - - public function uri(): Uri; - - /** - * @template TResponse of AbstractResponse - * - * @param class-string $responseClass - * @param array $headers - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError - * - * @return TResponse - */ - public function get( - string $responseClass, - Uri $uri, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse; - /** - * @template TResponse of AbstractResponse - * - * @param class-string $responseClass - * @param array $headers - * @param int|null $expectedResponseStatusCode Will raise and failed - * exception if response - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError - * - * @return TResponse + * @return IgnoreLoggersOnExceptionClosure */ - public function post( - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse; + public function shouldIgnoreLoggersOnException(): ?Closure; - /** - * @template TResponse of AbstractResponse - * - * @param class-string $responseClass - * @param array $headers - * @param int|null $expectedResponseStatusCode Will raise and failed - * exception if response - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError - * - * @return TResponse - */ - public function put( - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse; + public function environment(): AbstractEnvironment; - /** - * @template TResponse of AbstractResponse - * - * @param class-string $responseClass - * @param array $headers - * @param int|null $expectedResponseStatusCode Will raise and failed - * exception if response - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError - * - * @return TResponse - */ - public function delete( - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse; + public function factory(): ApiFactoryContract; - /** - * Sends a fake request with fake response (will all events in place). - * - * @template TResponse of AbstractResponse - * - * @param class-string $responseClass - * @param array $headers - * @param int|null $expectedResponseStatusCode Will raise and failed - * exception if response - * @param Closure(Throwable):array|null $shouldIgnoreLoggersOnError - * - * @return TResponse - */ - public function fake( - ResponseInterface $response, - string $responseClass, - Uri $uri, - OptionsInterface|StreamInterface|string $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse; + public function uri(): Uri; public function createFailedResponseException(int $statusCode, ResponseInterface $response): ResponseException; } diff --git a/src/Laravel/LaravelContainerFactory.php b/src/Laravel/LaravelContainerFactory.php index 22aa807..dd7fb78 100644 --- a/src/Laravel/LaravelContainerFactory.php +++ b/src/Laravel/LaravelContainerFactory.php @@ -6,9 +6,10 @@ use Illuminate\Contracts\Container\Container; use Psr\Http\Message\ResponseInterface; -use WrkFlow\ApiSdkBuilder\AbstractApi; use WrkFlow\ApiSdkBuilder\Contracts\SDKContainerFactoryContract; use WrkFlow\ApiSdkBuilder\Endpoints\AbstractEndpoint; +use WrkFlow\ApiSdkBuilder\Entities\EndpointDIEntity; +use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; use Wrkflow\GetValue\GetValue; @@ -25,10 +26,12 @@ public function __construct( * * @return T */ - public function makeEndpoint(AbstractApi $api, string $endpointClass): AbstractEndpoint + public function makeEndpoint(ApiInterface $api, string $endpointClass): AbstractEndpoint { $endpoint = $this->container->make($endpointClass, [ - 'api' => $api, + 'di' => $this->container->make(EndpointDIEntity::class, [ + 'api' => $api, + ]), ]); assert($endpoint instanceof $endpointClass); diff --git a/src/Laravel/LaravelServiceProvider.php b/src/Laravel/LaravelServiceProvider.php index cf7bac8..81aa349 100644 --- a/src/Laravel/LaravelServiceProvider.php +++ b/src/Laravel/LaravelServiceProvider.php @@ -19,8 +19,10 @@ use LogicException; use Psr\EventDispatcher\EventDispatcherInterface; use WrkFlow\ApiSdkBuilder\Actions\MakeApiFactory; +use WrkFlow\ApiSdkBuilder\Actions\SendRequestAction; use WrkFlow\ApiSdkBuilder\Contracts\ApiFactoryContract; use WrkFlow\ApiSdkBuilder\Contracts\SDKContainerFactoryContract; +use WrkFlow\ApiSdkBuilder\Contracts\SendRequestActionContract; use WrkFlow\ApiSdkBuilder\Events\RequestConnectionFailedEvent; use WrkFlow\ApiSdkBuilder\Events\RequestFailedEvent; use WrkFlow\ApiSdkBuilder\Events\ResponseReceivedEvent; @@ -106,7 +108,7 @@ protected static function getFileSystemOperator(Container $container): Filesyste return $disk->getDriver(); } - private function getConfig(Container $container): ApiSdkConfig + protected static function getConfig(Container $container): ApiSdkConfig { $config = self::make(container: $container, class: ApiSdkConfig::class); assert($config instanceof ApiSdkConfig); @@ -156,6 +158,7 @@ private function bindLogs(): void $this->app->singleton(abstract: FileLoggerContract::class, concrete: FileLogger::class); $this->app->singleton(abstract: InfoLoggerContract::class, concrete: InfoLogger::class); $this->app->singleton(abstract: InfoOrFailFileLoggerContract::class, concrete: InfoOrFailFileLogger::class); + $this->app->singleton(abstract: SendRequestActionContract::class, concrete: SendRequestAction::class); // Not sure if we provided FilesystemOperator to whole Laravel application // if it would break some package usage... So lets be explicit. diff --git a/src/Testing/ApiMock.php b/src/Testing/ApiMock.php index 35e23c7..bd4d372 100644 --- a/src/Testing/ApiMock.php +++ b/src/Testing/ApiMock.php @@ -6,42 +6,24 @@ use Closure; use JustSteveKing\UriBuilder\Uri; -use Mockery; -use PHPUnit\Framework\Assert; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamInterface; use WrkFlow\ApiSdkBuilder\Contracts\ApiFactoryContract; use WrkFlow\ApiSdkBuilder\Environments\AbstractEnvironment; use WrkFlow\ApiSdkBuilder\Exceptions\ResponseException; use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; -use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; -use WrkFlow\ApiSdkBuilder\Responses\AbstractResponse; -use WrkFlow\ApiSdkBuilder\Testing\Endpoints\EndpointExpectation; -use WrkFlow\ApiSdkBuilder\Testing\Environments\TestingEnvironmentMock; +use WrkFlow\ApiSdkBuilder\Testing\Environments\TestingEnvironment; use WrkFlow\ApiSdkBuilder\Testing\Factories\ApiFactoryMock; -class ApiMock implements ApiInterface +final class ApiMock implements ApiInterface { - public array $postExpectations = []; - - public array $getExpectations = []; - - public array $putExpectations = []; - - public array $deleteExpectations = []; - - public array $fakeExpectations = []; - - public readonly TestingEnvironmentMock $environment; - - public function __construct() + public function shouldIgnoreLoggersOnException(): ?Closure { - $this->environment = new TestingEnvironmentMock(); + return null; } public function environment(): AbstractEnvironment { - return $this->environment; + return new TestingEnvironment(); } public function factory(): ApiFactoryContract @@ -51,192 +33,16 @@ public function factory(): ApiFactoryContract public function uri(): Uri { - return $this->environment() - ->uri(); - } - - public function get( - string $responseClass, - Uri $uri, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $this->getExpectations = $this->assertRequest( - $this->getExpectations, - $responseClass, - $uri, - null, - $headers, - $expectedResponseStatusCode - ); - - return $this->returnResponseMock($responseClass); - } - - public function post( - string $responseClass, - Uri $uri, - StreamInterface|string|OptionsInterface $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $this->postExpectations = $this->assertRequest( - $this->postExpectations, - $responseClass, - $uri, - $body, - $headers, - $expectedResponseStatusCode - ); - - return $this->returnResponseMock($responseClass); - } - - public function fake( - ResponseInterface $response, - string $responseClass, - Uri $uri, - StreamInterface|string|OptionsInterface $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $this->fakeExpectations = $this->assertRequest( - $this->fakeExpectations, - $responseClass, - $uri, - $body, - $headers, - $expectedResponseStatusCode - ); - - return $this->returnResponseMock($responseClass); - } - - public function put( - string $responseClass, - Uri $uri, - StreamInterface|string|OptionsInterface $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $this->putExpectations = $this->assertRequest( - $this->putExpectations, - $responseClass, - $uri, - $body, - $headers, - $expectedResponseStatusCode - ); - - return $this->returnResponseMock($responseClass); - } - - public function delete( - string $responseClass, - Uri $uri, - StreamInterface|string|OptionsInterface $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - Closure $shouldIgnoreLoggersOnError = null, - ): AbstractResponse { - $this->deleteExpectations = $this->assertRequest( - $this->deleteExpectations, - $responseClass, - $uri, - $body, - $headers, - $expectedResponseStatusCode - ); - return $this->returnResponseMock($responseClass); + return Uri::fromString('https://test.localhost'); } public function createFailedResponseException(int $statusCode, ResponseInterface $response): ResponseException { - return new ResponseException($response, 'Error code ' . $statusCode); + return new ResponseException($response); } public function headers(): array { - return [ - 'X-Testing-Header' => 'true', - ]; - } - - /** - * @param array $expectations - */ - protected function assertRequest( - array $expectations, - string $responseClass, - Uri $uri, - StreamInterface|string|OptionsInterface $body = null, - array $headers = [], - ?int $expectedResponseStatusCode = null, - ): array { - $expectation = array_shift($expectations); - - Assert::assertNotNull( - $expectation, - 'Request expectation missing - endpoint is probably using different HTTP/s method' - ); - - $environment = $this->environment(); - $baseUri = $environment->uri(); - $expectedUrl = $baseUri->addPath($baseUri->path() . $expectation->expectedUriPath)->toString(); - Assert::assertEquals($expectedUrl, $uri->toString()); - - Assert::assertEquals($expectation->expectedResponseClass, $responseClass); - Assert::assertEquals($expectation->expectedHeaders, $headers); - Assert::assertEquals($expectation->expectedResponseStatusCode, $expectedResponseStatusCode); - - if ($expectation->assertBody === null) { - Assert::assertNull($body, 'Request body should be null'); - } elseif ($expectation->assertBody instanceof OptionsInterface) { - Assert::assertInstanceOf(OptionsInterface::class, $body, 'Request body should be OptionsContract'); - Assert::assertEquals( - json_decode($expectation->assertBody->toBody($environment), true, 512, JSON_THROW_ON_ERROR), - json_decode($body->toBody($environment), true, 512, JSON_THROW_ON_ERROR), - 'Request body is different' - ); - } elseif (is_string($expectation->assertBody)) { - Assert::assertTrue(is_string($body), 'Request body should be string'); - Assert::assertEquals($expectation->assertBody, $body, 'Request body is different'); - } elseif ($expectation->assertBody instanceof StreamInterface) { - Assert::assertInstanceOf(StreamInterface::class, $body, 'Request body should be StreamInterface'); - Assert::assertEquals( - $expectation->assertBody->getContents(), - $body->getContents(), - 'Request body is different' - ); - } elseif (is_array($expectation->assertBody)) { - Assert::assertInstanceOf(OptionsInterface::class, $body, 'Request body should be OptionsContract'); - Assert::assertEquals( - $expectation->assertBody, - json_decode($body->toBody($environment), true, 512, JSON_THROW_ON_ERROR), - 'Request body is different' - ); - } elseif (is_callable($expectation->assertBody)) { - call_user_func($expectation->assertBody, $body); - } - - return $expectations; - } - - /** - * @template TResponse of AbstractResponse - * - * @param class-string $responseClass - * - * @return TResponse - */ - protected function returnResponseMock(string $responseClass): AbstractResponse - { - /** @var TResponse $mock */ - $mock = Mockery::mock($responseClass); - return $mock; + return []; } } diff --git a/src/Testing/Assertions/SendTestRequestActionAssert.php b/src/Testing/Assertions/SendTestRequestActionAssert.php new file mode 100644 index 0000000..efb4a43 --- /dev/null +++ b/src/Testing/Assertions/SendTestRequestActionAssert.php @@ -0,0 +1,104 @@ +> $expectedHeaders + */ + public function __construct( + private readonly string $expectedUri = '', + private readonly string $expectedMethod = 'GET', + private readonly Closure|StreamInterface|string|OptionsInterface|array|null $expectedBody = null, + private readonly array $expectedHeaders = [], + private readonly ?int $expectedResponseStatusCode = null, + ) { + $this->buildHeadersAction = new BuildHeadersAction(); + } + + public function execute( + ApiInterface $api, + RequestInterface $request, + string $responseClass, + StreamInterface|string|OptionsInterface|null $body = null, + array $headers = [], + ?int $expectedResponseStatusCode = null, + ?ResponseInterface $fakedResponse = null, + Closure $shouldIgnoreLoggersOnError = null, + ): TResponse { + Assert::assertEquals( + expected: 'https://test.localhost' . $this->expectedUri, + actual: $request->getUri() + ->__toString(), + message: 'Expected URI does not match the request URI.' + ); + Assert::assertEquals( + expected: $this->expectedMethod, + actual: $request->getMethod(), + message: 'Expected method does not match the request method.' + ); + + $environment = $api->environment(); + + if ($this->expectedBody === null) { + Assert::assertNull($body, 'Request body should be null'); + } elseif ($this->expectedBody instanceof OptionsInterface) { + Assert::assertInstanceOf(OptionsInterface::class, $body, 'Request body should be OptionsContract'); + Assert::assertEquals( + json_decode($this->expectedBody->toBody($environment), true, 512, JSON_THROW_ON_ERROR), + json_decode($body->toBody($environment), true, 512, JSON_THROW_ON_ERROR), + 'Request body is different' + ); + } elseif (is_string($this->expectedBody)) { + Assert::assertTrue(is_string($body), 'Request body should be string'); + Assert::assertEquals($this->expectedBody, $body, 'Request body is different'); + } elseif ($this->expectedBody instanceof StreamInterface) { + Assert::assertInstanceOf(StreamInterface::class, $body, 'Request body should be StreamInterface'); + Assert::assertEquals( + $this->expectedBody->getContents(), + $body->getContents(), + 'Request body is different' + ); + } elseif (is_array($this->expectedBody)) { + Assert::assertInstanceOf(OptionsInterface::class, $body, 'Request body should be OptionsContract'); + Assert::assertEquals( + $this->expectedBody, + json_decode($body->toBody($environment), true, 512, JSON_THROW_ON_ERROR), + 'Request body is different' + ); + } elseif (is_callable($this->expectedBody)) { + call_user_func($this->expectedBody, $body); + } + + Assert::assertEquals($this->expectedResponseStatusCode, $expectedResponseStatusCode); + $actualHeaders = $this->buildHeadersAction->execute($headers, $request) + ->getHeaders(); + // Remove host header that is added automatically. + unset($actualHeaders['Host']); + Assert::assertEquals( + expected: $this->expectedHeaders, + actual: $actualHeaders, + message: 'Expected headers do not match the request headers.' + ); + + throw new TestRequestSentException(); + } +} diff --git a/src/Testing/Endpoints/EndpointMock.php b/src/Testing/Endpoints/EndpointMock.php index 2b9a203..707a3eb 100644 --- a/src/Testing/Endpoints/EndpointMock.php +++ b/src/Testing/Endpoints/EndpointMock.php @@ -5,15 +5,15 @@ namespace WrkFlow\ApiSdkBuilder\Testing\Endpoints; use WrkFlow\ApiSdkBuilder\Endpoints\AbstractEndpoint; -use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; +use WrkFlow\ApiSdkBuilder\Entities\EndpointDIEntity; class EndpointMock extends AbstractEndpoint { public function __construct( - ApiInterface $api, + EndpointDIEntity $di, public readonly string $endpointClass ) { - parent::__construct($api); + parent::__construct($di); } protected function basePath(): string diff --git a/src/Testing/Endpoints/EndpointTestCase.php b/src/Testing/Endpoints/EndpointTestCase.php index a8b2164..0700b85 100644 --- a/src/Testing/Endpoints/EndpointTestCase.php +++ b/src/Testing/Endpoints/EndpointTestCase.php @@ -5,50 +5,34 @@ namespace WrkFlow\ApiSdkBuilder\Testing\Endpoints; use Closure; -use JustSteveKing\UriBuilder\Uri; use Mockery\Adapter\Phpunit\MockeryTestCase; -use Mockery\MockInterface; -use WrkFlow\ApiSdkBuilder\Testing\ApiMock; +use Psr\Http\Message\StreamInterface; +use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; +use WrkFlow\ApiSdkBuilder\Testing\Assertions\SendTestRequestActionAssert; +use WrkFlow\ApiSdkBuilder\Testing\Exceptions\TestRequestSentException; +use WrkFlow\ApiSdkBuilder\Testing\Factories\EndpointDIEntityFactory; abstract class EndpointTestCase extends MockeryTestCase { - protected ApiMock $api; - - protected Uri $uri; - - protected function setUp(): void - { - parent::setUp(); - - $this->api = new ApiMock(); - } - - public function assertGet(EndpointExpectation $expectation, Closure $callEndpoint): void - { - $this->api->getExpectations[] = $expectation; - $this->assertEndpointCall($callEndpoint); - } - - public function assertPost(EndpointExpectation $expectation, Closure $callEndpoint): void - { - $this->api->postExpectations[] = $expectation; - $this->assertEndpointCall($callEndpoint); - } - - public function assertPut(EndpointExpectation $expectation, Closure $callEndpoint): void - { - $this->api->putExpectations[] = $expectation; - $this->assertEndpointCall($callEndpoint); - } - - public function assertDelete(EndpointExpectation $expectation, Closure $callEndpoint): void - { - $this->api->deleteExpectations[] = $expectation; - $this->assertEndpointCall($callEndpoint); - } - - private function assertEndpointCall(Closure $callEndpoint): void - { - $this->assertInstanceOf(MockInterface::class, $callEndpoint(), 'Endpoint response mock not returned'); + protected function assertEndpoint( + Closure $call, + string $expectedUri, + string $expectedMethod = 'GET', + StreamInterface|string|OptionsInterface|array|null $expectedBody = null, + array $expectedHeaders = [], + ?int $expectedResponseStatusCode = null, + ): void { + $this->expectException(TestRequestSentException::class); + + $di = EndpointDIEntityFactory::make( + sendAssert: new SendTestRequestActionAssert( + expectedUri: $expectedUri, + expectedMethod: $expectedMethod, + expectedBody: $expectedBody, + expectedHeaders: $expectedHeaders, + expectedResponseStatusCode: $expectedResponseStatusCode, + ), + ); + $call($di); } } diff --git a/src/Testing/Environments/TestingEnvironmentMock.php b/src/Testing/Environments/TestingEnvironment.php similarity index 83% rename from src/Testing/Environments/TestingEnvironmentMock.php rename to src/Testing/Environments/TestingEnvironment.php index 4e4479e..f9ceea7 100644 --- a/src/Testing/Environments/TestingEnvironmentMock.php +++ b/src/Testing/Environments/TestingEnvironment.php @@ -7,7 +7,7 @@ use JustSteveKing\UriBuilder\Uri; use WrkFlow\ApiSdkBuilder\Environments\AbstractEnvironment; -class TestingEnvironmentMock extends AbstractEnvironment +class TestingEnvironment extends AbstractEnvironment { public function uri(): Uri { diff --git a/src/Testing/Exceptions/BindingResolutionException.php b/src/Testing/Exceptions/BindingResolutionException.php new file mode 100644 index 0000000..87d3e43 --- /dev/null +++ b/src/Testing/Exceptions/BindingResolutionException.php @@ -0,0 +1,11 @@ +responseFactory = $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory(); + $this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory(); + } + + public function response(): ResponseFactoryInterface { - return Mockery::mock(ResponseFactoryInterface::class); + return $this->responseFactory; } public function request(): RequestFactoryInterface { - return Mockery::mock(RequestFactoryInterface::class); + return $this->requestFactory; } public function client(): ClientInterface { - return Mockery::mock(ClientInterface::class); + return $this->mockClient; } public function stream(): StreamFactoryInterface @@ -38,12 +54,12 @@ public function stream(): StreamFactoryInterface public function container(): SDKContainerFactoryContract { - return Mockery::mock(SDKContainerFactoryContract::class); + return $this->testSDKContainerFactory; } public function eventDispatcher(): ?EventDispatcherInterface { - return Mockery::mock(EventDispatcherInterface::class); + return $this->eventDispatcher; } public function loggerConfig(): LoggerConfigEntity diff --git a/src/Testing/Factories/EndpointDIEntityFactory.php b/src/Testing/Factories/EndpointDIEntityFactory.php new file mode 100644 index 0000000..bf15762 --- /dev/null +++ b/src/Testing/Factories/EndpointDIEntityFactory.php @@ -0,0 +1,20 @@ +, object|Closure():(object|null)> $makeBindings + * A map of closures mapped to a class that should create the instance. + * @param array, Closure(EndpointDIEntity):(AbstractEndpoint|null)> $makeEndpointBindings + * A map of closures mapped to a class that should create the instance. + * @param array, Closure(ResponseInterface, ?GetValue):(AbstractResponse|null)> $makeResponseBindings A map of closures mapped to a class that should create the instance. + */ + public function __construct( + private readonly array $makeBindings = [], + private readonly array $makeEndpointBindings = [], + private readonly array $makeResponseBindings = [], + ) { + } + + public function makeEndpoint(ApiInterface $api, string $endpointClass): AbstractEndpoint + { + $result = $this->makeFrom( + abstract: $endpointClass, + bindings: $this->makeEndpointBindings, + makeGiven: static fn (Closure $make) => $make(EndpointDIEntityFactory::make(api: $api)), + ); + assert($result instanceof $endpointClass, 'Binding must be instance of ' . AbstractEndpoint::class); + return $result; + } + + public function make(string $class): mixed + { + $result = $this->makeFrom( + abstract: $class, + bindings: $this->makeBindings, + makeGiven: static fn (Closure $make) => $make(), + ); + assert($result instanceof $class, 'Binding must be instance of ' . $class); + + return $result; + } + + public function has(string $classOrKey): bool + { + $bindings = [$this->makeBindings, $this->makeResponseBindings, $this->makeEndpointBindings]; + foreach ($bindings as $binding) { + if (array_key_exists($classOrKey, $binding)) { + return true; + } + } + + return false; + } + + public function makeResponse(string $class, ResponseInterface $response, ?GetValue $body): AbstractResponse + { + $result = $this->makeFrom( + abstract: $class, + bindings: $this->makeResponseBindings, + makeGiven: static fn (Closure $make) => $make($response, $body), + ); + assert($result instanceof $class, 'Binding must be instance of ' . AbstractResponse::class); + + return $result; + } + + /** + * @template T of object + * + * @param class-string $abstract + * @param array, T|Closure> $bindings + * + * @return T + */ + private function makeFrom(string $abstract, array $bindings, Closure $makeGiven): object + { + $make = $bindings[$abstract] ?? null; + + if ($make === null) { + throw new BindingResolutionException('Binding not set ' . $abstract); + } + + if ($make instanceof Closure === false) { + assert($make instanceof $abstract, 'Binding must be instance of ' . $abstract); + return $make; + } + + $result = $makeGiven($make); + + if ($result === null) { + throw new BindingResolutionException('Failed to resolve ' . $abstract); + } + + assert($result instanceof $abstract, 'Binding must be instance of ' . $abstract); + + return $result; + } +} diff --git a/src/Testing/Options/OptionsTestCase.php b/src/Testing/Options/OptionsTestCase.php new file mode 100644 index 0000000..1b85394 --- /dev/null +++ b/src/Testing/Options/OptionsTestCase.php @@ -0,0 +1,21 @@ +toBody(new TestingEnvironment()); + if (is_array($expected)) { + $body = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } + $this->assertEquals($expected, $body); + } +} diff --git a/tests/Laravel/ApiTestCase.php b/tests/Laravel/ApiTestCase.php index fd35e32..4754fc8 100644 --- a/tests/Laravel/ApiTestCase.php +++ b/tests/Laravel/ApiTestCase.php @@ -7,7 +7,7 @@ use Http\Mock\Client; use Psr\Http\Client\ClientInterface; use WrkFlow\ApiSdkBuilder\Contracts\ApiFactoryContract; -use WrkFlow\ApiSdkBuilderTests\TestApi\Environments\TestEnvironment; +use WrkFlow\ApiSdkBuilder\Testing\Environments\TestingEnvironment; use WrkFlow\ApiSdkBuilderTests\TestApi\TestApi; abstract class ApiTestCase extends TestCase @@ -21,7 +21,7 @@ protected function setUp(): void $this->mockBeforeApiFactory(); $this->api = new TestApi( - environment: new TestEnvironment(), + environment: new TestingEnvironment(), factory: $this->make(ApiFactoryContract::class), ); } diff --git a/tests/Laravel/LaravelServiceProviderTest.php b/tests/Laravel/LaravelServiceProviderTest.php index bb72786..734cbf5 100644 --- a/tests/Laravel/LaravelServiceProviderTest.php +++ b/tests/Laravel/LaravelServiceProviderTest.php @@ -5,8 +5,12 @@ namespace WrkFlow\ApiSdkBuilderTests\Laravel; use Illuminate\Console\Scheduling\Schedule; +use WrkFlow\ApiSdkBuilder\Log\Actions\ClearFileLogsAction; use WrkFlow\ApiSdkBuilder\Log\Constants\LoggerConstants; +use WrkFlow\ApiSdkBuilder\Log\Entities\LoggerConfigEntity; +use WrkFlow\ApiSdkBuilder\Log\Entities\LoggersMapEntity; use WrkFlow\ApiSdkBuilder\Log\Interfaces\ApiLoggerInterface; +use WrkFlow\ApiSdkBuilder\Log\Loggers\FileLogger; class LaravelServiceProviderTest extends TestCase { @@ -45,4 +49,29 @@ public function testClearFilesLogsCommandRegistered(): void $this->assertTrue($found, sprintf('Command %s not found in schedule', $command)); } } + + public function testLoggerConfigEntity(): void + { + $logger = $this->make(LoggerConfigEntity::class); + + $this->assertEquals( + expected: new LoggerConfigEntity( + logger: '*:info_file', + loggersMap: new LoggersMapEntity(LoggerConstants::DefaultLoggersMap), + fileBaseDir: 'requests', + keepLogFilesForDays: 14, + ), + actual: $logger, + ); + } + + public function testFilesystemOperatorIsInjectable(): void + { + $this->assertInstanceOf(expected: FileLogger::class, actual: $this->app() ->make(FileLogger::class)); + $this->assertInstanceOf( + expected: ClearFileLogsAction::class, + actual: $this->app() + ->make(ClearFileLogsAction::class), + ); + } } diff --git a/tests/TestApi/Endpoints/AbstractTestEndpoint.php b/tests/TestApi/Endpoints/AbstractTestEndpoint.php index 7196b0c..01423f6 100644 --- a/tests/TestApi/Endpoints/AbstractTestEndpoint.php +++ b/tests/TestApi/Endpoints/AbstractTestEndpoint.php @@ -6,17 +6,19 @@ use Http\Mock\Client; use WrkFlow\ApiSdkBuilder\Endpoints\AbstractEndpoint; -use WrkFlow\ApiSdkBuilder\Interfaces\ApiInterface; +use WrkFlow\ApiSdkBuilder\Entities\EndpointDIEntity; abstract class AbstractTestEndpoint extends AbstractEndpoint { protected Client $client; - public function __construct(ApiInterface $api) + public function __construct(EndpointDIEntity $di) { - parent::__construct($api); + parent::__construct($di); - $client = $this->api->factory() + $client = $di + ->api() + ->factory() ->client(); assert($client instanceof Client); diff --git a/tests/TestApi/Endpoints/Json/JsonEndpoint.php b/tests/TestApi/Endpoints/Json/JsonEndpoint.php index 8692358..edcf96c 100644 --- a/tests/TestApi/Endpoints/Json/JsonEndpoint.php +++ b/tests/TestApi/Endpoints/Json/JsonEndpoint.php @@ -5,7 +5,9 @@ namespace WrkFlow\ApiSdkBuilderTests\TestApi\Endpoints\Json; use Exception; +use const true; use WrkFlow\ApiSdkBuilder\Headers\JsonHeaders; +use WrkFlow\ApiSdkBuilder\Interfaces\OptionsInterface; use WrkFlow\ApiSdkBuilder\Testing\Responses\JsonResponseMock; use WrkFlow\ApiSdkBuilderTests\TestApi\Endpoints\AbstractTestEndpoint; @@ -13,36 +15,61 @@ class JsonEndpoint extends AbstractTestEndpoint { public function success(): JsonResponse { - $this->client->addResponse(new JsonResponseMock(json: [ - JsonResponse::KeySuccess => true, - ],)); + $this->expectResponse(); - return $this->api->get(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); + return $this->sendGet(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); + } + + + public function store(OptionsInterface $body = null): JsonResponse + { + $this->expectResponse(); + + return $this->sendPost( + responseClass: JsonResponse::class, + uri: $this->uri(), + body: $body, + headers: $this->headers() + ); } public function failOnStatusCode(int $statusCode): JsonResponse { - $this->client->addResponse(new JsonResponseMock( - json: [ - JsonResponse::KeySuccess => false, - ], - statusCode: $statusCode, - )); + $this->expectResponse(status: false, statusCode: $statusCode); - return $this->api->get(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); + return $this->sendGet(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); } public function failOnServer(): JsonResponse { $this->client->addException(new Exception('Server failed')); - return $this->api->get(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); + return $this->sendGet(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); + } + + /** + * We are checking if our PHPStan rules are working correctly. The error is in phpstan-baseline.neon. + */ + public function phpStanShouldReportThis(): JsonResponse + { + return $this->sendGet(responseClass: JsonResponse::class, uri: $this->uri(), headers: $this->headers()); } + protected function basePath(): string { return 'json'; } + protected function expectResponse(bool $status = true, int $statusCode = 200): void + { + $this->client->addResponse(new JsonResponseMock( + json: [ + JsonResponse::KeySuccess => $status, + ], + statusCode: $statusCode, + )); + } + private function headers(): array { return [new JsonHeaders()]; diff --git a/tests/TestApi/Endpoints/Json/JsonOptions.php b/tests/TestApi/Endpoints/Json/JsonOptions.php new file mode 100644 index 0000000..f7cf30e --- /dev/null +++ b/tests/TestApi/Endpoints/Json/JsonOptions.php @@ -0,0 +1,28 @@ + $keys + */ + public function __construct( + public readonly string $input, + public readonly array $keys + ) { + } + + public function toArray(AbstractEnvironment $environment): array + { + return array_filter([ + 'input' => $this->input, + 'keys' => $this->keys, + ]); + } +} diff --git a/tests/TestApi/Environments/TestEnvironment.php b/tests/TestApi/Environments/TestEnvironment.php deleted file mode 100644 index 75d4382..0000000 --- a/tests/TestApi/Environments/TestEnvironment.php +++ /dev/null @@ -1,16 +0,0 @@ - ['application/json'], + 'Accept' => ['application/json'], + ]; + + public function testEndpointGet(): void + { + $this->assertEndpoint( + call: static fn (EndpointDIEntity $di) => (new JsonEndpoint($di))->success(), + expectedUri: '/json', + expectedHeaders: self::ExpectedHeaders, + ); + } + + public function testStoreWithoutOptions(): void + { + $this->assertEndpoint( + call: static fn (EndpointDIEntity $di) => (new JsonEndpoint($di))->store(), + expectedMethod: 'POST', + expectedUri: '/json', + expectedHeaders: self::ExpectedHeaders, + ); + } + + public function testStoreWithJsonOptions(): void + { + $data = new JsonOptions(input: 'input', keys: [1, 2, 3]); + $this->assertEndpoint( + call: static fn (EndpointDIEntity $di) => (new JsonEndpoint($di))->store($data), + expectedUri: '/json', + expectedMethod: 'POST', + expectedBody: $data, + expectedHeaders: self::ExpectedHeaders, + ); + } +} diff --git a/tests/Testing/Factories/TestSDKContainerFactoryTest.php b/tests/Testing/Factories/TestSDKContainerFactoryTest.php new file mode 100644 index 0000000..33a97d5 --- /dev/null +++ b/tests/Testing/Factories/TestSDKContainerFactoryTest.php @@ -0,0 +1,225 @@ + + */ + public function dataMake(): array + { + $data = [ + JsonResponse::KeySuccess => true, + ]; + return [ + 'makeEndpoint' => [ + static fn (self $self, TestSDKContainerFactory $container, EndpointDIEntity $di) => $self->assertInstanceOf( + expected: JsonEndpoint::class, + actual: $container->makeEndpoint($di->api(), JsonEndpoint::class), + ), + ], + 'make' => [ + static fn (self $self, TestSDKContainerFactory $container, EndpointDIEntity $di) => $self->assertInstanceOf( + expected: SendTestRequestActionAssert::class, + actual: $container->make(SendRequestActionContract::class), + ), + ], + 'makeResponse' => [ + static fn (self $self, TestSDKContainerFactory $container, EndpointDIEntity $di) => $self->assertTrue( + condition: $container + ->makeResponse( + class: JsonResponse::class, + response: new JsonResponseMock($data), + body: new GetValue(new ArrayData($data)), + ) + ->success, + ), + ], + ]; + } + + /** + * @param MakeClosure $assert + * + * @dataProvider dataMake + */ + public function testMakeClosureSyntax(Closure $assert): void + { + $assert($this, $this->closureSyntax(), EndpointDIEntityFactory::make()); + } + + /** + * @param MakeClosure $assert + * + * @dataProvider dataMake + */ + public function testMakeObjectSyntax(Closure $assert): void + { + $assert($this, $this->objectSyntax(), EndpointDIEntityFactory::make()); + } + + /** + * @return array + */ + public function dataHas(): array + { + return [ + 'SendRequestActionContract' => [ + static fn (self $self, TestSDKContainerFactory $container) => $self->assertTrue( + $container->has(SendRequestActionContract::class) + ), + ], + 'JsonEndpoint' => [ + static fn (self $self, TestSDKContainerFactory $container) => $self->assertTrue( + $container->has(JsonEndpoint::class) + ), + ], + 'JsonResponse' => [ + static fn (self $self, TestSDKContainerFactory $container) => $self->assertTrue( + $container->has(JsonResponse::class) + ), + ], + 'no found' => [ + static fn (self $self, TestSDKContainerFactory $container) => $self->assertFalse( + $container->has(AbstractResponse::class) + ), + ], + ]; + } + + /** + * @param ContainerClosure $assert + * + * @dataProvider dataHas + */ + public function testHasClosureSyntax(Closure $assert): void + { + $assert($this, $this->closureSyntax()); + } + + /** + * @param ContainerClosure $assert + * + * @dataProvider dataHas + */ + public function testHasObjectSyntax(Closure $assert): void + { + $assert($this, $this->objectSyntax()); + } + + /** + * @return array + */ + public function dataMakeFail(): array + { + return [ + 'makeEndpoint' => [ + static function (self $self, TestSDKContainerFactory $container, EndpointDIEntity $di): void { + $container->makeEndpoint(api: $di->api(), endpointClass: AbstractEndpoint::class); + }, + ], + 'make' => [ + static function (self $self, TestSDKContainerFactory $container, EndpointDIEntity $di): void { + $container->make(class: SendTestRequestActionAssert::class); + }, + ], + 'makeResponse' => [ + static function (self $self, TestSDKContainerFactory $container, EndpointDIEntity $di): void { + $container + ->makeResponse( + class: AbstractResponse::class, + response: new JsonResponseMock([]), + body: new GetValue(new ArrayData([])), + ); + }, + ], + ]; + } + + /** + * @param MakeClosure $assert + * + * @dataProvider dataMakeFail + */ + public function testMakeFailClosureSyntax(Closure $assert): void + { + $this->expectException(BindingResolutionException::class); + $assert($this, $this->closureSyntax(), EndpointDIEntityFactory::make()); + } + + /** + * @param MakeClosure $assert + * + * @dataProvider dataMakeFail + */ + public function testMakeFailObjectSyntax(Closure $assert): void + { + $this->expectException(BindingResolutionException::class); + $assert($this, $this->objectSyntax(), EndpointDIEntityFactory::make()); + } + + protected function closureSyntax(): TestSDKContainerFactory + { + return new TestSDKContainerFactory( + makeBindings: [ + SendRequestActionContract::class => static fn () => new SendTestRequestActionAssert(), + ], + makeEndpointBindings: [ + JsonEndpoint::class => static fn (EndpointDIEntity $di) => new JsonEndpoint($di), + ], + makeResponseBindings: [ + JsonResponse::class => static fn (ResponseInterface $response, $body) => new JsonResponse( + response: $response, + body: $body, + ), + ] + ); + } + + protected function objectSyntax(): TestSDKContainerFactory + { + return new TestSDKContainerFactory( + // Only makeBindings supports object syntax + makeBindings: [ + SendRequestActionContract::class => new SendTestRequestActionAssert(), + ], + makeEndpointBindings: [ + JsonEndpoint::class => static fn (EndpointDIEntity $di) => new JsonEndpoint($di), + ], + makeResponseBindings: [ + JsonResponse::class => static fn (ResponseInterface $response, $body) => new JsonResponse( + response: $response, + body: $body, + ), + ] + ); + } +} diff --git a/tests/Testing/Options/OptionsTestCaseTest.php b/tests/Testing/Options/OptionsTestCaseTest.php new file mode 100644 index 0000000..fa5e33a --- /dev/null +++ b/tests/Testing/Options/OptionsTestCaseTest.php @@ -0,0 +1,58 @@ + + */ + public function data(): array + { + return [ + 'all values' => [ + static fn (self $self) => $self->assertOptions( + input: new JsonOptions(input: 'test', keys: [1, 2, 3]), + expected: [ + self::KeyInput => 'test', + self::KeyKeys => [1, 2, 3], + ] + ), + ], + 'empty keys' => [ + static fn (self $self) => $self->assertOptions( + input: new JsonOptions(input: 'test 22', keys: []), + expected: [ + self::KeyInput => 'test 22', + ] + ), + ], + 'empty values' => [ + static fn (self $self) => $self->assertOptions( + input: new JsonOptions(input: '', keys: []), + expected: [] + ), + ], + ]; + } + + + /** + * @param Closure(static):void $assert + * + * @dataProvider data + */ + public function test(Closure $assert): void + { + $assert($this); + } +}