Skip to content

Commit

Permalink
Merge pull request #644 from tienvx/wrap-ffi-call-with-multipart-file-v2
Browse files Browse the repository at this point in the history
refactor: Wrap ffi call > pactffi_with_multipart_file_v2
  • Loading branch information
tienvx authored Sep 25, 2024
2 parents 1bc9bc7 + dfdf034 commit c9008b5
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 36 deletions.
12 changes: 5 additions & 7 deletions src/PhpPact/Consumer/Driver/Body/InteractionBodyDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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:
Expand All @@ -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;
Expand Down
44 changes: 42 additions & 2 deletions src/PhpPact/FFI/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/PhpPact/FFI/ClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, mixed> $arguments
*/
Expand Down
7 changes: 7 additions & 0 deletions src/PhpPact/FFI/Exception/InvalidEnumException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace PhpPact\FFI\Exception;

class InvalidEnumException extends FFIException
{
}
7 changes: 7 additions & 0 deletions src/PhpPact/FFI/Exception/InvalidResultException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace PhpPact\FFI\Exception;

class InvalidResultException extends FFIException
{
}
12 changes: 12 additions & 0 deletions src/PhpPact/FFI/Model/Result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace PhpPact\FFI\Model;

class Result
{
public function __construct(
public readonly bool $success,
public readonly string $message
) {
}
}
68 changes: 42 additions & 26 deletions tests/PhpPact/Consumer/Driver/Body/InteractionBodyDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace PhpPactTest\Consumer\Driver\Body;

use FFI;
use FFI\CData;
use PhpPact\Consumer\Driver\Body\InteractionBodyDriver;
use PhpPact\Consumer\Driver\Body\InteractionBodyDriverInterface;
use PhpPact\Consumer\Driver\Enum\InteractionPart;
Expand All @@ -17,6 +15,7 @@
use PhpPact\Consumer\Model\Interaction;
use PhpPact\Consumer\Model\ProviderResponse;
use PhpPact\FFI\ClientInterface;
use PhpPact\FFI\Model\Result;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
Expand All @@ -37,19 +36,11 @@ class InteractionBodyDriverTest extends TestCase
*/
private array $parts;
private string $boundary = 'abcde12345';
private CData $failed;
private string $message = 'error';

public function setUp(): void
{
$this->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);
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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())
Expand All @@ -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())
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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);
}
}
}
18 changes: 17 additions & 1 deletion tests/PhpPact/FFI/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace PhpPactTest\FFI;

use FFI;
use PhpPact\FFI\Client;
use PhpPact\FFI\ClientInterface;
use PHPUnit\Framework\Attributes\TestWith;
Expand All @@ -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'));
Expand Down

0 comments on commit c9008b5

Please sign in to comment.