From df757bb470cf215576ec40abe209634961e8a84c Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 1 Jun 2024 06:33:32 +0200 Subject: [PATCH 1/4] feat(testing): allow merging data in mocked fixture responses --- src/Http/Faking/FakeResponse.php | 4 ++-- src/Http/Faking/Fixture.php | 30 ++++++++++++++++++++++++++++-- tests/Unit/FixtureDataTest.php | 15 +++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Http/Faking/FakeResponse.php b/src/Http/Faking/FakeResponse.php index dfa7db01..f7025998 100644 --- a/src/Http/Faking/FakeResponse.php +++ b/src/Http/Faking/FakeResponse.php @@ -111,9 +111,9 @@ public function getException(PendingRequest $pendingRequest): ?Throwable /** * Create a new mock response from a fixture */ - public static function fixture(string $name): Fixture + public static function fixture(string $name, array $merge = []): Fixture { - return new Fixture($name); + return new Fixture($name, merge: $merge); } /** diff --git a/src/Http/Faking/Fixture.php b/src/Http/Faking/Fixture.php index c35d36c7..7268276a 100644 --- a/src/Http/Faking/Fixture.php +++ b/src/Http/Faking/Fixture.php @@ -9,7 +9,9 @@ use Saloon\Data\RecordedResponse; use Saloon\Helpers\FixtureHelper; use Saloon\Exceptions\FixtureException; +use Saloon\Contracts\Body\MergeableBody; use Saloon\Exceptions\FixtureMissingException; +use Saloon\Repositories\Body\StringBodyRepository; class Fixture { @@ -28,13 +30,19 @@ class Fixture */ protected Storage $storage; + /** + * Data to merge in the mocked response. + */ + protected array $merge = []; + /** * Constructor */ - public function __construct(string $name = '', Storage $storage = null) + public function __construct(string $name = '', Storage $storage = null, array $merge = []) { $this->name = $name; $this->storage = $storage ?? new Storage(MockConfig::getFixturePath(), true); + $this->merge = $merge; } /** @@ -46,7 +54,25 @@ public function getMockResponse(): ?MockResponse $fixturePath = $this->getFixturePath(); if ($storage->exists($fixturePath)) { - return RecordedResponse::fromFile($storage->get($fixturePath))->toMockResponse(); + $response = RecordedResponse::fromFile($storage->get($fixturePath))->toMockResponse(); + + if ($response->body() instanceof MergeableBody && is_array($this->merge)) { + $response->body()->merge($this->merge); + + return; + } + + if ($response->body() instanceof StringBodyRepository && is_array($this->merge)) { + try { + $response->body()->set(json_encode(array_merge( + json_decode($response->body()->all() ?: '[]', associative: true, flags: JSON_THROW_ON_ERROR), + $this->merge + ))); + } catch (\Throwable) { + } + } + + return $response; } if (MockConfig::isThrowingOnMissingFixtures() === true) { diff --git a/tests/Unit/FixtureDataTest.php b/tests/Unit/FixtureDataTest.php index c2ed998e..6b8a4e53 100644 --- a/tests/Unit/FixtureDataTest.php +++ b/tests/Unit/FixtureDataTest.php @@ -5,7 +5,9 @@ namespace Saloon\Tests\Unit; use Saloon\Data\RecordedResponse; +use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; +use Saloon\Tests\Fixtures\Requests\DTORequest; test('you can create a fixture data object from a file string', function () { $data = [ @@ -60,3 +62,16 @@ expect($serialized)->toEqual(json_encode($data, JSON_PRETTY_PRINT)); expect($fixtureData->toFile())->toEqual($serialized); }); + +test('arbitrary data can be merged in the fixture', function () { + $response = connector()->send(new DTORequest, new MockClient([ + MockResponse::fixture('user', merge: [ + 'name' => 'Sam Carré', + ]), + ])); + + expect($response->dto()) + ->name->toBe('Sam Carré') + ->actualName->toBe('Sam') + ->twitter->toBe('@carre_sam'); +}); From f0f95ca532d88b64ed1a008f51536e7052a58654 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Sat, 22 Jun 2024 04:23:43 +0200 Subject: [PATCH 2/4] refactor: support dot-notation --- src/Helpers/ArrayHelpers.php | 39 ++++++++++++++++++++++++++++++++ src/Http/Faking/Fixture.php | 32 ++++++++++++++++---------- tests/Fixtures/Saloon/users.json | 1 + tests/Unit/FixtureDataTest.php | 16 +++++++++++++ 4 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 tests/Fixtures/Saloon/users.json diff --git a/src/Helpers/ArrayHelpers.php b/src/Helpers/ArrayHelpers.php index 6117e96b..cc4eb25c 100644 --- a/src/Helpers/ArrayHelpers.php +++ b/src/Helpers/ArrayHelpers.php @@ -75,4 +75,43 @@ public static function get(array $array, string|int|null $key, mixed $default = return $array; } + + /** + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * @param array $array + * @param string|int|null $key + * @return array + */ + public static function set(&$array, $key, $value) + { + if (is_null($key)) { + return $array = $value; + } + + $keys = explode('.', $key); + + foreach ($keys as $i => $key) { + if (count($keys) === 1) { + break; + } + + unset($keys[$i]); + + // If the key doesn't exist at this depth, we will just create an empty array + // to hold the next value, allowing us to create the arrays to hold final + // values at the correct depth. Then we'll keep digging into the array. + if (! isset($array[$key]) || ! is_array($array[$key])) { + $array[$key] = []; + } + + $array = &$array[$key]; + } + + $array[array_shift($keys)] = $value; + + return $array; + } } diff --git a/src/Http/Faking/Fixture.php b/src/Http/Faking/Fixture.php index 7268276a..d804068d 100644 --- a/src/Http/Faking/Fixture.php +++ b/src/Http/Faking/Fixture.php @@ -6,10 +6,10 @@ use Saloon\MockConfig; use Saloon\Helpers\Storage; +use Saloon\Helpers\ArrayHelpers; use Saloon\Data\RecordedResponse; use Saloon\Helpers\FixtureHelper; use Saloon\Exceptions\FixtureException; -use Saloon\Contracts\Body\MergeableBody; use Saloon\Exceptions\FixtureMissingException; use Saloon\Repositories\Body\StringBodyRepository; @@ -56,22 +56,30 @@ public function getMockResponse(): ?MockResponse if ($storage->exists($fixturePath)) { $response = RecordedResponse::fromFile($storage->get($fixturePath))->toMockResponse(); - if ($response->body() instanceof MergeableBody && is_array($this->merge)) { - $response->body()->merge($this->merge); + if (! is_array($this->merge)) { + return $response; + } - return; + // First, we get the body as an array. If we're dealing with + // a `StringBodyRepository`, we have to encode it first. + if (! is_array($body = $response->body()->all())) { + $body = json_decode($body ?: '[]', associative: true, flags: \JSON_THROW_ON_ERROR); } - if ($response->body() instanceof StringBodyRepository && is_array($this->merge)) { - try { - $response->body()->set(json_encode(array_merge( - json_decode($response->body()->all() ?: '[]', associative: true, flags: JSON_THROW_ON_ERROR), - $this->merge - ))); - } catch (\Throwable) { - } + // We can then merge the data in the body using + // the ArrayHelpers for dot-notation support. + foreach ($this->merge as $key => $value) { + ArrayHelpers::set($body, $key, $value); } + // We then set the mutated data back in the repository. If we're dealing + // with a `StringBodyRepository`, we need to encode it back to string. + $response->body()->set( + $response->body() instanceof StringBodyRepository + ? json_encode($body) + : $body + ); + return $response; } diff --git a/tests/Fixtures/Saloon/users.json b/tests/Fixtures/Saloon/users.json new file mode 100644 index 00000000..da3dc98d --- /dev/null +++ b/tests/Fixtures/Saloon/users.json @@ -0,0 +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\"}]}"} diff --git a/tests/Unit/FixtureDataTest.php b/tests/Unit/FixtureDataTest.php index 6b8a4e53..8b62729e 100644 --- a/tests/Unit/FixtureDataTest.php +++ b/tests/Unit/FixtureDataTest.php @@ -4,6 +4,7 @@ namespace Saloon\Tests\Unit; +use Pest\Expectation; use Saloon\Data\RecordedResponse; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; @@ -75,3 +76,18 @@ ->actualName->toBe('Sam') ->twitter->toBe('@carre_sam'); }); + +test('arbitrary data using dot-notation can be merged in the fixture', function () { + $response = connector()->send(new DTORequest, new MockClient([ + MockResponse::fixture('users', merge: [ + 'data.0.twitter' => '@jon_doe', + ]), + ])); + + expect($response->json('data')) + ->toHaveCount(2) + ->sequence( + fn (Expectation $e) => $e->twitter->toBe('@jon_doe'), + fn (Expectation $e) => $e->twitter->toBe('@janedoe'), + ); +}); From b2c232860f1697dcf9f14d3d43c113108b89baea Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Wed, 26 Jun 2024 08:51:23 +0200 Subject: [PATCH 3/4] refactor: add chainable `merge` instead of using a parameter --- src/Http/Faking/FakeResponse.php | 4 ++-- src/Http/Faking/Fixture.php | 11 ++++++++++- tests/Unit/FixtureDataTest.php | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Http/Faking/FakeResponse.php b/src/Http/Faking/FakeResponse.php index f7025998..dfa7db01 100644 --- a/src/Http/Faking/FakeResponse.php +++ b/src/Http/Faking/FakeResponse.php @@ -111,9 +111,9 @@ public function getException(PendingRequest $pendingRequest): ?Throwable /** * Create a new mock response from a fixture */ - public static function fixture(string $name, array $merge = []): Fixture + public static function fixture(string $name): Fixture { - return new Fixture($name, merge: $merge); + return new Fixture($name); } /** diff --git a/src/Http/Faking/Fixture.php b/src/Http/Faking/Fixture.php index d804068d..f21c7bfa 100644 --- a/src/Http/Faking/Fixture.php +++ b/src/Http/Faking/Fixture.php @@ -38,11 +38,20 @@ class Fixture /** * Constructor */ - public function __construct(string $name = '', Storage $storage = null, array $merge = []) + public function __construct(string $name = '', Storage $storage = null) { $this->name = $name; $this->storage = $storage ?? new Storage(MockConfig::getFixturePath(), true); + } + + /** + * Specify data to merge with the mock response data. + */ + public function merge(array $merge = []): static + { $this->merge = $merge; + + return $this; } /** diff --git a/tests/Unit/FixtureDataTest.php b/tests/Unit/FixtureDataTest.php index 8b62729e..0b936adf 100644 --- a/tests/Unit/FixtureDataTest.php +++ b/tests/Unit/FixtureDataTest.php @@ -66,7 +66,7 @@ test('arbitrary data can be merged in the fixture', function () { $response = connector()->send(new DTORequest, new MockClient([ - MockResponse::fixture('user', merge: [ + MockResponse::fixture('user')->merge([ 'name' => 'Sam Carré', ]), ])); @@ -79,7 +79,7 @@ test('arbitrary data using dot-notation can be merged in the fixture', function () { $response = connector()->send(new DTORequest, new MockClient([ - MockResponse::fixture('users', merge: [ + MockResponse::fixture('users')->merge([ 'data.0.twitter' => '@jon_doe', ]), ])); From e3b69417f463819a99bcf1fcf83d33602b2def76 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Mon, 5 Aug 2024 02:12:05 +0200 Subject: [PATCH 4/4] feat: support closure to modify mock response data --- src/Http/Faking/Fixture.php | 33 ++++++++++++++++++++++++++++----- tests/Unit/FixtureDataTest.php | 22 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Http/Faking/Fixture.php b/src/Http/Faking/Fixture.php index f21c7bfa..f939872e 100644 --- a/src/Http/Faking/Fixture.php +++ b/src/Http/Faking/Fixture.php @@ -33,7 +33,12 @@ class Fixture /** * Data to merge in the mocked response. */ - protected array $merge = []; + protected ?array $merge = null; + + /** + * Closure to modify the returned data with. + */ + protected ?\Closure $through = null; /** * Constructor @@ -54,6 +59,16 @@ public function merge(array $merge = []): static return $this; } + /** + * Specify a closure to modify the mock response data with. + */ + public function through(\Closure $through): static + { + $this->through = $through; + + return $this; + } + /** * Attempt to get the mock response from the fixture. */ @@ -65,7 +80,7 @@ public function getMockResponse(): ?MockResponse if ($storage->exists($fixturePath)) { $response = RecordedResponse::fromFile($storage->get($fixturePath))->toMockResponse(); - if (! is_array($this->merge)) { + if (is_null($this->merge) && is_null($this->through)) { return $response; } @@ -75,10 +90,18 @@ public function getMockResponse(): ?MockResponse $body = json_decode($body ?: '[]', associative: true, flags: \JSON_THROW_ON_ERROR); } - // We can then merge the data in the body using + // We can then merge the data in the body usingthrough // the ArrayHelpers for dot-notation support. - foreach ($this->merge as $key => $value) { - ArrayHelpers::set($body, $key, $value); + if (is_array($this->merge)) { + foreach ($this->merge as $key => $value) { + ArrayHelpers::set($body, $key, $value); + } + } + + // If specified, we pass the body through a function that + // may modify the mock response data. + if (! is_null($this->through)) { + $body = call_user_func($this->through, $body); } // We then set the mutated data back in the repository. If we're dealing diff --git a/tests/Unit/FixtureDataTest.php b/tests/Unit/FixtureDataTest.php index 0b936adf..81e239c4 100644 --- a/tests/Unit/FixtureDataTest.php +++ b/tests/Unit/FixtureDataTest.php @@ -91,3 +91,25 @@ fn (Expectation $e) => $e->twitter->toBe('@janedoe'), ); }); + +test('a closure can be used to modify the mock response data', function () { + $response = connector()->send(new DTORequest, new MockClient([ + MockResponse::fixture('users')->through(fn (array $data) => array_merge_recursive($data, [ + 'data' => [ + [ + 'name' => 'Sam', + 'actual_name' => 'Carré', + 'twitter' => '@carre_sam', + ], + ], + ])), + ])); + + expect($response->json('data')) + ->toHaveCount(3) + ->sequence( + fn (Expectation $e) => $e->twitter->toBe('@jondoe'), + fn (Expectation $e) => $e->twitter->toBe('@janedoe'), + fn (Expectation $e) => $e->twitter->toBe('@carre_sam'), + ); +});