From 480026cd7f10709ddddbb930be0bd31c88060c21 Mon Sep 17 00:00:00 2001 From: parsilver Date: Wed, 29 May 2024 14:54:09 +0700 Subject: [PATCH 1/3] Refactor code --- .github/workflows/run-tests.yml | 4 +- composer.json | 2 +- src/Contracts/MessageParser.php | 13 +++ src/Contracts/ServerRequestFactory.php | 13 +++ src/Entity/AbstractEntity.php | 21 ----- src/MessageParser.php | 44 ++++++++++ src/Postman.php | 113 ++++++++++++------------- src/ServerRequestFactory.php | 28 ++++++ tests/MessageEntityTest.php | 28 ++++-- tests/WebhookCallbackTest.php | 2 + 10 files changed, 179 insertions(+), 89 deletions(-) create mode 100644 src/Contracts/MessageParser.php create mode 100644 src/Contracts/ServerRequestFactory.php create mode 100644 src/MessageParser.php create mode 100644 src/ServerRequestFactory.php diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index e6fdcc1..72c40ac 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,7 +9,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [8.2, 8.1, 8.0] + php: [8.3, 8.2, 8.1, 8.0] stability: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -22,7 +22,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, soap, intl, gd, exif, iconv, fileinfo + extensions: dom, curl, libxml, mbstring, zip, pcntl, bcmath, soap, intl coverage: pcov - name: Setup problem matchers diff --git a/composer.json b/composer.json index 780c521..269cd38 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "authors": [ { "name": "PA", - "role": "DevOps" + "role": "Developer" } ], "config": { diff --git a/src/Contracts/MessageParser.php b/src/Contracts/MessageParser.php new file mode 100644 index 0000000..470cf1a --- /dev/null +++ b/src/Contracts/MessageParser.php @@ -0,0 +1,13 @@ +getBody(), true) ?: []); - } - /** * Entity data */ diff --git a/src/MessageParser.php b/src/MessageParser.php new file mode 100644 index 0000000..aea5f63 --- /dev/null +++ b/src/MessageParser.php @@ -0,0 +1,44 @@ + null, + + // Algorithm used to sign the token + 'alg' => 'HS256', + ]; + + /** + * Parser constructor. + */ + public function __construct(array $config) + { + $this->config = array_merge($this->config, $config); + } + + /** + * Parse the request from Truemoney webhook. + * + * @return mixed + */ + public function parse(array $data) + { + if (! $data || ! $data['message']) { + throw new RuntimeException('Invalid request body.'); + } + + $data = JWT::decode($data['message'], new Key($this->config['secret'], $this->config['alg'])); + + return new Message((array) $data); + } +} diff --git a/src/Postman.php b/src/Postman.php index 358f14f..5da0c78 100644 --- a/src/Postman.php +++ b/src/Postman.php @@ -2,12 +2,10 @@ namespace Farzai\TruemoneyWebhook; +use Farzai\TruemoneyWebhook\Contracts\MessageParser as MessageParserContract; +use Farzai\TruemoneyWebhook\Contracts\ServerRequestFactory as ServerRequestFactoryContract; use Farzai\TruemoneyWebhook\Entity\Message; -use Firebase\JWT\JWT; -use Firebase\JWT\Key; use InvalidArgumentException; -use Nyholm\Psr7\Factory\Psr17Factory; -use Nyholm\Psr7Server\ServerRequestCreator; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; @@ -21,23 +19,23 @@ class Postman 'alg' => 'HS256', ]; + private ServerRequestFactoryContract $serverRequestFactory; + + private MessageParserContract $messageParser; + /** * Postman constructor. * * @param array $config - * Notes: Config are required keys: 'secret', - * Optional keys: 'alg', + * Notes: Config are required keys: 'secret', + * Optional keys: 'alg', */ public function __construct(array $config) { - // Validate config - // Check secret key is required - if (! isset($config['secret']) || empty($config['secret'])) { - throw new InvalidArgumentException('Invalid config. "secret" is required.'); - } - - // Set config $this->config = array_merge($this->config, $config); + $this->messageParser = new MessageParser($this->config); + + $this->setServerRequestFactory(new ServerRequestFactory); } /** @@ -47,70 +45,67 @@ public function __construct(array $config) * * @return Message */ - public function capture(ServerRequestInterface $request = null) + public function capture(?ServerRequestInterface $request = null) { + $this->validateConfig($this->getConfig()); + if (! $request) { - $factory = new Psr17Factory(); + $request = $this->serverRequestFactory->create(); + } - $creator = new ServerRequestCreator( - $factory, - $factory, - $factory, - $factory - ); + $this->validateRequest($request); - $request = $creator->fromGlobals(); - } + $jsonData = @json_decode($request->getBody()->getContents(), true) ?: []; - if ($request->getMethod() !== 'POST') { - throw new RuntimeException('Invalid request method.'); - } + return $this->messageParser->parse($jsonData); + } + + public function setServerRequestFactory(ServerRequestFactoryContract $serverRequestFactory) + { + $this->serverRequestFactory = $serverRequestFactory; - return $this->parseMessageFromRequest($request); + return $this; } - /** - * Decode jwt and return message entity. - * - * - * @return Message - * - * @throws RuntimeException - */ - public function parseMessageFromRequest(ServerRequestInterface $request) + public function getServerRequestFactory(): ServerRequestFactoryContract { - if (count($request->getHeader('Content-Type')) === 0 || $request->getHeader('Content-Type')[0] !== 'application/json') { - throw new RuntimeException('Invalid content type.'); - } + return $this->serverRequestFactory; + } - if (! $request->getBody()->getContents()) { - throw new RuntimeException('Invalid request body.'); - } + public function setConfig(array $config) + { + $this->config = array_merge($this->config, $config); - $jsonData = @json_decode($request->getBody()->getContents(), true) ?: []; + return $this; + } - return $this->parseMessageFromJsonArray($jsonData); + public function getConfig(): array + { + return $this->config; } - /** - * Parse all input data to message entity. - * - * - * @return Message - * - * @throws RuntimeException - */ - public function parseMessageFromJsonArray(array $jsonData) + private function validateConfig(array $config) { - if (! $jsonData || ! $jsonData['message']) { - throw new RuntimeException('Invalid request body.'); + // Check secret key is required + if (! isset($config['secret']) || empty($config['secret'])) { + throw new InvalidArgumentException('Invalid config. "secret" is required.'); } + } - $data = JWT::decode($jsonData['message'], new Key($this->config['secret'], $this->config['alg'])); + private function validateRequest(ServerRequestInterface $request) + { + if ($request->getMethod() !== 'POST') { + throw new RuntimeException('Invalid request method.'); + } + + $contentType = $request->getHeader('Content-Type')[0] ?? ''; - // Convert to array - $data = (array) $data; + if ($contentType !== 'application/json') { + throw new RuntimeException('Invalid content type.'); + } - return Message::fromArray($data); + if (empty($request->getBody()->getContents())) { + throw new RuntimeException('Invalid request body.'); + } } } diff --git a/src/ServerRequestFactory.php b/src/ServerRequestFactory.php new file mode 100644 index 0000000..a0b9678 --- /dev/null +++ b/src/ServerRequestFactory.php @@ -0,0 +1,28 @@ +fromGlobals(); + } +} diff --git a/tests/MessageEntityTest.php b/tests/MessageEntityTest.php index 592ac3a..77bece3 100644 --- a/tests/MessageEntityTest.php +++ b/tests/MessageEntityTest.php @@ -1,7 +1,10 @@ 1653538793, ]; - $entity = Message::fromArray($data); + $entity = new Message($data); expect($entity->event_type)->toBe('P2P'); expect($entity->received_time)->toBe('2022-01-31T13:02:23+0700'); @@ -24,17 +27,30 @@ }); it('should parse data from incoming request success', function () { - $server = $this->createMock(ServerRequestInterface::class); - $server->method('getBody')->willReturn(json_encode([ + $secretKey = 'secret'; + + $payload = [ 'event_type' => 'P2P', 'received_time' => '2022-01-31T13:02:23+0700', 'amount' => 100, 'sender_mobile' => '0988882222', 'message' => 'ค่าไอเทม', 'lat' => 1653538793, + ]; + + $steam = $this->createMock(StreamInterface::class); + $steam->method('getContents')->willReturn(json_encode([ + 'message' => JWT::encode($payload, $secretKey, 'HS256'), ])); - $entity = Message::fromRequest($server); + $server = $this->createMock(ServerRequestInterface::class); + $server->method('getBody')->willReturn($steam); + + $jsonData = @json_decode($server->getBody()->getContents(), true) ?: []; + + $entity = (new MessageParser([ + 'secret' => $secretKey, + ]))->parse($jsonData); expect($entity->event_type)->toBe('P2P'); expect($entity->received_time)->toBe('2022-01-31T13:02:23+0700'); @@ -54,7 +70,7 @@ 'lat' => 1653538793, ]; - $entity = Message::fromArray($data); + $entity = new Message($data); expect($entity->asJson())->toBe(json_encode($data)); }); @@ -69,7 +85,7 @@ 'lat' => 1653538793, ]; - $entity = Message::fromArray($data); + $entity = new Message($data); expect($entity->asArray())->toBe($data); }); diff --git a/tests/WebhookCallbackTest.php b/tests/WebhookCallbackTest.php index 4fd628d..c6d3256 100644 --- a/tests/WebhookCallbackTest.php +++ b/tests/WebhookCallbackTest.php @@ -45,6 +45,8 @@ $postman = new Postman([ 'secret' => '', ]); + + $postman->capture(); })->throws(InvalidArgumentException::class, 'Invalid config. "secret" is required.'); it('should error if invalid method', function () { From d009ef6eb1eb478040d1155035247250eefefcf5 Mon Sep 17 00:00:00 2001 From: parsilver Date: Wed, 29 May 2024 15:37:00 +0700 Subject: [PATCH 2/3] Add unit tests for MessageEntity and ServerFactory classes --- tests/MessageEntityTest.php | 10 +++++++++- tests/ServerFactoryTest.php | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/ServerFactoryTest.php diff --git a/tests/MessageEntityTest.php b/tests/MessageEntityTest.php index 77bece3..b191fc8 100644 --- a/tests/MessageEntityTest.php +++ b/tests/MessageEntityTest.php @@ -58,6 +58,9 @@ expect($entity->sender_mobile)->toBe('0988882222'); expect($entity->message)->toBe('ค่าไอเทม'); expect($entity->lat)->toBe(1653538793); + + expect((string) $entity)->toBe(json_encode($payload)); + }); it('can encode to json string success', function () { @@ -75,7 +78,7 @@ expect($entity->asJson())->toBe(json_encode($data)); }); -it('can get array data success', function () { +it('can get and set array data success', function () { $data = [ 'event_type' => 'P2P', 'received_time' => '2022-01-31T13:02:23+0700', @@ -88,4 +91,9 @@ $entity = new Message($data); expect($entity->asArray())->toBe($data); + + // Set new data + $entity->event_type = 'P2M'; + + expect($entity->event_type)->toBe('P2M'); }); diff --git a/tests/ServerFactoryTest.php b/tests/ServerFactoryTest.php new file mode 100644 index 0000000..d7b121a --- /dev/null +++ b/tests/ServerFactoryTest.php @@ -0,0 +1,16 @@ +create(); + + expect($request)->toBeInstanceOf(ServerRequestInterface::class); + + // Get the request method. + expect($request->getMethod())->toBe('GET'); +}); From 5d662f2388d27f5ddd96386a5b70f18e077a3f4a Mon Sep 17 00:00:00 2001 From: parsilver Date: Wed, 29 May 2024 15:38:10 +0700 Subject: [PATCH 3/3] Rename ServerFactoryTest.php --- tests/{ServerFactoryTest.php => ServerRequestFactoryTest.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ServerFactoryTest.php => ServerRequestFactoryTest.php} (100%) diff --git a/tests/ServerFactoryTest.php b/tests/ServerRequestFactoryTest.php similarity index 100% rename from tests/ServerFactoryTest.php rename to tests/ServerRequestFactoryTest.php