Skip to content

Commit 9b48406

Browse files
Stephan Wentzpl-github
authored andcommitted
feat: Improve multipart handling in request builder
BREAKING CHANGE: Use MockRequestBuilder->multipart() instead of MockRequestBuilder->multipartFile()
1 parent 8939704 commit 9b48406

File tree

6 files changed

+227
-46
lines changed

6 files changed

+227
-46
lines changed

src/HttpClientMock/MockRequestBuilder.php

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ final class MockRequestBuilder
6262

6363
private string|null $content = null;
6464

65-
/** @var mixed[]|null */
65+
/** @var array<string, array{filename?: string|null, mimetype?: string|null, content?: string|null}>|null */
6666
private array|null $multiparts = null;
6767

6868
/** @var callable(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): ?string|null */
@@ -277,27 +277,43 @@ public function hasRequestParams(): bool
277277
return (bool) preg_match('/[^=]+=[^=]*(&[^=]+=[^=]*)*/', (string) $this->content) && !$this->isJson();
278278
}
279279

280-
public function multipartFile(string $name, string|null $filename, string $mimetype, int $size): self
281-
{
280+
public function multipart(
281+
string $name,
282+
string|null $mimetype = null,
283+
string|null $filename = null,
284+
string|null $content = null,
285+
): self {
282286
$this->multiparts ??= [];
283-
$this->multiparts[$name] = [
284-
'type' => $filename !== null ? 'file' : 'data',
285-
'filename' => $filename,
286-
'mimetype' => $mimetype,
287-
'size' => $size,
288-
];
287+
$this->multiparts[$name] = [];
288+
289+
if ($mimetype !== null) {
290+
$this->multiparts[$name]['mimetype'] = $mimetype;
291+
}
292+
293+
if ($filename !== null) {
294+
$this->multiparts[$name]['filename'] = $filename;
295+
}
296+
297+
if ($content !== null) {
298+
$this->multiparts[$name]['content'] = $content;
299+
}
289300

290301
return $this;
291302
}
292303

293-
public function multipartFileFromFile(string $name, File $file): self
304+
public function multipartFromFile(string $name, File $file): self
294305
{
295-
$this->multipartFile($name, $file->getBasename(), $file->getMimeType(), $file->getSize());
306+
$this->multipart(
307+
$name,
308+
mimetype: $file->getMimeType(),
309+
filename: $file->getBasename(),
310+
content: $file->getContent(),
311+
);
296312

297313
return $this;
298314
}
299315

300-
/** @return mixed[]|null */
316+
/** @return array<string, array{filename?: string|null, mimetype?: string|null, content?: string|null}>|null */
301317
public function getMultiparts(): array|null
302318
{
303319
return $this->multiparts;

src/HttpClientMock/MockRequestBuilderFactory.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use function Safe\json_decode;
1919
use function Safe\preg_match;
2020
use function Safe\rewind;
21-
use function strlen;
2221
use function strpos;
2322
use function urldecode;
2423

@@ -113,11 +112,12 @@ private function processBody(
113112
$mp = new StreamedPart($stream);
114113
foreach ($mp->getParts() as $part) {
115114
assert($part instanceof StreamedPart);
116-
$mockRequestBuilder->multipartFile(
115+
116+
$mockRequestBuilder->multipart(
117117
$part->getName(),
118-
$part->getFileName(),
119-
$part->getMimeType(),
120-
strlen($part->getBody()),
118+
mimetype: $part->getMimeType(),
119+
filename: $part->getFileName(),
120+
content: $part->getBody(),
121121
);
122122
}
123123

src/HttpClientMock/MockRequestMatcher.php

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,10 @@ public function __invoke(MockRequestBuilder $expectation, MockRequestBuilder $re
9898
}
9999

100100
if ($expectation->getMultiparts() !== null) {
101-
if (!($this->compare)($expectation->getMultiparts(), $realRequest->getMultiparts())) {
101+
if (!$this->isMultipartMatching($expectation, $realRequest)) {
102102
return MockRequestMatch::mismatchingMultiparts(
103103
$expectation->getMultiparts(),
104-
$realRequest->getMultiparts(),
104+
$this->reduceMultiparts($expectation, $realRequest),
105105
);
106106
}
107107
}
@@ -166,6 +166,42 @@ private function isPlainContentMatching(MockRequestBuilder $expectation, MockReq
166166
return $expectation->getContent() === $realRequest->getContent();
167167
}
168168

169+
/** @return array<string, array{mimetype?: string|null, filename?: string|null, content?: string|null}> */
170+
private function reduceMultiparts(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): array
171+
{
172+
$realMultiparts = $realRequest->getMultiparts();
173+
$expectationMultiparts = $expectation->getMultiparts();
174+
175+
$reducedMultiparts = $realMultiparts;
176+
foreach ($expectationMultiparts as $key => $data) {
177+
if (!($realMultiparts[$key] ?? false)) {
178+
continue;
179+
}
180+
181+
$reducedMultiparts[$key] = [];
182+
foreach ($data as $name => $value) {
183+
$reducedMultiparts[$key][$name] = $value !== null ? $realMultiparts[$key][$name] : null;
184+
}
185+
}
186+
187+
return $reducedMultiparts;
188+
}
189+
190+
private function isMultipartMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool
191+
{
192+
if (!$expectation->hasMultiparts() && !$realRequest->hasMultiparts()) {
193+
return true;
194+
}
195+
196+
if (!$expectation->hasMultiparts() || !$realRequest->hasMultiparts()) {
197+
return false;
198+
}
199+
200+
$realMultiparts = $this->reduceMultiparts($expectation, $realRequest);
201+
202+
return ($this->compare)($expectation->getMultiparts(), $realMultiparts);
203+
}
204+
169205
private function isJsonContentMatching(MockRequestBuilder $expectation, MockRequestBuilder $realRequest): bool
170206
{
171207
if (!$expectation->hasContent() || !$expectation->isJson()) {

tests/HttpClientMock/MockRequestBuilderCollectionTest.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66

77
use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilder;
88
use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection;
9+
use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatch;
10+
use Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatcher;
911
use Brainbits\FunctionalTestHelpers\HttpClientMock\MockResponseBuilder;
1012
use Brainbits\FunctionalTestHelpers\HttpClientMock\SymfonyMockResponseFactory;
13+
use PHPUnit\Framework\Attributes\CoversClass;
1114
use PHPUnit\Framework\Attributes\DataProvider;
1215
use PHPUnit\Framework\TestCase;
1316

14-
/**
15-
* @covers \Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilder
16-
* @covers \Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestBuilderCollection
17-
* @covers \Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatch
18-
* @covers \Brainbits\FunctionalTestHelpers\HttpClientMock\MockRequestMatcher
19-
*/
17+
#[CoversClass(MockRequestBuilder::class)]
18+
#[CoversClass(MockRequestBuilderCollection::class)]
19+
#[CoversClass(MockRequestMatch::class)]
20+
#[CoversClass(MockRequestMatcher::class)]
2021
final class MockRequestBuilderCollectionTest extends TestCase
2122
{
2223
private MockRequestBuilderCollection $collection;
@@ -82,6 +83,12 @@ public function setUp(): void
8283
->uri('/bar')
8384
->content('content')
8485
->willRespond(new MockResponseBuilder()),
86+
87+
'postBarWithMultipart' => (new MockRequestBuilder())
88+
->method('POST')
89+
->uri('/barx')
90+
->multipart('key', 'application/octet-stream', null, 'content')
91+
->willRespond(new MockResponseBuilder()),
8592
];
8693

8794
$this->collection = new MockRequestBuilderCollection(new SymfonyMockResponseFactory());
@@ -135,6 +142,21 @@ public static function requests(): array
135142
['body' => 'content', 'headers' => ['Content-Type: application/x-www-form-urlencoded']],
136143
'postBarWithContent',
137144
],
145+
'postBarWithMultipart' => [
146+
'POST',
147+
'/barx',
148+
[
149+
'body' => <<<'BODY'
150+
--12345
151+
Content-Disposition: form-data; name="key"
152+
153+
content
154+
--12345--
155+
BODY,
156+
'headers' => ['Content-Type: multipart/form-data; boundary=12345'],
157+
],
158+
'postBarWithMultipart',
159+
],
138160
];
139161
}
140162
}

tests/HttpClientMock/MockRequestBuilderTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
use PHPUnit\Framework\Attributes\CoversClass;
1313
use PHPUnit\Framework\TestCase;
1414
use RuntimeException;
15+
use Symfony\Component\HttpFoundation\File\File;
16+
17+
use function Safe\file_get_contents;
1518

1619
#[CoversClass(MockRequestBuilder::class)]
1720
final class MockRequestBuilderTest extends TestCase
@@ -288,4 +291,39 @@ public function testWithBasicAuthentication(): void
288291

289292
$this->assertSame('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $mockRequestBuilder->getHeader('Authorization'));
290293
}
294+
295+
public function testWithMultipart(): void
296+
{
297+
$mockRequestBuilder = new MockRequestBuilder();
298+
$mockRequestBuilder->multipart('key', 'mimetype', 'filename', 'content');
299+
300+
$this->assertTrue($mockRequestBuilder->hasMultiparts());
301+
$this->assertSame(
302+
[
303+
'key' => [
304+
'mimetype' => 'mimetype',
305+
'filename' => 'filename',
306+
'content' => 'content',
307+
],
308+
],
309+
$mockRequestBuilder->getMultiparts(),
310+
);
311+
}
312+
313+
public function testWithMultipartFromFile(): void
314+
{
315+
$mockRequestBuilder = new MockRequestBuilder();
316+
$mockRequestBuilder->multipartFromFile('key', new File(__DIR__ . '/../files/test.zip'));
317+
318+
$this->assertSame(
319+
[
320+
'key' => [
321+
'mimetype' => 'application/zip',
322+
'filename' => 'test.zip',
323+
'content' => file_get_contents(__DIR__ . '/../files/test.zip'),
324+
],
325+
],
326+
$mockRequestBuilder->getMultiparts(),
327+
);
328+
}
291329
}

0 commit comments

Comments
 (0)