From 2a21c7091bddd653379cf7d2a86de301762cec61 Mon Sep 17 00:00:00 2001
From: Krishan Koenig <krishan.koenig@googlemail.com>
Date: Wed, 4 Dec 2024 16:06:50 +0100
Subject: [PATCH] wip

---
 src/Contracts/Hydratable.php                  |  2 +-
 src/EndpointCollection/EndpointCollection.php |  2 +-
 src/Helpers/Arr.php                           | 39 +++++++++++
 src/Resources/CursorCollection.php            | 40 +++++++++++-
 src/Traits/HandlesAutoHydration.php           |  2 +-
 tests/Fixtures/SequenceMockResponse.php       |  5 ++
 tests/Helpers/ArrTest.php                     | 19 ++++++
 tests/Http/Adapter/MockMollieHttpAdapter.php  | 34 ++++++++--
 .../Requests/CreatePaymentRequestTest.php     | 39 +++++++++++
 .../GetPaginatedPaymentsRequestTest.php       | 65 +++++++++++++++++++
 tests/Resources/CursorCollectionTest.php      | 17 +++--
 tests/TestCase.php                            |  2 +-
 12 files changed, 246 insertions(+), 20 deletions(-)
 create mode 100644 tests/Http/Requests/CreatePaymentRequestTest.php
 create mode 100644 tests/Http/Requests/GetPaginatedPaymentsRequestTest.php

diff --git a/src/Contracts/Hydratable.php b/src/Contracts/Hydratable.php
index 5ec2d0b7..53563a5e 100644
--- a/src/Contracts/Hydratable.php
+++ b/src/Contracts/Hydratable.php
@@ -4,7 +4,7 @@
 
 interface Hydratable
 {
-    public static function shouldAutoHydrate(bool $shouldAutoHydrate = true): void;
+    public static function setAutoHydrate(bool $shouldAutoHydrate = true): void;
 
     /**
      * @return mixed
diff --git a/src/EndpointCollection/EndpointCollection.php b/src/EndpointCollection/EndpointCollection.php
index 4825f403..11bf8e47 100644
--- a/src/EndpointCollection/EndpointCollection.php
+++ b/src/EndpointCollection/EndpointCollection.php
@@ -16,7 +16,7 @@ public function __construct(Connector $connector)
         /**
          * Default hydration decision to true to maintain legacy compatibility.
          */
-        $connector::shouldAutoHydrate();
+        $connector::setAutoHydrate();
     }
 
     /**
diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php
index 511457c2..588e8644 100644
--- a/src/Helpers/Arr.php
+++ b/src/Helpers/Arr.php
@@ -26,6 +26,45 @@ public static function get(array $array, string $keys, $default = null)
         return $value;
     }
 
+    /**
+     * Get and remove an item from an array using "dot" notation.
+     *
+     * @param  array  $array
+     * @param  string  $key
+     * @param  mixed  $default
+     * @return mixed
+     */
+    public static function pull(array &$array, string $key, $default = null)
+    {
+        $value = static::get($array, $key, $default);
+
+        static::forget($array, $key);
+
+        return $value;
+    }
+
+    /**
+     * Remove an item from an array using "dot" notation.
+     *
+     * @param  array  $array
+     * @param  string  $key
+     */
+    public static function forget(array &$array, string $key): void
+    {
+        $keys = explode('.', $key);
+        $last = array_pop($keys);
+        $array = &$array;
+
+        foreach ($keys as $segment) {
+            if (!is_array($array) || !array_key_exists($segment, $array)) {
+                break;
+            }
+            $array = &$array[$segment];
+        }
+
+        unset($array[$last]);
+    }
+
     /**
      * Checks if the given key/s exist in the provided array.
      *
diff --git a/src/Resources/CursorCollection.php b/src/Resources/CursorCollection.php
index 68280ee0..31b55b1c 100644
--- a/src/Resources/CursorCollection.php
+++ b/src/Resources/CursorCollection.php
@@ -4,15 +4,43 @@
 
 use Generator;
 use Mollie\Api\Http\Requests\DynamicGetRequest;
+use Mollie\Api\Http\Requests\ResourceHydratableRequest;
 
 abstract class CursorCollection extends ResourceCollection
 {
+    private bool $autoHydrate = false;
+
+    public function setAutoHydrate(bool $shouldAutoHydrate = true): void
+    {
+        $this->autoHydrate = $shouldAutoHydrate;
+    }
+
+    public function shouldAutoHydrate(): bool
+    {
+        if ($this->response === null) {
+            return $this->autoHydrate;
+        }
+
+        $request = $this->response->getRequest();
+
+        /**
+         * Don't try to hydrate when the request
+         * already has auto-hydration enabled. The
+         * Hydrate Middleware will take care of that.
+         */
+        if ($request instanceof ResourceHydratableRequest && $request->shouldAutoHydrate()) {
+            return false;
+        }
+
+        return $this->autoHydrate;
+    }
+
     /**
      * Return the next set of resources when available
      *
      * @throws \Mollie\Api\Exceptions\ApiException
      */
