diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml deleted file mode 100644 index a8b756a..0000000 --- a/.github/workflows/cs.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: CS Fixer - -on: - push: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install CS fixer - run: composer global require friendsofphp/php-cs-fixer - - - name: Run CS fixer - run: ~/.composer/vendor/bin/php-cs-fixer fix - - - name: Commit changes - uses: EndBug/add-and-commit@v7 - with: - author_name: Github Actions - author_email: hello@ricorocks.agency - message: 'Automated CS fixes' - add: '.' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98acc8a..66c0d4f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,9 +11,19 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.4', '8.0'] - - name: PHP ${{ matrix.php }} + php: [ '8.1', '8.2', '8.3' ] + laravel: [ '10.*', '11.*' ] + stability: [ prefer-stable ] + include: + - laravel: '11.*' + testbench: '9.*' + - laravel: '10.*' + testbench: '8.*' + exclude: + - laravel: 11.* + php: 8.1 + + name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }} steps: - name: Checkout @@ -30,7 +40,15 @@ jobs: run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-progress --no-suggest + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Check Code Style + run: composer test:lint + + - name: Run Static Analysis + run: composer test:types - - name: Run Pest - run: php vendor/bin/pest + - name: Run Tests + run: composer test:parallel diff --git a/.gitignore b/.gitignore index c3e33b7..5ece615 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ vendor/ composer.lock .php-cs-fixer.cache -.phpunit.result.cache +.phpunit.result.cache \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php deleted file mode 100644 index bd494dc..0000000 --- a/.php-cs-fixer.dist.php +++ /dev/null @@ -1,23 +0,0 @@ -in(__DIR__ . DIRECTORY_SEPARATOR . 'tests') - ->in(__DIR__ . DIRECTORY_SEPARATOR . 'src') - ->append(['.php-cs-fixer.dist.php']); - -$rules = [ - '@Symfony' => true, - 'phpdoc_no_empty_return' => false, - 'array_syntax' => ['syntax' => 'short'], - 'yoda_style' => false, - 'concat_space' => ['spacing' => 'one'], - 'not_operator_with_space' => false, - 'php_unit_method_casing' => ['case' => 'snake_case'], -]; - -$rules['increment_style'] = ['style' => 'post']; - -return (new PhpCsFixer\Config()) - ->setUsingCache(true) - ->setRules($rules) - ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5230aaa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [Unreleased] + +### Added +- Added Laravel 11 support [#70](https://github.com/Ricorocks-Digital-Agency/Soap/pull/70) +- Added PHP 8.3 support [#70](https://github.com/Ricorocks-Digital-Agency/Soap/pull/70) +- Added Laravel Pint [#70](https://github.com/Ricorocks-Digital-Agency/Soap/pull/70) + +### Changed +- Updated to Pest v2 [#70](https://github.com/Ricorocks-Digital-Agency/Soap/pull/70) + +### Removed +- Removed support for PHP 7.4 and 8.0 [#70](https://github.com/Ricorocks-Digital-Agency/Soap/pull/70) +- Removed support for Laravel 8 and 9 [#70](https://github.com/Ricorocks-Digital-Agency/Soap/pull/70) + +## [1.7.0] - 2023-03-03 +### Added +- Added Laravel 10 support [#60](https://github.com/Ricorocks-Digital-Agency/Soap/pull/60) +- Added PHP 8.2 support [#60](https://github.com/Ricorocks-Digital-Agency/Soap/pull/60) + +### Fixed +- Fixed aliases attribute in composer.json [#49](https://github.com/Ricorocks-Digital-Agency/Soap/pull/49) + +## [1.6.0] - 2022-02-12 +### Added +- Added Laravel 9 support [#48](https://github.com/Ricorocks-Digital-Agency/Soap/pull/48) +- Added PHP 8.1 support [#48](https://github.com/Ricorocks-Digital-Agency/Soap/pull/48) + +## [1.5.0] - 2021-08-03 +### Added +- Added Pest [#32](https://github.com/Ricorocks-Digital-Agency/Soap/pull/32) + +## [1.4.0] - 2021-06-24 +### Added +- Added CS Fixer [#31](https://github.com/Ricorocks-Digital-Agency/Soap/pull/31) + +## [1.3.1] - 2021-06-19 +### Fixed +- Fixed an issue with SOAP headers with no actor [#28](https://github.com/Ricorocks-Digital-Agency/Soap/pull/28) + +## [1.3.0] - 2021-06-03 +### Added +- Added support for specifying SOAP headers [#23](https://github.com/Ricorocks-Digital-Agency/Soap/pull/23) + +## [1.2.0] - 2021-05-03 +### Added +- Added a new Soapable interface [#18](https://github.com/Ricorocks-Digital-Agency/Soap/pull/18) + +## [1.1.0] - 2021-03-23 +### Added +- Added Auth and Options helper methods [#15](https://github.com/Ricorocks-Digital-Agency/Soap/pull/15) + +## [1.0.0] - 2021-02-26 +v1 of SOAP diff --git a/README.md b/README.md index 41f539a..306235d 100644 --- a/README.md +++ b/README.md @@ -6,25 +6,42 @@ A Laravel SOAP client that provides a clean interface for handling requests and ## Docs -- [Installation](#installation) -- [Using Soap](#using-soap) -- [Features/API](#features/api) - * [Headers](#headers) - * [Global Headers](#global-headers) - * [To](#to) - * [Functions](#functions) - * [Call](#call) - * [Parameters](#parameters) - * [Nodes](#nodes) - * [Options](#options) - * [Tracing](#tracing) - * [Authentication](#authentication) - * [Global Options](#global-options) -- [Hooks](#hooks) -- [Faking](#faking) -- [Configuration](#configuration) - * [Include](#include) -- [Ray Support](#ray-support) +- [Soap](#soap) + - [Docs](#docs) + - [Requirements](#requirements) + - [Installation](#installation) + - [Using Soap](#using-soap) + - [Features/API](#featuresapi) + - [Headers](#headers) + - [Global Headers](#global-headers) + - [To](#to) + - [Functions](#functions) + - [Call](#call) + - [Parameters](#parameters) + - [Nodes](#nodes) + - [Options](#options) + - [Tracing](#tracing) + - [Authentication](#authentication) + - [Global Options](#global-options) + - [Hooks](#hooks) + - [Local](#local) + - [Global](#global) + - [Faking](#faking) + - [Inspecting requests](#inspecting-requests) + - [`Soap::assertSentCount($count)`](#soapassertsentcountcount) + - [`Soap::assertSent(callable $callback)`](#soapassertsentcallable-callback) + - [`Soap::assertNotSent(callable $callback)`](#soapassertnotsentcallable-callback) + - [`Soap::assertNothingSent()`](#soapassertnothingsent) + - [Configuration](#configuration) + - [Include](#include) + - [Ray Support](#ray-support) + - [`ray()->showSoapRequests()`](#ray-showsoaprequests) + - [`ray()->stopShowingSoapRequests()`](#ray-stopshowingsoaprequests) + - [Changelog](#changelog) + - [Contributing](#contributing) + - [Security](#security) + - [Credits](#credits) + - [License](#license) ## Requirements @@ -329,7 +346,7 @@ specific, so you can pass the `fake` method an array of endpoints as keys and response objects as values: ```php -Soap::fake(['http://endpoint.com' => Response(['foo' => 'bar'])]); +Soap::fake(['http://endpoint.com' => Soap::response(['foo' => 'bar'])]); ``` In the above example, any SOAP request made to `http://endpoint.com` will @@ -339,13 +356,13 @@ be returned instead. What if you want to specify the SOAP action too? Easy! Just add `:{ActionName}` after your endpoint, like so: ```php -Soap::fake(['http://endpoint.com:Details' => Response(['foo' => 'bar'])]); +Soap::fake(['http://endpoint.com:Details' => Soap::response(['foo' => 'bar'])]); ``` Now, only SOAP requests to the `Details` actions will be mocked. You can also specify multiple actions with the `|` operator: ```php -Soap::fake(['http://endpoint.com:Details|Information|Overview' => Response(['foo' => 'bar'])]); +Soap::fake(['http://endpoint.com:Details|Information|Overview' => Soap::response(['foo' => 'bar'])]); ``` Now, only SOAP requests to the `Details`, `Information` and `Overview` actions will be mocked. diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..b0ede46 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,11 @@ +# Upgrade Guide + +All major releases will document the needed steps to upgrade here. Upgrades should be carried out sequentially. + +## [Unreleased] + +### Laravel +SOAP now supports Laravel ^10. Support for Laravel 8 and 9 has been removed. + +### PHP +SOAP now supports PHP ^8.2. Support for PHP 7.4 and 8.0 has been removed. diff --git a/composer.json b/composer.json index 80dab88..5f66c74 100644 --- a/composer.json +++ b/composer.json @@ -14,15 +14,33 @@ } }, "require": { - "php": "^7.4|^8.0", - "illuminate/support": "^8.16" + "php": "^8.1|^8.2|^8.3", + "illuminate/support": "^10.0|^11.0" }, "require-dev": { - "phpunit/phpunit": "^9.4", - "orchestra/testbench": "^6.5", - "spatie/ray": "^1.17", "ext-soap": "*", - "pestphp/pest": "^1.0" + "mockery/mockery": "^1.4", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.0", + "phpoption/phpoption": "^1.8.1", + "spatie/ray": "^1.17", + "laravel/pint": "^1.14", + "larastan/larastan": "^2.0" + }, + "scripts": { + "lint": "./vendor/bin/pint", + "test:lint": "./vendor/bin/pint --test", + "test:types": "./vendor/bin/phpstan analyse", + "test:parallel": "./vendor/bin/pest --parallel", + "test": [ + "@lint", + "\n\n", + "@test:lint", + "\n\n", + "@test:types", + "\n\n", + "@test:parallel" + ] }, "prefer-stable": true, "authors": [ @@ -38,11 +56,17 @@ "extra": { "laravel": { "providers": [ - "RicorocksDigitalAgency\\Soap\\Providers\\SoapServiceProvider" + "RicorocksDigitalAgency\\Soap\\Providers\\SoapServiceProvider", + "RicorocksDigitalAgency\\Soap\\Providers\\SoapRayServiceProvider" ], - "aliases": [ - "RicorocksDigitalAgency\\Soap\\Facades\\Soap" - ] + "aliases": { + "Soap": "RicorocksDigitalAgency\\Soap\\Facades\\Soap" + } + } + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true } } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..8355ee5 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + paths: + - src + + level: max diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..7d3c82f --- /dev/null +++ b/pint.json @@ -0,0 +1,42 @@ +{ + "preset": "symfony", + "rules": { + "phpdoc_no_alias_tag": false, + "nullable_type_declaration_for_default_null_value": true, + "no_null_property_initialization": false, + "yoda_style": { + "equal": false, + "identical": false, + "less_and_greater": false + }, + "concat_space": { + "spacing": "one" + }, + "not_operator_with_space": false, + "increment_style": { + "style": "post" + }, + "php_unit_method_casing": { + "case": "snake_case" + }, + "global_namespace_import": { + "import_constants": true, + "import_functions": true + }, + "method_chaining_indentation": true, + "phpdoc_align": { + "align": "vertical", + "tags": [ + "param", + "property", + "property-read", + "property-write", + "return", + "throws", + "type", + "var", + "method" + ] + } + } +} diff --git a/src/Contracts/Builder.php b/src/Contracts/Builder.php new file mode 100644 index 0000000..d9bf86e --- /dev/null +++ b/src/Contracts/Builder.php @@ -0,0 +1,15 @@ +|Soapable>|Soapable $parameters + * + * @return array + */ + public function handle(array|Soapable $parameters): array; +} diff --git a/src/Contracts/PhpSoap/Client.php b/src/Contracts/PhpSoap/Client.php new file mode 100644 index 0000000..c96ed4c --- /dev/null +++ b/src/Contracts/PhpSoap/Client.php @@ -0,0 +1,44 @@ + $options + */ + public function setOptions(array $options): static; + + /** + * Set the given headers on the request. + * + * @param array $headers + */ + public function setHeaders(array $headers): static; + + /** + * Make a request and return its response. + */ + public function call(string $method, mixed $body): mixed; + + /** + * Get an array of supported SOAP functions. + * + * @return array + */ + public function getFunctions(): array; + + public function __getLastRequest(): ?string; + + public function __getLastResponse(): ?string; + + public function __getLastRequestHeaders(): ?string; + + public function __getLastResponseHeaders(): ?string; +} diff --git a/src/Contracts/Request.php b/src/Contracts/Request.php new file mode 100644 index 0000000..776d002 --- /dev/null +++ b/src/Contracts/Request.php @@ -0,0 +1,77 @@ + $parameters + */ + public function __call(string $name, array $parameters); + + /** + * @return array + */ + public function functions(): array; + + /** + * @param array|Soapable $body + */ + public function call(string $method, array|Soapable $body = []): Response; + + /** + * @param callable(Request): mixed ...$closures + */ + public function beforeRequesting(callable ...$closures): self; + + /** + * @param callable(Request, Response): mixed ...$closures + */ + public function afterRequesting(callable ...$closures): self; + + /** + * @param callable(Request): Response|Response|null $response + */ + public function fakeUsing(callable|Response|null $response): self; + + public function getEndpoint(): string; + + public function getMethod(): string; + + /** + * @return array|Soapable + */ + public function getBody(): array|Soapable; + + /** + * @return array + */ + public function getOptions(): array; + + public function set(string $key, mixed $value): self; + + public function trace(bool $shouldTrace = true): self; + + /** + * @param array $options + */ + public function withOptions(array $options): self; + + public function withBasicAuth(string $login, string $password): self; + + public function withDigestAuth(string $login, string $password): self; + + public function withHeaders(Header ...$headers): self; + + /** + * @return array + */ + public function getHeaders(): array; +} diff --git a/src/Contracts/Soapable.php b/src/Contracts/Soapable.php index 6fbfba0..33f2f60 100644 --- a/src/Contracts/Soapable.php +++ b/src/Contracts/Soapable.php @@ -1,8 +1,10 @@ headers = $headers; - } - - public function getHeaders() - { - return $this->headers; - } -} diff --git a/src/Inclusion.php b/src/Inclusion.php deleted file mode 100644 index 9dc2d73..0000000 --- a/src/Inclusion.php +++ /dev/null @@ -1,20 +0,0 @@ -parameters = $parameters; - } - - public function getParameters() - { - return $this->parameters; - } -} diff --git a/src/OptionSet.php b/src/OptionSet.php deleted file mode 100644 index a0ab75c..0000000 --- a/src/OptionSet.php +++ /dev/null @@ -1,20 +0,0 @@ -options = $options; - } - - public function getOptions() - { - return $this->options; - } -} diff --git a/src/Parameters/Builder.php b/src/Parameters/Builder.php deleted file mode 100644 index b9b8eaa..0000000 --- a/src/Parameters/Builder.php +++ /dev/null @@ -1,8 +0,0 @@ -|Soapable>|Soapable $parameters + * + * @return array + */ + public function handle(array|Soapable $parameters): array { return $this->handleParameter($parameters); } - protected function handleParameter($parameter) + private function handleParameter(mixed $parameter): mixed { if ($parameter instanceof Soapable) { $parameter = $parameter->toSoap(); @@ -24,7 +35,12 @@ protected function handleParameter($parameter) return $parameter; } - protected function walk($parameters) + /** + * @param array $parameters + * + * @return array + */ + private function walk(array $parameters): array { return collect($parameters) ->map(fn ($parameter) => $this->handleParameter($parameter)) diff --git a/src/Parameters/Node.php b/src/Parameters/Node.php index 4f566ab..6248d46 100644 --- a/src/Parameters/Node.php +++ b/src/Parameters/Node.php @@ -1,36 +1,59 @@ + */ + private array $attributes = []; + + /** + * @var array + */ + private array $body = []; + + /** + * @param array $attributes + */ + public function __construct(array $attributes) { $this->attributes = $attributes; } - public function body($content) + /** + * @param array $content + */ + public function body(array $content): self { $this->body = $content; return $this; } - public function toArray() + /** + * @return array + */ + public function toArray(): array { return empty($this->body) ? array_merge(['_' => ''], $this->attributes) : array_merge($this->body, $this->attributes); } - public function toSoap() + /** + * @return array + */ + public function toSoap(): array { return $this->toArray(); } diff --git a/src/Providers/SoapRayServiceProvider.php b/src/Providers/SoapRayServiceProvider.php new file mode 100644 index 0000000..8988750 --- /dev/null +++ b/src/Providers/SoapRayServiceProvider.php @@ -0,0 +1,25 @@ +app->singleton(SoapWatcher::class); + + resolve(SoapWatcher::class)->register(); + } +} diff --git a/src/Providers/SoapServiceProvider.php b/src/Providers/SoapServiceProvider.php index f325712..59f9ba8 100644 --- a/src/Providers/SoapServiceProvider.php +++ b/src/Providers/SoapServiceProvider.php @@ -1,37 +1,35 @@ app->singleton('soap', fn () => app(Soap::class)); - $this->app->bind(Request::class, SoapClientRequest::class); + $this->app->singleton('soap', fn (Application $app) => $app->make(Soap::class)); $this->app->bind(Builder::class, IntelligentBuilder::class); - - $this->registerRay(); + $this->app->bind(Request::class, fn (Application $app) => new SoapPhpRequest( + $app->make(Builder::class), + new DecoratedClient() + )); } - public function boot() + public function boot(): void { require_once __DIR__ . '/../helpers.php'; } - - protected function registerRay() - { - if (!class_exists('Spatie\\LaravelRay\\Ray')) { - return; - } - $this->app->singleton(SoapWatcher::class); - app(SoapWatcher::class)->register(); - } } diff --git a/src/Ray/SoapWatcher.php b/src/Ray/SoapWatcher.php index daa8921..caa5b9a 100644 --- a/src/Ray/SoapWatcher.php +++ b/src/Ray/SoapWatcher.php @@ -1,15 +1,21 @@ table( [ @@ -41,10 +47,11 @@ protected function handleRequest(Request $request, Response $response) ); } - protected function headers(Request $request) + /** + * @return array> + */ + protected function headers(Request $request): array { - return $request->getHeaders() - ? collect($request->getHeaders())->map->toArray()->toArray() - : []; + return collect($request->getHeaders())->map(fn (Header $header) => $header->toArray())->toArray(); } } diff --git a/src/Request/Request.php b/src/Request/Request.php deleted file mode 100644 index 06d88f2..0000000 --- a/src/Request/Request.php +++ /dev/null @@ -1,51 +0,0 @@ -builder = $builder; - $this->client = $client; - } - - public function to(string $endpoint): Request - { - $this->endpoint = $endpoint; - - return $this; - } - - public function __call($name, $parameters) - { - return $this->call($name, $parameters[0] ?? []); - } - - public function call($method, $parameters = []) - { - $this->method = $method; - $this->body = $parameters; - - $this->hooks['beforeRequesting']->each(fn ($callback) => $callback($this)); - $this->body = $this->builder->handle($this->body); - - $response = $this->getResponse(); - $this->hooks['afterRequesting']->each(fn ($callback) => $callback($this, $response)); - - return $response; - } - - protected function getResponse() - { - return $this->response ??= $this->getRealResponse(); - } - - protected function getRealResponse() - { - return tap( - Response::new($this->makeRequest()), - fn ($response) => data_get($this->options, 'trace') - ? $response->setTrace(Trace::client($this->client())) - : $response - ); - } - - protected function makeRequest() - { - return $this->client()->{$this->getMethod()}($this->getBody()); - } - - protected function client() - { - return $this->client ??= $this->constructClient(); - } - - protected function constructClient() - { - $this->client ??= resolve(SoapClient::class, [ - 'wsdl' => $this->endpoint, - 'options' => $this->options, - ]); - - return tap($this->client, fn ($client) => $client->__setSoapHeaders($this->constructHeaders())); - } - - protected function constructHeaders() - { - if (empty($this->headers)) { - return; - } - - return array_map( - fn ($header) => resolve(SoapHeader::class, [ - 'namespace' => $header->namespace, - 'name' => $header->name, - 'data' => $header->data, - 'mustunderstand' => $header->mustUnderstand, - 'actor' => $header->actor ?? SOAP_ACTOR_NONE, - ]), - $this->headers - ); - } - - public function getMethod() - { - return $this->method; - } - - public function getBody() - { - return $this->body; - } - - public function functions(): array - { - return $this->client()->__getFunctions(); - } - - public function beforeRequesting(...$closures): Request - { - ($this->hooks['beforeRequesting'] ??= collect())->push(...$closures); - - return $this; - } - - public function afterRequesting(...$closures): Request - { - ($this->hooks['afterRequesting'] ??= collect())->push(...$closures); - - return $this; - } - - public function getEndpoint() - { - return $this->endpoint; - } - - public function fakeUsing($response): Request - { - if (empty($response)) { - return $this; - } - - $this->response = $response instanceof Response ? $response : $response($this); - - return $this; - } - - public function set($key, $value): Request - { - data_set($this->body, $key, $value); - - return $this; - } - - public function trace($shouldTrace = true): Request - { - $this->options['trace'] = $shouldTrace; - - return $this; - } - - public function withBasicAuth($login, $password): Request - { - $this->options['authentication'] = SOAP_AUTHENTICATION_BASIC; - $this->options['login'] = $login; - $this->options['password'] = $password; - - return $this; - } - - public function withDigestAuth($login, $password): Request - { - $this->options['authentication'] = SOAP_AUTHENTICATION_DIGEST; - $this->options['login'] = $login; - $this->options['password'] = $password; - - return $this; - } - - public function getOptions(): array - { - return $this->options; - } - - public function withOptions(array $options): Request - { - $this->options = array_merge($this->getOptions(), $options); - - return $this; - } - - public function withHeaders(Header ...$headers): Request - { - $this->headers = array_merge($this->getHeaders(), $headers); - - return $this; - } - - public function getHeaders(): array - { - return $this->headers; - } -} diff --git a/src/Request/SoapPhpRequest.php b/src/Request/SoapPhpRequest.php new file mode 100644 index 0000000..f6e37df --- /dev/null +++ b/src/Request/SoapPhpRequest.php @@ -0,0 +1,262 @@ +|Soapable + */ + private $body = []; + + /** + * @var array{beforeRequesting: array, afterRequesting: array} + */ + private array $hooks = [ + 'beforeRequesting' => [], + 'afterRequesting' => [], + ]; + + /** + * @var array + */ + private array $options = []; + + /** + * @var array + */ + private array $headers = []; + + public function __construct(Builder $builder, Client $client) + { + $this->builder = $builder; + $this->client = $client; + } + + public function to(string $endpoint): self + { + $this->endpoint = $endpoint; + + return $this; + } + + /** + * @param array $parameters + */ + public function __call(string $name, array $parameters): mixed + { + return $this->call($name, $parameters[0] ?? []); + } + + /** + * @param array|Soapable $body + */ + public function call(string $method, array|Soapable $body = []): Response + { + $this->method = $method; + $this->body = $body; + + collect($this->hooks['beforeRequesting'])->each(fn ($callback) => $callback($this)); + $this->body = $this->builder->handle($this->body); + + $response = $this->getResponse(); + collect($this->hooks['afterRequesting'])->each(fn ($callback) => $callback($this, $response)); + + return $response; + } + + private function getResponse(): Response + { + return $this->response ??= $this->getRealResponse(); + } + + private function getRealResponse(): Response + { + return tap( + Response::new($this->makeRequest()), + fn ($response) => data_get($this->options, 'trace') + ? $response->setTrace(Trace::client($this->client())) + : $response + ); + } + + private function makeRequest(): mixed + { + return $this->client()->call($this->getMethod(), $this->getBody()); + } + + private function client(): Client + { + return $this->client + ->setEndpoint($this->endpoint) + ->setOptions($this->options) + ->setHeaders($this->constructHeaders()); + } + + /** + * @return array + */ + private function constructHeaders(): array + { + if (empty($this->headers)) { + return []; + } + + return array_map(fn ($header) => new SoapHeader( + $header->namespace, + $header->name, + $header->data, + $header->mustUnderstand, + $header->actor ?? SOAP_ACTOR_NONE + ), $this->headers); + } + + public function getMethod(): string + { + return $this->method; + } + + /** + * @return array|Soapable + */ + public function getBody(): array|Soapable + { + return $this->body; + } + + /** + * @return array + */ + public function functions(): array + { + return $this->client()->getFunctions(); + } + + /** + * @param callable(Request): mixed ...$closures + */ + public function beforeRequesting(callable ...$closures): self + { + $this->hooks['beforeRequesting'] = array_merge($this->hooks['beforeRequesting'], $closures); + + return $this; + } + + /** + * @param callable(Request, Response): mixed ...$closures + */ + public function afterRequesting(callable ...$closures): self + { + $this->hooks['afterRequesting'] = array_merge($this->hooks['afterRequesting'], $closures); + + return $this; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + /** + * @param callable(Request): Response|Response|null $response + */ + public function fakeUsing(Response|callable|null $response): self + { + if (is_null($response)) { + return $this; + } + + $this->response = $response instanceof Response ? $response : $response($this); + + return $this; + } + + public function set(string $key, mixed $value): self + { + data_set($this->body, $key, $value); + + return $this; + } + + public function trace(bool $shouldTrace = true): self + { + $this->options['trace'] = $shouldTrace; + + return $this; + } + + public function withBasicAuth(string $login, string $password): self + { + $this->options['authentication'] = SOAP_AUTHENTICATION_BASIC; + $this->options['login'] = $login; + $this->options['password'] = $password; + + return $this; + } + + public function withDigestAuth(string $login, string $password): Request + { + $this->options['authentication'] = SOAP_AUTHENTICATION_DIGEST; + $this->options['login'] = $login; + $this->options['password'] = $password; + + return $this; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * @param array $options + */ + public function withOptions(array $options): Request + { + $this->options = array_merge($this->getOptions(), $options); + + return $this; + } + + public function withHeaders(Header ...$headers): Request + { + $this->headers = array_merge($this->getHeaders(), $headers); + + return $this; + } + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } +} diff --git a/src/Response/Response.php b/src/Response/Response.php index e2524b0..7660816 100644 --- a/src/Response/Response.php +++ b/src/Response/Response.php @@ -1,37 +1,47 @@ |stdClass + */ + public array|stdClass $response; + + private Trace $trace; - public static function new($response = []): self + /** + * @param array|stdClass $response + */ + public static function new(array|stdClass $response = []): self { - return tap(new static(), fn ($instance) => $instance->response = $response); + return tap(new self(), fn (Response $instance) => $instance->response = $response); } - public function __get($name) + public function __get(string $name): mixed { return data_get($this->response, $name); } - public function setTrace(Trace $trace) + public function setTrace(Trace $trace): self { $this->trace = $trace; return $this; } - public function trace() + public function trace(): Trace { return $this->trace ??= app(Trace::class); } - public function set($key, $value): self + public function set(string $key, mixed $value): self { data_set($this->response, $key, $value); diff --git a/src/Soap.php b/src/Soap.php index cb48546..e078091 100644 --- a/src/Soap.php +++ b/src/Soap.php @@ -1,17 +1,28 @@ + */ + protected array $headerSets = []; + + /** + * @var array + */ + protected array $inclusions = []; + + /** + * @var array + */ + protected array $optionsSets = []; + + /** + * @var array{beforeRequesting: array, afterRequesting: array} + */ + protected array $globalHooks = [ + 'beforeRequesting' => [], + 'afterRequesting' => [], + ]; public function __construct(Fakery $fakery, Request $request) { $this->fakery = $fakery; $this->request = $request; - $this->beforeRequesting(fn ($requestInstance) => $requestInstance->fakeUsing($this->fakery->mockResponseIfAvailable($requestInstance))) - ->beforeRequesting(fn ($requestInstance) => $this->mergeHeadersFor($requestInstance)) - ->beforeRequesting(fn ($requestInstance) => $this->mergeInclusionsFor($requestInstance)) - ->beforeRequesting(fn ($requestInstance) => $this->mergeOptionsFor($requestInstance)) - ->afterRequesting(fn ($requestInstance, $response) => $this->record($requestInstance, $response)); + $this->beforeRequesting(fn (Request $requestInstance) => $requestInstance->fakeUsing($this->fakery->mockResponseIfAvailable($requestInstance))) + ->beforeRequesting(fn (Request $requestInstance) => $this->mergeHeadersFor($requestInstance)) + ->beforeRequesting(fn (Request $requestInstance) => $this->mergeInclusionsFor($requestInstance)) + ->beforeRequesting(fn (Request $requestInstance) => $this->mergeOptionsFor($requestInstance)) + ->afterRequesting(fn (Request $requestInstance, Response $response) => $this->record($requestInstance, $response)); } - public function to(string $endpoint) + public function to(string $endpoint): Request { return (clone $this->request) ->beforeRequesting(...$this->globalHooks['beforeRequesting']) @@ -46,17 +76,26 @@ public function to(string $endpoint) ->to($endpoint); } - public function node($attributes = []): Node + /** + * @param array $attributes + */ + public function node(array $attributes = []): Node { return new Node($attributes); } - public function header(?string $name = null, ?string $namespace = null, $data = null, bool $mustUnderstand = false, $actor = null): Header + /** + * @param array|SoapVar|null $data + */ + public function header(string $name = '', string $namespace = '', array|SoapVar|null $data = null, bool $mustUnderstand = false, string|int|null $actor = null): Header { return new Header($name, $namespace, $data, $mustUnderstand, $actor); } - public function include($parameters) + /** + * @param non-empty-array $parameters + */ + public function include(array $parameters): Inclusion { $inclusion = new Inclusion($parameters); $this->inclusions[] = $inclusion; @@ -64,7 +103,10 @@ public function include($parameters) return $inclusion; } - public function options(array $options) + /** + * @param array $options + */ + public function options(array $options): OptionSet { $options = new OptionSet($options); $this->optionsSets[] = $options; @@ -72,66 +114,78 @@ public function options(array $options) return $options; } - public function headers(Header ...$headers) + /** + * @throws InvalidArgumentException when you have not provided any headers + */ + public function headers(Header ...$headers): HeaderSet { - $headers = new HeaderSet(...$headers); + if (empty($headers)) { + throw new InvalidArgumentException('You must pass at least one header to the headers method.'); + } + + $headers = new HeaderSet($headers); $this->headerSets[] = $headers; return $headers; } - protected function mergeHeadersFor(Request $request) + protected function mergeHeadersFor(Request $request): void { collect($this->headerSets) - ->filter - ->matches($request->getEndpoint(), $request->getMethod()) - ->flatMap - ->getHeaders() - ->pipe(fn ($headers) => $request->withHeaders(...$headers)); + ->filter(fn (HeaderSet $set) => $set->matches($request->getEndpoint(), $request->getMethod())) + ->flatMap(fn (HeaderSet $set) => $set->getHeaders()) + ->pipe(fn (Collection $headers) => $request->withHeaders(...$headers)); } - protected function mergeInclusionsFor(Request $request) + protected function mergeInclusionsFor(Request $request): void { collect($this->inclusions) - ->filter - ->matches($request->getEndpoint(), $request->getMethod()) - ->flatMap - ->getParameters() - ->each(fn ($value, $key) => $request->set($key, $value)); + ->filter(fn (Inclusion $inclusion) => $inclusion->matches($request->getEndpoint(), $request->getMethod())) + ->flatMap(fn (Inclusion $inclusion) => $inclusion->getParameters()) + ->each(fn (mixed $value, string $key) => $request->set($key, $value)); } - protected function mergeOptionsFor(Request $request) + protected function mergeOptionsFor(Request $request): void { collect($this->optionsSets) - ->filter - ->matches($request->getEndpoint(), $request->getMethod()) - ->map - ->getOptions() - ->each(fn ($options) => $request->withOptions($options)); + ->filter(fn (OptionSet $set) => $set->matches($request->getEndpoint(), $request->getMethod())) + ->map(fn (OptionSet $set) => $set->getOptions()) + ->each(fn (array $options) => $request->withOptions($options)); } - public function beforeRequesting(callable $hook) + public function beforeRequesting(callable $hook): self { - ($this->globalHooks['beforeRequesting'] ??= collect())->push($hook); + $this->globalHooks['beforeRequesting'][] = $hook; return $this; } - public function afterRequesting(callable $hook) + public function afterRequesting(callable $hook): self { - ($this->globalHooks['afterRequesting'] ??= collect())->push($hook); + $this->globalHooks['afterRequesting'][] = $hook; return $this; } - public function trace($shouldTrace = true) + public function trace(bool $shouldTrace = true): self { $this->beforeRequesting(fn ($request) => $request->trace($shouldTrace)); return $this; } - public function __call($method, $parameters) + /** + * @param array|stdClass $response + */ + public function response(array|stdClass $response): Response + { + return Response::new($response); + } + + /** + * @param array $parameters + */ + public function __call(string $method, array $parameters): mixed { if (static::hasMacro($method)) { return $this->__macroableCall($method, $parameters); diff --git a/src/Support/Fakery/Fakery.php b/src/Support/Fakery/Fakery.php index 3fa9875..d8e69de 100644 --- a/src/Support/Fakery/Fakery.php +++ b/src/Support/Fakery/Fakery.php @@ -1,83 +1,99 @@ + */ + private Collection $recordedRequests; + + private Stubs $stubs; public function __construct(Stubs $stubs) { $this->stubs = $stubs; + $this->recordedRequests = collect(); } - public function fake($callback = null) + /** + * @param array|null $callback + */ + public function fake(?array $callback = null): void { $this->shouldRecord = true; - if (is_null($callback)) { - $this->stubs->new('*', fn () => Response::new()); - - return; - } - - if (is_array($callback)) { - collect($callback)->each(fn ($callable, $url) => $this->stubs->new($url, $callable)); - - return; - } + match (true) { + is_null($callback) => $this->stubs->new('*', fn () => Response::new()), + is_array($callback) => collect($callback)->each(fn ($callable, $url) => $this->stubs->new($url, $callable)), + }; } - public function mockResponseIfAvailable(Request $request) + public function mockResponseIfAvailable(Request $request): ?Response { return $this->stubs->getForRequest($request); } - public function record(Request $request, Response $response) + public function record(Request $request, Response $response): void { if (!$this->shouldRecord) { return; } - ($this->recordedRequests ??= collect())->push([$request, $response]); + $this->recordedRequests->push([$request, $response]); } - public function assertSentCount($count) + public function assertSentCount(int $count): void { PHPUnit::assertCount($count, $this->recordedRequests); } - public function assertNothingSent() + public function assertNothingSent(): void { PHPUnit::assertEmpty($this->recordedRequests, 'Requests were recorded'); } - public function assertSent(callable $callback) + /** + * @param Closure(Request, Response): bool $callback + */ + public function assertSent(Closure $callback): void { PHPUnit::assertTrue($this->recorded($callback)->isNotEmpty()); } - public function assertNotSent(callable $callback) + /** + * @param Closure(Request, Response): bool $callback + */ + public function assertNotSent(Closure $callback): void { PHPUnit::assertTrue($this->recorded($callback)->isEmpty()); } - protected function recorded($callback) + /** + * @param Closure(Request, Response): bool $callback + * + * @return Collection + */ + private function recorded(Closure $callback): Collection { if ($this->recordedRequests->isEmpty()) { return collect(); } - $callback = $callback ?: function () { - return true; - }; - return $this->recordedRequests->filter(fn ($pair) => $callback(...$pair)); } } diff --git a/src/Support/Fakery/Stub.php b/src/Support/Fakery/Stub.php index e5b59b4..8ed0862 100644 --- a/src/Support/Fakery/Stub.php +++ b/src/Support/Fakery/Stub.php @@ -1,58 +1,72 @@ $instance->register($endpoint)); + return tap(new static(), fn (Stub $instance) => $instance->register($endpoint)); } - public function respondWith($callback): self + /** + * @param Closure(Request): Response|Response $callback + */ + public function respondWith(Closure|Response $callback): self { $this->callback = $callback; return $this; } - public function getResponse(Request $request) + public function getResponse(Request $request): Response { return $this->callback instanceof Closure ? call_user_func($this->callback, $request) : $this->callback; } - protected function register($endpoint) + private function register(string $endpoint): void { $this->endpoint = Str::of($endpoint)->replaceMatches(self::REGEX_PATTERN, '')->start('*')->__toString(); $this->methods = Str::of($endpoint)->afterLast('.')->match(self::REGEX_PATTERN)->start('*')->__toString(); } - public function isForEndpoint($endpoint) + public function isForEndpoint(string $endpoint): bool { return Str::is($this->endpoint, $endpoint); } - public function isForMethod($method) + public function isForMethod(string $method): bool { return Str::of($this->methods) - ->explode('|') - ->map(fn ($availableMethod) => Str::start($availableMethod, '*')) - ->contains(fn ($availableMethod) => Str::is($availableMethod, $method)); + ->explode('|') + ->map(fn ($availableMethod) => Str::start($availableMethod, '*')) + ->contains(fn ($availableMethod) => Str::is($availableMethod, $method)); } - public function hasWildcardMethods() + public function hasWildcardMethods(): bool { return Str::is($this->methods, '*'); } diff --git a/src/Support/Fakery/Stubs.php b/src/Support/Fakery/Stubs.php index ad2fd0e..bcd1897 100644 --- a/src/Support/Fakery/Stubs.php +++ b/src/Support/Fakery/Stubs.php @@ -1,57 +1,91 @@ + */ + private Collection $stubs; public function __construct() { $this->stubs = collect(); } - public function new($url, $callback) + /** + * @param Closure(Request): Response|Response $callback + */ + public function new(string $url, Closure|Response $callback): void { $this->stubs->push(Stub::for($url)->respondWith($callback)); } - public function getForRequest(Request $request) + public function getForRequest(Request $request): ?Response { return $this->stubs - ->pipe(fn ($stubs) => $this->filterAndSortStubs($stubs, $request)) - ->map - ->getResponse($request) - ->filter() - ->first(); + ->pipe(fn ($stubs) => $this->filterAndSortStubs($stubs, $request)) + ->map + ->getResponse($request) + ->filter() + ->first(); } - protected function filterAndSortStubs($stubs, Request $request) + /** + * @param Collection $stubs + * + * @return Collection + */ + private function filterAndSortStubs(Collection $stubs, Request $request): Collection { return $stubs - ->filter(fn (Stub $stub) => $stub->isForEndpoint($request->getEndpoint())) - ->pipe(fn ($stubs) => $this->retrieveCorrectStubsForMethod($stubs, $request)); + ->filter(fn (Stub $stub) => $stub->isForEndpoint($request->getEndpoint())) + ->pipe(fn ($stubs) => $this->retrieveCorrectStubsForMethod($stubs, $request)); } - protected function retrieveCorrectStubsForMethod($stubs, Request $request) + /** + * @param Collection $stubs + * + * @return Collection + */ + private function retrieveCorrectStubsForMethod(Collection $stubs, Request $request): Collection { return $request->getMethod() ? $this->getStubsForMethod($stubs, $request) : $stubs->sortByDesc('endpoint'); } - protected function getStubsForMethod($stubs, $request) + /** + * @param Collection $stubs + * + * @return Collection + */ + private function getStubsForMethod(Collection $stubs, Request $request): Collection { return $stubs - ->filter(fn (Stub $stub) => $stub->isForMethod($request->getMethod())) - ->pipe(fn ($stubs) => $this->sortMethodStubs($stubs)); + ->filter(fn (Stub $stub) => $stub->isForMethod($request->getMethod())) + ->pipe(fn ($stubs) => $this->sortMethodStubs($stubs)); } - protected function sortMethodStubs($stubs) + /** + * @param Collection $stubs + * + * @return Collection + */ + private function sortMethodStubs(Collection $stubs): Collection { - return $stubs->every->hasWildcardMethods() + return $stubs->every(fn (Stub $stub) => $stub->hasWildcardMethods()) ? $stubs->sortByDesc('endpoint') : $stubs->sortByDesc('methods'); } diff --git a/src/Header.php b/src/Support/Header.php similarity index 53% rename from src/Header.php rename to src/Support/Header.php index abf4e25..92a63f1 100644 --- a/src/Header.php +++ b/src/Support/Header.php @@ -1,24 +1,26 @@ name = $name; - $this->namespace = $namespace; - $this->data = $data; - $this->mustUnderstand = $mustUnderstand; - $this->actor = $actor; +final class Header implements Arrayable +{ + /** + * @param SoapVar|array|null $data + */ + public function __construct( + public string $name = '', + public string $namespace = '', + public SoapVar|array|null $data = null, + public bool $mustUnderstand = false, + public string|int|null $actor = null + ) { } public function name(string $name): self @@ -31,12 +33,15 @@ public function namespace(string $namespace): self return tap($this, fn () => $this->namespace = $namespace); } - public function data($data = null): self + /** + * @param SoapVar|array|null $data + */ + public function data(SoapVar|array|null $data = null): self { return tap($this, fn () => $this->data = $data); } - public function actor($actor = null): self + public function actor(?string $actor = null): self { return tap($this, fn () => $this->actor = $actor); } @@ -46,7 +51,10 @@ public function mustUnderstand(bool $mustUnderstand = true): self return tap($this, fn () => $this->mustUnderstand = $mustUnderstand); } - public function toArray() + /** + * @return array + */ + public function toArray(): array { return [ 'name' => $this->name, diff --git a/src/Support/Scoped.php b/src/Support/Scoped.php deleted file mode 100644 index f11f14b..0000000 --- a/src/Support/Scoped.php +++ /dev/null @@ -1,28 +0,0 @@ -endpoint = $endpoint; - $this->method = $method; - } - - public function matches(string $endpoint, $method = null) - { - if (empty($this->endpoint)) { - return true; - } - - if ($this->endpoint !== $endpoint) { - return false; - } - - return empty($this->method) || $this->method === $method; - } -} diff --git a/src/Support/Scopes/HeaderSet.php b/src/Support/Scopes/HeaderSet.php new file mode 100644 index 0000000..cc35439 --- /dev/null +++ b/src/Support/Scopes/HeaderSet.php @@ -0,0 +1,36 @@ + + */ + protected array $headers; + + /** + * @param non-empty-array $headers + */ + public function __construct(array $headers) + { + $this->headers = $headers; + } + + /** + * @return non-empty-array + */ + public function getHeaders(): array + { + return $this->headers; + } +} diff --git a/src/Support/Scopes/Inclusion.php b/src/Support/Scopes/Inclusion.php new file mode 100644 index 0000000..da53c81 --- /dev/null +++ b/src/Support/Scopes/Inclusion.php @@ -0,0 +1,34 @@ + + */ + protected array $parameters; + + /** + * @param non-empty-array $parameters + */ + public function __construct(array $parameters) + { + $this->parameters = $parameters; + } + + /** + * @return non-empty-array + */ + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/src/Support/Scopes/OptionSet.php b/src/Support/Scopes/OptionSet.php new file mode 100644 index 0000000..8564fdb --- /dev/null +++ b/src/Support/Scopes/OptionSet.php @@ -0,0 +1,34 @@ + + */ + protected array $options = []; + + /** + * @param array $options + */ + public function __construct(array $options) + { + $this->options = $options; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } +} diff --git a/src/Support/Scopes/Scopeable.php b/src/Support/Scopes/Scopeable.php new file mode 100644 index 0000000..88c5717 --- /dev/null +++ b/src/Support/Scopes/Scopeable.php @@ -0,0 +1,33 @@ +endpoint = $endpoint; + $this->method = $method; + } + + public function matches(string $endpoint, ?string $method = null): bool + { + if (empty($this->endpoint)) { + return true; + } + + if ($this->endpoint !== $endpoint) { + return false; + } + + return empty($this->method) || $this->method === $method; + } +} diff --git a/src/Support/SoapClients/DecoratedClient.php b/src/Support/SoapClients/DecoratedClient.php new file mode 100644 index 0000000..629d4dc --- /dev/null +++ b/src/Support/SoapClients/DecoratedClient.php @@ -0,0 +1,99 @@ + + */ + private array $options = []; + + /** + * @param array $headers + */ + public function setHeaders(array $headers): static + { + $this->client()->__setSoapHeaders($headers); + + return $this; + } + + public function setEndpoint(string $endpoint): static + { + $this->endpoint = $endpoint; + + return $this; + } + + /** + * @param array $options + */ + public function setOptions(array $options): static + { + $this->options = $options; + + return $this; + } + + public function call(string $method, mixed $body): mixed + { + return $this->client()->{$method}($body); + } + + public function getFunctions(): array + { + return $this->client()->__getFunctions(); + } + + public function __get(string $name): mixed + { + return $this->client()->$name; + } + + /** + * @param array $arguments + */ + public function __call(string $name, array $arguments): mixed + { + return $this->client()->$name(...$arguments); + } + + public function __getLastRequest(): ?string + { + return $this->client()->__getLastRequest(); + } + + public function __getLastResponse(): ?string + { + return $this->client()->__getLastResponse(); + } + + public function __getLastRequestHeaders(): ?string + { + return $this->client()->__getLastRequestHeaders(); + } + + public function __getLastResponseHeaders(): ?string + { + return $this->client()->__getLastResponseHeaders(); + } + + private function client(): SoapClient + { + return $this->client ??= new SoapClient($this->endpoint, $this->options); + } +} diff --git a/src/Support/Tracing/Trace.php b/src/Support/Tracing/Trace.php index b96406d..b8f5c5d 100644 --- a/src/Support/Tracing/Trace.php +++ b/src/Support/Tracing/Trace.php @@ -1,19 +1,28 @@ client = $client; + $trace->xmlRequest = $client->__getLastRequest(); $trace->xmlResponse = $client->__getLastResponse(); $trace->requestHeaders = $client->__getLastRequestHeaders(); diff --git a/src/helpers.php b/src/helpers.php index dc298ea..5aae401 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,16 +1,26 @@ $attributes + */ + function soap_node(array $attributes = []): Node { return Soap::node($attributes); } } if (!function_exists('soap_header')) { - function soap_header(?string $name = null, ?string $namespace = null, $data = null, bool $mustUnderstand = false, $actor = null) + /** + * @param array|null $data + */ + function soap_header(string $name = '', string $namespace = '', ?array $data = null, bool $mustUnderstand = false, string|int|null $actor = null): Header { return Soap::header($name, $namespace, $data, $mustUnderstand, $actor); } diff --git a/tests/Concerns/ProvidesIncrementingCounter.php b/tests/Concerns/ProvidesIncrementingCounter.php index 18f3334..cb561ad 100644 --- a/tests/Concerns/ProvidesIncrementingCounter.php +++ b/tests/Concerns/ProvidesIncrementingCounter.php @@ -1,5 +1,7 @@ Soap::response(['foo' => 'bar'])]); + + $response = Soap::to(EXAMPLE_SOAP_ENDPOINT)->call('hello'); + + expect($response->response)->toEqual(['foo' => 'bar']); +}); diff --git a/tests/Feature/Headers/HeadersTest.php b/tests/Feature/Headers/HeadersTest.php index 4f611a7..94857b3 100644 --- a/tests/Feature/Headers/HeadersTest.php +++ b/tests/Feature/Headers/HeadersTest.php @@ -1,39 +1,43 @@ client = new MockSoapClient(); + $this->instance(Request::class, soapRequest(null, $this->client)); +}); +it('can create a header with the helper method', function () { Soap::to(EXAMPLE_SOAP_ENDPOINT) ->withHeaders(soap_header('Auth', 'test.com', ['foo' => 'bar'])) ->call('Add', ['intA' => 10, 'intB' => 25]); - Soap::assertSent(fn ($request) => $request->getHeaders() == [ - Soap::header('Auth', 'test.com')->data(['foo' => 'bar']), - ]); + expect($this->client->headers[0]) + ->name->toBe('Auth') + ->namespace->toBe('test.com') + ->data->toBe(['foo' => 'bar']); }); it('will pass a provided actor to the php soap header', function () { - app()->afterResolving(SoapHeader::class, function ($header) { - $this->assertEquals('test.com', $header->actor); - }); - Soap::to(EXAMPLE_SOAP_ENDPOINT) ->withHeaders(Soap::header('Auth', 'test.com')->actor('test.com')) ->withHeaders(soap_header('Brand', 'test.com', ['hi'], false, 'test.com')) ->withHeaders(soap_header('Service', 'bar.com')->actor('test.com')) ->call('Add', ['intA' => 10, 'intB' => 25]); -})->skip('This makes a real API call to assert the correct header construction'); -it('will send the SOAP_ACTOR_NONE constant if no actor is provided', function () { - app()->afterResolving(SoapHeader::class, function ($header) { - $this->assertEquals(SOAP_ACTOR_NONE, $header->actor); - }); + expect($this->client->headers)->each(fn ($header) => $header->actor->toBe('test.com')); +}); +it('will send the SOAP_ACTOR_NONE constant if no actor is provided', function () { Soap::to(EXAMPLE_SOAP_ENDPOINT) ->withHeaders(Soap::header('Auth', 'test.com')->actor(null)) ->withHeaders(soap_header('Brand', 'test.com', null, false, null)) ->withHeaders(soap_header('Service', 'bar.com')) ->call('Add', ['intA' => 10, 'intB' => 25]); -})->skip('This makes a real API call to assert the correct header construction'); + + expect($this->client->headers)->each(fn ($header) => $header->actor->toBe(SOAP_ACTOR_NONE)); +}); diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php index ce86767..8100f63 100644 --- a/tests/Feature/TestCase.php +++ b/tests/Feature/TestCase.php @@ -1,5 +1,7 @@ shouldTrace = true; - } + $this->headers = $headers; + + return $this; } - public function __call(string $function_name, array $arguments) + public function setEndpoint(string $endpoint): static { + $this->endpoint = $endpoint; + + return $this; } - public function __doRequest(string $request, string $location, string $action, int $version, $one_way = 0) + public function setOptions(array $options): static { + $this->options = $options; + + return $this; } - public function __getCookies() + public function call(string $method, mixed $body): mixed { + return []; } - public function __getFunctions() + public function getFunctions(): array { return [ 'The mock client does not actually have functions!', ]; } - public function __getLastRequest() + public function __getLastRequest(): ?string { - if (!$this->shouldTrace) { - return null; - } - return 'World'; } - public function __getLastRequestHeaders() + public function __getLastResponse(): ?string { - if (!$this->shouldTrace) { - return null; - } - - return 'Hello World'; + return 'Success!'; } - public function __getLastResponse() + public function __getLastRequestHeaders(): ?string { - if (!$this->shouldTrace) { - return null; - } - - return 'Success!'; + return 'Hello World'; } - public function __getLastResponseHeaders() + public function __getLastResponseHeaders(): ?string { - if (!$this->shouldTrace) { - return null; - } - return 'Foo Bar'; } @@ -73,11 +69,11 @@ public function __getTypes() { } - public function __setCookie(string $name, string $value = null) + public function __setCookie(string $name, ?string $value = null) { } - public function __setLocation(string $new_location = null) + public function __setLocation(?string $new_location = null) { } diff --git a/tests/Pest.php b/tests/Pest.php index 45ad72b..4781380 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,10 +1,17 @@ in('Unit'); uses(Tests\Feature\TestCase::class)->in('Feature'); -function soap(?Fakery $fakery = null, ?RicorocksDigitalAgency\Soap\Request\Request $request = null) +function soap(?Fakery $fakery = null, ?Request $request = null) { return new Soap( $fakery ?? new Fakery(new Stubs()), - $request ?? new RicorocksDigitalAgency\Soap\Request\SoapClientRequest(new IntelligentBuilder()) + $request ?? soapRequest(), + ); +} + +function soapRequest(?Builder $builder = null, ?Client $client = null): SoapPhpRequest +{ + return new SoapPhpRequest( + $builder ?? new IntelligentBuilder(), + $client ?? new DecoratedClient(), ); } -class ExampleSoapable implements Soapable +final class ExampleSoapable implements Soapable { - public function toSoap() + public function toSoap(): mixed { return [ 'intA' => 10, diff --git a/tests/Unit/BasicSyntaxTest.php b/tests/Unit/BasicSyntaxTest.php index fbb10e5..05d800d 100644 --- a/tests/Unit/BasicSyntaxTest.php +++ b/tests/Unit/BasicSyntaxTest.php @@ -1,7 +1,9 @@ fake() ->tap(fn () => $this->soap()->headers($this->soap()->header('Auth', 'test.com', ['foo' => 'bar']))) - ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])) + ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]) ->test()->assertSent(fn ($request) => $request->getHeaders() == [ $this->soap()->header('Auth', 'test.com', ['foo' => 'bar']), ]); @@ -12,7 +14,7 @@ ->fake() ->tap(fn () => $this->soap()->headers($this->soap()->header('Brand', 'test.coms', ['hello' => 'world']))->for('https://foo.bar')) ->tap(fn () => $this->soap()->headers($this->soap()->header('Auth', 'test.com', ['foo' => 'bar']))->for(EXAMPLE_SOAP_ENDPOINT)) - ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])) + ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]) ->test()->assertSent(fn ($request) => $request->getHeaders() == [ $this->soap()->header('Auth', 'test.com', ['foo' => 'bar']), ]); @@ -21,7 +23,7 @@ ->fake() ->tap(fn () => $this->soap()->headers($this->soap()->header('Brand', 'test.coms', ['hello' => 'world']))->for(EXAMPLE_SOAP_ENDPOINT, 'Add')) ->tap(fn () => $this->soap()->headers($this->soap()->header('Auth', 'test.com', ['foo' => 'bar']))->for(EXAMPLE_SOAP_ENDPOINT, 'Subtract')) - ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])) + ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]) ->test()->assertSent(fn ($request) => $request->getHeaders() == [ $this->soap()->header('Brand', 'test.coms', ['hello' => 'world']), ]); @@ -32,7 +34,7 @@ ->tap(fn () => $this->soap() ->to(EXAMPLE_SOAP_ENDPOINT) ->withHeaders($this->soap()->header('Auth', 'test.com', ['foo' => 'bar'])) - ->call('Add', (['intB' => 25])) + ->call('Add', ['intB' => 25]) ) ->assertSent(fn ($request) => $request->getHeaders() == [ $this->soap()->header('Auth', 'test.com', ['foo' => 'bar']), diff --git a/tests/Unit/Headers/HeadersTest.php b/tests/Unit/Headers/HeadersTest.php index b75edca..3ab5724 100644 --- a/tests/Unit/Headers/HeadersTest.php +++ b/tests/Unit/Headers/HeadersTest.php @@ -1,6 +1,8 @@ fake() @@ -8,9 +10,9 @@ ->withHeaders($this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar'])) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), + ]); it('can define multiple headers in the same method') ->fake() @@ -21,10 +23,10 @@ ) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), - $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), + $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), + ]); it('can define multiple headers with an array in the same method') ->fake() @@ -35,10 +37,10 @@ ]) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), - $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), + $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), + ]); it('can define multiple headers using a collection in the same method') ->fake() @@ -49,10 +51,10 @@ ])) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), - $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), + $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), + ]); it('can define multiple headers in multiple methods') ->fake() @@ -61,10 +63,10 @@ ->withHeaders($this->soap()->header('Brand', 'test.com')->data(['hello' => 'world'])) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), - $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar']), + $this->soap()->header('Brand', 'test.com')->data(['hello' => 'world']), + ]); it('can create a header without any parameters and be composed fluently') ->fake() @@ -75,12 +77,12 @@ ->data(['foo' => 'bar']) ->mustUnderstand() ->actor('this.test') - ) + ) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar'])->mustUnderstand()->actor('this.test'), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(['foo' => 'bar'])->mustUnderstand()->actor('this.test'), + ]); it('can set up a header using a SoapVar') ->fake() @@ -88,9 +90,9 @@ ->withHeaders($this->soap()->header('Auth', 'test.com', new SoapVar(['foo' => 'bar'], null))) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com')->data(new SoapVar(['foo' => 'bar'], null)), - ]); + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com')->data(new SoapVar(['foo' => 'bar'], null)), + ]); it('does not require the data parameter') ->fake() @@ -99,7 +101,24 @@ ->withHeaders($this->soap()->header('Brand', 'test.com', null)) ->call('Add', ['intA' => 10, 'intB' => 25]) ) - ->assertSent(fn (SoapClientRequest $request) => $request->getHeaders() == [ - $this->soap()->header('Auth', 'test.com', null), - $this->soap()->header('Brand', 'test.com', null), + ->assertSent(fn (SoapPhpRequest $request) => $request->getHeaders() == [ + $this->soap()->header('Auth', 'test.com', null), + $this->soap()->header('Brand', 'test.com', null), + ]); + +it('can be converted to an array') + ->expect(soap() + ->header() + ->name('Auth') + ->namespace('test.com') + ->data(['foo' => 'bar']) + ->mustUnderstand() + ->actor('this.test') + ->toArray() + )->toBe([ + 'name' => 'Auth', + 'namespace' => 'test.com', + 'data' => ['foo' => 'bar'], + 'mustUnderstand' => true, + 'actor' => 'this.test', ]); diff --git a/tests/Unit/Hooks/GlobalHooksTest.php b/tests/Unit/Hooks/GlobalHooksTest.php index d9794e5..4e1d343 100644 --- a/tests/Unit/Hooks/GlobalHooksTest.php +++ b/tests/Unit/Hooks/GlobalHooksTest.php @@ -1,6 +1,8 @@ once() ->withArgs(fn ($parameters) => $parameters == ['intA' => 10, 'intB' => 25]); - $this->soap(null, new SoapClientRequest($mock))->fake(); + $this->soap(null, soapRequest($mock))->fake(); $this->soap()->include(['intA' => 10]); - $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])); + $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]); }); it('can include an array at the root when specified using the include method', function () { @@ -22,14 +23,14 @@ ->once() ->withArgs(fn ($parameters) => $parameters == ['intA' => 10, 'intB' => 25]); - $this->soap(null, new SoapClientRequest($mock))->fake(); + $this->soap(null, soapRequest($mock))->fake(); $this->soap()->include(['intA' => 10])->for(EXAMPLE_SOAP_ENDPOINT); - $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])); + $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]); }); it('can include a node at the root when specified using the include method', function () { - $this->soap(null, new SoapClientRequest($mock = m::mock(Builder::class))); + $this->soap(null, soapRequest($mock = m::mock(Builder::class))); $mock->shouldReceive('handle') ->once() @@ -44,7 +45,7 @@ $this->soap()->fake(); $this->soap()->include(['foo' => $this->soap()->node(['foo' => 'bar'])])->for(EXAMPLE_SOAP_ENDPOINT); - $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intA' => 10, 'intB' => 25])); + $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intA' => 10, 'intB' => 25]); }); it('only includes the inclusion if the method name matches', function () { @@ -56,16 +57,16 @@ 'intB' => 25, ]); - $this->soap(null, new SoapClientRequest($mock))->fake(); + $this->soap(null, soapRequest($mock))->fake(); $this->soap()->include(['foo' => $this->soap()->node(['foo' => 'bar'])])->for(EXAMPLE_SOAP_ENDPOINT, 'Bar'); - $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intA' => 10, 'intB' => 25])); + $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intA' => 10, 'intB' => 25]); }); it('allows inclusions to permeate further down the XML DOM using dot syntax', function () { $this->soap()->fake(); $this->soap()->include(['foo.bar' => 'Hello World'])->for(EXAMPLE_SOAP_ENDPOINT, 'Bar'); - $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Bar', (['foo' => ['baz' => 'cool']])); + $this->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Bar', ['foo' => ['baz' => 'cool']]); $this->soap()->assertSent(fn ($request) => $request->getBody() == ['foo' => ['baz' => 'cool', 'bar' => 'Hello World']]); }); diff --git a/tests/Unit/MacroableTest.php b/tests/Unit/MacroableTest.php index fa9369d..e790660 100644 --- a/tests/Unit/MacroableTest.php +++ b/tests/Unit/MacroableTest.php @@ -1,5 +1,7 @@ expectExceptionObject(new Exception('You sucessfully called this!')); diff --git a/tests/Unit/Options/AuthTest.php b/tests/Unit/Options/AuthTest.php index 93af0e8..5abea7c 100644 --- a/tests/Unit/Options/AuthTest.php +++ b/tests/Unit/Options/AuthTest.php @@ -1,13 +1,15 @@ fake() ->soap()->to(EXAMPLE_SOAP_ENDPOINT) ->withBasicAuth('hello', 'world') ->call('Add', ['intA' => 10, 'intB' => 25]) - ->test()->assertSent(function (SoapClientRequest $request) { + ->test()->assertSent(function (SoapPhpRequest $request) { return $request->getOptions() == [ 'authentication' => SOAP_AUTHENTICATION_BASIC, 'login' => 'hello', @@ -20,7 +22,7 @@ ->soap()->to(EXAMPLE_SOAP_ENDPOINT) ->withDigestAuth('hello', 'world') ->call('Add', ['intA' => 10, 'intB' => 25]) - ->test()->assertSent(function (SoapClientRequest $request) { + ->test()->assertSent(function (SoapPhpRequest $request) { return $request->getOptions() == [ 'authentication' => SOAP_AUTHENTICATION_DIGEST, 'login' => 'hello', diff --git a/tests/Unit/Options/GlobalOptionsTest.php b/tests/Unit/Options/GlobalOptionsTest.php index 6676325..d3e9dbe 100644 --- a/tests/Unit/Options/GlobalOptionsTest.php +++ b/tests/Unit/Options/GlobalOptionsTest.php @@ -1,9 +1,11 @@ fake() ->soap()->options(['login' => 'foo', 'password' => 'bar']) - ->test()->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])) + ->test()->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]) ->test()->assertSent(function ($request) { return $request->getOptions() == ['login' => 'foo', 'password' => 'bar']; }); @@ -12,7 +14,7 @@ ->fake() ->tap(fn () => $this->soap()->options(['login' => 'foo', 'password' => 'bar'])->for('https://foo.bar')) ->tap(fn () => $this->soap()->options(['compression' => SOAP_COMPRESSION_GZIP])->for(EXAMPLE_SOAP_ENDPOINT)) - ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])) + ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]) ->test()->assertSent(function ($request) { return $request->getOptions() == ['compression' => SOAP_COMPRESSION_GZIP]; }); @@ -21,7 +23,7 @@ ->fake() ->tap(fn () => $this->soap()->options(['login' => 'foo', 'password' => 'bar'])->for(EXAMPLE_SOAP_ENDPOINT, 'Add')) ->tap(fn () => $this->soap()->options(['compression' => SOAP_COMPRESSION_GZIP])->for(EXAMPLE_SOAP_ENDPOINT, 'Subtract')) - ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', (['intB' => 25])) + ->soap()->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intB' => 25]) ->test()->assertSent(function ($request) { return $request->getOptions() == ['login' => 'foo', 'password' => 'bar']; }); diff --git a/tests/Unit/Options/OptionsTest.php b/tests/Unit/Options/OptionsTest.php index 1a07fe8..20759cc 100644 --- a/tests/Unit/Options/OptionsTest.php +++ b/tests/Unit/Options/OptionsTest.php @@ -1,13 +1,15 @@ fake() ->soap()->to(EXAMPLE_SOAP_ENDPOINT) ->withOptions(['compression' => SOAP_COMPRESSION_GZIP]) ->call('Add', ['intA' => 10, 'intB' => 25]) - ->test()->assertSent(fn (SoapClientRequest $request) => $request->getOptions() == [ + ->test()->assertSent(fn (SoapPhpRequest $request) => $request->getOptions() == [ 'compression' => SOAP_COMPRESSION_GZIP, ]); @@ -17,7 +19,7 @@ ->withBasicAuth('foo', 'bar') ->withOptions(['compression' => SOAP_COMPRESSION_GZIP]) ->call('Add', ['intA' => 10, 'intB' => 25]) - ->test()->assertSent(fn (SoapClientRequest $request) => $request->getOptions() == [ + ->test()->assertSent(fn (SoapPhpRequest $request) => $request->getOptions() == [ 'authentication' => SOAP_AUTHENTICATION_BASIC, 'login' => 'foo', 'password' => 'bar', @@ -30,6 +32,6 @@ ->withOptions(['compression' => SOAP_COMPRESSION_ACCEPT]) ->withOptions(['compression' => SOAP_COMPRESSION_GZIP]) ->call('Add', ['intA' => 10, 'intB' => 25]) - ->test()->assertSent(fn (SoapClientRequest $request) => $request->getOptions() == [ + ->test()->assertSent(fn (SoapPhpRequest $request) => $request->getOptions() == [ 'compression' => SOAP_COMPRESSION_GZIP, ]); diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php index 90a07fb..d4397c5 100644 --- a/tests/Unit/TestCase.php +++ b/tests/Unit/TestCase.php @@ -1,13 +1,13 @@ true])) - ); + return soap(null, soapRequest(null, new MockSoapClient())); } public function fake($callback = null) diff --git a/tests/Unit/Tracing/SoapTracingTest.php b/tests/Unit/Tracing/SoapTracingTest.php index 3f9b5a6..ec3314a 100644 --- a/tests/Unit/Tracing/SoapTracingTest.php +++ b/tests/Unit/Tracing/SoapTracingTest.php @@ -1,5 +1,7 @@ expect(fn () => $this->traceableSoap()->to(EXAMPLE_SOAP_ENDPOINT)->trace()->call('Add', ['intA' => 10, 'intB' => 25])->trace()) ->xmlRequest->toEqual('World') @@ -9,8 +11,8 @@ it('can request traces globally') ->expect(fn () => with( - $this->traceableSoap()->trace(), - fn ($soap) => $soap->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intA' => 10, 'intB' => 25])->trace()) + $this->traceableSoap()->trace(), + fn ($soap) => $soap->to(EXAMPLE_SOAP_ENDPOINT)->call('Add', ['intA' => 10, 'intB' => 25])->trace()) ) ->xmlRequest->toEqual('World') ->xmlResponse->toEqual('Success!') diff --git a/tests/Unit/Tracing/TraceTest.php b/tests/Unit/Tracing/TraceTest.php index b4bee8b..22f8481 100644 --- a/tests/Unit/Tracing/TraceTest.php +++ b/tests/Unit/Tracing/TraceTest.php @@ -1,16 +1,19 @@ setEndpoint(EXAMPLE_SOAP_ENDPOINT)); expect($client) ->__getLastRequest()->toBe($trace->xmlRequest) ->__getLastResponse()->toBe($trace->xmlResponse) ->__getLastRequestHeaders()->toBe($trace->requestHeaders) ->__getLastResponseHeaders()->toBe($trace->responseHeaders); -})->skip('This makes a real API call to retrieve the WSDL'); +})->group('integration'); it('returns gracefully') ->expect(new Trace())