From eaefa2b073eb1fdb33f4ca4e687acd73a9269106 Mon Sep 17 00:00:00 2001 From: ousid Date: Thu, 24 Apr 2025 16:45:35 +0400 Subject: [PATCH 1/9] add resource tests scaffolding --- tests/ExampleTest.php | 5 ----- tests/Resources/BrandsTest.php | 3 +++ tests/Resources/CompaignsTest.php | 3 +++ tests/Resources/ListsTest.php | 3 +++ tests/Resources/SubscribersTest.php | 3 +++ 5 files changed, 12 insertions(+), 5 deletions(-) delete mode 100644 tests/ExampleTest.php create mode 100644 tests/Resources/BrandsTest.php create mode 100644 tests/Resources/CompaignsTest.php create mode 100644 tests/Resources/ListsTest.php create mode 100644 tests/Resources/SubscribersTest.php diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 5d36321..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/Resources/BrandsTest.php b/tests/Resources/BrandsTest.php new file mode 100644 index 0000000..3d9949b --- /dev/null +++ b/tests/Resources/BrandsTest.php @@ -0,0 +1,3 @@ + Date: Thu, 24 Apr 2025 17:59:31 +0400 Subject: [PATCH 2/9] refactoring & create first test --- src/DTOs/SubscribersDTO.php | 28 +++++------ src/LaravelSendy.php | 75 +++++++++++++---------------- src/Resources/Brands.php | 2 +- src/Resources/Campaigns.php | 2 +- src/Resources/Lists.php | 2 +- src/Resources/Subscribers.php | 22 ++++----- tests/Resources/BrandsTest.php | 2 - tests/Resources/SubscribersTest.php | 32 +++++++++++- 8 files changed, 93 insertions(+), 72 deletions(-) diff --git a/src/DTOs/SubscribersDTO.php b/src/DTOs/SubscribersDTO.php index 0ba3734..6515604 100644 --- a/src/DTOs/SubscribersDTO.php +++ b/src/DTOs/SubscribersDTO.php @@ -19,18 +19,18 @@ public function __construct( public ?bool $boolean, ) {} - public static function rules(ValidationContext $context): array - { - return [ - 'name' => ['string', 'nullable'], - 'email' => ['required', 'string', 'email'], - 'list' => ['required', 'string'], - 'country' => ['string', 'nullable'], - 'ipaddress' => ['string', 'nullable', 'ip'], - 'referrer' => ['string', 'nullable'], - 'gdpr' => ['boolean', 'nullable'], - 'silent' => ['boolean', 'nullable'], - 'boolean' => ['boolean', 'nullable'], - ]; - } + // public static function rules(ValidationContext $context): array + // { + // return [ + // 'name' => ['string', 'nullable'], + // 'email' => ['required', 'string', 'email'], + // 'list' => ['required', 'string'], + // 'country' => ['string', 'nullable'], + // 'ipaddress' => ['string', 'nullable', 'ip'], + // 'referrer' => ['string', 'nullable'], + // 'gdpr' => ['boolean', 'nullable'], + // 'silent' => ['boolean', 'nullable'], + // 'boolean' => ['boolean', 'nullable'], + // ]; + // } } diff --git a/src/LaravelSendy.php b/src/LaravelSendy.php index 5dd50c2..93fc072 100644 --- a/src/LaravelSendy.php +++ b/src/LaravelSendy.php @@ -2,13 +2,8 @@ namespace Coderflex\LaravelSendy; -use Coderflex\LaravelSendy\Resources\Resources\Brands; -use Coderflex\LaravelSendy\Resources\Resources\Campaigns; -use Coderflex\LaravelSendy\Resources\Resources\Lists; -use Coderflex\LaravelSendy\Resources\Resources\Subscribers; use Exception; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ClientException; +use Illuminate\Support\Facades\Http; class LaravelSendy { @@ -30,77 +25,75 @@ public function __construct() $this->apiUrl = config('laravel-sendy.api_url'); } - public function subscribers(): Subscribers + public function subscribers(): Resources\Subscribers { - return new Subscribers; + return new Resources\Subscribers; } - public function lists(): Lists + public function lists(): Resources\Lists { - return new Lists; + return new Resources\Lists; } - public function brands(): Brands + public function brands(): Resources\Brands { - return new Brands; + return new Resources\Brands; } - public function campaigns(): Campaigns + public function campaigns(): Resources\Campaigns { - return new Campaigns; + return new Resources\Campaigns; } - public function __call(string $function, array $args) + public function __call(string $function, array $args): mixed { $options = ['get', 'post', 'put', 'delete', 'patch']; - $path = (isset($args[0])) ? $args[0] : null; - $data = (isset($args[1])) ? $args[1] : []; - $headers = (isset($args[2])) ? $args[2] : []; + $path = $args[0] ?? null; + $data = $args[1] ?? []; + $async = $args[2] ?? false; + $headers = $args[3] ?? []; if (! in_array($function, $options)) { throw new Exception("Method {$function} not found."); } - return self::guzzle( + return self::sendRequest( type: $function, request: $path, data: $data, - headers: $headers + headers: $headers, + async: $async ); } /** * @throws \Exception */ - protected function guzzle(string $type, string $request, array $data = [], array $headers = []): mixed - { + protected function sendRequest( + string $type, + string $request, + array $data = [], + array $headers = [], + bool $async = false + ): mixed { try { - $client = new Client; - - $mainHeaders = [ + $mainHeaders = array_merge([ 'Content-Type' => 'application/json', 'Accept' => 'application/json', - ]; - - $headers = is_array($headers) && count($headers) > 0 - ? array_merge($mainHeaders, $headers) - : $mainHeaders; + ], $headers ?? []); - $response = $client->{$type}($this->apiUrl.$request, [ - 'headers' => $headers, - 'body' => json_encode(array_merge($data, [ - 'api_key' => $this->apiKey, - ])), + $payload = array_merge($data, [ + 'api_key' => $this->apiKey, ]); - $responseObject = $response->getBody()->getContents(); + $url = str_replace('//', '/', "$this->apiUrl/$request"); - return $this->isJson($responseObject) - ? json_decode($responseObject, true) - : $responseObject; + $client = Http::withHeaders($headers); + + return $async + ? $client->async()->{$type}($url, $payload) + : $client->{$type}($url, $payload); - } catch (ClientException $th) { - throw new Exception('Error: '.$th->getMessage()); } catch (Exception $th) { throw new Exception('Error: '.$th->getMessage()); } diff --git a/src/Resources/Brands.php b/src/Resources/Brands.php index 03da1af..9afd645 100644 --- a/src/Resources/Brands.php +++ b/src/Resources/Brands.php @@ -1,6 +1,6 @@ toArray(); - return LaravelSendy::post('subscribe', $data); + return LaravelSendy::post('subscribe', $data, $async); } - public function unsubscribe(int $listId, string $email, bool $plainTextResponse) + public function unsubscribe(int $listId, string $email, bool $plainTextResponse, bool $async = false) { $data = http_build_query([ 'list' => $listId, @@ -22,35 +22,35 @@ public function unsubscribe(int $listId, string $email, bool $plainTextResponse) 'boolean' => $plainTextResponse, ]); - return LaravelSendy::post('/api/subscribers/unsubscribe.php', $data); + return LaravelSendy::post('api/subscribers/unsubscribe.php', $data, $async); } - public function delete(int $listId, string $email) + public function delete(int $listId, string $email, bool $async = false) { $data = http_build_query([ 'list_id' => $listId, 'email' => $email, ]); - return LaravelSendy::post('/api/subscribers/delete.php', $data); + return LaravelSendy::post('api/subscribers/delete.php', $data, $async); } - public function status(int $listId, string $email) + public function status(int $listId, string $email, bool $async = false) { $data = http_build_query([ 'list_id' => $listId, 'email' => $email, ]); - return LaravelSendy::post('/api/subscribers/subscription-status.php', $data); + return LaravelSendy::post('api/subscribers/subscription-status.php', $data, $async); } - public function count(int $listId) + public function count(int $listId, bool $async = false) { $data = http_build_query([ 'list_id' => $listId, ]); - return LaravelSendy::post('/api/subscribers/active-subscriber-count.php', $data); + return LaravelSendy::post('api/subscribers/subscriber-count.php', $data, $async); } } diff --git a/tests/Resources/BrandsTest.php b/tests/Resources/BrandsTest.php index 3d9949b..b3d9bbc 100644 --- a/tests/Resources/BrandsTest.php +++ b/tests/Resources/BrandsTest.php @@ -1,3 +1 @@ 'test_api_key', + 'laravel-sendy.api_url' => 'https://sendy.test/', + ]); +}); + +it('can subscribe a user', function () { + Http::fake([ + 'https://sendy.test/subscribe' => Http::response(['success' => true], 200), + ]); + + $response = LaravelSendy::subscribers()->subscribe([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'list' => 'abc123', + 'country' => 'UAE', + ]); + + expect($response)->toBe(['success' => true]); + + Http::assertSent(function ($request) { + return $request->url() === 'https://sendy.test/subscribe' && + $request['email'] === 'john@example.com' && + $request['list'] === 'abc123' && + $request['api_key'] === 'test_api_key'; + }); +}); From 6bc7338cb393ebc5540d13468f2edc7bd09bbfeb Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 10:42:41 +0400 Subject: [PATCH 3/9] make first test passes --- composer.json | 2 -- src/DTOs/SubscribersDTO.php | 23 +++++++++-------------- src/LaravelSendy.php | 11 +++-------- src/Resources/Subscribers.php | 2 +- tests/Resources/SubscribersTest.php | 2 +- 5 files changed, 14 insertions(+), 26 deletions(-) diff --git a/composer.json b/composer.json index b221f9d..b24cf97 100644 --- a/composer.json +++ b/composer.json @@ -19,9 +19,7 @@ "php": "^8.4", "ext-curl": "*", "ext-json": "*", - "guzzlehttp/guzzle": "7.x", "illuminate/contracts": "^10.0||^11.0||^12.0", - "league/oauth2-client": "^2", "spatie/laravel-data": "^4.15", "spatie/laravel-package-tools": "^1.16" }, diff --git a/src/DTOs/SubscribersDTO.php b/src/DTOs/SubscribersDTO.php index 6515604..b6bc067 100644 --- a/src/DTOs/SubscribersDTO.php +++ b/src/DTOs/SubscribersDTO.php @@ -2,9 +2,11 @@ namespace Coderflex\LaravelSendy\DTOs; +use Spatie\LaravelData\Attributes\MergeValidationRules; use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Validation\ValidationContext; +#[MergeValidationRules] class SubscribersDTO extends Data { public function __construct( @@ -19,18 +21,11 @@ public function __construct( public ?bool $boolean, ) {} - // public static function rules(ValidationContext $context): array - // { - // return [ - // 'name' => ['string', 'nullable'], - // 'email' => ['required', 'string', 'email'], - // 'list' => ['required', 'string'], - // 'country' => ['string', 'nullable'], - // 'ipaddress' => ['string', 'nullable', 'ip'], - // 'referrer' => ['string', 'nullable'], - // 'gdpr' => ['boolean', 'nullable'], - // 'silent' => ['boolean', 'nullable'], - // 'boolean' => ['boolean', 'nullable'], - // ]; - // } + public static function rules(ValidationContext $context): array + { + return [ + 'email' => ['email'], + 'ipaddress' => ['ip'], + ]; + } } diff --git a/src/LaravelSendy.php b/src/LaravelSendy.php index 93fc072..d9505f3 100644 --- a/src/LaravelSendy.php +++ b/src/LaravelSendy.php @@ -69,13 +69,8 @@ public function __call(string $function, array $args): mixed /** * @throws \Exception */ - protected function sendRequest( - string $type, - string $request, - array $data = [], - array $headers = [], - bool $async = false - ): mixed { + protected function sendRequest(string $type, string $request, array $data = [], array $headers = [], bool $async = false): mixed + { try { $mainHeaders = array_merge([ 'Content-Type' => 'application/json', @@ -86,7 +81,7 @@ protected function sendRequest( 'api_key' => $this->apiKey, ]); - $url = str_replace('//', '/', "$this->apiUrl/$request"); + $url = rtrim($this->apiUrl, '/').'/'.ltrim($request, '/'); $client = Http::withHeaders($headers); diff --git a/src/Resources/Subscribers.php b/src/Resources/Subscribers.php index e089655..74f1adb 100644 --- a/src/Resources/Subscribers.php +++ b/src/Resources/Subscribers.php @@ -9,7 +9,7 @@ class Subscribers { public function subscribe(array $data, bool $async = false) { - $data = SubscribersDTO::validateAndCreate($data)->toArray(); + $data = SubscribersDTO::validate($data); return LaravelSendy::post('subscribe', $data, $async); } diff --git a/tests/Resources/SubscribersTest.php b/tests/Resources/SubscribersTest.php index 0dd4404..f6730e3 100644 --- a/tests/Resources/SubscribersTest.php +++ b/tests/Resources/SubscribersTest.php @@ -22,7 +22,7 @@ 'country' => 'UAE', ]); - expect($response)->toBe(['success' => true]); + expect($response->json())->toBe(['success' => true]); Http::assertSent(function ($request) { return $request->url() === 'https://sendy.test/subscribe' && From d34e3e7be4e032d688a3e92029a006d7aaa04a64 Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 11:30:19 +0400 Subject: [PATCH 4/9] update subscribers unit tests --- src/DTOs/Subscribers/DeleteSubscriberDTO.php | 23 +++++++ .../SubscribeDTO.php} | 4 +- src/DTOs/Subscribers/SubscriberStatusDTO.php | 23 +++++++ src/DTOs/Subscribers/UnsubscribeDTO.php | 24 +++++++ src/Resources/Subscribers.php | 33 ++++------ tests/Resources/SubscribersTest.php | 64 ++++++++++++++++++- 6 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 src/DTOs/Subscribers/DeleteSubscriberDTO.php rename src/DTOs/{SubscribersDTO.php => Subscribers/SubscribeDTO.php} (89%) create mode 100644 src/DTOs/Subscribers/SubscriberStatusDTO.php create mode 100644 src/DTOs/Subscribers/UnsubscribeDTO.php diff --git a/src/DTOs/Subscribers/DeleteSubscriberDTO.php b/src/DTOs/Subscribers/DeleteSubscriberDTO.php new file mode 100644 index 0000000..a679be0 --- /dev/null +++ b/src/DTOs/Subscribers/DeleteSubscriberDTO.php @@ -0,0 +1,23 @@ + ['email'], + ]; + } +} diff --git a/src/DTOs/SubscribersDTO.php b/src/DTOs/Subscribers/SubscribeDTO.php similarity index 89% rename from src/DTOs/SubscribersDTO.php rename to src/DTOs/Subscribers/SubscribeDTO.php index b6bc067..ed32fdf 100644 --- a/src/DTOs/SubscribersDTO.php +++ b/src/DTOs/Subscribers/SubscribeDTO.php @@ -1,13 +1,13 @@ ['email'], + ]; + } +} diff --git a/src/DTOs/Subscribers/UnsubscribeDTO.php b/src/DTOs/Subscribers/UnsubscribeDTO.php new file mode 100644 index 0000000..cb69897 --- /dev/null +++ b/src/DTOs/Subscribers/UnsubscribeDTO.php @@ -0,0 +1,24 @@ + ['email'], + ]; + } +} diff --git a/src/Resources/Subscribers.php b/src/Resources/Subscribers.php index 74f1adb..61500b1 100644 --- a/src/Resources/Subscribers.php +++ b/src/Resources/Subscribers.php @@ -2,54 +2,47 @@ namespace Coderflex\LaravelSendy\Resources; -use Coderflex\LaravelSendy\DTOs\SubscribersDTO; +use Coderflex\LaravelSendy\DTOs\Subscribers\DeleteSubscriberDTO; +use Coderflex\LaravelSendy\DTOs\Subscribers\SubscribeDTO; +use Coderflex\LaravelSendy\DTOs\Subscribers\SubscriberStatusDTO; +use Coderflex\LaravelSendy\DTOs\Subscribers\UnsubscribeDTO; use Coderflex\LaravelSendy\Facades\LaravelSendy; class Subscribers { public function subscribe(array $data, bool $async = false) { - $data = SubscribersDTO::validate($data); + $data = SubscribeDTO::validate($data); return LaravelSendy::post('subscribe', $data, $async); } - public function unsubscribe(int $listId, string $email, bool $plainTextResponse, bool $async = false) + public function unsubscribe(array $data, bool $async = false) { - $data = http_build_query([ - 'list' => $listId, - 'email' => $email, - 'boolean' => $plainTextResponse, - ]); + $data = UnsubscribeDTO::validate($data); return LaravelSendy::post('api/subscribers/unsubscribe.php', $data, $async); } - public function delete(int $listId, string $email, bool $async = false) + public function delete(array $data, bool $async = false) { - $data = http_build_query([ - 'list_id' => $listId, - 'email' => $email, - ]); + $data = DeleteSubscriberDTO::validate($data); return LaravelSendy::post('api/subscribers/delete.php', $data, $async); } - public function status(int $listId, string $email, bool $async = false) + public function status(array $data, bool $async = false) { - $data = http_build_query([ - 'list_id' => $listId, - 'email' => $email, - ]); + $data = SubscriberStatusDTO::validate($data); return LaravelSendy::post('api/subscribers/subscription-status.php', $data, $async); } public function count(int $listId, bool $async = false) { - $data = http_build_query([ + $data = [ 'list_id' => $listId, - ]); + ]; return LaravelSendy::post('api/subscribers/subscriber-count.php', $data, $async); } diff --git a/tests/Resources/SubscribersTest.php b/tests/Resources/SubscribersTest.php index f6730e3..7d7ba94 100644 --- a/tests/Resources/SubscribersTest.php +++ b/tests/Resources/SubscribersTest.php @@ -12,7 +12,7 @@ it('can subscribe a user', function () { Http::fake([ - 'https://sendy.test/subscribe' => Http::response(['success' => true], 200), + 'https://sendy.test/subscribe' => Http::response(true, 200), ]); $response = LaravelSendy::subscribers()->subscribe([ @@ -22,7 +22,7 @@ 'country' => 'UAE', ]); - expect($response->json())->toBe(['success' => true]); + expect($response->json())->toBe(1); Http::assertSent(function ($request) { return $request->url() === 'https://sendy.test/subscribe' && @@ -31,3 +31,63 @@ $request['api_key'] === 'test_api_key'; }); }); + +it('can unsubscribe a user', function () { + Http::fake([ + 'https://sendy.test/api/subscribers/unsubscribe.php' => Http::response(true, 200), + ]); + + $response = LaravelSendy::subscribers()->unsubscribe([ + 'list' => 123, + 'email' => 'jane@example.com', + 'boolean' => true, + ]); + + expect($response->json())->toBe(1); + + Http::assertSent(function ($request) { + return $request->url() === 'https://sendy.test/api/subscribers/unsubscribe.php' && + $request['email'] === 'jane@example.com' && + $request['list'] === 123; + }); +}); + +it('can delete a subscriber', function () { + Http::fake([ + 'https://sendy.test/api/subscribers/delete.php' => Http::response(true, 200), + ]); + + $response = LaravelSendy::subscribers()->delete([ + 'list_id' => 123, + 'email' => 'john@example.com', + ]); + + expect($response->json())->toBe(1); + + Http::assertSent(fn ($request) => $request['email'] === 'john@example.com' && + $request['list_id'] === 123 + ); +}); + +it('can get subscriber status', function () { + Http::fake([ + 'https://sendy.test/api/subscribers/subscription-status.php' => Http::response(['status' => 'Subscribed'], 200), + ]); + + $response = LaravelSendy::subscribers()->status([ + 'list_id' => 123, + 'email' => 'john@example.com', + ]); + + expect($response->json())->toBe(['status' => 'Subscribed']); +}); + +it('can get subscriber count', function () { + Http::fake([ + 'https://sendy.test/api/subscribers/subscriber-count.php' => Http::response(25, 200), + ]); + + $response = LaravelSendy::subscribers()->count(123); + + expect($response->json())->toBe(25); +}); From b6fbf3ecb5e344f003ef0630e01f970e20fc13f9 Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 12:01:32 +0400 Subject: [PATCH 5/9] add campaign tests --- src/DTOs/CompaignDTO.php | 4 +-- src/Facades/LaravelSendy.php | 10 +++++++ src/Resources/Campaigns.php | 2 +- tests/Resources/CompaignsTest.php | 46 ++++++++++++++++++++++++++++++- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/DTOs/CompaignDTO.php b/src/DTOs/CompaignDTO.php index 9ec47e1..118845d 100644 --- a/src/DTOs/CompaignDTO.php +++ b/src/DTOs/CompaignDTO.php @@ -19,7 +19,7 @@ public function __construct( public string $segment_ids, public ?string $exclude_list_ids, public ?string $exclude_segment_ids, - public string $brand_id, + public ?string $brand_id, public ?string $query_string, public ?int $track_opens, public ?int $track_clicks, @@ -42,7 +42,7 @@ public static function rules(ValidationContext $context): array 'segment_ids' => ['required', 'string'], 'exclude_list_ids' => ['string', 'nullable'], 'exclude_segment_ids' => ['string', 'nullable'], - 'brand_id' => ['required', 'string'], + 'brand_id' => ['required_if', 'string'], 'query_string' => ['string', 'nullable'], 'track_opens' => ['integer', 'nullable', 'in:0,1,2'], 'track_clicks' => ['integer', 'nullable', 'in:0,1,2'], diff --git a/src/Facades/LaravelSendy.php b/src/Facades/LaravelSendy.php index 702b104..9acda2b 100644 --- a/src/Facades/LaravelSendy.php +++ b/src/Facades/LaravelSendy.php @@ -6,6 +6,16 @@ /** * @see \Coderflex\LaravelSendy\LaravelSendy + * + * @method static \Coderflex\LaravelSendy\Resources\Subscribers subscribers() + * @method static \Coderflex\LaravelSendy\Resources\Lists lists() + * @method static \Coderflex\LaravelSendy\Resources\Brands brands() + * @method static \Coderflex\LaravelSendy\Resources\Campaigns campaigns() + * @method static \Illuminate\Http\Client\Response get(string $path, array $data = [], bool $async = false, array $headers = []) + * @method static \Illuminate\Http\Client\Response post(string $path, array $data = [], bool $async = false, array $headers = []) + * @method static \Illuminate\Http\Client\Response put(string $path, array $data = [], bool $async = false, array $headers = []) + * @method static \Illuminate\Http\Client\Response delete(string $path, array $data = [], bool $async = false, array $headers = []) + * @method static \Illuminate\Http\Client\Response patch(string $path, array $data = [], bool $async = false, array $headers = []) */ class LaravelSendy extends Facade { diff --git a/src/Resources/Campaigns.php b/src/Resources/Campaigns.php index bbbeaae..a887f99 100644 --- a/src/Resources/Campaigns.php +++ b/src/Resources/Campaigns.php @@ -9,7 +9,7 @@ class Campaigns { public function create(array $data) { - $data = CompaignDTO::validateAndCreate($data)->toArray(); + $data = CompaignDTO::validate($data); return LaravelSendy::post('/api/campaigns/create.php', $data); } diff --git a/tests/Resources/CompaignsTest.php b/tests/Resources/CompaignsTest.php index 3d9949b..5f8351f 100644 --- a/tests/Resources/CompaignsTest.php +++ b/tests/Resources/CompaignsTest.php @@ -1,3 +1,47 @@ 'test_api_key', + 'laravel-sendy.api_url' => 'https://sendy.test/', + ]); +}); + +it('can create and send a campaigns', function () { + Http::fake([ + 'https://sendy.test/api/campaigns/create.php' => Http::response(['status' => 'Campaign created and now sending'], 200), + ]); + + $response = LaravelSendy::campaigns()->create([ + 'subject' => 'Test Subject', + 'from_name' => 'John Doe', + 'from_email' => 'john@example.com', + 'reply_to' => 'alex@example.com', + 'title' => 'Test Title', + 'plain_text' => 'This is a plain text version of the email.', + 'html_text' => '

