diff --git a/.cirrus.yml b/.cirrus.yml index dde4e49c..80abe231 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -27,7 +27,7 @@ linux_arm64_task: container: image: php:$VERSION pre_req_script: - - apt update --yes && apt install --yes zip unzip git libffi-dev + - apt update --yes && apt install --yes zip unzip git libffi-dev shared-mime-info - curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php - php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer - docker-php-ext-install sockets @@ -46,7 +46,7 @@ macos_arm64_task: macos_instance: image: ghcr.io/cirruslabs/macos-ventura-base:latest pre_req_script: - - brew install php@$VERSION composer + - brew install php@$VERSION composer shared-mime-info version_check_script: - php --version << : *BUILD_TEST_TASK_TEMPLATE diff --git a/composer.json b/composer.json index f506240c..e558f038 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,11 @@ "MessageConsumer\\": "example/message/consumer/src", "MessageConsumer\\Tests\\": "example/message/consumer/tests", "MessageProvider\\": "example/message/provider/src", - "MessageProvider\\Tests\\": "example/message/provider/tests" + "MessageProvider\\Tests\\": "example/message/provider/tests", + "BinaryConsumer\\": "example/binary/consumer/src", + "BinaryConsumer\\Tests\\": "example/binary/consumer/tests", + "BinaryProvider\\": "example/binary/provider/src", + "BinaryProvider\\Tests\\": "example/binary/provider/tests" } }, "scripts": { diff --git a/example/binary/consumer/phpunit.xml b/example/binary/consumer/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/binary/consumer/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/binary/consumer/src/Service/HttpClientService.php b/example/binary/consumer/src/Service/HttpClientService.php new file mode 100644 index 00000000..3f8d22a7 --- /dev/null +++ b/example/binary/consumer/src/Service/HttpClientService.php @@ -0,0 +1,31 @@ +httpClient = new Client(); + $this->baseUri = $baseUri; + } + + public function getImageContent(): string + { + $response = $this->httpClient->get(new Uri("{$this->baseUri}/image.jpg"), [ + 'headers' => ['Accept' => 'image/jpeg'] + ]); + + return $response->getBody(); + } +} diff --git a/example/binary/consumer/tests/Service/HttpClientServiceTest.php b/example/binary/consumer/tests/Service/HttpClientServiceTest.php new file mode 100644 index 00000000..42fe6b82 --- /dev/null +++ b/example/binary/consumer/tests/Service/HttpClientServiceTest.php @@ -0,0 +1,53 @@ +setMethod('GET') + ->setPath('/image.jpg') + ->addHeader('Accept', 'image/jpeg'); + + $response = new ProviderResponse(); + $response + ->setStatus(200) + ->addHeader('Content-Type', 'image/jpeg') + ->setBody(new Binary($imageContent, in_array(php_uname('m'), ['AMD64', 'arm64']) ? 'application/octet-stream' : 'image/jpeg')); + + $config = new MockServerConfig(); + $config + ->setConsumer('binaryConsumer') + ->setProvider('binaryProvider') + ->setPactDir(__DIR__.'/../../../pacts'); + if ($logLevel = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($logLevel); + } + $builder = new InteractionBuilder($config); + $builder + ->given('Image file image.jpg exists') + ->uponReceiving('A get request to /image.jpg') + ->with($request) + ->willRespondWith($response); + + $service = new HttpClientService($config->getBaseUri()); + $imageContentResult = $service->getImageContent(); + $verifyResult = $builder->verify(); + + $this->assertTrue($verifyResult); + $this->assertEquals($imageContent, $imageContentResult); + } +} diff --git a/example/binary/consumer/tests/_resource/image.jpg b/example/binary/consumer/tests/_resource/image.jpg new file mode 100644 index 00000000..6e93db1c Binary files /dev/null and b/example/binary/consumer/tests/_resource/image.jpg differ diff --git a/example/binary/pacts/binaryConsumer-binaryProvider.json b/example/binary/pacts/binaryConsumer-binaryProvider.json new file mode 100644 index 00000000..7ecf427f --- /dev/null +++ b/example/binary/pacts/binaryConsumer-binaryProvider.json @@ -0,0 +1,56 @@ +{ + "consumer": { + "name": "binaryConsumer" + }, + "interactions": [ + { + "description": "A get request to /image.jpg", + "providerStates": [ + { + "name": "Image file image.jpg exists" + } + ], + "request": { + "headers": { + "Accept": "image/jpeg" + }, + "method": "GET", + "path": "/image.jpg" + }, + "response": { + "body": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAPAA8BAREA/8QAFgABAQEAAAAAAAAAAAAAAAAAAgME/8QAIBAAAgIDAAIDAQAAAAAAAAAAAQIDBAUREhMhACIxUf/aAAgBAQAAPwBNlclNW6S0s7wbvv4K6iNI2HJcp9Rrnk+T0ByNofwZIjdnyYx0stWrYWAP5y03SBjt+GQ/YnmHbaUkfoA107Br40Wa+DrRLNXhjchECDwyjjlXPpdFSo3Gw5CbDFdmUVOrHcfEU4JYb2PWSJCzbVPUEmh7PQaOZHOyD0x/hHz/2Q==", + "headers": { + "Content-Type": "image/jpeg" + }, + "matchingRules": { + "body": { + "$": { + "combine": "AND", + "matchers": [ + { + "match": "contentType", + "value": "image/jpeg" + } + ] + } + }, + "header": {} + }, + "status": 200 + } + } + ], + "metadata": { + "pactRust": { + "ffi": "0.4.7", + "mockserver": "1.2.3", + "models": "1.1.9" + }, + "pactSpecification": { + "version": "3.0.0" + } + }, + "provider": { + "name": "binaryProvider" + } +} \ No newline at end of file diff --git a/example/binary/provider/phpunit.xml b/example/binary/provider/phpunit.xml new file mode 100644 index 00000000..62a9eb00 --- /dev/null +++ b/example/binary/provider/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests + + + + + + diff --git a/example/binary/provider/public/image.jpg b/example/binary/provider/public/image.jpg new file mode 100644 index 00000000..5477a6da Binary files /dev/null and b/example/binary/provider/public/image.jpg differ diff --git a/example/binary/provider/tests/PactVerifyTest.php b/example/binary/provider/tests/PactVerifyTest.php new file mode 100644 index 00000000..cc30e108 --- /dev/null +++ b/example/binary/provider/tests/PactVerifyTest.php @@ -0,0 +1,62 @@ +process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); + + $this->process->start(); + $this->process->waitUntil(function (): bool { + $fp = @fsockopen('127.0.0.1', 7202); + $isOpen = is_resource($fp); + if ($isOpen) { + fclose($fp); + } + + return $isOpen; + }); + } + + /** + * Stop the web server process once complete. + */ + protected function tearDown(): void + { + $this->process->stop(); + } + + /** + * This test will run after the web server is started. + */ + public function testPactVerifyConsumer() + { + $config = new VerifierConfig(); + $config->getProviderInfo() + ->setName('binaryProvider') // Providers name to fetch. + ->setHost('localhost') + ->setPort(7202); + if ($level = \getenv('PACT_LOGLEVEL')) { + $config->setLogLevel($level); + } + + $verifier = new Verifier($config); + $verifier->addFile(__DIR__ . '/../../pacts/binaryConsumer-binaryProvider.json'); + + $verifyResult = $verifier->verify(); + + $this->assertTrue($verifyResult); + } +} diff --git a/example/json/provider/tests/PactVerifyTest.php b/example/json/provider/tests/PactVerifyTest.php index 73dd84c9..408dade1 100644 --- a/example/json/provider/tests/PactVerifyTest.php +++ b/example/json/provider/tests/PactVerifyTest.php @@ -17,9 +17,7 @@ class PactVerifyTest extends TestCase */ protected function setUp(): void { - $publicPath = __DIR__ . '/../public/'; - - $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', $publicPath]); + $this->process = new Process(['php', '-S', '127.0.0.1:7202', '-t', __DIR__ . '/../public/']); $this->process->start(); $this->process->waitUntil(function (): bool { diff --git a/phpunit.xml b/phpunit.xml index ff6fe126..3c2344a0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -19,6 +19,12 @@ ./example/json/provider/tests + + ./example/binary/consumer/tests + + + ./example/binary/provider/tests + ./example/message/consumer/tests diff --git a/src/PhpPact/Consumer/AbstractMessageBuilder.php b/src/PhpPact/Consumer/AbstractMessageBuilder.php index d25a743c..71a23275 100644 --- a/src/PhpPact/Consumer/AbstractMessageBuilder.php +++ b/src/PhpPact/Consumer/AbstractMessageBuilder.php @@ -56,11 +56,4 @@ public function withContent(mixed $contents): self return $this; } - - public function withContentType(?string $contentType): self - { - $this->message->setContentType($contentType); - - return $this; - } } diff --git a/src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php b/src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php new file mode 100644 index 00000000..4cd40881 --- /dev/null +++ b/src/PhpPact/Consumer/Exception/MessageContentsNotAddedException.php @@ -0,0 +1,9 @@ +setContents(StringData::createFrom($contents, false)); + $this->setContentType($contentType); + } + + public function getContents(): StringData + { + return $this->contents; + } + + public function setContents(StringData $contents): self + { + $this->contents = $contents; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php b/src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php new file mode 100644 index 00000000..41acc2cf --- /dev/null +++ b/src/PhpPact/Consumer/Model/Body/ContentTypeTrait.php @@ -0,0 +1,20 @@ +contentType; + } + + public function setContentType(string $contentType): self + { + $this->contentType = $contentType; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/Body/Text.php b/src/PhpPact/Consumer/Model/Body/Text.php new file mode 100644 index 00000000..1d2e54f7 --- /dev/null +++ b/src/PhpPact/Consumer/Model/Body/Text.php @@ -0,0 +1,28 @@ +setContents($contents); + $this->setContentType($contentType); + } + + public function getContents(): string + { + return $this->contents; + } + + public function setContents(string $contents): self + { + $this->contents = $contents; + + return $this; + } +} diff --git a/src/PhpPact/Consumer/Model/ConsumerRequest.php b/src/PhpPact/Consumer/Model/ConsumerRequest.php index 58d98740..1c0b4c08 100644 --- a/src/PhpPact/Consumer/Model/ConsumerRequest.php +++ b/src/PhpPact/Consumer/Model/ConsumerRequest.php @@ -3,7 +3,6 @@ namespace PhpPact\Consumer\Model; use PhpPact\Consumer\Model\Interaction\BodyTrait; -use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; use PhpPact\Consumer\Model\Interaction\HeadersTrait; use PhpPact\Consumer\Model\Interaction\MethodTrait; use PhpPact\Consumer\Model\Interaction\PathTrait; @@ -16,7 +15,6 @@ class ConsumerRequest { use HeadersTrait; use BodyTrait; - use ContentTypeTrait; use MethodTrait; use PathTrait; use QueryTrait; diff --git a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php index 3a7c6b37..3cb31e7f 100644 --- a/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php +++ b/src/PhpPact/Consumer/Model/Interaction/BodyTrait.php @@ -3,32 +3,29 @@ namespace PhpPact\Consumer\Model\Interaction; use JsonException; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; trait BodyTrait { - use ContentTypeTrait; + private Text|Binary|null $body = null; - private ?string $body = null; - - public function getBody(): ?string + public function getBody(): Text|Binary|null { return $this->body; } /** - * @param array|string|null $body - * * @throws JsonException */ - public function setBody(array|string|null $body): self + public function setBody(mixed $body): self { - if (\is_string($body) || \is_null($body)) { + if (\is_string($body)) { + $this->body = new Text($body, 'text/plain'); + } elseif (\is_null($body) || $body instanceof Text || $body instanceof Binary) { $this->body = $body; } else { - $this->body = \json_encode($body, JSON_THROW_ON_ERROR); - if (!isset($this->contentType)) { - $this->setContentType('application/json'); - } + $this->body = new Text(\json_encode($body, JSON_THROW_ON_ERROR), 'application/json'); } return $this; diff --git a/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php b/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php deleted file mode 100644 index 257f6cbe..00000000 --- a/src/PhpPact/Consumer/Model/Interaction/ContentTypeTrait.php +++ /dev/null @@ -1,20 +0,0 @@ -contentType; - } - - public function setContentType(?string $contentType): self - { - $this->contentType = $contentType; - - return $this; - } -} diff --git a/src/PhpPact/Consumer/Model/Message.php b/src/PhpPact/Consumer/Model/Message.php index 796a683f..8d2d2a4e 100644 --- a/src/PhpPact/Consumer/Model/Message.php +++ b/src/PhpPact/Consumer/Model/Message.php @@ -3,7 +3,8 @@ namespace PhpPact\Consumer\Model; use JsonException; -use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; /** * Message metadata and contents to be posted to the Mock Server for PACT tests. @@ -11,7 +12,6 @@ class Message { use ProviderStates; - use ContentTypeTrait; private string $description; @@ -20,7 +20,7 @@ class Message */ private array $metadata = []; - private ?string $contents = null; + private Text|Binary|null $contents = null; public function getDescription(): string { @@ -60,7 +60,7 @@ private function setMetadataValue(string $key, string $value): void $this->metadata[$key] = $value; } - public function getContents(): ?string + public function getContents(): Text|Binary|null { return $this->contents; } @@ -70,13 +70,12 @@ public function getContents(): ?string */ public function setContents(mixed $contents): self { - if (\is_string($contents) || \is_null($contents)) { + if (\is_string($contents)) { + $this->contents = new Text($contents, 'text/plain'); + } elseif (\is_null($contents) || $contents instanceof Text || $contents instanceof Binary) { $this->contents = $contents; } else { - $this->contents = \json_encode($contents, JSON_THROW_ON_ERROR); - if (!isset($this->contentType)) { - $this->setContentType('application/json'); - } + $this->contents = new Text(\json_encode($contents, JSON_THROW_ON_ERROR), 'application/json'); } return $this; diff --git a/src/PhpPact/Consumer/Model/ProviderResponse.php b/src/PhpPact/Consumer/Model/ProviderResponse.php index 3201811f..2abbe24b 100644 --- a/src/PhpPact/Consumer/Model/ProviderResponse.php +++ b/src/PhpPact/Consumer/Model/ProviderResponse.php @@ -3,7 +3,6 @@ namespace PhpPact\Consumer\Model; use PhpPact\Consumer\Model\Interaction\BodyTrait; -use PhpPact\Consumer\Model\Interaction\ContentTypeTrait; use PhpPact\Consumer\Model\Interaction\HeadersTrait; use PhpPact\Consumer\Model\Interaction\StatusTrait; @@ -14,6 +13,5 @@ class ProviderResponse { use HeadersTrait; use BodyTrait; - use ContentTypeTrait; use StatusTrait; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php new file mode 100644 index 00000000..aa3760d9 --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/AbstractBodyRegistry.php @@ -0,0 +1,32 @@ +client->call('pactffi_with_binary_file', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()); + } else { + $success = $this->client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); + } + if (!$success) { + throw new InteractionBodyNotAddedException(); + } + } + + abstract protected function getPart(): int; +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php new file mode 100644 index 00000000..d7f69d3e --- /dev/null +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/BodyRegistryInterface.php @@ -0,0 +1,11 @@ +client->call('pactffi_with_binary_file', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()->getValue(), $body->getContents()->getSize()); + } else { + $success = $this->client->call('pactffi_with_body', $this->messageRegistry->getId(), $this->getPart(), $body->getContentType(), $body->getContents()); + } + if (!$success) { + throw new MessageContentsNotAddedException(); + } + } +} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php similarity index 73% rename from src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php rename to src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php index 1a4ed86e..f1266404 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Contents/RequestBodyRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Body/RequestBodyRegistry.php @@ -1,6 +1,6 @@ client->call('pactffi_with_body', $this->interactionRegistry->getId(), $this->getPart(), $contentType, $body); - if (!$success) { - throw new InteractionBodyNotAddedException(); - } - } - - abstract protected function getPart(): int; -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php deleted file mode 100644 index 3e3394ff..00000000 --- a/src/PhpPact/Consumer/Registry/Interaction/Contents/ContentsRegistryInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -client->call('pactffi_message_with_contents', $this->messageRegistry->getId(), $contentType, $data->getValue(), $data->getSize()); - } -} diff --git a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php index 7a917416..70a4b083 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/InteractionRegistry.php @@ -76,7 +76,7 @@ private function with(ConsumerRequest $request): self ->withRequest($request->getMethod(), $request->getPath()) ->withQueryParameters($request->getQuery()) ->withHeaders($request->getHeaders()) - ->withBody($request->getContentType(), $request->getBody()); + ->withBody($request->getBody()); return $this; } @@ -86,7 +86,7 @@ private function willRespondWith(ProviderResponse $response): self $this->responseRegistry ->withResponse($response->getStatus()) ->withHeaders($response->getHeaders()) - ->withBody($response->getContentType(), $response->getBody()); + ->withBody($response->getBody()); return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php index 2da4e9b5..607b9bdf 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/MessageRegistry.php @@ -2,21 +2,23 @@ namespace PhpPact\Consumer\Registry\Interaction; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; -use PhpPact\Consumer\Registry\Interaction\Contents\MessageContentsRegistry; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; +use PhpPact\Consumer\Registry\Interaction\Body\MessageContentsRegistry; use PhpPact\Consumer\Registry\Pact\PactRegistryInterface; use PhpPact\FFI\ClientInterface; class MessageRegistry extends AbstractRegistry implements MessageRegistryInterface { - private ContentsRegistryInterface $messageContentsRegistry; + private BodyRegistryInterface $messageContentsRegistry; public function __construct( ClientInterface $client, PactRegistryInterface $pactRegistry, - ?ContentsRegistryInterface $messageContentsRegistry = null + ?BodyRegistryInterface $messageContentsRegistry = null ) { parent::__construct($client, $pactRegistry); $this->messageContentsRegistry = $messageContentsRegistry ?? new MessageContentsRegistry($client, $this); @@ -30,7 +32,7 @@ public function registerMessage(Message $message): void ->given($message->getProviderStates()) ->expectsToReceive($message->getDescription()) ->withMetadata($message->getMetadata()) - ->withContents($message->getContentType(), $message->getContents()); + ->withContents($message->getContents()); } protected function newInteraction(string $description): self @@ -40,9 +42,11 @@ protected function newInteraction(string $description): self return $this; } - private function withContents(?string $contentType = null, ?string $contents = null): self + private function withContents(Text|Binary|null $contents): self { - $this->messageContentsRegistry->withContents($contentType, $contents); + if ($contents) { + $this->messageContentsRegistry->withBody($contents); + } return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php index 9531a07c..787d71dc 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/AbstractPartRegistry.php @@ -2,7 +2,9 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -11,13 +13,15 @@ abstract class AbstractPartRegistry implements PartRegistryInterface public function __construct( protected ClientInterface $client, protected InteractionRegistryInterface $interactionRegistry, - private ContentsRegistryInterface $contentsRegistry + private BodyRegistryInterface $bodyRegistry ) { } - public function withBody(?string $contentType = null, ?string $body = null): self + public function withBody(Text|Binary|null $body): self { - $this->contentsRegistry->withContents($contentType, $body); + if ($body) { + $this->bodyRegistry->withBody($body); + } return $this; } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php index 90a6516b..5f7f59f8 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/PartRegistryInterface.php @@ -2,9 +2,12 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; +use PhpPact\Consumer\Model\Body\Binary; +use PhpPact\Consumer\Model\Body\Text; + interface PartRegistryInterface { - public function withBody(?string $contentType = null, ?string $body = null): self; + public function withBody(Text|Binary|null $body): self; /** * @param array $headers diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php index 026dc57c..890ad9b7 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/RequestRegistry.php @@ -2,8 +2,8 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; -use PhpPact\Consumer\Registry\Interaction\Contents\RequestBodyRegistry; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; +use PhpPact\Consumer\Registry\Interaction\Body\RequestBodyRegistry; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -14,7 +14,7 @@ class RequestRegistry extends AbstractPartRegistry implements RequestRegistryInt public function __construct( ClientInterface $client, InteractionRegistryInterface $interactionRegistry, - ?ContentsRegistryInterface $requestBodyRegistry = null + ?BodyRegistryInterface $requestBodyRegistry = null ) { parent::__construct($client, $interactionRegistry, $requestBodyRegistry ?? new RequestBodyRegistry($client, $interactionRegistry)); } diff --git a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php index 3649a408..b74c0078 100644 --- a/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php +++ b/src/PhpPact/Consumer/Registry/Interaction/Part/ResponseRegistry.php @@ -2,8 +2,8 @@ namespace PhpPact\Consumer\Registry\Interaction\Part; -use PhpPact\Consumer\Registry\Interaction\Contents\ContentsRegistryInterface; -use PhpPact\Consumer\Registry\Interaction\Contents\ResponseBodyRegistry; +use PhpPact\Consumer\Registry\Interaction\Body\BodyRegistryInterface; +use PhpPact\Consumer\Registry\Interaction\Body\ResponseBodyRegistry; use PhpPact\Consumer\Registry\Interaction\InteractionRegistryInterface; use PhpPact\FFI\ClientInterface; @@ -14,7 +14,7 @@ class ResponseRegistry extends AbstractPartRegistry implements ResponseRegistryI public function __construct( ClientInterface $client, InteractionRegistryInterface $interactionRegistry, - ?ContentsRegistryInterface $responseBodyRegistry = null + ?BodyRegistryInterface $responseBodyRegistry = null ) { parent::__construct($client, $interactionRegistry, $responseBodyRegistry ?? new ResponseBodyRegistry($client, $interactionRegistry)); } diff --git a/src/PhpPact/FFI/Model/StringData.php b/src/PhpPact/FFI/Model/StringData.php index 622f457a..82e8fe57 100644 --- a/src/PhpPact/FFI/Model/StringData.php +++ b/src/PhpPact/FFI/Model/StringData.php @@ -23,10 +23,10 @@ public function getSize(): int return $this->size; } - public static function createFrom(string $value): ?self + public static function createFrom(string $value, bool $nullTerminated = true): self { $length = \strlen($value); - $size = $length + 1; + $size = $length + ($nullTerminated ? 1 : 0); $cData = FFI::new("uint8_t[{$size}]"); FFI::memcpy($cData, $value, $length); diff --git a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php index f30aeca7..6ddcb9d7 100644 --- a/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php +++ b/tests/PhpPact/Consumer/Model/ConsumerRequestTest.php @@ -3,6 +3,7 @@ namespace PhpPactTest\Consumer\Model; use PhpPact\Consumer\Matcher\Matcher; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ConsumerRequest; use PHPUnit\Framework\TestCase; @@ -24,7 +25,11 @@ public function testSerializing() $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals(['fruit' => ['apple', 'banana']], $model->getQuery()); $this->assertEquals('/somepath', $model->getPath()); - $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); + + $body = $model->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertEquals('{"currentCity":"Austin"}', $body->getContents()); + $this->assertEquals('application/json', $body->getContentType()); } public function testSerializingWhenPathUsingMatcher() @@ -45,6 +50,10 @@ public function testSerializingWhenPathUsingMatcher() $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); $this->assertEquals(['food' => ['milk']], $model->getQuery()); $this->assertEquals('{"value":"\/somepath\/474d610b-c6e3-45bd-9f70-529e7ad21df0\/status","regex":"\\\\\\/somepath\\\\\\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\\\\\\/status","pact:matcher:type":"regex"}', $model->getPath()); - $this->assertEquals('{"status":"finished"}', $model->getBody()); + + $body = $model->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertEquals('{"status":"finished"}', $body->getContents()); + $this->assertEquals('application/json', $body->getContentType()); } } diff --git a/tests/PhpPact/Consumer/Model/MessageTest.php b/tests/PhpPact/Consumer/Model/MessageTest.php index 165ed8a8..8182706d 100644 --- a/tests/PhpPact/Consumer/Model/MessageTest.php +++ b/tests/PhpPact/Consumer/Model/MessageTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\Message; use PhpPact\Consumer\Model\ProviderState; use PHPUnit\Framework\TestCase; @@ -29,6 +30,10 @@ public function testSetters() static::assertEquals($providerStateName, $providerStates[0]->getName()); static::assertEquals($providerStateParams, $providerStates[0]->getParams()); static::assertSame($metadata, $subject->getMetadata()); - static::assertSame($contents, $subject->getContents()); + + $messageContents = $subject->getContents(); + $this->assertInstanceOf(Text::class, $messageContents); + $this->assertEquals($contents, $messageContents->getContents()); + $this->assertEquals('text/plain', $messageContents->getContentType()); } } diff --git a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php index 2da664be..315322e7 100644 --- a/tests/PhpPact/Consumer/Model/ProviderResponseTest.php +++ b/tests/PhpPact/Consumer/Model/ProviderResponseTest.php @@ -2,6 +2,7 @@ namespace PhpPactTest\Consumer\Model; +use PhpPact\Consumer\Model\Body\Text; use PhpPact\Consumer\Model\ProviderResponse; use PHPUnit\Framework\TestCase; @@ -19,6 +20,10 @@ public function testSerializing() $this->assertEquals(200, $model->getStatus()); $this->assertEquals(['Content-Type' => ['application/json']], $model->getHeaders()); - $this->assertEquals('{"currentCity":"Austin"}', $model->getBody()); + + $body = $model->getBody(); + $this->assertInstanceOf(Text::class, $body); + $this->assertEquals('{"currentCity":"Austin"}', $body->getContents()); + $this->assertEquals('application/json', $body->getContentType()); } } diff --git a/tests/PhpPact/FFI/Model/StringDataTest.php b/tests/PhpPact/FFI/Model/StringDataTest.php new file mode 100644 index 00000000..075399b6 --- /dev/null +++ b/tests/PhpPact/FFI/Model/StringDataTest.php @@ -0,0 +1,43 @@ +getValue(); + $size = \strlen($value) + 1; + + $this->assertSame($size, FFI::sizeof($cData)); + $this->assertSame($value, $this->cDataToString($cData, $size - 1)); // ignore null + } + + public function testCreateBinaryString() + { + $value = file_get_contents(__DIR__ . '/../../../_resources/image.jpg'); + $stringData = StringData::createFrom($value, false); + $cData = $stringData->getValue(); + $size = \strlen($value); + + $this->assertSame($size, FFI::sizeof($cData)); + $this->assertEquals($value, $this->cDataToString($cData, $size)); + } + + private function cDataToString(CData $cData, int $size): string + { + $result = ''; + for ($index = 0; $index < $size; $index++) { + $result .= chr($cData[$index]); // @phpstan-ignore-line + } + + return $result; + } +} diff --git a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php index 245d60a5..cf302a7c 100644 --- a/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php +++ b/tests/PhpPact/Standalone/ProviderVerifier/VerifierTest.php @@ -55,7 +55,7 @@ public function testVerify(): void } $verifier = new Verifier($config); - $verifier->addDirectory(__DIR__ . '/../../../_resources'); + $verifier->addFile(__DIR__ . '/../../../_resources/someconsumer-someprovider.json'); $verifyResult = $verifier->verify(); diff --git a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php index 6ae7c400..1e0e77f3 100644 --- a/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php +++ b/tests/PhpPact/Standalone/StubServer/StubServerConfigTest.php @@ -18,7 +18,7 @@ public function testSetters() $providerStateHeaderName = 'header'; $token = 'token'; $user = 'user:password'; - $dirs = [__DIR__ . '/../../../_resources']; + $dirs = ['/path/to/pacts']; $files = ['/path/to/pact.json']; $urls = ['http://example.com/path/to/file.json']; $consumerNames = ['consumer-1', 'consumer-2']; diff --git a/tests/_resources/image.jpg b/tests/_resources/image.jpg new file mode 100644 index 00000000..8c1702a1 Binary files /dev/null and b/tests/_resources/image.jpg differ