diff --git a/src/Fake/ListResponseBuilder.php b/src/Fake/ListResponseBuilder.php index 92145eed..ce0dc06f 100644 --- a/src/Fake/ListResponseBuilder.php +++ b/src/Fake/ListResponseBuilder.php @@ -41,12 +41,13 @@ public function create(): MockResponse { $contents = FakeResponseLoader::load('empty-list'); - $contents = str_replace('{{ RESOURCE_ID }}', $this->collectionClass::$collectionName, $contents); + $collectionKey = $this->collectionClass::$collectionName; + $contents = str_replace('{{ RESOURCE_ID }}', $collectionKey, $contents); $data = json_decode($contents, true); $data['count'] = count($this->items); - $data['_embedded'][$this->collectionClass::$collectionName] = $this->items; + $data['_embedded'][$collectionKey] = $this->items; return new MockResponse($data); } diff --git a/src/Fake/MockResponse.php b/src/Fake/MockResponse.php index a05f8f10..68558cb0 100644 --- a/src/Fake/MockResponse.php +++ b/src/Fake/MockResponse.php @@ -76,11 +76,16 @@ public static function unprocessableEntity($body = [], string $resourceKey = '') return new self($body, 422, $resourceKey); } - public static function list(string $resourceKey = ''): ListResponseBuilder + public static function list(string $resourceKey): ListResponseBuilder { return new ListResponseBuilder($resourceKey); } + public static function resource(string $resourceKey): ResourceResponseBuilder + { + return new ResourceResponseBuilder($resourceKey); + } + public function createPsrResponse(): ResponseInterface { $psrResponse = $this diff --git a/src/Fake/ResourceResponseBuilder.php b/src/Fake/ResourceResponseBuilder.php new file mode 100644 index 00000000..00bc4146 --- /dev/null +++ b/src/Fake/ResourceResponseBuilder.php @@ -0,0 +1,104 @@ + */ + private array $embeddedBuilders = []; + + /** @var ?string */ + private ?string $currentEmbedKey = null; + + public function __construct(string $resourceClass) + { + if (! is_subclass_of($resourceClass, BaseResource::class)) { + throw new LogicException('Resource class must be a subclass of ' . BaseResource::class); + } + + $this->resourceClass = $resourceClass; + } + + /** + * Set the resource data. + */ + public function with(array $data): self + { + $this->data = $data; + + return $this; + } + + /** + * Create the mock response with the resource data and any embedded collections. + */ + public function create(): MockResponse + { + $data = $this->data; + $data['_embedded'] = []; + + foreach ($this->embeddedBuilders as $key => $builder) { + $embeddedResponse = $builder->create(); + $embeddedData = $embeddedResponse->json(); + + $data['_embedded'] = array_merge($data['_embedded'], $embeddedData['_embedded']); + } + + if (empty($data['_embedded'])) { + unset($data['_embedded']); + } + + // add standard links + if (empty($data['_links'])) { + $data['_links'] = [ + 'self' => [ + 'href' => '...', + 'type' => 'application/hal+json', + ], + 'documentation' => [ + 'href' => '...', + 'type' => 'text/html', + ], + ]; + } + + return new MockResponse($data); + } + + public function __call($method, $parameters) + { + if ($method === 'embed') { + /** @var string $collectionClass */ + $collectionClass = $parameters[0]; + + if (!isset($this->embeddedBuilders[$collectionClass])) { + $this->embeddedBuilders[$collectionClass] = new ListResponseBuilder($collectionClass); + } + + $this->currentEmbedKey = $collectionClass; + + return $this; + } + + if ($this->currentEmbedKey && isset($this->embeddedBuilders[$this->currentEmbedKey]) && method_exists($this->embeddedBuilders[$this->currentEmbedKey], $method)) { + return $this->forwardDecoratedCallTo($this->embeddedBuilders[$this->currentEmbedKey], $method, $parameters); + } + + throw new \BadMethodCallException("Method {$method} does not exist."); + } +} diff --git a/tests/Fake/MockResponseTest.php b/tests/Fake/MockResponseTest.php index 6debfb85..e2a040f5 100644 --- a/tests/Fake/MockResponseTest.php +++ b/tests/Fake/MockResponseTest.php @@ -3,7 +3,9 @@ namespace Tests\Fake; use Mollie\Api\Fake\ListResponseBuilder; +use Mollie\Api\Fake\ResourceResponseBuilder; use Mollie\Api\Fake\MockResponse; +use Mollie\Api\Resources\Payment; use Mollie\Api\Resources\PaymentCollection; use PHPUnit\Framework\TestCase; @@ -56,4 +58,12 @@ public function list_returns_list_builder() $this->assertInstanceOf(ListResponseBuilder::class, $response); } + + /** @test */ + public function resource_returns_resource_builder() + { + $response = MockResponse::resource(Payment::class); + + $this->assertInstanceOf(ResourceResponseBuilder::class, $response); + } } diff --git a/tests/Fake/ResourceResponseBuilderTest.php b/tests/Fake/ResourceResponseBuilderTest.php new file mode 100644 index 00000000..bef9a07b --- /dev/null +++ b/tests/Fake/ResourceResponseBuilderTest.php @@ -0,0 +1,269 @@ +with([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + 'status' => PaymentStatus::PAID, + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + ]) + ->create(); + + $this->assertInstanceOf(MockResponse::class, $response); + $this->assertEquals([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + 'status' => PaymentStatus::PAID, + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + ], Arr::except($response->json(), ['_links'])); + } + + /** @test */ + public function it_can_create_a_resource_response_with_embedded_resources() + { + $builder = new ResourceResponseBuilder(Payment::class); + + /** @var MockResponse $response */ + $response = $builder + ->with([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + 'status' => PaymentStatus::PAID, + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + 'createdAt' => '2023-11-07T14:42:51+00:00', + 'description' => 'the description', + 'paymentId' => 'tr_foobarfoobar', + ]) + ->embed(RefundCollection::class) + ->add([ + 'resource' => 'refund', + 'id' => 're_cFiJjuLhSw', + 'status' => RefundStatus::REFUNDED, + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + 'paymentId' => 'tr_foobarfoobar', + ]) + ->create(); + + $this->assertInstanceOf(MockResponse::class, $response); + $this->assertEquals([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + 'status' => PaymentStatus::PAID, + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + 'createdAt' => '2023-11-07T14:42:51+00:00', + 'description' => 'the description', + 'paymentId' => 'tr_foobarfoobar', + '_embedded' => [ + 'refunds' => [ + [ + 'resource' => 'refund', + 'id' => 're_cFiJjuLhSw', + 'status' => RefundStatus::REFUNDED, + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + 'paymentId' => 'tr_foobarfoobar', + ], + ], + ], + ], Arr::except($response->json(), ['_links'])); + } + + /** @test */ + public function it_can_add_multiple_embedded_resources() + { + $builder = new ResourceResponseBuilder(Payment::class); + + /** @var MockResponse $response */ + $response = $builder + ->with([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + ]) + ->embed(RefundCollection::class) + ->addMany([ + [ + 'resource' => 'refund', + 'id' => 're_1', + 'status' => RefundStatus::REFUNDED, + ], + [ + 'resource' => 'refund', + 'id' => 're_2', + 'status' => RefundStatus::PENDING, + ], + ]) + ->create(); + + $embedded = $response->json()['_embedded']['refunds']; + $this->assertCount(2, $embedded); + $this->assertEquals('re_1', $embedded[0]['id']); + $this->assertEquals('re_2', $embedded[1]['id']); + } + + /** @test */ + public function it_can_embed_multiple_collections() + { + $builder = new ResourceResponseBuilder(Payment::class); + + /** @var MockResponse $response */ + $response = $builder + ->with([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + ]) + ->embed(RefundCollection::class) + ->add([ + 'resource' => 'refund', + 'id' => 're_1', + 'status' => RefundStatus::REFUNDED, + ]) + ->embed(ChargebackCollection::class) + ->add([ + 'resource' => 'chargeback', + 'id' => 'chb_1', + 'amount' => [ + 'currency' => 'EUR', + 'value' => '14.52', + ], + ]) + ->create(); + + $embedded = $response->json()['_embedded']; + + // Assert refunds are present + $this->assertArrayHasKey('refunds', $embedded); + $this->assertCount(1, $embedded['refunds']); + $this->assertEquals('re_1', $embedded['refunds'][0]['id']); + + // Assert chargebacks are present + $this->assertArrayHasKey('chargebacks', $embedded); + $this->assertCount(1, $embedded['chargebacks']); + $this->assertEquals('chb_1', $embedded['chargebacks'][0]['id']); + } + + /** @test */ + public function it_can_switch_between_embedded_collections() + { + $builder = new ResourceResponseBuilder(Payment::class); + + /** @var MockResponse $response */ + $response = $builder + ->with([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + ]) + ->embed(RefundCollection::class) + ->add([ + 'resource' => 'refund', + 'id' => 're_1', + ]) + ->embed(ChargebackCollection::class) + ->add([ + 'resource' => 'chargeback', + 'id' => 'chb_1', + ]) + ->embed(RefundCollection::class) // Switch back to refunds + ->add([ + 'resource' => 'refund', + 'id' => 're_2', + ]) + ->create(); + + $embedded = $response->json()['_embedded']; + + // Assert both refunds were added + $this->assertCount(2, $embedded['refunds']); + $this->assertEquals('re_1', $embedded['refunds'][0]['id']); + $this->assertEquals('re_2', $embedded['refunds'][1]['id']); + + // Assert chargeback was added + $this->assertCount(1, $embedded['chargebacks']); + $this->assertEquals('chb_1', $embedded['chargebacks'][0]['id']); + } + + /** @test */ + public function it_omits_embedded_key_when_no_collections_are_embedded() + { + $builder = new ResourceResponseBuilder(Payment::class); + + /** @var MockResponse $response */ + $response = $builder + ->with([ + 'resource' => 'payment', + 'id' => 'tr_foobarfoobar', + ]) + ->create(); + + $this->assertArrayNotHasKey('_embedded', $response->json()); + } + + /** @test */ + public function it_throws_an_exception_when_resource_class_is_invalid() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Resource class must be a subclass of Mollie\Api\Resources\BaseResource'); + + new ResourceResponseBuilder(stdClass::class); + } + + /** @test */ + public function it_throws_an_exception_when_calling_undefined_methods() + { + $builder = new ResourceResponseBuilder(Payment::class); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method undefinedMethod does not exist'); + + $builder->undefinedMethod(); + } + + /** @test */ + public function it_throws_an_exception_when_adding_items_without_embedding_first() + { + $builder = new ResourceResponseBuilder(Payment::class); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Method add does not exist'); + + $builder->add(['id' => 'test']); + } +}