-    final public function next(): ?CursorCollection
+    public function next(): ?CursorCollection
     {
         if (! $this->hasNext()) {
             return null;
@@ -26,7 +54,7 @@ final public function next(): ?CursorCollection
      *
      * @throws \Mollie\Api\Exceptions\ApiException
      */
-    final public function previous(): ?CursorCollection
+    public function previous(): ?CursorCollection
     {
         if (! $this->hasPrevious()) {
             return null;
@@ -37,9 +65,13 @@ final public function previous(): ?CursorCollection
 
     private function fetchCollection(string $url): CursorCollection
     {
-        return $this
+        $response = $this
             ->connector
             ->send(new DynamicGetRequest($url, static::class));
+
+        return $this->shouldAutoHydrate()
+            ? $response->toResource()
+            : $response;
     }
 
     /**
@@ -66,6 +98,8 @@ public function getAutoIterator(bool $iterateBackwards = false): LazyCollection
         $page = $this;
 
         return new LazyCollection(function () use ($page, $iterateBackwards): Generator {
+            $page->setAutoHydrate();
+
             while (true) {
                 foreach ($page as $item) {
                     yield $item;
diff --git a/src/Traits/HandlesAutoHydration.php b/src/Traits/HandlesAutoHydration.php
index 26736eee..0c868529 100644
--- a/src/Traits/HandlesAutoHydration.php
+++ b/src/Traits/HandlesAutoHydration.php
@@ -8,7 +8,7 @@ trait HandlesAutoHydration
 {
     protected static $hydrationSettingResolver = null;
 
-    public static function shouldAutoHydrate(bool $shouldAutoHydrate = true): void
+    public static function setAutoHydrate(bool $shouldAutoHydrate = true): void
     {
         static::$hydrationSettingResolver = static function () use ($shouldAutoHydrate) {
             ResourceHydratableRequest::hydrate($shouldAutoHydrate);
diff --git a/tests/Fixtures/SequenceMockResponse.php b/tests/Fixtures/SequenceMockResponse.php
index f7d2bed1..7e4fad96 100644
--- a/tests/Fixtures/SequenceMockResponse.php
+++ b/tests/Fixtures/SequenceMockResponse.php
@@ -30,4 +30,9 @@ public function pop(): MockResponse
 
         return $response;
     }
+
+    public function isEmpty(): bool
+    {
+        return count($this->responses) === 0;
+    }
 }
diff --git a/tests/Helpers/ArrTest.php b/tests/Helpers/ArrTest.php
index 4d65138f..22f2af87 100644
--- a/tests/Helpers/ArrTest.php
+++ b/tests/Helpers/ArrTest.php
@@ -17,6 +17,25 @@ public function get(): void
         $this->assertEquals('default', Arr::get($array, 'foo.baz', 'default'));
     }
 
+    /** @test */
+    public function pull(): void
+    {
+        $array = ['foo' => ['bar' => 'baz']];
+
+        $this->assertEquals('baz', Arr::pull($array, 'foo.bar'));
+        $this->assertEquals(['foo' => []], $array);
+    }
+
+    /** @test */
+    public function forget(): void
+    {
+        $array = ['foo' => ['bar' => 'baz']];
+
+        Arr::forget($array, 'foo.bar');
+
+        $this->assertEquals(['foo' => []], $array);
+    }
+
     /** @test */
     public function has(): void
     {
diff --git a/tests/Http/Adapter/MockMollieHttpAdapter.php b/tests/Http/Adapter/MockMollieHttpAdapter.php
index a54a2d5b..789e09cd 100644
--- a/tests/Http/Adapter/MockMollieHttpAdapter.php
+++ b/tests/Http/Adapter/MockMollieHttpAdapter.php
@@ -29,15 +29,13 @@ public function __construct(array $expectedResponses = [])
      */
     public function sendRequest(PendingRequest $pendingRequest): Response
     {
-        if (! Arr::has($this->expectedResponses, $requestClass = get_class($pendingRequest->getRequest()))) {
+        $requestClass = get_class($pendingRequest->getRequest());
+
+        if (! Arr::has($this->expectedResponses, $requestClass)) {
             throw new \RuntimeException('The request class '.$requestClass.' is not expected.');
         }
 
-        $mockedResponse = $this->expectedResponses[$requestClass];
-
-        if ($mockedResponse instanceof SequenceMockResponse) {
-            $mockedResponse = $mockedResponse->pop();
-        }
+        $mockedResponse = $this->getResponse($requestClass);
 
         return new Response(
             $mockedResponse->createPsrResponse(),
@@ -46,6 +44,30 @@ public function sendRequest(PendingRequest $pendingRequest): Response
         );
     }
 
+    /**
+     * Get the mocked response and remove it from the expected responses.
+     *
+     * @param string $requestClass
+     * @return MockResponse
+     */
+    private function getResponse(string $requestClass): MockResponse
+    {
+        $mockedResponse = Arr::get($this->expectedResponses, $requestClass);
+
+        if (!($mockedResponse instanceof SequenceMockResponse)) {
+            Arr::forget($this->expectedResponses, $requestClass);
+            return $mockedResponse;
+        }
+
+        $response = $mockedResponse->pop();
+
+        if ($mockedResponse->isEmpty()) {
+            Arr::forget($this->expectedResponses, $requestClass);
+        }
+
+        return $response;
+    }
+
     /**
      * {@inheritDoc}
      */
diff --git a/tests/Http/Requests/CreatePaymentRequestTest.php b/tests/Http/Requests/CreatePaymentRequestTest.php
new file mode 100644
index 00000000..14f0700c
--- /dev/null
+++ b/tests/Http/Requests/CreatePaymentRequestTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Tests\Http\Requests;
+
+use Mollie\Api\Http\Payload\CreatePaymentPayload;
+use Mollie\Api\Http\Payload\Money;
+use Mollie\Api\Http\Query\CreatePaymentQuery;
+use Mollie\Api\Http\Requests\CreatePaymentRequest;
+use Mollie\Api\Http\Response;
+use Mollie\Api\Resources\Payment;
+use Tests\Fixtures\MockClient;
+use Tests\Fixtures\MockResponse;
+use Tests\TestCase;
+
+class CreatePaymentRequestTest extends TestCase
+{
+    /** @test */
+    public function it_can_create_payment()
+    {
+        $client = new MockClient([
+            CreatePaymentRequest::class => new MockResponse(201, 'payment'),
+        ]);
+
+        $payload = new CreatePaymentPayload(
+            'Test payment',
+            new Money('EUR', '10.00'),
+            'https://example.org/redirect',
+            'https://example.org/webhook'
+        );
+
+        $request = new CreatePaymentRequest($payload);
+
+        /** @var Response */
+        $response = $client->send($request);
+
+        $this->assertTrue($response->successful());
+        $this->assertInstanceOf(Payment::class, $response->toResource());
+    }
+}
diff --git a/tests/Http/Requests/GetPaginatedPaymentsRequestTest.php b/tests/Http/Requests/GetPaginatedPaymentsRequestTest.php
new file mode 100644
index 00000000..6517fd92
--- /dev/null
+++ b/tests/Http/Requests/GetPaginatedPaymentsRequestTest.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Tests\Http\Requests;
+
+use Mollie\Api\Http\Requests\DynamicGetRequest;
+use Mollie\Api\Http\Requests\GetPaginatedPaymentsRequest;
+use Mollie\Api\Http\Response;
+use Mollie\Api\Resources\LazyCollection;
+use Mollie\Api\Resources\Payment;
+use Mollie\Api\Resources\PaymentCollection;
+use Tests\Fixtures\MockClient;
+use Tests\Fixtures\MockResponse;
+use Tests\TestCase;
+
+class GetPaginatedPaymentsRequestTest extends TestCase
+{
+    /** @test */
+    public function it_can_get_paginated_payments()
+    {
+        $client = new MockClient([
+            GetPaginatedPaymentsRequest::class => new MockResponse(200, 'payment-list'),
+        ]);
+
+        $request = new GetPaginatedPaymentsRequest();
+
+        /** @var Response */
+        $response = $client->send($request);
+
+        $this->assertTrue($response->successful());
+
+        /** @var PaymentCollection */
+        $payments = $response->toResource();
+        // Assert response was properly handled
+        $this->assertInstanceOf(PaymentCollection::class, $payments);
+        $this->assertGreaterThan(0, $payments->count());
+
+        foreach ($payments as $payment) {
+            $this->assertInstanceOf(Payment::class, $payment);
+            $this->assertEquals('payment', $payment->resource);
+        }
+    }
+
+    /** @test */
+    public function it_can_iterate_over_payments()
+    {
+        $client = new MockClient([
+            GetPaginatedPaymentsRequest::class => new MockResponse(200, 'payment-list'),
+            DynamicGetRequest::class => new MockResponse(200, 'payment-list'),
+            DynamicGetRequest::class => new MockResponse(200, 'empty-list', 'payments'),
+        ]);
+
+        $request = (new GetPaginatedPaymentsRequest())->useIterator();
+
+        /** @var Response */
+        $response = $client->send($request);
+        $this->assertTrue($response->successful());
+
+        /** @var LazyCollection */
+        $payments = $response->toResource();
+
+        foreach ($payments as $payment) {
+            $this->assertInstanceOf(Payment::class, $payment);
+        }
+    }
+}
diff --git a/tests/Resources/CursorCollectionTest.php b/tests/Resources/CursorCollectionTest.php
index 60b476ae..3500064c 100644
--- a/tests/Resources/CursorCollectionTest.php
+++ b/tests/Resources/CursorCollectionTest.php
@@ -13,13 +13,6 @@
 
 class CursorCollectionTest extends TestCase
 {
-    protected function setUp(): void
-    {
-        parent::setUp();
-
-        MockClient::shouldAutoHydrate();
-    }
-
     /** @test */
     public function can_get_next_collection_result_when_next_link_is_available()
     {
@@ -37,6 +30,8 @@ public function can_get_next_collection_result_when_next_link_is_available()
             ])
         );
 
+        $collection->setAutoHydrate();
+
         $this->assertTrue($collection->hasNext());
 
         $nextPage = $collection->next();
@@ -54,6 +49,8 @@ public function test_will_return_null_if_no_next_result_is_available()
             (object) []
         );
 
+        $collection->setAutoHydrate();
+
         $this->assertFalse($collection->hasNext());
         $this->assertNull($collection->next());
     }
@@ -74,6 +71,8 @@ public function test_can_get_previous_collection_result_when_previous_link_is_av
             ])
         );
 
+        $collection->setAutoHydrate();
+
         $this->assertTrue($collection->hasPrevious());
 
         $previousPage = $collection->previous();
@@ -91,6 +90,8 @@ public function test_will_return_null_if_no_previous_result_is_available()
             (object) []
         );
 
+        $collection->setAutoHydrate();
+
         $this->assertFalse($collection->hasPrevious());
         $this->assertNull($collection->previous());
     }
@@ -105,6 +106,8 @@ public function test_auto_paginator_returns_lazy_collection()
             (object) []
         );
 
+        $collection->setAutoHydrate();
+
         $this->assertInstanceOf(LazyCollection::class, $collection->getAutoIterator());
     }
 
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 66d6a386..3a0f5f90 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -11,6 +11,6 @@ protected function setUp(): void
     {
         parent::setUp();
 
-        MockClient::shouldAutoHydrate(false);
+        MockClient::setAutoHydrate(false);
     }
 }