This is a HTML version of the email.

', + 'list_ids' => 'abc123', + 'segment_ids' => 'xyz456', + 'exclude_list_ids' => null, + 'exclude_segment_ids' => null, + 'brand_id' => 'brand123', + 'query_string' => null, + 'track_opens' => 1, + 'track_clicks' => 1, + 'send_compaign' => 1, + 'schedule_date_time' => null, + 'schedule_timezone' => null, + ]); + + expect($response->json())->toBe(['status' => 'Campaign created and now sending']); + + Http::assertSent(function ($request) { + return $request->url() === 'https://sendy.test/api/campaigns/create.php' && + $request['from_email'] === 'john@example.com' && + $request['from_name'] === 'John Doe' && + $request['api_key'] === 'test_api_key'; + }); +}); From bb9884f25a49706028af7730e30a5fe46d1140cb Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 12:08:23 +0400 Subject: [PATCH 6/9] fix typo & refactoring --- .../CampaignDTO.php} | 18 +++++++++--------- src/Resources/Campaigns.php | 4 ++-- tests/Resources/CompaignsTest.php | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) rename src/DTOs/{CompaignDTO.php => Campaigns/CampaignDTO.php} (77%) diff --git a/src/DTOs/CompaignDTO.php b/src/DTOs/Campaigns/CampaignDTO.php similarity index 77% rename from src/DTOs/CompaignDTO.php rename to src/DTOs/Campaigns/CampaignDTO.php index 118845d..a2cf8cd 100644 --- a/src/DTOs/CompaignDTO.php +++ b/src/DTOs/Campaigns/CampaignDTO.php @@ -1,11 +1,11 @@ ['required', 'string'], 'plain_text' => ['string', 'nullable'], 'html_text' => ['required', 'string'], - 'list_ids' => ['required', 'string'], - 'segment_ids' => ['required', 'string'], + 'list_ids' => ['required_if:send_campaign,1', 'string'], + 'segment_ids' => ['required_if:send_campaign,1', 'string'], 'exclude_list_ids' => ['string', 'nullable'], 'exclude_segment_ids' => ['string', 'nullable'], - 'brand_id' => ['required_if', 'string'], + 'brand_id' => ['required_if:send_campaign,0', 'string'], 'query_string' => ['string', 'nullable'], 'track_opens' => ['integer', 'nullable', 'in:0,1,2'], 'track_clicks' => ['integer', 'nullable', 'in:0,1,2'], - 'send_compaign' => ['integer', 'nullable', 'in:0,1'], + 'send_campaign' => ['integer', 'nullable', 'in:0,1'], 'schedule_date_time' => ['date', 'nullable'], 'schedule_timezone' => ['date', 'nullable'], ]; diff --git a/src/Resources/Campaigns.php b/src/Resources/Campaigns.php index a887f99..b3fb387 100644 --- a/src/Resources/Campaigns.php +++ b/src/Resources/Campaigns.php @@ -2,14 +2,14 @@ namespace Coderflex\LaravelSendy\Resources; -use Coderflex\LaravelSendy\DTOs\CompaignDTO; +use Coderflex\LaravelSendy\DTOs\Campaigns\CampaignDTO; use Coderflex\LaravelSendy\Facades\LaravelSendy; class Campaigns { public function create(array $data) { - $data = CompaignDTO::validate($data); + $data = CampaignDTO::validate($data); return LaravelSendy::post('/api/campaigns/create.php', $data); } diff --git a/tests/Resources/CompaignsTest.php b/tests/Resources/CompaignsTest.php index 5f8351f..cd83292 100644 --- a/tests/Resources/CompaignsTest.php +++ b/tests/Resources/CompaignsTest.php @@ -31,7 +31,7 @@ 'query_string' => null, 'track_opens' => 1, 'track_clicks' => 1, - 'send_compaign' => 1, + 'send_campaign' => 1, 'schedule_date_time' => null, 'schedule_timezone' => null, ]); From 9c46cd14c6328aa39991277cc3693cd0ae3cf424 Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 12:43:43 +0400 Subject: [PATCH 7/9] add tests & refactoring --- src/Concerns/InteractsWithHttpRequests.php | 75 +++++++++++++++++++++ src/DTOs/Lists/ListsDTO.php | 21 ++++++ src/Facades/LaravelSendy.php | 5 -- src/LaravelSendy.php | 76 +--------------------- src/Resources/Lists.php | 10 ++- tests/Resources/ListsTest.php | 30 ++++++++- 6 files changed, 131 insertions(+), 86 deletions(-) create mode 100644 src/Concerns/InteractsWithHttpRequests.php create mode 100644 src/DTOs/Lists/ListsDTO.php diff --git a/src/Concerns/InteractsWithHttpRequests.php b/src/Concerns/InteractsWithHttpRequests.php new file mode 100644 index 0000000..f396d76 --- /dev/null +++ b/src/Concerns/InteractsWithHttpRequests.php @@ -0,0 +1,75 @@ + $apiKey, + ]); + + $url = rtrim($apiUrl, '/').'/'.ltrim($request, '/'); + + $client = Http::withHeaders(array_merge([ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ], $headers ?? [])); + + return $async + ? $client->async()->{$type}($url, $payload) + : $client->{$type}($url, $payload); + + } catch (Exception $th) { + throw new Exception('Error: '.$th->getMessage()); + } + } +} diff --git a/src/DTOs/Lists/ListsDTO.php b/src/DTOs/Lists/ListsDTO.php new file mode 100644 index 0000000..0c99256 --- /dev/null +++ b/src/DTOs/Lists/ListsDTO.php @@ -0,0 +1,21 @@ + ['in:yes,no'], + ]; + } +} diff --git a/src/Facades/LaravelSendy.php b/src/Facades/LaravelSendy.php index 9acda2b..f98ecbc 100644 --- a/src/Facades/LaravelSendy.php +++ b/src/Facades/LaravelSendy.php @@ -11,11 +11,6 @@ * @method static \Coderflex\LaravelSendy\Resources\Lists lists() * @method static \Coderflex\LaravelSendy\Resources\Brands brands() * @method static \Coderflex\LaravelSendy\Resources\Campaigns campaigns() - * @method static \Illuminate\Http\Client\Response get(string $path, array $data = [], bool $async = false, array $headers = []) - * @method static \Illuminate\Http\Client\Response post(string $path, array $data = [], bool $async = false, array $headers = []) - * @method static \Illuminate\Http\Client\Response put(string $path, array $data = [], bool $async = false, array $headers = []) - * @method static \Illuminate\Http\Client\Response delete(string $path, array $data = [], bool $async = false, array $headers = []) - * @method static \Illuminate\Http\Client\Response patch(string $path, array $data = [], bool $async = false, array $headers = []) */ class LaravelSendy extends Facade { diff --git a/src/LaravelSendy.php b/src/LaravelSendy.php index d9505f3..59e7257 100644 --- a/src/LaravelSendy.php +++ b/src/LaravelSendy.php @@ -2,28 +2,11 @@ namespace Coderflex\LaravelSendy; -use Exception; -use Illuminate\Support\Facades\Http; +use Coderflex\LaravelSendy\Concerns\InteractsWithHttpRequests; class LaravelSendy { - protected string $apiKey; - - protected string $apiUrl; - - public function __construct() - { - if (blank(config('laravel-sendy.api_key'))) { - throw new Exception('API Key is not set in the config file.'); - } - - if (blank(config('laravel-sendy.api_url'))) { - throw new Exception('API URL is not set in the config file.'); - } - - $this->apiKey = config('laravel-sendy.api_key'); - $this->apiUrl = config('laravel-sendy.api_url'); - } + use InteractsWithHttpRequests; public function subscribers(): Resources\Subscribers { @@ -44,59 +27,4 @@ public function campaigns(): Resources\Campaigns { return new Resources\Campaigns; } - - public function __call(string $function, array $args): mixed - { - $options = ['get', 'post', 'put', 'delete', 'patch']; - $path = $args[0] ?? null; - $data = $args[1] ?? []; - $async = $args[2] ?? false; - $headers = $args[3] ?? []; - - if (! in_array($function, $options)) { - throw new Exception("Method {$function} not found."); - } - - return self::sendRequest( - type: $function, - request: $path, - data: $data, - headers: $headers, - async: $async - ); - } - - /** - * @throws \Exception - */ - protected function sendRequest(string $type, string $request, array $data = [], array $headers = [], bool $async = false): mixed - { - try { - $mainHeaders = array_merge([ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ], $headers ?? []); - - $payload = array_merge($data, [ - 'api_key' => $this->apiKey, - ]); - - $url = rtrim($this->apiUrl, '/').'/'.ltrim($request, '/'); - - $client = Http::withHeaders($headers); - - return $async - ? $client->async()->{$type}($url, $payload) - : $client->{$type}($url, $payload); - - } catch (Exception $th) { - throw new Exception('Error: '.$th->getMessage()); - } - } - - protected function isJson(string $string): bool - { - return is_array(json_decode($string)) && - (json_last_error() === JSON_ERROR_NONE); - } } diff --git a/src/Resources/Lists.php b/src/Resources/Lists.php index e2ea148..dde78e3 100644 --- a/src/Resources/Lists.php +++ b/src/Resources/Lists.php @@ -2,6 +2,7 @@ namespace Coderflex\LaravelSendy\Resources; +use Coderflex\LaravelSendy\DTOs\Lists\ListsDTO; use Coderflex\LaravelSendy\Facades\LaravelSendy; class Lists @@ -11,13 +12,10 @@ class Lists * * @return array */ - public function get(int $brandId, bool $includeHidden = false) + public function get(array $data, bool $async = false) { - $params = http_build_query([ - 'brand_id' => $brandId, - 'include_hidden' => $includeHidden, - ]); + $data = ListsDTO::validate($data); - return LaravelSendy::get('/api/lists/get-lists.php', $params); + return LaravelSendy::post('/api/lists/get-lists.php', $data, $async); } } diff --git a/tests/Resources/ListsTest.php b/tests/Resources/ListsTest.php index 3d9949b..87a7b6d 100644 --- a/tests/Resources/ListsTest.php +++ b/tests/Resources/ListsTest.php @@ -1,3 +1,31 @@ 'test_api_key', + 'laravel-sendy.api_url' => 'https://sendy.test/', + ]); +}); + +it('can get subscriber lists', function () { + Http::fake([ + 'https://sendy.test/api/lists/get-lists.php' => Http::response([123 => 'Custom List'], 200), + ]); + + $response = LaravelSendy::lists()->get([ + 'brand_id' => 123, + 'include_hidden' => 'yes', + ]); + + expect($response->json())->toBe([123 => 'Custom List']); + + Http::assertSent(function ($request) { + return $request->url() === 'https://sendy.test/api/lists/get-lists.php' && + $request['brand_id'] === 123 && + $request['include_hidden'] === 'yes' && + $request['api_key'] === 'test_api_key'; + }); +}); From c4c0f372a4bf0c98f5f09ff25ff20c04b4d918f6 Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 13:50:11 +0400 Subject: [PATCH 8/9] add brands test --- src/Resources/Brands.php | 2 +- tests/Resources/BrandsTest.php | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Resources/Brands.php b/src/Resources/Brands.php index 9afd645..ba94cf1 100644 --- a/src/Resources/Brands.php +++ b/src/Resources/Brands.php @@ -8,6 +8,6 @@ class Brands { public function get() { - return LaravelSendy::get('/api/brands/get-brands.php'); + return LaravelSendy::post('/api/brands/get-brands.php'); } } diff --git a/tests/Resources/BrandsTest.php b/tests/Resources/BrandsTest.php index b3d9bbc..3e98357 100644 --- a/tests/Resources/BrandsTest.php +++ b/tests/Resources/BrandsTest.php @@ -1 +1,26 @@ 'test_api_key', + 'laravel-sendy.api_url' => 'https://sendy.test/', + ]); +}); + +it('can get subscriber brands', function () { + Http::fake([ + 'https://sendy.test/api/brands/get-brands.php' => Http::response([123 => 'Brand Name'], 200), + ]); + + $response = LaravelSendy::brands()->get(); + + expect($response->json())->toBe([123 => 'Brand Name']); + + Http::assertSent(function ($request) { + return $request->url() === 'https://sendy.test/api/brands/get-brands.php' && + $request['api_key'] === 'test_api_key'; + }); +}); From 500dd0c1beefe0c5d6ce607e50e8122ea9b97bc4 Mon Sep 17 00:00:00 2001 From: ousid Date: Fri, 25 Apr 2025 14:26:40 +0400 Subject: [PATCH 9/9] add endpoint http requests tests --- src/Concerns/InteractsWithHttpRequests.php | 11 ++++++-- src/Exceptions/CompaingException.php | 8 ------ src/Exceptions/InvalidApiKeyException.php | 10 +++++++ src/Exceptions/InvalidApiUrlException.php | 10 +++++++ src/Exceptions/SubscribersException.php | 8 ------ tests/Resources/EndpointsTest.php | 32 ++++++++++++++++++++++ 6 files changed, 61 insertions(+), 18 deletions(-) delete mode 100644 src/Exceptions/CompaingException.php create mode 100644 src/Exceptions/InvalidApiKeyException.php create mode 100644 src/Exceptions/InvalidApiUrlException.php delete mode 100644 src/Exceptions/SubscribersException.php create mode 100644 tests/Resources/EndpointsTest.php diff --git a/src/Concerns/InteractsWithHttpRequests.php b/src/Concerns/InteractsWithHttpRequests.php index f396d76..8469568 100644 --- a/src/Concerns/InteractsWithHttpRequests.php +++ b/src/Concerns/InteractsWithHttpRequests.php @@ -2,6 +2,8 @@ namespace Coderflex\LaravelSendy\Concerns; +use Coderflex\LaravelSendy\Exceptions\InvalidApiKeyException; +use Coderflex\LaravelSendy\Exceptions\InvalidApiUrlException; use Exception; use Illuminate\Support\Facades\Http; @@ -46,11 +48,12 @@ protected function sendRequest(string $type, string $request, array $data = [], throw_if( blank($apiKey), - new Exception('API Key is not set in the config file.') + InvalidApiKeyException::class, ); + throw_if( blank($apiUrl), - new Exception('API URL is not set in the config file.') + InvalidApiUrlException::class, ); $payload = array_merge($data, [ @@ -68,6 +71,10 @@ protected function sendRequest(string $type, string $request, array $data = [], ? $client->async()->{$type}($url, $payload) : $client->{$type}($url, $payload); + } catch (InvalidApiKeyException $th) { + throw new InvalidApiKeyException('Error: '.$th->getMessage()); + } catch (InvalidApiUrlException $th) { + throw new InvalidApiUrlException('Error: '.$th->getMessage()); } catch (Exception $th) { throw new Exception('Error: '.$th->getMessage()); } diff --git a/src/Exceptions/CompaingException.php b/src/Exceptions/CompaingException.php deleted file mode 100644 index f2aa4df..0000000 --- a/src/Exceptions/CompaingException.php +++ /dev/null @@ -1,8 +0,0 @@ - null, + 'laravel-sendy.api_url' => 'https://sendy.test', + ]); + + Http::fake([ + 'https://sendy.test/api/brands/get-brands.php' => Http::response(true, 200), + ]); + + $response = LaravelSendy::brands()->get(); + +})->throws(\Coderflex\LaravelSendy\Exceptions\InvalidApiKeyException::class); + +it('throw and exception if the api url not defined', function () { + Http::fake([ + 'https://sendy.test/api/brands/get-brands.php' => Http::response(true, 200), + ]); + + config([ + 'laravel-sendy.api_key' => 'test_api_key', + 'laravel-sendy.api_url' => null, + ]); + + $response = LaravelSendy::brands()->get(); + +})->throws(\Coderflex\LaravelSendy\Exceptions\InvalidApiUrlException::class);