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();
+ }
+}