diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6537ca4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1f35192 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Auto detect text files and perform LF normalization +* text=auto + +.gitattributes export-ignore +.github export-ignore +.gitignore export-ignore +.github/ export-ignore +tests/ export-ignore +phpunit.xml export-ignore +phpstan.neon export-ignore +psalm.xml export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5c62217 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: ci + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + laravel-tests: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + php-version: [7.4,8.0] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php-version }}" + coverage: "pcov" + + - name: Install PHP dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Run PHPUnit + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage to codecov + uses: codecov/codecov-action@v2 + with: + files: ./coverage.xml + + - name: Upload coverage to codeclimate + uses: paambaati/codeclimate-action@v3.0.0 + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: ./coverage.xml diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..5f950f4 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,18 @@ +name: release-please + +on: + push: + branches: + - main + +jobs: + update_release_draft: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: google-github-actions/release-please-action@v2 + with: + release-type: php + - uses: actions/checkout@v2 + if: ${{ steps.release.outputs.release_created }} diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..b178d90 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,37 @@ +name: static analysis + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + php-version: [7.4,8.0] + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php-version }}" + coverage: none + extensions: mbstring + tools: composer + + - name: Download dependencies + run: composer update --no-interaction --no-progress + + - name: Download PHPStan + run: composer bin phpstan require phpstan/phpstan + + - name: Execute PHPStan + run: vendor/bin/phpstan analyze --no-progress diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca314ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +/vendor/ +/vendor-bin/ +node_modules/ +npm-debug.log +yarn-error.log + +# Laravel 4 specific +bootstrap/compiled.php +app/storage/ + +# Laravel 5 & Lumen specific +public/storage +public/hot + +# Laravel 5 & Lumen specific with changed public path +public_html/storage +public_html/hot + +storage/*.key +.env +Homestead.yaml +Homestead.json +/.vagrant +.phpunit.result.cache +composer.lock diff --git a/README.md b/README.md index 7258369..63233d0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # termii-api-client-php + +[![Latest Stable Version](https://img.shields.io/github/v/release/brokeyourbike/termii-api-client-php)](https://github.com/brokeyourbike/termii-api-client-php/releases) +[![Total Downloads](https://poser.pugx.org/brokeyourbike/termii-api-client/downloads)](https://packagist.org/packages/brokeyourbike/termii-api-client) +[![License: MPL-2.0](https://img.shields.io/badge/license-MPL--2.0-purple.svg)](https://github.com/brokeyourbike/termii-api-client-php/blob/main/LICENSE) + +[![ci](https://github.com/brokeyourbike/termii-api-client-php/actions/workflows/ci.yml/badge.svg)](https://github.com/brokeyourbike/termii-api-client-php/actions/workflows/ci.yml) +[![Maintainability](https://api.codeclimate.com/v1/badges/1cd42fecafb04e6ed6ff/maintainability)](https://codeclimate.com/github/brokeyourbike/termii-api-client-php/maintainability) +[![codecov](https://codecov.io/gh/brokeyourbike/termii-api-client-php/branch/main/graph/badge.svg?token=ImcgnxzGfc)](https://codecov.io/gh/brokeyourbike/termii-api-client-php) + Termii API Client for PHP + +## Installation + +```bash +composer require brokeyourbike/termii-api-client +``` + +## Usage + +```php +use BrokeYourBike\Termii\Client; + +$apiClient = new Client($config, $httpClient); +$apiClient->fetchBalanceRaw(); +``` + +## License +[Mozilla Public License v2.0](https://github.com/brokeyourbike/termii-api-client-php/blob/main/LICENSE) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..dc1df62 --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "brokeyourbike/termii-api-client", + "description": "Termii API Client for PHP", + "type": "library", + "license": "MPL-2.0", + "autoload": { + "psr-4": { + "BrokeYourBike\\Termii\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "BrokeYourBike\\Termii\\Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "Ivan Stasiuk", + "email": "brokeyourbike@gmail.com", + "homepage": "https://github.com/brokeyourbike" + } + ], + "minimum-stability": "stable", + "require": { + "php": "^7.4 || ^8.0", + "brokeyourbike/http-client": "^1.0", + "brokeyourbike/resolve-uri": "^1.0", + "myclabs/php-enum": "^1.8", + "brokeyourbike/http-enums": "^1.0", + "brokeyourbike/has-source-model": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "phpunit/phpunit": "^9.5", + "mockery/mockery": "^1.4" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..27bc5cc --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + checkMissingIterableValueType: false + level: max + paths: + - src diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..78754c5 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,19 @@ + + + + + ./tests + + + + + + src + + + diff --git a/src/ApiConfigInterface.php b/src/ApiConfigInterface.php new file mode 100644 index 0000000..f21ad0f --- /dev/null +++ b/src/ApiConfigInterface.php @@ -0,0 +1,20 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii; + +/** + * @author Ivan Stasiuk + */ +interface ApiConfigInterface +{ + public function getUrl(): string; + public function getPublicKey(): string; + public function getSecretKey(): string; + public function getWebhookSecret(): string; +} diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..a73df56 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,124 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii; + +use Psr\Http\Message\ResponseInterface; +use BrokeYourBike\Termii\OtpRequestInterface; +use BrokeYourBike\Termii\MessageInterface; +use BrokeYourBike\Termii\ApiConfigInterface; +use BrokeYourBike\ResolveUri\ResolveUriTrait; +use BrokeYourBike\HttpEnums\HttpMethodEnum; +use BrokeYourBike\HttpClient\HttpClientTrait; +use BrokeYourBike\HttpClient\HttpClientInterface; +use BrokeYourBike\HasSourceModel\SourceModelInterface; +use BrokeYourBike\HasSourceModel\HasSourceModelTrait; +use BrokeYourBike\HasSourceModel\Enums\RequestOptions; + +/** + * @author Ivan Stasiuk + */ +class Client implements HttpClientInterface +{ + use HttpClientTrait; + use ResolveUriTrait; + use HasSourceModelTrait; + + private ApiConfigInterface $config; + + public function __construct(ApiConfigInterface $config, \GuzzleHttp\ClientInterface $httpClient) + { + $this->config = $config; + $this->httpClient = $httpClient; + } + + public function fetchBalanceRaw(): ResponseInterface + { + return $this->performRequest(HttpMethodEnum::GET(), 'get-balance', []); + } + + public function sendMessage(MessageInterface $message): ResponseInterface + { + if ($message instanceof SourceModelInterface) { + $this->setSourceModel($message); + } + + return $this->performRequest(HttpMethodEnum::POST(), 'sms/send', [ + 'from' => $message->getFrom(), + 'to' => $message->getTo(), + 'sms' => $message->getMessageText(), + 'type' => $message->getMessageType(), + 'channel' => $message->getChannelType(), + ]); + } + + public function sendOneTimePassword(OtpRequestInterface $otpRequest): ResponseInterface + { + if ($otpRequest instanceof SourceModelInterface) { + $this->setSourceModel($otpRequest); + } + + return $this->performRequest(HttpMethodEnum::POST(), 'sms/otp/send', [ + 'from' => $otpRequest->getFrom(), + 'to' => $otpRequest->getTo(), + 'channel' => $otpRequest->getChannelType(), + 'message_type' => $otpRequest->getMessageType(), + 'message_text' => $otpRequest->getMessageText(), + 'pin_type' => $otpRequest->getPinType(), + 'pin_attempts' => $otpRequest->getPinAttempts(), + 'pin_time_to_live' => $otpRequest->getPinTtlMinutes(), + 'pin_length' => $otpRequest->getPinLength(), + 'pin_placeholder' => $otpRequest->getPinPlaceholder(), + ]); + } + + public function verifyOneTimePassword(OtpRequestInterface $otpRequest, string $pin): ResponseInterface + { + if ($otpRequest instanceof SourceModelInterface) { + $this->setSourceModel($otpRequest); + } + + return $this->performRequest(HttpMethodEnum::POST(), 'sms/otp/verify', [ + 'pin_id' => $otpRequest->getPinId(), + 'pin' => $pin, + ]); + } + + /** + * @param HttpMethodEnum $method + * @param string $uri + * @param array $data + * @return ResponseInterface + * + * @throws \Exception + */ + private function performRequest(HttpMethodEnum $method, string $uri, array $data): ResponseInterface + { + $options = [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + ]; + + $data['api_key'] = $this->config->getPublicKey(); + + if (HttpMethodEnum::GET()->equals($method)) { + $options[\GuzzleHttp\RequestOptions::QUERY] = $data; + } elseif (HttpMethodEnum::POST()->equals($method)) { + $options[\GuzzleHttp\RequestOptions::JSON] = $data; + } + + if ($this->getSourceModel()) { + $options[RequestOptions::SOURCE_MODEL] = $this->getSourceModel(); + } + + $uri = (string) $this->resolveUriFor($this->config->getUrl(), $uri); + return $this->httpClient->request((string) $method, $uri, $options); + } +} diff --git a/src/Enums/ChannelType.php b/src/Enums/ChannelType.php new file mode 100644 index 0000000..fb30faa --- /dev/null +++ b/src/Enums/ChannelType.php @@ -0,0 +1,24 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Enums; + +/** + * @author Ivan Stasiuk + * + * @method static ChannelType DND() + * @method static ChannelType WHATSAPP() + * @method static ChannelType GENERIC() + * @psalm-immutable + */ +final class ChannelType extends \MyCLabs\Enum\Enum +{ + private const DND = 'dnd'; + private const WHATSAPP = 'WhatsApp'; + private const GENERIC = 'generic'; +} diff --git a/src/Enums/MessageType.php b/src/Enums/MessageType.php new file mode 100644 index 0000000..9daecb9 --- /dev/null +++ b/src/Enums/MessageType.php @@ -0,0 +1,24 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Enums; + +/** + * @author Ivan Stasiuk + * + * @method static MessageType NUMERIC() + * @method static MessageType ALPHANUMERIC() + * @method static MessageType PLAIN() + * @psalm-immutable + */ +final class MessageType extends \MyCLabs\Enum\Enum +{ + private const NUMERIC = 'NUMERIC'; + private const ALPHANUMERIC = 'ALPHANUMERIC'; + private const PLAIN = 'plain'; +} diff --git a/src/Enums/PinType.php b/src/Enums/PinType.php new file mode 100644 index 0000000..160f465 --- /dev/null +++ b/src/Enums/PinType.php @@ -0,0 +1,22 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Enums; + +/** + * @author Ivan Stasiuk + * + * @method static PinType NUMERIC() + * @method static PinType ALPHANUMERIC() + * @psalm-immutable + */ +final class PinType extends \MyCLabs\Enum\Enum +{ + private const NUMERIC = 'NUMERIC'; + private const ALPHANUMERIC = 'ALPHANUMERIC'; +} diff --git a/src/MessageInterface.php b/src/MessageInterface.php new file mode 100644 index 0000000..71b8ac0 --- /dev/null +++ b/src/MessageInterface.php @@ -0,0 +1,24 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii; + +use BrokeYourBike\Termii\Enums\MessageType; +use BrokeYourBike\Termii\Enums\ChannelType; + +/** + * @author Ivan Stasiuk + */ +interface MessageInterface +{ + public function getFrom(): string; + public function getTo(): string; + public function getMessageText(): string; + public function getMessageType(): MessageType; + public function getChannelType(): ChannelType; +} diff --git a/src/OtpConfigInterface.php b/src/OtpConfigInterface.php new file mode 100644 index 0000000..3da73a6 --- /dev/null +++ b/src/OtpConfigInterface.php @@ -0,0 +1,29 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii; + +use App\Enums\Termii\PinType; +use App\Enums\Termii\MessageType; +use App\Enums\Termii\ChannelType; + +/** + * @author Ivan Stasiuk + */ +interface OtpConfigInterface +{ + public function getFrom(): string; + public function getChannelType(): ChannelType; + public function getMessageType(): MessageType; + public function getPinType(): PinType; + public function getPinAttempts(): int; + public function getPinTtlMinutes(): int; + public function getPinLength(): int; + public function getPinPlaceholder(): string; + public function getPinMessage(): string; +} diff --git a/src/OtpRequestInterface.php b/src/OtpRequestInterface.php new file mode 100644 index 0000000..0d81a12 --- /dev/null +++ b/src/OtpRequestInterface.php @@ -0,0 +1,31 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii; + +use BrokeYourBike\Termii\Enums\PinType; +use BrokeYourBike\Termii\Enums\MessageType; +use BrokeYourBike\Termii\Enums\ChannelType; + +/** + * @author Ivan Stasiuk + */ +interface OtpRequestInterface +{ + public function getPinId(): ?string; + public function getFrom(): string; + public function getTo(): string; + public function getMessageText(): string; + public function getMessageType(): MessageType; + public function getChannelType(): ChannelType; + public function getPinType(): PinType; + public function getPinAttempts(): int; + public function getPinTtlMinutes(): int; + public function getPinLength(): int; + public function getPinPlaceholder(): string; +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..2e75d37 --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,61 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\Client; +use BrokeYourBike\Termii\ApiConfigInterface; +use BrokeYourBike\ResolveUri\ResolveUriTrait; +use BrokeYourBike\HttpClient\HttpClientTrait; +use BrokeYourBike\HttpClient\HttpClientInterface; +use BrokeYourBike\HasSourceModel\HasSourceModelTrait; + +/** + * @author Ivan Stasiuk + */ +class ClientTest extends TestCase +{ + /** @test */ + public function it_implemets_http_client_interface(): void + { + /** @var ApiConfigInterface */ + $mockedConfig = $this->getMockBuilder(ApiConfigInterface::class)->getMock(); + + /** @var \GuzzleHttp\ClientInterface */ + $mockedHttpClient = $this->getMockBuilder(\GuzzleHttp\ClientInterface::class)->getMock(); + + $api = new Client($mockedConfig, $mockedHttpClient); + + $this->assertInstanceOf(HttpClientInterface::class, $api); + } + + /** @test */ + public function it_uses_http_client_trait(): void + { + $usedTraits = class_uses(Client::class); + + $this->assertArrayHasKey(HttpClientTrait::class, $usedTraits); + } + + /** @test */ + public function it_uses_resolve_uri_trait(): void + { + $usedTraits = class_uses(Client::class); + + $this->assertArrayHasKey(ResolveUriTrait::class, $usedTraits); + } + + /** @test */ + public function it_uses_has_source_model_trait(): void + { + $usedTraits = class_uses(Client::class); + + $this->assertArrayHasKey(HasSourceModelTrait::class, $usedTraits); + } +} diff --git a/tests/Enums/ChannelTypeTest.php b/tests/Enums/ChannelTypeTest.php new file mode 100644 index 0000000..d74f744 --- /dev/null +++ b/tests/Enums/ChannelTypeTest.php @@ -0,0 +1,37 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests\Enums; + +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\Enums\ChannelType; + +/** + * @author Ivan Stasiuk + */ +class ChannelTypeTest extends TestCase +{ + /** @test */ + public function it_extends_myclabs_enum(): void + { + $parent = get_parent_class(ChannelType::class); + + $this->assertSame(\MyCLabs\Enum\Enum::class, $parent); + } + + /** @test */ + public function it_has_not_duplicate_values() + { + $allValuesRaw = ChannelType::toArray(); + $this->assertNotEmpty($allValuesRaw); + + $uniqueValuesraw = array_unique($allValuesRaw, SORT_STRING); + + $this->assertEquals($allValuesRaw, $uniqueValuesraw); + } +} diff --git a/tests/Enums/MessageTypeTest.php b/tests/Enums/MessageTypeTest.php new file mode 100644 index 0000000..2e7dca2 --- /dev/null +++ b/tests/Enums/MessageTypeTest.php @@ -0,0 +1,37 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests\Enums; + +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\Enums\MessageType; + +/** + * @author Ivan Stasiuk + */ +class MessageTypeTest extends TestCase +{ + /** @test */ + public function it_extends_myclabs_enum(): void + { + $parent = get_parent_class(MessageType::class); + + $this->assertSame(\MyCLabs\Enum\Enum::class, $parent); + } + + /** @test */ + public function it_has_not_duplicate_values() + { + $allValuesRaw = MessageType::toArray(); + $this->assertNotEmpty($allValuesRaw); + + $uniqueValuesraw = array_unique($allValuesRaw, SORT_STRING); + + $this->assertEquals($allValuesRaw, $uniqueValuesraw); + } +} diff --git a/tests/Enums/PinTypeTest.php b/tests/Enums/PinTypeTest.php new file mode 100644 index 0000000..d30ecbf --- /dev/null +++ b/tests/Enums/PinTypeTest.php @@ -0,0 +1,37 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests\Enums; + +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\Enums\PinType; + +/** + * @author Ivan Stasiuk + */ +class PinTypeTest extends TestCase +{ + /** @test */ + public function it_extends_myclabs_enum(): void + { + $parent = get_parent_class(PinType::class); + + $this->assertSame(\MyCLabs\Enum\Enum::class, $parent); + } + + /** @test */ + public function it_has_not_duplicate_values() + { + $allValuesRaw = PinType::toArray(); + $this->assertNotEmpty($allValuesRaw); + + $uniqueValuesraw = array_unique($allValuesRaw, SORT_STRING); + + $this->assertEquals($allValuesRaw, $uniqueValuesraw); + } +} diff --git a/tests/FetchBalanceRawTest.php b/tests/FetchBalanceRawTest.php new file mode 100644 index 0000000..eca5c76 --- /dev/null +++ b/tests/FetchBalanceRawTest.php @@ -0,0 +1,73 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\Client; +use BrokeYourBike\Termii\ApiConfigInterface; + +/** + * @author Ivan Stasiuk + */ +class FetchBalanceRawTest extends TestCase +{ + /** + * @test + */ + public function it_can_prepare_request(): void + { + $publicKey = 'some-public-key'; + + $mockedConfig = $this->getMockBuilder(ApiConfigInterface::class)->getMock(); + $mockedConfig->method('getUrl')->willReturn('https://api.example/'); + $mockedConfig->method('getPublicKey')->willReturn($publicKey); + + $mockedResponse = $this->getMockBuilder(ResponseInterface::class)->getMock(); + $mockedResponse->method('getStatusCode')->willReturn(200); + $mockedResponse->method('getBody') + ->willReturn('{ + "user": "JOHN DOE", + "balance": 4662.3, + "currency": "NGN" + }'); + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'GET', + 'https://api.example/get-balance', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::QUERY => [ + 'api_key' => $publicKey, + ], + ], + ])->once()->andReturn($mockedResponse); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($mockedConfig, $mockedClient); + $requestResult = $api->fetchBalanceRaw(); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + $this->assertSame(200, $requestResult->getStatusCode()); + } + + protected function tearDown(): void + { + parent::tearDown(); + \Mockery::close(); + } +} diff --git a/tests/SendMessageTest.php b/tests/SendMessageTest.php new file mode 100644 index 0000000..656a861 --- /dev/null +++ b/tests/SendMessageTest.php @@ -0,0 +1,125 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\MessageInterface; +use BrokeYourBike\Termii\Enums\MessageType; +use BrokeYourBike\Termii\Enums\ChannelType; +use BrokeYourBike\Termii\Client; +use BrokeYourBike\Termii\ApiConfigInterface; +use BrokeYourBike\HasSourceModel\Enums\RequestOptions; + +/** + * @author Ivan Stasiuk + */ +class SendMessageTest extends TestCase +{ + private string $publicKey = 'some-public-key'; + private object $mockedConfig; + + protected function setUp(): void + { + $this->mockedConfig = $this->getMockBuilder(ApiConfigInterface::class)->getMock(); + $this->mockedConfig->method('getUrl')->willReturn('https://api.example/'); + $this->mockedConfig->method('getPublicKey')->willReturn($this->publicKey); + } + + /** @test */ + public function it_can_prepare_request(): void + { + $mockedMessage = $this->getMockBuilder(MessageInterface::class)->getMock(); + $mockedMessage->method('getFrom')->willReturn('Jane Doe'); + $mockedMessage->method('getTo')->willReturn('John Doe'); + $mockedMessage->method('getMessageText')->willReturn('Hello John!'); + $mockedMessage->method('getMessageType')->willReturn(MessageType::ALPHANUMERIC()); + $mockedMessage->method('getChannelType')->willReturn(ChannelType::GENERIC()); + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'POST', + 'https://api.example/sms/send', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::JSON => [ + 'from' => 'Jane Doe', + 'to' => 'John Doe', + 'sms' => 'Hello John!', + 'type' => MessageType::ALPHANUMERIC(), + 'channel' => ChannelType::GENERIC(), + 'api_key' => $this->publicKey, + ], + ], + ])->once(); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($this->mockedConfig, $mockedClient); + + /** @var MessageInterface $mockedMessage */ + $requestResult = $api->sendMessage($mockedMessage); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + } + + /** @test */ + public function it_will_pass_source_model_as_option() + { + $model = $this->getMockBuilder(SourceMessageFixture::class)->getMock(); + $model->method('getMessageType')->willReturn(MessageType::ALPHANUMERIC()); + $model->method('getChannelType')->willReturn(ChannelType::GENERIC()); + + /** @var SourceMessageFixture $model */ + $model; + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'POST', + 'https://api.example/sms/send', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::JSON => [ + 'from' => $model->getFrom(), + 'to' => $model->getTo(), + 'sms' => $model->getMessageText(), + 'type' => $model->getMessageType(), + 'channel' => $model->getChannelType(), + 'api_key' => $this->publicKey, + ], + RequestOptions::SOURCE_MODEL => $model, + ], + ])->once(); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($this->mockedConfig, $mockedClient); + $requestResult = $api->sendMessage($model); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + } + + protected function tearDown(): void + { + parent::tearDown(); + \Mockery::close(); + } +} diff --git a/tests/SendOneTimePasswordTest.php b/tests/SendOneTimePasswordTest.php new file mode 100644 index 0000000..b57599d --- /dev/null +++ b/tests/SendOneTimePasswordTest.php @@ -0,0 +1,142 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\OtpRequestInterface; +use BrokeYourBike\Termii\Enums\PinType; +use BrokeYourBike\Termii\Enums\MessageType; +use BrokeYourBike\Termii\Enums\ChannelType; +use BrokeYourBike\Termii\Client; +use BrokeYourBike\Termii\ApiConfigInterface; +use BrokeYourBike\HasSourceModel\Enums\RequestOptions; + +/** + * @author Ivan Stasiuk + */ +class SendOneTimePasswordTest extends TestCase +{ + private string $publicKey = 'some-public-key'; + private object $mockedConfig; + + protected function setUp(): void + { + $this->mockedConfig = $this->getMockBuilder(ApiConfigInterface::class)->getMock(); + $this->mockedConfig->method('getUrl')->willReturn('https://api.example/'); + $this->mockedConfig->method('getPublicKey')->willReturn($this->publicKey); + } + + /** @test */ + public function it_can_prepare_request(): void + { + $mockedOtpRequest = $this->getMockBuilder(OtpRequestInterface::class)->getMock(); + $mockedOtpRequest->method('getFrom')->willReturn('Jane Doe'); + $mockedOtpRequest->method('getTo')->willReturn('John Doe'); + $mockedOtpRequest->method('getMessageText')->willReturn('Hello John!'); + $mockedOtpRequest->method('getMessageType')->willReturn(MessageType::ALPHANUMERIC()); + $mockedOtpRequest->method('getChannelType')->willReturn(ChannelType::GENERIC()); + $mockedOtpRequest->method('getPinType')->willReturn(PinType::NUMERIC()); + $mockedOtpRequest->method('getPinAttempts')->willReturn(1); + $mockedOtpRequest->method('getPinTtlMinutes')->willReturn(10); + $mockedOtpRequest->method('getPinLength')->willReturn(5); + $mockedOtpRequest->method('getPinPlaceholder')->willReturn('<12345>'); + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'POST', + 'https://api.example/sms/otp/send', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::JSON => [ + 'from' => 'Jane Doe', + 'to' => 'John Doe', + 'channel' => ChannelType::GENERIC(), + 'message_type' => MessageType::ALPHANUMERIC(), + 'message_text' => 'Hello John!', + 'pin_type' => PinType::NUMERIC(), + 'pin_attempts' => 1, + 'pin_time_to_live' => 10, + 'pin_length' => 5, + 'pin_placeholder' => '<12345>', + 'api_key' => $this->publicKey, + ], + ], + ])->once(); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($this->mockedConfig, $mockedClient); + + /** @var OtpRequestInterface $mockedOtpRequest */ + $requestResult = $api->sendOneTimePassword($mockedOtpRequest); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + } + + /** @test */ + public function it_will_pass_source_model_as_option() + { + $model = $this->getMockBuilder(SourceOtpRequestFixture::class)->getMock(); + $model->method('getMessageType')->willReturn(MessageType::ALPHANUMERIC()); + $model->method('getChannelType')->willReturn(ChannelType::GENERIC()); + $model->method('getPinType')->willReturn(PinType::NUMERIC()); + + /** @var SourceOtpRequestFixture $model */ + $model; + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'POST', + 'https://api.example/sms/otp/send', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::JSON => [ + 'from' => $model->getFrom(), + 'to' => $model->getTo(), + 'channel' => $model->getChannelType(), + 'message_type' => $model->getMessageType(), + 'message_text' => $model->getMessageText(), + 'pin_type' => $model->getPinType(), + 'pin_attempts' => $model->getPinAttempts(), + 'pin_time_to_live' => $model->getPinTtlMinutes(), + 'pin_length' => $model->getPinLength(), + 'pin_placeholder' => $model->getPinPlaceholder(), + 'api_key' => $this->publicKey, + ], + RequestOptions::SOURCE_MODEL => $model, + ], + ])->once(); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($this->mockedConfig, $mockedClient); + $requestResult = $api->sendOneTimePassword($model); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + } + + protected function tearDown(): void + { + parent::tearDown(); + \Mockery::close(); + } +} diff --git a/tests/SourceMessageFixture.php b/tests/SourceMessageFixture.php new file mode 100644 index 0000000..5669487 --- /dev/null +++ b/tests/SourceMessageFixture.php @@ -0,0 +1,18 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use BrokeYourBike\Termii\MessageInterface; +use BrokeYourBike\HasSourceModel\SourceModelInterface; + +/** + * @author Ivan Stasiuk + */ +abstract class SourceMessageFixture implements MessageInterface, SourceModelInterface +{} diff --git a/tests/SourceOtpRequestFixture.php b/tests/SourceOtpRequestFixture.php new file mode 100644 index 0000000..b907ca2 --- /dev/null +++ b/tests/SourceOtpRequestFixture.php @@ -0,0 +1,18 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use BrokeYourBike\Termii\OtpRequestInterface; +use BrokeYourBike\HasSourceModel\SourceModelInterface; + +/** + * @author Ivan Stasiuk + */ +abstract class SourceOtpRequestFixture implements OtpRequestInterface, SourceModelInterface +{} diff --git a/tests/VerifyOneTimePasswordTest.php b/tests/VerifyOneTimePasswordTest.php new file mode 100644 index 0000000..7df62e6 --- /dev/null +++ b/tests/VerifyOneTimePasswordTest.php @@ -0,0 +1,120 @@ +. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +namespace BrokeYourBike\Termii\Tests; + +use Psr\Http\Message\ResponseInterface; +use PHPUnit\Framework\TestCase; +use BrokeYourBike\Termii\OtpRequestInterface; +use BrokeYourBike\Termii\Enums\PinType; +use BrokeYourBike\Termii\Enums\MessageType; +use BrokeYourBike\Termii\Enums\ChannelType; +use BrokeYourBike\Termii\Client; +use BrokeYourBike\Termii\ApiConfigInterface; +use BrokeYourBike\HasSourceModel\Enums\RequestOptions; + +/** + * @author Ivan Stasiuk + */ +class VerifyOneTimePasswordTest extends TestCase +{ + private string $publicKey = 'some-public-key'; + private object $mockedConfig; + + protected function setUp(): void + { + $this->mockedConfig = $this->getMockBuilder(ApiConfigInterface::class)->getMock(); + $this->mockedConfig->method('getUrl')->willReturn('https://api.example/'); + $this->mockedConfig->method('getPublicKey')->willReturn($this->publicKey); + } + + /** @test */ + public function it_can_prepare_request(): void + { + $mockedOtpRequest = $this->getMockBuilder(OtpRequestInterface::class)->getMock(); + $mockedOtpRequest->method('getPinId')->willReturn('1234567'); + $mockedOtpRequest->method('getMessageType')->willReturn(MessageType::ALPHANUMERIC()); + $mockedOtpRequest->method('getChannelType')->willReturn(ChannelType::GENERIC()); + $mockedOtpRequest->method('getPinType')->willReturn(PinType::NUMERIC()); + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'POST', + 'https://api.example/sms/otp/verify', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::JSON => [ + 'pin_id' => '1234567', + 'pin' => '000111', + 'api_key' => $this->publicKey, + ], + ], + ])->once(); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($this->mockedConfig, $mockedClient); + + /** @var OtpRequestInterface $mockedOtpRequest */ + $requestResult = $api->verifyOneTimePassword($mockedOtpRequest, '000111'); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + } + + /** @test */ + public function it_will_pass_source_model_as_option(): void + { + $model = $this->getMockBuilder(SourceOtpRequestFixture::class)->getMock(); + $model->method('getPinId')->willReturn('1234567'); + $model->method('getMessageType')->willReturn(MessageType::ALPHANUMERIC()); + $model->method('getChannelType')->willReturn(ChannelType::GENERIC()); + $model->method('getPinType')->willReturn(PinType::NUMERIC()); + + /** @var \Mockery\MockInterface $mockedClient */ + $mockedClient = \Mockery::mock(\GuzzleHttp\Client::class); + $mockedClient->shouldReceive('request')->withArgs([ + 'POST', + 'https://api.example/sms/otp/verify', + [ + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'Accept' => 'application/json', + ], + \GuzzleHttp\RequestOptions::JSON => [ + 'pin_id' => '1234567', + 'pin' => '000111', + 'api_key' => $this->publicKey, + ], + RequestOptions::SOURCE_MODEL => $model, + ], + ])->once(); + + /** + * @var ApiConfigInterface $mockedConfig + * @var \GuzzleHttp\Client $mockedClient + * */ + $api = new Client($this->mockedConfig, $mockedClient); + + /** @var OtpRequestInterface $model */ + $requestResult = $api->verifyOneTimePassword($model, '000111'); + + $this->assertInstanceOf(ResponseInterface::class, $requestResult); + } + + protected function tearDown(): void + { + parent::tearDown(); + \Mockery::close(); + } +}