Skip to content

Commit de1e0be

Browse files
authored
Merge pull request #425 from innocenzi/feat/merge-fixture
Feature | Allow merging data in mocked fixture responses
2 parents 8d721de + e3b6941 commit de1e0be

File tree

4 files changed

+160
-1
lines changed

4 files changed

+160
-1
lines changed

src/Helpers/ArrayHelpers.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,43 @@ public static function get(array $array, string|int|null $key, mixed $default =
7575

7676
return $array;
7777
}
78+
79+
/**
80+
* Set an array item to a given value using "dot" notation.
81+
*
82+
* If no key is given to the method, the entire array will be replaced.
83+
*
84+
* @param array $array
85+
* @param string|int|null $key
86+
* @return array
87+
*/
88+
public static function set(&$array, $key, $value)
89+
{
90+
if (is_null($key)) {
91+
return $array = $value;
92+
}
93+
94+
$keys = explode('.', $key);
95+
96+
foreach ($keys as $i => $key) {
97+
if (count($keys) === 1) {
98+
break;
99+
}
100+
101+
unset($keys[$i]);
102+
103+
// If the key doesn't exist at this depth, we will just create an empty array
104+
// to hold the next value, allowing us to create the arrays to hold final
105+
// values at the correct depth. Then we'll keep digging into the array.
106+
if (! isset($array[$key]) || ! is_array($array[$key])) {
107+
$array[$key] = [];
108+
}
109+
110+
$array = &$array[$key];
111+
}
112+
113+
$array[array_shift($keys)] = $value;
114+
115+
return $array;
116+
}
78117
}

src/Http/Faking/Fixture.php

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
use Saloon\MockConfig;
88
use Saloon\Helpers\Storage;
9+
use Saloon\Helpers\ArrayHelpers;
910
use Saloon\Data\RecordedResponse;
1011
use Saloon\Helpers\FixtureHelper;
1112
use Saloon\Exceptions\FixtureException;
1213
use Saloon\Exceptions\FixtureMissingException;
14+
use Saloon\Repositories\Body\StringBodyRepository;
1315

1416
class Fixture
1517
{
@@ -28,6 +30,16 @@ class Fixture
2830
*/
2931
protected Storage $storage;
3032

33+
/**
34+
* Data to merge in the mocked response.
35+
*/
36+
protected ?array $merge = null;
37+
38+
/**
39+
* Closure to modify the returned data with.
40+
*/
41+
protected ?\Closure $through = null;
42+
3143
/**
3244
* Constructor
3345
*/
@@ -37,6 +49,26 @@ public function __construct(string $name = '', ?Storage $storage = null)
3749
$this->storage = $storage ?? new Storage(MockConfig::getFixturePath(), true);
3850
}
3951

52+
/**
53+
* Specify data to merge with the mock response data.
54+
*/
55+
public function merge(array $merge = []): static
56+
{
57+
$this->merge = $merge;
58+
59+
return $this;
60+
}
61+
62+
/**
63+
* Specify a closure to modify the mock response data with.
64+
*/
65+
public function through(\Closure $through): static
66+
{
67+
$this->through = $through;
68+
69+
return $this;
70+
}
71+
4072
/**
4173
* Attempt to get the mock response from the fixture.
4274
*/
@@ -46,7 +78,41 @@ public function getMockResponse(): ?MockResponse
4678
$fixturePath = $this->getFixturePath();
4779

4880
if ($storage->exists($fixturePath)) {
49-
return RecordedResponse::fromFile($storage->get($fixturePath))->toMockResponse();
81+
$response = RecordedResponse::fromFile($storage->get($fixturePath))->toMockResponse();
82+
83+
if (is_null($this->merge) && is_null($this->through)) {
84+
return $response;
85+
}
86+
87+
// First, we get the body as an array. If we're dealing with
88+
// a `StringBodyRepository`, we have to encode it first.
89+
if (! is_array($body = $response->body()->all())) {
90+
$body = json_decode($body ?: '[]', associative: true, flags: \JSON_THROW_ON_ERROR);
91+
}
92+
93+
// We can then merge the data in the body usingthrough
94+
// the ArrayHelpers for dot-notation support.
95+
if (is_array($this->merge)) {
96+
foreach ($this->merge as $key => $value) {
97+
ArrayHelpers::set($body, $key, $value);
98+
}
99+
}
100+
101+
// If specified, we pass the body through a function that
102+
// may modify the mock response data.
103+
if (! is_null($this->through)) {
104+
$body = call_user_func($this->through, $body);
105+
}
106+
107+
// We then set the mutated data back in the repository. If we're dealing
108+
// with a `StringBodyRepository`, we need to encode it back to string.
109+
$response->body()->set(
110+
$response->body() instanceof StringBodyRepository
111+
? json_encode($body)
112+
: $body
113+
);
114+
115+
return $response;
50116
}
51117

