diff --git a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php index 77bad1ec..f04cc5f2 100644 --- a/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php +++ b/src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php @@ -2,8 +2,6 @@ namespace PhpPact\Consumer\Driver\Body; -use FFI; -use FFI\CData; use PhpPact\Consumer\Driver\Enum\InteractionPart; use PhpPact\Consumer\Driver\Exception\InteractionBodyNotAddedException; use PhpPact\Consumer\Driver\Exception\PartNotAddedException; @@ -23,8 +21,8 @@ public function registerBody(Interaction $interaction, InteractionPart $interact { $body = $interaction->getBody($interactionPart); $partId = match ($interactionPart) { - InteractionPart::REQUEST => $this->client->get('InteractionPart_Request'), - InteractionPart::RESPONSE => $this->client->get('InteractionPart_Response'), + InteractionPart::REQUEST => $this->client->getInteractionPartRequest(), + InteractionPart::RESPONSE => $this->client->getInteractionPartResponse(), }; switch (true) { case $body instanceof Binary: @@ -38,9 +36,9 @@ public function registerBody(Interaction $interaction, InteractionPart $interact case $body instanceof Multipart: foreach ($body->getParts() as $part) { - $result = $this->client->call('pactffi_with_multipart_file_v2', $interaction->getHandle(), $partId, $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); - if ($result->failed instanceof CData) { - throw new PartNotAddedException(sprintf("Can not add part '%s': %s", $part->getName(), FFI::string($result->failed))); + $result = $this->client->withMultipartFileV2($interaction->getHandle(), $partId, $part->getContentType(), $part->getPath(), $part->getName(), $body->getBoundary()); + if (!$result->success) { + throw new PartNotAddedException(sprintf("Can not add part '%s': %s", $part->getName(), $result->message)); } } $success = true; diff --git a/src/PhpPact/FFI/Client.php b/src/PhpPact/FFI/Client.php index 38b54a2b..d61cb01a 100644 --- a/src/PhpPact/FFI/Client.php +++ b/src/PhpPact/FFI/Client.php @@ -3,7 +3,11 @@ namespace PhpPact\FFI; use FFI; +use FFI\CData; use PhpPact\FFI\Exception\HeaderNotReadException; +use PhpPact\FFI\Exception\InvalidEnumException; +use PhpPact\FFI\Exception\InvalidResultException; +use PhpPact\FFI\Model\Result; use PhpPact\Standalone\Installer\Model\Scripts; class Client implements ClientInterface @@ -12,13 +16,49 @@ class Client implements ClientInterface public function __construct() { - $code = \file_get_contents(Scripts::getHeader()); + $headerFile = Scripts::getHeader(); + $code = \file_get_contents($headerFile); if (!is_string($code)) { - throw new HeaderNotReadException(); + throw new HeaderNotReadException(sprintf('Can not read header file "%s"', $headerFile)); } $this->ffi = FFI::cdef($code, Scripts::getLibrary()); } + public function withMultipartFileV2(int $interaction, int $part, string $contentType, string $path, string $name, string $boundary): Result + { + $method = 'pactffi_with_multipart_file_v2'; + $result = $this->call($method, $interaction, $part, $contentType, $path, $name, $boundary); + if (!$result instanceof CData) { + throw new InvalidResultException(sprintf('Invalid result of "%s". Expected "%s", but got "%s"', $method, CData::class, get_debug_type($result))); + } + if ($result->tag === $this->getEnum('StringResult_Ok')) { // @phpstan-ignore-line + return new Result(true, $result->ok instanceof CData ? FFI::string($result->ok) : ''); // @phpstan-ignore-line + } + if ($result->tag === $this->getEnum('StringResult_Failed')) { // @phpstan-ignore-line + return new Result(false, $result->failed instanceof CData ? FFI::string($result->failed) : ''); // @phpstan-ignore-line + } + throw new InvalidResultException(sprintf('Invalid result of "%s". Neither ok or failed', $method)); + } + + public function getInteractionPartRequest(): int + { + return $this->getEnum('InteractionPart_Request'); + } + + public function getInteractionPartResponse(): int + { + return $this->getEnum('InteractionPart_Response'); + } + + private function getEnum(string $name): int + { + $value = $this->get($name); + if (!is_int($value)) { + throw new InvalidEnumException(sprintf('Invalid enum "%s". Expected "int", but got "%s"', $name, get_debug_type($value))); + } + return $value; + } + public function call(string $name, ...$arguments): mixed { return $this->ffi->{$name}(...$arguments); diff --git a/src/PhpPact/FFI/ClientInterface.php b/src/PhpPact/FFI/ClientInterface.php index c0e767ff..11927df6 100644 --- a/src/PhpPact/FFI/ClientInterface.php +++ b/src/PhpPact/FFI/ClientInterface.php @@ -2,8 +2,16 @@ namespace PhpPact\FFI; +use PhpPact\FFI\Model\Result; + interface ClientInterface { + public function withMultipartFileV2(int $interaction, int $part, string $contentType, string $path, string $name, string $boundary): Result; + + public function getInteractionPartRequest(): int; + + public function getInteractionPartResponse(): int; + /** * @param array $arguments */ diff --git a/src/PhpPact/FFI/Exception/InvalidEnumException.php b/src/PhpPact/FFI/Exception/InvalidEnumException.php new file mode 100644 index 00000000..e08e6786 --- /dev/null +++ b/src/PhpPact/FFI/Exception/InvalidEnumException.php @@ -0,0 +1,7 @@ +client = $this->createMock(ClientInterface::class); - $this->client - ->expects($this->once()) - ->method('get') - ->willReturnMap([ - ['InteractionPart_Request', $this->requestPartId], - ['InteractionPart_Response', $this->responsePartId], - ]); $this->driver = new InteractionBodyDriver($this->client); $this->interaction = new Interaction(); $this->interaction->setHandle($this->interactionHandle); @@ -63,14 +54,13 @@ public function setUp(): void new Part('/path/to//image.png', 'profileImage', 'image/png'), ]; $this->multipart = new Multipart($this->parts, $this->boundary); - $this->failed = FFI::new('char[5]'); - FFI::memcpy($this->failed, $this->message, 5); } #[TestWith([true])] #[TestWith([false])] public function testRequestBinaryBody(bool $success): void { + $this->expectsGetEnumMethods(InteractionPart::REQUEST); $data = $this->binary->getData(); $this->interaction->getRequest()->setBody($this->binary); $this->client @@ -88,6 +78,7 @@ public function testRequestBinaryBody(bool $success): void #[TestWith([false])] public function testResponseBinaryBody(bool $success): void { + $this->expectsGetEnumMethods(InteractionPart::RESPONSE); $data = $this->binary->getData(); $this->interaction->getResponse()->setBody($this->binary); $this->client @@ -105,6 +96,7 @@ public function testResponseBinaryBody(bool $success): void #[TestWith([false])] public function testRequestTextBody(bool $success): void { + $this->expectsGetEnumMethods(InteractionPart::REQUEST); $this->interaction->getRequest()->setBody($this->text); $this->client ->expects($this->once()) @@ -121,6 +113,7 @@ public function testRequestTextBody(bool $success): void #[TestWith([false])] public function testResponseTextBody(bool $success): void { + $this->expectsGetEnumMethods(InteractionPart::RESPONSE); $this->interaction->getResponse()->setBody($this->text); $this->client ->expects($this->once()) @@ -137,25 +130,26 @@ public function testResponseTextBody(bool $success): void #[TestWith([false])] public function testRequestMultipartBody(bool $success): void { + $this->expectsGetEnumMethods(InteractionPart::REQUEST); $this->interaction->getRequest()->setBody($this->multipart); $matcher = $this->exactly(count($this->parts)); $calls = [ [ - 'args' => ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary], - 'return' => (object) ['failed' => null], + 'args' => [$this->interactionHandle, $this->requestPartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary], + 'return' => new Result(true, ''), ], [ - 'args' => ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary], - 'return' => (object) ['failed' => null], + 'args' => [$this->interactionHandle, $this->requestPartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary], + 'return' => new Result(true, ''), ], [ - 'args' => ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->requestPartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary], - 'return' => (object) (['failed' => $success ? null : $this->failed]), + 'args' => [$this->interactionHandle, $this->requestPartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary], + 'return' => new Result($success, $success ? '' : $this->message), ] ]; $this->client ->expects($matcher) - ->method('call') + ->method('withMultipartFileV2') ->willReturnCallback( function (...$args) use ($calls, $matcher) { $index = $matcher->numberOfInvocations() - 1; @@ -176,25 +170,26 @@ function (...$args) use ($calls, $matcher) { #[TestWith([false])] public function testResponseMultipartBody(bool $success): void { + $this->expectsGetEnumMethods(InteractionPart::RESPONSE); $this->interaction->getResponse()->setBody($this->multipart); $matcher = $this->exactly(count($this->parts)); $calls = [ [ - 'args' => ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary], - 'return' => (object) ['failed' => null], + 'args' => [$this->interactionHandle, $this->responsePartId, $this->parts[0]->getContentType(), $this->parts[0]->getPath(), $this->parts[0]->getName(), $this->boundary], + 'return' => new Result(true, ''), ], [ - 'args' => ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary], - 'return' => (object) ['failed' => null], + 'args' => [$this->interactionHandle, $this->responsePartId, $this->parts[1]->getContentType(), $this->parts[1]->getPath(), $this->parts[1]->getName(), $this->boundary], + 'return' => new Result(true, ''), ], [ - 'args' => ['pactffi_with_multipart_file_v2', $this->interactionHandle, $this->responsePartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary], - 'return' => (object) (['failed' => $success ? null : $this->failed]), + 'args' => [$this->interactionHandle, $this->responsePartId, $this->parts[2]->getContentType(), $this->parts[2]->getPath(), $this->parts[2]->getName(), $this->boundary], + 'return' => new Result($success, $success ? '' : $this->message), ] ]; $this->client ->expects($matcher) - ->method('call') + ->method('withMultipartFileV2') ->willReturnCallback( function (...$args) use ($calls, $matcher) { $index = $matcher->numberOfInvocations() - 1; @@ -215,9 +210,30 @@ function (...$args) use ($calls, $matcher) { #[TestWith([InteractionPart::RESPONSE])] public function testEmptyBody(InteractionPart $part): void { + $this->expectsGetEnumMethods($part); $this->client ->expects($this->never()) ->method('call'); + $this->client + ->expects($this->never()) + ->method('withMultipartFileV2'); $this->driver->registerBody($this->interaction, $part); } + + private function expectsGetEnumMethods(InteractionPart $part): void + { + if ($part === InteractionPart::REQUEST) { + $this->client + ->expects($this->once()) + ->method('getInteractionPartRequest') + ->willReturn($this->requestPartId); + $this->client->expects($this->never())->method('getInteractionPartResponse'); + } else { + $this->client->expects($this->never())->method('getInteractionPartRequest'); + $this->client + ->expects($this->once()) + ->method('getInteractionPartResponse') + ->willReturn($this->responsePartId); + } + } } diff --git a/tests/PhpPact/FFI/ClientTest.php b/tests/PhpPact/FFI/ClientTest.php index 9aca443c..7baec03b 100644 --- a/tests/PhpPact/FFI/ClientTest.php +++ b/tests/PhpPact/FFI/ClientTest.php @@ -2,7 +2,6 @@ namespace PhpPactTest\FFI; -use FFI; use PhpPact\FFI\Client; use PhpPact\FFI\ClientInterface; use PHPUnit\Framework\Attributes\TestWith; @@ -17,6 +16,23 @@ public function setUp(): void $this->client = new Client(); } + public function testWithMultipartFileV2(): void + { + $result = $this->client->withMultipartFileV2(1, 2, 'text/plain', './path/to/file.txt', 'text', 'abc123'); + $this->assertFalse($result->success); + $this->assertSame('with_multipart_file: Interaction handle is invalid', $result->message); + } + + public function testGetInteractionPartRequest(): void + { + $this->assertSame(0, $this->client->getInteractionPartRequest()); + } + + public function testGetInteractionPartResponse(): void + { + $this->assertSame(1, $this->client->getInteractionPartResponse()); + } + public function testGet(): void { $this->assertSame(5, $this->client->get('LevelFilter_Trace'));