diff --git a/src/Resources/Model.php b/src/Resources/Model.php index d8e3e29..b388892 100644 --- a/src/Resources/Model.php +++ b/src/Resources/Model.php @@ -25,4 +25,14 @@ abstract class Model { $this->connection = $connection; } + + protected function convertDataArrayToUrlParameters(array $data): array + { + $urlFields = []; + foreach (array_filter($data) as $key => $value) { + $urlFields['fields['.$key.']'] = $value; + } + + return $urlFields; + } } diff --git a/src/Resources/Subscriber.php b/src/Resources/Subscriber.php index a8637d9..9ba442f 100644 --- a/src/Resources/Subscriber.php +++ b/src/Resources/Subscriber.php @@ -18,7 +18,6 @@ namespace D3\KlicktippPhpClient\Resources; use D3\KlicktippPhpClient\Entities\Subscriber as SubscriberEntity; use D3\KlicktippPhpClient\Entities\SubscriberList; use D3\KlicktippPhpClient\Exceptions\BaseException; -use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\RequestOptions; class Subscriber extends Model @@ -83,13 +82,16 @@ class Subscriber extends Model 'subscriber.json', [ RequestOptions::FORM_PARAMS => array_filter( - [ - 'email' => trim($mailAddress), - 'listid' => trim($listId ?? ''), - 'tagid' => trim($tagId ?? ''), - 'fields' => array_filter($fields ?? []), - 'smsnumber' => trim($smsNumber ?? ''), - ] + array_merge( + $this->convertDataArrayToUrlParameters($fields ?? []), + [ + 'email' => trim($mailAddress), + 'listid' => trim($listId ?? ''), + 'tagid' => trim($tagId ?? ''), + 'fields' => array_filter($fields ?? []), + 'smsnumber' => trim($smsNumber ?? ''), + ] + ) ), ] ) @@ -175,21 +177,24 @@ class Subscriber extends Model */ public function update( string $subscriberId, - array $fields, - string $newEmail = '', - string $newSmsNumber = '' - ): string { - return current( + ?array $fields = null, + ?string $newEmail = null, + ?string $newSmsNumber = null + ): bool { + return (bool) current( $this->connection->requestAndParse( 'PUT', 'subscriber/'.urlencode(trim($subscriberId)).'.json', [ - RequestOptions::FORM_PARAMS => [ - //ToDo: apply trim to array - 'fields' => array_filter(array_map('trim', $fields)), - 'newemail' => trim($newEmail), - 'newsmsnumber' => trim($newSmsNumber), - ], + RequestOptions::FORM_PARAMS => array_filter( + array_merge( + $this->convertDataArrayToUrlParameters($fields ?? []), + [ + 'newemail' => trim($newEmail ?? ''), + 'newsmsnumber' => trim($newSmsNumber ?? ''), + ] + ) + ), ] ) ); @@ -197,10 +202,11 @@ class Subscriber extends Model /** * @throws BaseException + * @return true */ - public function delete(string $subscriberId): string + public function delete(string $subscriberId): bool { - return current( + return (bool) current( $this->connection->requestAndParse( 'DELETE', 'subscriber/'.urlencode(trim($subscriberId)).'.json' @@ -211,20 +217,23 @@ class Subscriber extends Model /** * @throws BaseException */ - public function signin(string $apikey, string $emailAddress, array $fields, string $smsNumber): string + public function signin(string $apikey, string $emailAddress, ?array $fields = null, ?string $smsNumber = null): string { return current( $this->connection->requestAndParse( 'POST', 'subscriber/signin.json', [ - RequestOptions::FORM_PARAMS => [ - 'apikey' => trim($apikey), - 'email' => trim($emailAddress), - //ToDo: apply trim to array - 'fields' => $fields, - 'smsnumber' => trim($smsNumber), - ], + RequestOptions::FORM_PARAMS => array_filter( + array_merge( + $this->convertDataArrayToUrlParameters($fields ?? []), + [ + 'apikey' => trim($apikey), + 'email' => trim($emailAddress), + 'smsnumber' => trim($smsNumber ?? ''), + ] + ) + ), ] ) ); diff --git a/tests/integration/IntegrationTestCase.php b/tests/integration/IntegrationTestCase.php index aba36ee..ef53c9b 100644 --- a/tests/integration/IntegrationTestCase.php +++ b/tests/integration/IntegrationTestCase.php @@ -20,6 +20,7 @@ use D3\KlicktippPhpClient\tests\TestCase; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use Psr\Http\Message\ResponseInterface; /** @@ -27,11 +28,16 @@ use Psr\Http\Message\ResponseInterface; */ class IntegrationTestCase extends TestCase { + protected array $historyContainer = []; + public function getConnectionMock(ResponseInterface $response): Connection { $mock = new MockHandler([$response]); + $history = Middleware::history($this->historyContainer); + $handlerStack = HandlerStack::create($mock); + $handlerStack->push($history); $client = new Client(['handler' => $handlerStack]); $connection = new Connection('userName', 'password'); @@ -39,4 +45,9 @@ class IntegrationTestCase extends TestCase return $connection; } + + protected function getHistoryContainer(): array + { + return $this->historyContainer; + } } diff --git a/tests/integration/Resources/SubscriberTest.php b/tests/integration/Resources/SubscriberTest.php new file mode 100644 index 0000000..1d70e67 --- /dev/null +++ b/tests/integration/Resources/SubscriberTest.php @@ -0,0 +1,569 @@ + + * @link https://www.oxidmodule.com + */ + +namespace D3\KlicktippPhpClient\tests\integration\Resources; + +use D3\KlicktippPhpClient\Entities\Subscriber as SubscriberEntity; +use D3\KlicktippPhpClient\Entities\SubscriberList; +use D3\KlicktippPhpClient\Exceptions\BaseException; +use D3\KlicktippPhpClient\Resources\Subscriber; +use D3\KlicktippPhpClient\tests\integration\IntegrationTestCase; +use Generator; +use GuzzleHttp\Psr7\Response; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use ReflectionException; + +/** + * @coversNothing + */ +class SubscriberTest extends IntegrationTestCase +{ + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::index + * @dataProvider indexDataProvider + */ + public function testIndex(ResponseInterface $response, ?SubscriberList $expected, bool $expectException = false) + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'index', + ) + ); + } + + public static function indexDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[ + "29513792", + "29513793", + "29513794", + "29513795" + ]'), new SubscriberList([ + "29513792", + "29513793", + "29513794", + "29513795", + ])]; + yield 'no subscriber available' => [new Response(404, [], '["Es existieren keine Kontakte."]'), null, true]; + yield 'wrong request type' => [new Response(406, [], '["Bei der Erstellung des Objekt ist ein Fehler aufgetreten."]'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::get + * @dataProvider getDataProvider + */ + public function testGet(ResponseInterface $response, ?SubscriberEntity $expected, bool $expectException = false) + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'get', + ['155988456'] + ) + ); + } + + public static function getDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '{ + "id": "155988456", + "listid": "368370", + "optin": "28.12.2024 22:52:09", + "optin_ip": "0.0.0.0 - By API Request", + "email": "testsubscriber@mydomain.com", + "status": "Opt-In Pending", + "bounce": "Not Bounced", + "date": "", + "ip": "0.0.0.0 - By API Request", + "unsubscription": "", + "unsubscription_ip": "0.0.0.0", + "referrer": "", + "sms_phone": null, + "sms_status": null, + "sms_bounce": null, + "sms_date": "", + "sms_unsubscription": "", + "sms_referrer": null, + "fieldFirstName": "", + "fieldLastName": "", + "fieldCompanyName": "", + "fieldStreet1": "", + "fieldStreet2": "", + "fieldCity": "", + "fieldState": "", + "fieldZip": "", + "fieldCountry": "", + "fieldPrivatePhone": "", + "fieldMobilePhone": "", + "fieldPhone": "", + "fieldFax": "", + "fieldWebsite": "", + "fieldBirthday": "", + "fieldLeadValue": "" + }'), new SubscriberEntity([ + "id" => "155988456", + "listid" => "368370", + "optin" => "28.12.2024 22:52:09", + "optin_ip" => "0.0.0.0 - By API Request", + "email" => "testsubscriber@mydomain.com", + "status" => "Opt-In Pending", + "bounce" => "Not Bounced", + "date" => "", + "ip" => "0.0.0.0 - By API Request", + "unsubscription" => "", + "unsubscription_ip" => "0.0.0.0", + "referrer" => "", + "sms_phone" => null, + "sms_status" => null, + "sms_bounce" => null, + "sms_date" => "", + "sms_unsubscription" => "", + "sms_referrer" => null, + "fieldFirstName" => "", + "fieldLastName" => "", + "fieldCompanyName" => "", + "fieldStreet1" => "", + "fieldStreet2" => "", + "fieldCity" => "", + "fieldState" => "", + "fieldZip" => "", + "fieldCountry" => "", + "fieldPrivatePhone" => "", + "fieldMobilePhone" => "", + "fieldPhone" => "", + "fieldFax" => "", + "fieldWebsite" => "", + "fieldBirthday" => "", + "fieldLeadValue" => "", + ])]; + yield 'unknown id' => [new Response(404, [], ''), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::search + * @dataProvider searchDataProvider + */ + public function testSearch(ResponseInterface $response, ?string $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'search', + ['testsubscriber@mydomain.com'] + ) + ); + } + + public static function searchDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[155986456]'), '155986456']; + yield 'unknown id' => [new Response(404, [], '[Diesen Kontakt gibt es nicht.]'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::subscribe + * @dataProvider subscribeDataProvider + */ + public function testSubscribe(ResponseInterface $response, ?string $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'subscribe', + ['testsubscriber@mydomain.com'] + ) + ); + } + + public static function subscribeDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '{ + "id": "155988456", + "listid": "368370", + "optin": "28.12.2024 22:52:09", + "optin_ip": "0.0.0.0 - By API Request", + "email": "testsubscriber@mydomain.com", + "status": "Opt-In Pending", + "bounce": "Not Bounced", + "date": "", + "ip": "0.0.0.0 - By API Request", + "unsubscription": "", + "unsubscription_ip": "0.0.0.0", + "referrer": "", + "sms_phone": null, + "sms_status": null, + "sms_bounce": null, + "sms_date": "", + "sms_unsubscription": "", + "sms_referrer": null, + "fieldFirstName": "", + "fieldLastName": "", + "fieldCompanyName": "", + "fieldStreet1": "", + "fieldStreet2": "", + "fieldCity": "", + "fieldState": "", + "fieldZip": "", + "fieldCountry": "", + "fieldPrivatePhone": "", + "fieldMobilePhone": "", + "fieldPhone": "", + "fieldFax": "", + "fieldWebsite": "", + "fieldBirthday": "", + "fieldLeadValue": "" + }'), '155988456']; + yield 'missing mail' => [new Response(401, [], '{"error": 32}'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::unsubscribe + * @dataProvider unsubscribeDataProvider + */ + public function testUnsubscribe(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'unsubscribe', + ['testsubscriber@mydomain.com'] + ) + ); + } + + public static function unsubscribeDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'missing mail' => [new Response(401, [], '[Missing required argument email]'), null, true]; + yield 'unknown mail' => [new Response(406, [], '{"error": 7}'), null, true]; + yield 'pending opt in' => [new Response(406, [], '{"error": 9}'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::tag + * @dataProvider tagDataProvider + */ + public function testTag(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'tag', + ['testsubscriber@mydomain.com', ['2354758', ' 2354858']] + ) + ); + } + + public static function tagDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'missing mail' => [new Response(401, [], '[Missing required argument email]'), null, true]; + yield 'unknown mail / tag' => [new Response(406, [], '{"error": 7}'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::untag + * @dataProvider untagDataProvider + */ + public function testUntag(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'untag', + ['testsubscriber@mydomain.com', '2354758'] + ) + ); + } + + public static function untagDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'missing mail' => [new Response(401, [], '[Missing required argument email]'), null, true]; + yield 'unknown mail' => [new Response(406, [], '{"error": 7}'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::tagged + * @dataProvider taggedDataProvider + */ + public function testTagged(ResponseInterface $response, ?array $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'tagged', + ['2354758'] + ) + ); + } + + public static function taggedDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '{ + "155986456": "1735426641", + "155986457": "1735426642" + }'), [ + "155986456" => "1735426641", + "155986457" => "1735426642", + ]]; + yield 'missing tagid' => [new Response(401, [], '[Missing required argument tagid]'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::update + * @dataProvider updateDataProvider + */ + public function testUpdate(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'update', + ['2354758', ['fieldCity' => 'Berlin', 'fieldStreet2' => 'Straße unter den Linden 25'], 'mymail@mydomain.com'] + ) + ); + + /** @var RequestInterface $request */ + $request = current($this->getHistoryContainer())['request']; + $requestFormParams = $request->getBody()->getContents(); + $this->assertMatchesRegularExpression('/fields%5BfieldCity%5D=Berlin/m', $requestFormParams); + $this->assertMatchesRegularExpression('/fields%5BfieldStreet2%5D=Stra%C3%9Fe\+unter\+den\+Linden\+25/m', $requestFormParams); + $this->assertMatchesRegularExpression('/newemail=mymail%40mydomain.com/m', $requestFormParams); + } + + public static function updateDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'unknown subscriber' => [new Response(406, [], '["Der Kontakt existiert nicht."]'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::delete + * @dataProvider deleteDataProvider + */ + public function testDelete(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'delete', + ['2354758'] + ) + ); + } + + public static function deleteDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::signin + * @dataProvider signinDataProvider + */ + public function testSignin(ResponseInterface $response, ?string $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'signin', + ['7gefzp8255z8z8469', 'subsriber@mydomain.com'] + ) + ); + } + + public static function signinDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '["https://klick.marketron.io/pending/p9i3z8e6az2xxgcnzzpze5fa"]'), 'https://klick.marketron.io/pending/p9i3z8e6az2xxgcnzzpze5fa']; + yield 'missing mandatory data' => [new Response(401, [], '["Missing required argument apikey"]'), null, true]; + yield 'wrong api key' => [new Response(500, [], ''), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::signout + * @dataProvider signoutDataProvider + */ + public function testSignout(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'signout', + ['7gefzp8255z8z8469', 'subsriber@mydomain.com'] + ) + ); + } + + public static function signoutDataProvider(): Generator + { + // ToDo: unknown success result + //yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'unknown error' => [new Response(406, [], '{"error": 403}'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } + + /** + * @test + * @throws ReflectionException + * @covers \D3\KlicktippPhpClient\Resources\Subscriber::signoff + * @dataProvider signoffDataProvider + */ + public function testSignoff(ResponseInterface $response, ?bool $expected, bool $expectException = false): void + { + $sut = new Subscriber($this->getConnectionMock($response)); + + if ($expectException) { + $this->expectException(BaseException::class); + } + + $this->assertEquals( + $expected, + $this->callMethod( + $sut, + 'signoff', + ['7gefzp8255z8z8469', 'subsriber@mydomain.com'] + ) + ); + } + + public static function signoffDataProvider(): Generator + { + yield 'success' => [new Response(200, [], '[true]'), true]; + yield 'unknown subsriber' => [new Response(406, [], '{"error": 9}'), null, true]; + yield 'access denied' => [new Response(403, [], '["API Zugriff verweigert"]'), null, true]; + } +}