52118
if (MockConfig::isThrowingOnMissingFixtures() === true) {

tests/Fixtures/Saloon/users.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"statusCode":200,"headers":{"Date":"Mon, 11 Sep 2023 21:20:43 GMT","Content-Type":"application\/json","Content-Length":"63","Connection":"keep-alive","access-control-allow-origin":"*","Cache-Control":"no-cache, private","x-ratelimit-limit":"1000","x-ratelimit-remaining":"999","x-frame-options":"SAMEORIGIN","x-xss-protection":"1; mode=block","x-content-type-options":"nosniff","CF-Cache-Status":"DYNAMIC","Report-To":"{\"endpoints\":[{\"url\":\"https:\\\/\\\/a.nel.cloudflare.com\\\/report\\\/v3?s=6FQ2ADSCfKyvoEWPs9KjRyZXMfPJmR6bUu%2BXwFY0wYhRdQacLvV%2FTlZdmMX8vS%2FkoEoaTr%2B0kDpLjTd8PFH0%2FhuFShA7T1FxFLE9b6kf%2BM8T4FIJPiaWJSq2MnsZzle07j%2BR\"}],\"group\":\"cf-nel\",\"max_age\":604800}","NEL":"{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}","Server":"cloudflare","CF-RAY":"8052f4d0a80f0743-MAN","alt-svc":"h3=\":443\"; ma=86400"},"data":"{\"data\":[{\"name\":\"Jon\",\"actual_name\":\"Jon Doe\",\"twitter\":\"@jondoe\"},{\"name\":\"Jane\",\"actual_name\":\"Jane Doe\",\"twitter\":\"@janedoe\"}]}"}

tests/Unit/FixtureDataTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
namespace Saloon\Tests\Unit;
66

7+
use Pest\Expectation;
78
use Saloon\Data\RecordedResponse;
9+
use Saloon\Http\Faking\MockClient;
810
use Saloon\Http\Faking\MockResponse;
11+
use Saloon\Tests\Fixtures\Requests\DTORequest;
912

1013
test('you can create a fixture data object from a file string', function () {
1114
$data = [
@@ -60,3 +63,53 @@
6063
expect($serialized)->toEqual(json_encode($data, JSON_PRETTY_PRINT));
6164
expect($fixtureData->toFile())->toEqual($serialized);
6265
});
66+
67+
test('arbitrary data can be merged in the fixture', function () {
68+
$response = connector()->send(new DTORequest, new MockClient([
69+
MockResponse::fixture('user')->merge([
70+
'name' => 'Sam Carré',
71+
]),
72+
]));
73+
74+
expect($response->dto())
75+
->name->toBe('Sam Carré')
76+
->actualName->toBe('Sam')
77+
->twitter->toBe('@carre_sam');
78+
});
79+
80+
test('arbitrary data using dot-notation can be merged in the fixture', function () {
81+
$response = connector()->send(new DTORequest, new MockClient([
82+
MockResponse::fixture('users')->merge([
83+
'data.0.twitter' => '@jon_doe',
84+
]),
85+
]));
86+
87+
expect($response->json('data'))
88+
->toHaveCount(2)
89+
->sequence(
90+
fn (Expectation $e) => $e->twitter->toBe('@jon_doe'),
91+
fn (Expectation $e) => $e->twitter->toBe('@janedoe'),
92+
);
93+
});
94+
95+
test('a closure can be used to modify the mock response data', function () {
96+
$response = connector()->send(new DTORequest, new MockClient([
97+
MockResponse::fixture('users')->through(fn (array $data) => array_merge_recursive($data, [
98+
'data' => [
99+
[
100+
'name' => 'Sam',
101+
'actual_name' => 'Carré',
102+
'twitter' => '@carre_sam',
103+
],
104+
],
105+
])),
106+
]));
107+
108+
expect($response->json('data'))
109+
->toHaveCount(3)
110+
->sequence(
111+
fn (Expectation $e) => $e->twitter->toBe('@jondoe'),
112+
fn (Expectation $e) => $e->twitter->toBe('@janedoe'),
113+
fn (Expectation $e) => $e->twitter->toBe('@carre_sam'),
114+
);
115+
});

0 commit comments

Comments
 (0)