From 2bcd42f4a541a7cade5db3633b0bbac96e5dd472 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Mon, 13 Jun 2022 18:09:52 +0900 Subject: [PATCH] Add phpUnit testing for Amazon Incentives class --- composer.json | 3 +- composer.lock | 59 +- phpunit.xml | 4 + test/agcod_http_return_samples.md | 140 +++ test/aws_gift_card_tests.php | 55 +- test/phpUnit/AmazonIncentivesTest.php | 912 ++++++++++++++++++- test/phpUnit/Hook/BypassFinalHook.php | 21 + vendor/composer/autoload_classmap.php | 1 + vendor/composer/autoload_static.php | 1 + vendor/composer/installed.json | 58 ++ vendor/composer/installed.php | 15 +- vendor/dg/bypass-finals/composer.json | 26 + vendor/dg/bypass-finals/license.md | 49 + vendor/dg/bypass-finals/readme.md | 64 ++ vendor/dg/bypass-finals/src/BypassFinals.php | 312 +++++++ 15 files changed, 1665 insertions(+), 55 deletions(-) create mode 100644 test/agcod_http_return_samples.md create mode 100644 test/phpUnit/Hook/BypassFinalHook.php create mode 100644 vendor/dg/bypass-finals/composer.json create mode 100644 vendor/dg/bypass-finals/license.md create mode 100644 vendor/dg/bypass-finals/readme.md create mode 100644 vendor/dg/bypass-finals/src/BypassFinals.php diff --git a/composer.json b/composer.json index f394442..99db56c 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ }, "require-dev": { "phpunit/phpunit": "^9", - "gullevek/dotenv": "dev-master" + "gullevek/dotenv": "dev-master", + "dg/bypass-finals": "dev-master" } } diff --git a/composer.lock b/composer.lock index 6e9b29e..93f5eed 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "412ca7fe7d62707c222169f4a7deeaec", + "content-hash": "717f24a1578a5d25a0522cfb908b13cc", "packages": [], "packages-dev": [ + { + "name": "dg/bypass-finals", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/dg/bypass-finals.git", + "reference": "fb62dc6ab1a097e234fa1567943d8e87ea4d0842" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dg/bypass-finals/zipball/fb62dc6ab1a097e234fa1567943d8e87ea4d0842", + "reference": "fb62dc6ab1a097e234fa1567943d8e87ea4d0842", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.3", + "phpstan/phpstan": "^0.12" + }, + "default-branch": true, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes", + "keywords": [ + "finals", + "mocking", + "phpunit", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/dg/bypass-finals/issues", + "source": "https://github.com/dg/bypass-finals/tree/master" + }, + "time": "2022-04-14T12:24:34+00:00" + }, { "name": "doctrine/instantiator", "version": "1.5.x-dev", @@ -2090,7 +2144,8 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "gullevek/dotenv": 20 + "gullevek/dotenv": 20, + "dg/bypass-finals": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/phpunit.xml b/phpunit.xml index c333bf5..d472240 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,4 +2,8 @@ colors="true" verbose="true" > + + + + diff --git a/test/agcod_http_return_samples.md b/test/agcod_http_return_samples.md new file mode 100644 index 0000000..52673a0 --- /dev/null +++ b/test/agcod_http_return_samples.md @@ -0,0 +1,140 @@ +# Return data from AWS as json string + +## Mock tests self + +Tests run in local mock tests without connection to AWS + +### funds + +```json +{"availableFunds":{"amount":0.0,"currencyCode":"JPY"},"status":"SUCCESS","timestamp":"20220610T085450Z"} +``` + +### buy + +```json +{"cardInfo":{"cardNumber":null,"cardStatus":"Fulfilled","expirationDate":null,"value":{"amount":1000.0,"currencyCode":"JPY"}},"creationRequestId":"PartnerId_62a309167e7a4","gcClaimCode":"LJ49-AKDUV6-UYCP","gcExpirationDate":"Thu Jun 10 14:59:59 UTC 2032","gcId":"5535125272070255","status":"SUCCESS"} +``` + +### cancel + +```json +{"creationRequestId":"EG3bd_62a309167e7a4","gcId":"5535125272070255","status":"SUCCESS"} +``` + +### buy other 1 + +```json +{"cardInfo":{"cardNumber":null,"cardStatus":"RefundedToPurchaser","expirationDate":null,"value":{"amount":1000.0,"currencyCode":"JPY"}},"creationRequestId":"PartnerId_62a309167e7a4","gcClaimCode":"LJ49-AKDUV6-UYCP","gcExpirationDate":"Thu Jun 10 14:59:59 UTC 2032","gcId":"5535125272070255","status":"SUCCESS"} +``` + +### buy aother 2 + +```json +{"cardInfo":{"cardNumber":null,"cardStatus":"Fulfilled","expirationDate":null,"value":{"amount":1000.0,"currencyCode":"JPY"}},"creationRequestId":"PartnerId_62a30923e9705","gcClaimCode":"UM97-FD5QKK-WKCT","gcExpirationDate":"Thu Jun 10 14:59:59 UTC 2032","gcId":"5540334324324221","status":"SUCCESS"} +``` + +### buy other 2 (same create request id) + +```json +{"cardInfo":{"cardNumber":null,"cardStatus":"Fulfilled","expirationDate":null,"value":{"amount":1000.0,"currencyCode":"JPY"}},"creationRequestId":"PartnerId_62a30923e9705","gcClaimCode":"UM97-FD5QKK-WKCT","gcExpirationDate":"Thu Jun 10 14:59:59 UTC 2032","gcId":"5540334324324221","status":"SUCCESS"} +``` + +## AWS GCOD Mocks + +Mocking on AWS side, these will be returned if given request ID codes are sent. +A working test account must exist for this + +## success + +### buy gift card F0000 + +```json +{"cardInfo":{"cardNumber":null,"cardStatus":"Fulfilled","expirationDate":null,"value":{"amount":500.0,"currencyCode":"JPY"}},"creationRequestId":"F0000","gcClaimCode":"ZYXW-VUTS-RQPO","gcExpirationDate":null,"gcId":"ABC123ZYX987","status":"SUCCESS"} +``` + +## errors + +### F1000 -> F100 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F100","errorType":"GeneralError","message":"General Error"} +``` + +### F2003 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"InvalidAmountInput","message":"Amount can't be null"} +``` + +### F2004 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"InvalidAmountValue","message":"Amount must be larger than 0"} +``` + +### F2005 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"InvalidCurrencyCodeInput","message":"Currency Code can't be null or empty"} +``` + +### F2010 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"CardAlreadyActivatedWithDifferentRequestId","message":"The card was already activated with a different request id"} +``` + +### F2015 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"MaxAmountExceeded","message":"Max Amount Exceeded"} +``` + +### F2016 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"CurrencyCodeMismatch","message":"Currency Code Mismatch"} +``` + +### F2017 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"FractionalAmountNotAllowed","message":"Fractional Amount Not Allowed"} +``` + +### F2047 -> F200 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F200","errorType":"CancelRequestArrivedAfterTimeLimit","message":"Cancellation cannot be processed as too much time has elapsed since creation"} +``` + +### F3003 -> F300 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F300","errorType":"InsufficientFunds","message":"Insufficient Funds"} +``` + +### F3005 -> F300 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F300","errorType":"GeneralError","message":"General Error"} +``` + +### F3010 -> F300 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F300","errorType":"CustomerSurpassedDailyVelocityLimit","message":"Customer has exceeded daily velocity limit"} +``` + +### F4000 -> F400 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"RESEND"},"errorCode":"F400","errorType":"SystemTemporarilyUnavailable","message":"System Temporarily Unavailable"} +``` + +### F5000 -> F500 + +```json +{"agcodResponse":{"__type":"CreateGiftCardResponse:http://internal.amazon.com/coral/com.amazonaws.agcod/","cardInfo":null,"creationRequestId":null,"gcClaimCode":null,"gcExpirationDate":null,"gcId":null,"status":"FAILURE"},"errorCode":"F500","errorType":"GeneralError","message":"General Error"} +``` diff --git a/test/aws_gift_card_tests.php b/test/aws_gift_card_tests.php index 9808382..8f7b640 100644 --- a/test/aws_gift_card_tests.php +++ b/test/aws_gift_card_tests.php @@ -59,6 +59,7 @@ $loader->addPsr4('gullevek\\', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_ // print "LOADER:
" . print_r($loader, true) . "
"; use gullevek\AmazonIncentives\AmazonIncentives; +use gullevek\AmazonIncentives\Exceptions\AmazonErrors; use gullevek\dotEnv\DotEnv; // load env data with dotenv @@ -75,30 +76,34 @@ print "

Amazon Gift Card Incentives


"; // optional // debug: AWS_DEBUG (if not set: off) +// run info test (prints ENV vars) +$run_info_test = !empty($_GET['info']) ? true : false; +// run test to get funds info +$run_fund_test = !empty($_GET['fund']) ? true : false; +// run the normal get/cancel gift card tests +$run_gift_tests = !empty($_GET['gift']) ? true : false; +// run mock error check tests +$run_mocks = !empty($_GET['mocks']) ? true : false; + +// should we print debug info +$debug_print = !empty($_GET['debug']) ? true : false; +// how long to wait between each call +$debug_wait = 2; +// if set to true will print all the debug logs too +$mock_debug = !empty($_GET['debug_mock']) ? true : false; +// wait in seconds between mock tests +$mock_wait = 2; + +if (empty($_GET)) { + print "Use _GET parameters to start tests"; +} + // open debug file output $fp = fopen('log/debug.' . date('YmdHis') . '.log', 'w'); if (!is_resource($fp)) { die("Cannot open log debug file"); } -// run info test (prints ENV vars) -$run_info_test = false; -// run test to get funds info -$run_fund_test = true; -// run the normal get/cancel gift card tests -$run_gift_tests = true; -// run mock error check tests -$run_mocks = true; - -// should we print debug info -$debug_print = false; -// how long to wait between each call -$debug_wait = 2; -// if set to true will print all the debug logs too -$mock_debug = false; -// wait in seconds between mock tests -$mock_wait = 2; - if ($run_info_test === true) { $aws = new AmazonIncentives(); $aws_check_me = $aws->checkMe(); @@ -120,7 +125,7 @@ if ($run_fund_test === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); printException('getAvailableFunds', $e->getCode(), $error, $debug_print); fwrite($fp, writeLog($error)); }; @@ -152,7 +157,7 @@ if ($run_gift_tests === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (\Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); printException('buyGiftCard', $e->getCode(), $error, $debug_print); fwrite($fp, writeLog($error)); } @@ -169,7 +174,7 @@ if ($run_gift_tests === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (\Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); print "AWS: cancelGiftCard: " . $error['status'] . " [" . $e->getCode() . "]: " . $error['code'] . " | " . $error['type'] @@ -197,7 +202,7 @@ if ($run_gift_tests === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (\Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); printException('buyGiftCard', $e->getCode(), $error, $debug_print); fwrite($fp, writeLog($error)); } @@ -221,7 +226,7 @@ if ($run_gift_tests === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (\Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); printException('cancelGiftCard', $e->getCode(), $error, $debug_print); fwrite($fp, writeLog($error)); } @@ -242,7 +247,7 @@ if ($run_gift_tests === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (\Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); printException('buyGiftCard', $e->getCode(), $error, $debug_print); fwrite($fp, writeLog($error)); } @@ -295,7 +300,7 @@ if ($run_mocks === true) { } fwrite($fp, writeLog((array)$aws_test)); } catch (Exception $e) { - $error = AmazonIncentives::decodeExceptionMessage($e->getMessage()); + $error = AmazonErrors::decodeExceptionMessage($e->getMessage()); print "AWS: MOCK: " . $creation_id . ": buyGiftCard: " . $error['status'] . " [" . $e->getCode() . "]: " . $error['code'] . " | " . $error['type'] diff --git a/test/phpUnit/AmazonIncentivesTest.php b/test/phpUnit/AmazonIncentivesTest.php index 381decd..214e6d0 100644 --- a/test/phpUnit/AmazonIncentivesTest.php +++ b/test/phpUnit/AmazonIncentivesTest.php @@ -2,10 +2,11 @@ declare(strict_types=1); -namespace tests; +namespace test\phpUnit; use PHPUnit\Framework\TestCase; -use gullevek\AmazonIncentives\AmazonIncentives; +use PHPUnit\Framework\MockObject\MockObject; +use gullevek\AmazonIncentives; use gullevek\dotEnv\DotEnv; /** @@ -15,71 +16,932 @@ use gullevek\dotEnv\DotEnv; */ final class AmazonIncentivesTest extends TestCase { - public function amazonIncentivesProvider(): array + /** @var int wait tme in seconds between AWS side mock calls */ + private $mock_wait = 1; + + /** + * Client curl exception testing + * + * @testdox AWS Incentives curl exception handling + * + * @return void + */ + public function testAwsIncentivesCurlException(): void { + // this is the exceptio we want + $this->expectException(AmazonIncentives\Exceptions\AmazonErrors::class); + // we don't need a class here, we just need client + $client = new AmazonIncentives\Client\Client(); + // produce any error + $client->request('invalid', [], ''); + } + + /** + * curl/connection error checks + * + * @return array + */ + public function amazonIncentivesProviderErrors(): array + { + // parameter data only for this + // 0: url + // 1: expected status + // 2: expected code + // 3: expected type return [ - 'empty' => [ + // C001 + 'C002 error' => [ + 'url' => 'invalid', + 'expected_status' => 'FAILURE', + 'expected_error' => 'C002', + 'expected_type' => 'CurlError' + ], + // T001 timeout + // 'T001 error' => [ + // 'url' => 'https://timeout.teq.jp', + // 'expected_status' => 'RESEND', + // 'expected_error' => 'T001', + // 'expected_type' => 'RateExceeded' + // ], + // other error + 'E999 error' => [ + 'url' => 'https://www.yahoo.co.jp', + 'expected_status' => 'FAILURE', + 'expected_error' => 'E999', + 'expected_type' => 'OtherUnknownError' + ] + ]; + } + + /** + * Test errors thrown in Client class + * + * @dataProvider amazonIncentivesProviderErrors + * @testdox AWS Incentives error handling [$_dataName] + * + * @param string $url + * @return void + */ + public function testAwsIncentivesCurlErrors( + string $url, + string $expected_status, + string $expected_error, + string $expected_type + ): void { + // HANDLE: + // * Init error + // - C001/Curl init error + // * Client errors (C002)/false: + // - CURLE_COULDNT_CONNECT + // - CURLE_COULDNT_RESOLVE_HOST + // - CURLE_OPERATION_TIMEOUTED + // - CURLE_SSL_PEER_CERTIFICATE + // - 0/OTHER + // * Client errors other + // - T001/Rate exceeded + // - E999/Other error + + // try/catch + // -decodeExceptionMessage (static) + + // we don't need the full interface here, we just need client class + $client = new AmazonIncentives\Client\Client(); + try { + // set expected throw error + $result = $client->request($url, [], ''); + print "R: " . $result . "\n"; + } catch (AmazonIncentives\Exceptions\AmazonErrors $e) { + $curl_error = AmazonIncentives\Exceptions\AmazonErrors::decodeExceptionMessage($e->getMessage()); + // print "E-B: " . print_r($curl_error, true) . "\n"; + $this->assertEquals( + $expected_status, + $curl_error['status'], + 'Assert error status' + ); + $this->assertEquals( + $expected_error, + $curl_error['code'], + 'Assert error code' + ); + $this->assertEquals( + $expected_type, + $curl_error['type'], + 'Assert error type' + ); + } + } + + /** + * init amazon incentive interface + * + * @param array $connect + * @param bool $mock + * @param array|null $mock_response + * @return AmazonIncentives\AmazonIncentives + */ + private function awsIncentivesStartUp( + array $connect, + bool $mock, + ?array $mock_response, + ): AmazonIncentives\AmazonIncentives { + $env_folder = $connect['env_folder'] ?? ''; + $env_file = $connect['env_file'] ?? ''; + $parameters = $connect['parameters'] ?? []; + // reset _ENV always + $_ENV = []; + // env file read status + $status = null; + if (!empty($env_folder)) { + if (!empty($env_file)) { + $status = DotEnv::readEnvFile($env_folder, $env_file); + } else { + $status = DotEnv::readEnvFile($env_folder); + } + } + + // ENV must match _ENV vars if set + if (!empty($env_folder) && $status != 0) { + // abort with error + $this->markTestSkipped( + 'Cannot read .env file needed for AWS tests: ' . $status + ); + } + + // MOCK: + // - for all buyGiftCard|cancelGiftCard|getAvailableFunds + // WHAT: + // \AWS->getCode|cancelCode|getBalance + // -> \AWS->makeReqeust + // -> NEW Client->request <= MOCK this + // NOT MOCK: + // any error calls in Client->request or exceptions + + if ($mock === true) { + // create a new config with or without parameters + $agcod_config = new AmazonIncentives\Config\Config( + $parameters['key'] ?? null, + $parameters['secret'] ?? null, + $parameters['partner'] ?? null, + $parameters['endpoint'] ?? null, + $parameters['currency'] ?? null, + $parameters['debug'] ?? null + ); + + // MOCK CLIENT + // Master mock the Client class for request call + // If we wan't to get errors thrown + /** @var AmazonIncentives\Client\Client&MockObject */ + $client_mock = $this->createPartialMock(AmazonIncentives\Client\Client::class, ['request']); + // set the needed return here + $client_mock->method('request')->willReturn(json_encode($mock_response)); + + // MOCK AWS and attache above class in client return + /** @var AmazonIncentives\AWS\AWS&MockObject */ + $aws_mock = $this->getMockBuilder(AmazonIncentives\AWS\AWS::class) + ->setConstructorArgs([$agcod_config]) + ->onlyMethods(['newClient']) + ->getMock(); + // attach mocked client + $aws_mock->method('newClient')->willReturn($client_mock); + + // MOCK AMAZONINCENTIVES + /** @var AmazonIncentives\AmazonIncentives&MockObject */ + $agcod = $this->getMockBuilder(AmazonIncentives\AmazonIncentives::class) + ->setConstructorArgs([ + $parameters['key'] ?? null, + $parameters['secret'] ?? null, + $parameters['partner'] ?? null, + $parameters['endpoint'] ?? null, + $parameters['currency'] ?? null, + $parameters['debug'] ?? null + ]) + ->onlyMethods(['newAWS']) + ->getMock(); + // attach mocked AWS class + $agcod->method('newAWS')->willReturn($aws_mock); + } else { + // if we mock, we mock the Client->request + $agcod = new AmazonIncentives\AmazonIncentives( + $parameters['key'] ?? null, + $parameters['secret'] ?? null, + $parameters['partner'] ?? null, + $parameters['endpoint'] ?? null, + $parameters['currency'] ?? null, + $parameters['debug'] ?? null + ); + } + + return $agcod; + } + + /** + * Holds the configs for loading data from .env for parameter + * + * @return array + */ + public function awsIncentivesProvider(): array + { + // 0: .env file folder + // 1: .env file name (if not set use .env) + // 2: parameters that override _ENV variables + return [ + // this is with real test account data + 'env_test' => [ + 'env_folder' => __DIR__ . DIRECTORY_SEPARATOR . '..', + 'env_file' => null, + 'parameters' => null + ], + // this is for mocking only + 'parameter_dummy' => [ 'env_folder' => null, 'env_file' => null, - ] + 'parameters' => [ + null, + null, + null, + 'http://i.dont.exist.at.all', + 'JPY' + ] + ], ]; } /** * Undocumented function * - * @dataProvider amazonIncentivesProvider - * @testdox AmazonIncentives tests [$_dataName] + * @return array + */ + public function amazonIncentivesProviderGetFunds(): array + { + // remove final keyword + // BypassFinals::enable(); + // get connectors + $connectors = $this->awsIncentivesProvider(); + // 0: connect array (env file, env folder, parameters array) + // 1: mock or normal call + // 2: if mock connect response must be defined here + // 3: exepcted response array + return [ + 'non mock test data' => [ + 'connect' => $connectors['env_test'], + 'mock' => false, + 'mock_response' => null, + 'expected' => [ + // + ] + ], + 'mock data test' => [ + 'connect' => $connectors['parameter_dummy'], + 'mock' => true, + 'mock_response' => [ + 'availableFunds' => [ + 'amount' => 0.0, + 'currencyCode' => 'JPY', + ], + 'status' => 'SUCCESS', + 'timestamp' => '20220610T085450Z', + ], + ], + ]; + } + + /** + * Undocumented function * + * @dataProvider amazonIncentivesProviderGetFunds + * @testdox AWS Incentives get available funds [$_dataName] + * + * @param array $connect + * @param bool $mock + * @param array|null $mock_response * @return void */ - public function testAmazonIncentives(?string $env_folder, ?string $env_file): void + public function testAwsIncentivesGetAvailableFunds( + array $connect, + bool $mock, + ?array $mock_response + ): void { + // load class + $agcod = $this->awsIncentivesStartUp( + $connect, + $mock, + $mock_response, + ); + + // - getAvailableFunds: get available fund + // - getStatus + // - getAmount + // - getCurrency + // - getTimestamp + $funds = $agcod->getAvailableFunds(); + // if not mock do type check + // if mock do matching check from mcok + if ($mock === false) { + $this->assertEquals( + 'SUCCESS', + $funds->getStatus(), + 'Assert status is success' + ); + // numeric number + $this->assertIsNumeric( + $funds->getAmount(), + 'Assert amoount is numerc' + ); + // USD, JPY, etc + $this->assertIsString( + $funds->getCurrency(), + 'Assert currency is string' + ); + // 20220610T085450Z + $this->assertMatchesRegularExpression( + "/^\d{8}T\d{6}Z$/", + $funds->getTimestamp(), + 'Assert timestamp matches regex' + ); + } else { + $this->assertEquals( + $mock_response['status'], + $funds->getStatus(), + 'Assert mock status' + ); + $this->assertEquals( + $mock_response['availableFunds']['amount'], + $funds->getAmount(), + 'Assert mock amount' + ); + $this->assertEquals( + $mock_response['availableFunds']['currencyCode'], + $funds->getCurrency(), + 'Assert mock currency code' + ); + $this->assertEquals( + $mock_response['timestamp'], + $funds->getTimestamp(), + 'Assert mock timestamp' + ); + } + } + + /** + * Undocumented function + * + * @return array + */ + public function amazonIncentivesProviderBuy(): array { + // get connectors + $connectors = $this->awsIncentivesProvider(); + // 0: connect array (env file, env folder, parameters array) + // 1: mock or normal call + // 2: if mock connect response must be defined here + // 3: exepcted response array + // 4: value in float + return [ + 'non mock test data' => [ + 'connect' => $connectors['env_test'], + 'mock' => false, + 'mock_response' => null, + 'amount' => 500.0, + ], + 'mock data test' => [ + 'connect' => $connectors['parameter_dummy'], + 'mock' => true, + 'mock_response' => [ + 'cardInfo' => [ + 'cardNumber' => null, + 'cardStatus' => 'Fulfilled', + 'expirationDate' => null, + 'value' => [ + 'amount' => 1000.0, + 'currencyCode' => 'JPY', + ], + ], + 'creationRequestId' => 'PartnerId_62a309167e7a4', + 'gcClaimCode' => 'LJ49-AKDUV6-UYCP', + 'gcExpirationDate' => 'Thu Jun 10 14:59:59 UTC 2032', + 'gcId' => '5535125272070255', + 'status' => 'SUCCESS', + ], + 'amount' => 1000.0, + ], + ]; + } + + /** + * Undocumented function + * + * @dataProvider amazonIncentivesProviderBuy + * @testdox AWS Incentives buy gift card [$_dataName] + * + * @param array $connect + * @param bool $mock + * @param array|null $mock_response + * @param float $amount + * @return void + */ + public function testAwsIncentivesBuyGiftCard( + array $connect, + bool $mock, + ?array $mock_response, + float $amount + ): void { // - init plain // * via ::make() + // - buyGiftCard: buy gift card // - getCreationRequestId // - getId // - getClaimCode // - getExpirationDate // - getStatus - // - cancelGiftCard: cancel gift card - // - getAvailableFunds: get available fund - // - getAmount - // - getCurrency - // - getTimestamp - // try/catch - // -decodeExceptionMessage (static) - $this->markTestSkipped('Not yet implemented: AmazonIncentives'); + // load class + $agcod = $this->awsIncentivesStartUp( + $connect, + $mock, + $mock_response, + ); + + $response = $agcod->buyGiftCard($amount); + + if ($mock === false) { + // type check + $this->assertEquals( + 'SUCCESS', + $response->getStatus(), + 'Assert status' + ); + // creation request id must start with partner id + $this->assertStringStartsWith( + $agcod->checkMe()['CONFIG']->getPartner(), + $response->getCreationRequestId(), + 'Assert creation request id starts with partner id' + ); + // gift card id is number + $this->assertIsNumeric( + $response->getId(), + 'Assert gift card id is numeric' + ); + // claim code is 4-6-4 alphanumeric + $this->assertIsString( + $response->getClaimCode(), + 'Assert claim code is string' + ); + // only for requests outside US/Australia cards + // expiration date: Thu Jun 10 14:59:59 UTC 2032 + } else { + // value match to mock response + $this->assertEquals( + $mock_response['status'], + $response->getStatus(), + 'Assert mock status' + ); + $this->assertEquals( + $mock_response['creationRequestId'], + $response->getCreationRequestId(), + 'Assert mock creation request id' + ); + $this->assertEquals( + $mock_response['gcId'], + $response->getId(), + 'Assert mock gift card id' + ); + $this->assertEquals( + $mock_response['gcClaimCode'], + $response->getClaimCode(), + 'Assert mock claim code' + ); + $this->assertEquals( + $mock_response['gcExpirationDate'], + $response->getExpirationDate(), + 'Assert mock expiration date' + ); + } } - public function checkMeProvider(): array + /** + * Buy a gift card and use same creation request id to get another gift card + * has to return same data ggain + * + * @dataProvider amazonIncentivesProviderBuy + * @testdox AWS Incentives buy gift card and again with same creation request id [$_dataName] + * + * @param array $connect + * @param bool $mock + * @param array|null $mock_response + * @param float $amount + * @return void + */ + public function testAwsIncentivesSameBuyGiftCard( + array $connect, + bool $mock, + ?array $mock_response, + float $amount + ): void { + // load class + $agcod = $this->awsIncentivesStartUp( + $connect, + $mock, + $mock_response, + ); + // get one + $response_a = $agcod->buyGiftCard($amount); + // get one again with same code + $response_b = $agcod->buyGiftCard($amount, $response_a->getCreationRequestId()); + + // a and b must be equalt + $this->assertEquals( + $response_a->getStatus(), + $response_b->getStatus(), + 'Assert status' + ); + $this->assertEquals( + $response_a->getCreationRequestId(), + $response_b->getCreationRequestId(), + 'Assert creation request id' + ); + $this->assertEquals( + $response_a->getId(), + $response_b->getId(), + 'Assert gift card id' + ); + $this->assertEquals( + $response_a->getClaimCode(), + $response_b->getClaimCode(), + 'Assert claim code' + ); + $this->assertEquals( + $response_a->getExpirationDate(), + $response_b->getExpirationDate(), + 'Assert expiration date' + ); + } + + /** + * Undocumented function + * + * @return array + */ + public function amazonIncentivesProviderCancel(): array { + // get connectors + $connectors = $this->awsIncentivesProvider(); + // 0: connect array (env file, env folder, parameters array) + // 1: mock or normal call + // 2: if mock connect response must be defined here + // 3: exepcted response array return [ - 'default' => [ - 'env_folder' => null, - 'env_file' => null, - 'expected' => [], - ] + 'non mock test data' => [ + 'connect' => $connectors['env_test'], + 'mock' => false, + 'mock_response' => null, + ], + 'mock data test' => [ + 'connect' => $connectors['parameter_dummy'], + 'mock' => true, + 'mock_response' => [ + 'creationRequestId' => 'PartnerId_62a309167e7a4', + 'gcId' => '5535125272070255', + 'status' => 'SUCCESS', + ], + ], ]; } /** * Undocumented function * + * @dataProvider amazonIncentivesProviderCancel + * @testdox AWS Incentives cancel gift card [$_dataName] + * + * @param array $connect + * @param bool $mock + * @param array|null $mock_response + * @return void + */ + public function testAwsIncentivesCancelGiftCard( + array $connect, + bool $mock, + ?array $mock_response + ): void { + // - cancelGiftCard: cancel gift card + // load class + $agcod = $this->awsIncentivesStartUp( + $connect, + $mock, + $mock_response, + ); + + if ($mock === false) { + // get a gift card, then cancel it + $purchase = $agcod->buyGiftCard(500.0); + $response = $agcod->cancelGiftCard( + $purchase->getCreationRequestId(), + $purchase->getId() + ); + $this->assertEquals( + 'SUCCESS', + $response->getStatus(), + 'Assert mock status' + ); + // creation request id must start with partner id + $this->assertStringStartsWith( + $agcod->checkMe()['CONFIG']->getPartner(), + $response->getCreationRequestId(), + 'Assert creation request id starts with partner id' + ); + // gift card id is number + $this->assertIsNumeric( + $response->getId(), + 'Assert gift card id is numeric' + ); + } else { + $response = $agcod->cancelGiftCard( + $mock_response['creationRequestId'], + $mock_response['gcId'] + ); + $this->assertEquals( + $mock_response['status'], + $response->getStatus(), + 'Assert mock status' + ); + $this->assertEquals( + $mock_response['creationRequestId'], + $response->getCreationRequestId(), + 'Assert mock creation request id' + ); + $this->assertEquals( + $mock_response['gcId'], + $response->getId(), + 'Assert mock gift card id' + ); + } + } + + /** + * list of AWS mock codes for AWS side mock testing + * + * @return array + */ + public function awsIncentivesMockProvider(): array + { + return [ + 'successMock' => [ + 'creation_request_id' => 'F0000', + 'return_code' => '', + 'status' => 'SUCCESS' + ], + 'SimpleAmountIsNull' => [ + 'creation_request_id' => 'F1000', + 'return_code' => 'F100', + 'status' => 'FAILURE' + ], + 'InvalidAmountInput' => [ + 'creation_request_id' => 'F2003', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'InvalidAmountValue' => [ + 'creation_request_id' => 'F2004', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'InvalidCurrencyCodeInput' => [ + 'creation_request_id' => 'F2005', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'CardActivatedWithDifferentRequestId' => [ + 'creation_request_id' => 'F2010', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'MaxAmountExceeded' => [ + 'creation_request_id' => 'F2015', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'CurrencyCodeMismatch' => [ + 'creation_request_id' => 'F2016', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'FractionalAmountNotAllowed' => [ + 'creation_request_id' => 'F2017', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'CancelRequestArrivedAfterTimeLimit' => [ + 'creation_request_id' => 'F2047', + 'return_code' => 'F200', + 'status' => 'FAILURE' + ], + 'InsufficientFunds' => [ + 'creation_request_id' => 'F3003', + 'return_code' => 'F300', + 'status' => 'FAILURE' + ], + 'AccountHasProblems' => [ + 'creation_request_id' => 'F3005', + 'return_code' => 'F300', + 'status' => 'FAILURE' + ], + 'CustomerSurpassedDailyVelocityLimit' => [ + 'creation_request_id' => 'F3010', + 'return_code' => 'F300', + 'status' => 'FAILURE' + ], + 'SystemTemporarilyUnavailable' => [ + 'creation_request_id' => 'F4000', + 'return_code' => 'F400', + 'status' => 'RESEND' + ], + 'UnknownError' => [ + 'creation_request_id' => 'F5000', + 'return_code' => 'F500', + 'status' => 'FAILURE' + ], + ]; + } + + /** + * NOTE: Must have a valid test user connection setup + * This only works with a valid server connection. + * Runs through AWS Incentives mock values and checks the return code and status + * + * @dataProvider awsIncentivesMockProvider + * @testdox AWS Incentives Mock $creation_request_id will be $expected_status with $expected_code [$_dataName] + * + * @return void + */ + public function testAwsIncentivesWithMocks( + string $creation_request_id, + string $expected_code, + string $expected_status, + ): void { + // reset _ENV for reading + $_ENV = []; + // read the .env file + $status = DotEnv::readEnvFile(__DIR__ . DIRECTORY_SEPARATOR . '..'); + // if loading failed, abort + if ($status != 0) { + // abort with error + $this->markTestSkipped( + 'Cannot read .env file needed for AWS mock tests: ' . $status + ); + } + // if no value set, set to 500 + $value = $_ENV['AWS_MOCK_VALUE'] ?? 500; + // run tests + try { + $aws_gcod = AmazonIncentives\AmazonIncentives::make()->buyGiftCard( + (float)$value, + $creation_request_id + ); + $this->assertEquals( + $expected_status, + $aws_gcod->getStatus(), + 'Assert status ok in AWS GCOD mocks' + ); + } catch (\Exception $e) { + $error = AmazonIncentives\Exceptions\AmazonErrors::decodeExceptionMessage($e->getMessage()); + $this->assertEquals( + [ + 'code' => $expected_code, + 'status' => $expected_status, + ], + [ + 'code' => $error['code'], + 'status' => $error['status'], + ], + 'Assert status failed in AWS GCOD mocks' + ); + } + // wait a moment between tests + sleep($this->mock_wait); + } + + /** + * Undocumented function + * + * @return array + */ + public function checkMeProvider(): array + { + // 0: .env file folder + // 1: .env file name (if not set use .env) + // 2: parameters that override _ENV variables + return [ + 'default all empty' => [ + 'use_env' => null, + 'env_file' => null, + 'parameters' => null, + ], + 'set parameters' => [ + 'env_folder' => null, + 'env_file' => null, + 'parameters' => [ + 'key' => 'key', + 'secret' => 'secret', + 'partner' => 'partner id', + 'endpoint' => 'https://endpoint.test.com', + 'currency' => 'currency', + 'debug' => true, + ], + 'expected' => [], + ], + 'load from env' => [ + 'env_folder' => __DIR__ . DIRECTORY_SEPARATOR . '..', + 'env_file' => null, + 'parameters' => null, + ], + 'load from env, but override parameter' => [ + 'env_folder' => __DIR__ . DIRECTORY_SEPARATOR . '..', + 'env_file' => null, + 'parameters' => [ + 'key' => 'key', + 'secret' => 'secret', + 'partner' => 'partner id', + 'endpoint' => 'https://endpoint.test.com', + 'currency' => 'currency', + ] + ] + // test missing parameter, set vie _ENV + ]; + } + + /** + * Check the checkMe function that will work with or without any settings + * passed on. + * This also tests basic loading + * - parseing for endoint as url + * - override check for _ENV vs parameter + * * @cover ::checkMe * @dataProvider checkMeProvider * @testdox AmazonIncentives tests [$_dataName] * + * @param string|null $env_folder + * @param string|null $env_file + * @param array|null $parameters * @return void */ - public function testCheckMe(?string $env_folder, ?string $env_file, array $expected): void + public function testCheckMe(?string $env_folder, ?string $env_file, ?array $parameters): void { - $aws = new AmazonIncentives(); + // reset _ENV before each run to avoid nothing to load errors + $_ENV = []; + // env load status + $status = null; + if (!empty($env_folder)) { + if (!empty($env_file)) { + $status = DotEnv::readEnvFile($env_folder, $env_file); + } else { + $status = DotEnv::readEnvFile($env_folder); + } + } + if (!empty($parameters)) { + $aws = new AmazonIncentives\AmazonIncentives( + $parameters['key'], + $parameters['secret'], + $parameters['partner'], + $parameters['endpoint'], + $parameters['currency'], + $parameters['debug'] ?? null, + ); + } else { + $aws = new AmazonIncentives\AmazonIncentives(); + } $aws_check_me = $aws->checkMe(); + // ENV must match _ENV vars if set + if (!empty($env_folder) && $status != 0) { + // abort with error + $this->markTestSkipped( + 'Cannot read .env file needed: ' . $status + ); + } elseif (!empty($env_folder)) { + $this->assertEquals( + $_ENV, + $aws_check_me['ENV'], + 'Assert _ENV set equal' + ); + } // compare that data matches - print "CM: " . print_r($aws_check_me, true) . "\n"; + // print "CM: " . print_r($aws_check_me, true) . "\n"; + // CONFIG must match to parameters or ENV, parsed host name check + $this->assertEquals( + // parameter > _ENV -> empty + !empty($parameters['partner']) ? + $parameters['partner'] : + $_ENV['AWS_GIFT_CARD_PARTNER_ID'] ?? '', + $aws_check_me['CONFIG']->getPartner(), + 'Assert config matching input' + ); + // KEY must match access_key/AWS_GIFT_CARD_KEY + $this->assertEquals( + $aws_check_me['CONFIG']->getAccessKey(), + $aws_check_me['KEY'], + 'Assert access key m' + ); } } diff --git a/test/phpUnit/Hook/BypassFinalHook.php b/test/phpUnit/Hook/BypassFinalHook.php new file mode 100644 index 0000000..d36ef37 --- /dev/null +++ b/test/phpUnit/Hook/BypassFinalHook.php @@ -0,0 +1,21 @@ + $vendorDir . '/composer/InstalledVersions.php', + 'DG\\BypassFinals' => $vendorDir . '/dg/bypass-finals/src/BypassFinals.php', 'PHPUnit\\Exception' => $vendorDir . '/phpunit/phpunit/src/Exception.php', 'PHPUnit\\Framework\\ActualValueIsNotAnObjectException' => $vendorDir . '/phpunit/phpunit/src/Framework/Exception/ActualValueIsNotAnObjectException.php', 'PHPUnit\\Framework\\Assert' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert.php', diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 5211bf2..55d32cd 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -76,6 +76,7 @@ class ComposerStaticInit0c8f6bec90a6d60040a922f19a1f0e64 public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'DG\\BypassFinals' => __DIR__ . '/..' . '/dg/bypass-finals/src/BypassFinals.php', 'PHPUnit\\Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php', 'PHPUnit\\Framework\\ActualValueIsNotAnObjectException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Exception/ActualValueIsNotAnObjectException.php', 'PHPUnit\\Framework\\Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index e286566..a41a152 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -1,5 +1,62 @@ { "packages": [ + { + "name": "dg/bypass-finals", + "version": "dev-master", + "version_normalized": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/dg/bypass-finals.git", + "reference": "fb62dc6ab1a097e234fa1567943d8e87ea4d0842" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dg/bypass-finals/zipball/fb62dc6ab1a097e234fa1567943d8e87ea4d0842", + "reference": "fb62dc6ab1a097e234fa1567943d8e87ea4d0842", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.3", + "phpstan/phpstan": "^0.12" + }, + "time": "2022-04-14T12:24:34+00:00", + "default-branch": true, + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes", + "keywords": [ + "finals", + "mocking", + "phpunit", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/dg/bypass-finals/issues", + "source": "https://github.com/dg/bypass-finals/tree/master" + }, + "install-path": "../dg/bypass-finals" + }, { "name": "doctrine/instantiator", "version": "1.5.x-dev", @@ -2184,6 +2241,7 @@ ], "dev": true, "dev-package-names": [ + "dg/bypass-finals", "doctrine/instantiator", "gullevek/dotenv", "myclabs/deep-copy", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 6c0a3e5..6b02720 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -5,11 +5,22 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '1e836e9a2bd6e41c4555cf917fe645b453cb5b79', + 'reference' => 'fd5477269b9a133448cecb5854b784033d6c6c62', 'name' => 'gullevek/amazon-incentives', 'dev' => true, ), 'versions' => array( + 'dg/bypass-finals' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'type' => 'library', + 'install_path' => __DIR__ . '/../dg/bypass-finals', + 'aliases' => array( + 0 => '9999999-dev', + ), + 'reference' => 'fb62dc6ab1a097e234fa1567943d8e87ea4d0842', + 'dev_requirement' => true, + ), 'doctrine/instantiator' => array( 'pretty_version' => '1.5.x-dev', 'version' => '1.5.9999999.9999999-dev', @@ -25,7 +36,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '1e836e9a2bd6e41c4555cf917fe645b453cb5b79', + 'reference' => 'fd5477269b9a133448cecb5854b784033d6c6c62', 'dev_requirement' => false, ), 'gullevek/dotenv' => array( diff --git a/vendor/dg/bypass-finals/composer.json b/vendor/dg/bypass-finals/composer.json new file mode 100644 index 0000000..345d4b1 --- /dev/null +++ b/vendor/dg/bypass-finals/composer.json @@ -0,0 +1,26 @@ +{ + "name": "dg/bypass-finals", + "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes", + "keywords": ["testing", "unit", "phpunit", "mocking", "finals"], + "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.3", + "phpstan/phpstan": "^0.12" + }, + "autoload": { + "classmap": ["src/"] + }, + "scripts": { + "phpstan": "phpstan analyse", + "tester": "tester tests -s" + } +} diff --git a/vendor/dg/bypass-finals/license.md b/vendor/dg/bypass-finals/license.md new file mode 100644 index 0000000..03bf49f --- /dev/null +++ b/vendor/dg/bypass-finals/license.md @@ -0,0 +1,49 @@ +Licenses +======== + +Good news! You may use Nette Tester under the terms of either +the New BSD License or the GNU General Public License (GPL) version 2 or 3. + + + +New BSD License +--------------- + +Copyright (c) 2004, 2013 David Grudl (https://davidgrudl.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of "Nette Tester" nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and +any express or implied warranties, including, but not limited to, the implied +warranties of merchantability and fitness for a particular purpose are +disclaimed. In no event shall the copyright owner or contributors be liable for +any direct, indirect, incidental, special, exemplary, or consequential damages +(including, but not limited to, procurement of substitute goods or services; +loss of use, data, or profits; or business interruption) however caused and on +any theory of liability, whether in contract, strict liability, or tort +(including negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + + + +GNU General Public License +-------------------------- + +GPL licenses are very very long, so instead of including them here we offer +you URLs with full text: + +- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) +- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) diff --git a/vendor/dg/bypass-finals/readme.md b/vendor/dg/bypass-finals/readme.md new file mode 100644 index 0000000..50f0734 --- /dev/null +++ b/vendor/dg/bypass-finals/readme.md @@ -0,0 +1,64 @@ +Bypass Finals +============= + +[![Downloads this Month](https://img.shields.io/packagist/dm/dg/bypass-finals.svg)](https://packagist.org/packages/dg/bypass-finals) +[![Tests](https://github.com/dg/bypass-finals/workflows/Tests/badge.svg?branch=master)](https://github.com/dg/bypass-finals/actions) +[![Latest Stable Version](https://poser.pugx.org/dg/bypass-finals/v/stable)](https://github.com/dg/bypass-finals/releases) +[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/dg/bypass-finals/blob/master/license.md) + + +Introduction +------------ + +Removes final keywords from source code on-the-fly and allows mocking of final methods and classes. +It can be used together with any test tool such as PHPUnit, Mockery or [Nette Tester](https://tester.nette.org). + + +Installation +------------ + +The recommended way to install is through Composer: + +``` +composer require dg/bypass-finals --dev +``` + +It requires PHP version 7.1 (or 5.6 in case of release 1.1) and supports PHP up to 8.1. + + +Usage +----- + +Simply call this: + +```php +DG\BypassFinals::enable(); +``` + +You need to enable it before the classes you want to remove the final are loaded. So call it as soon as possible, +preferably right after `vendor/autoload.php` is loaded. + +Note that final internal PHP classes like `Closure` cannot be mocked. + +You can choose to only bypass finals in specific files or directories: + +```php +DG\BypassFinals::setWhitelist([ + '*/Nette/*', +]); +``` + +This gives you finer control and can solve issues with certain frameworks and libraries. + +You can try to increase performance by using the cache (the directory must exist): + +```php +DG\BypassFinals::$cacheDir = __DIR__ . '/tmp'; +``` + +Support Project +--------------- + +Do you like BypassFinals? + +[![Donate](https://files.nette.org/icons/donation-1.svg?)](https://nette.org/make-donation?to=bypass-finals) diff --git a/vendor/dg/bypass-finals/src/BypassFinals.php b/vendor/dg/bypass-finals/src/BypassFinals.php new file mode 100644 index 0000000..9fde53f --- /dev/null +++ b/vendor/dg/bypass-finals/src/BypassFinals.php @@ -0,0 +1,312 @@ +handle); + } + + + public function dir_opendir(string $path, int $options): bool + { + $this->handle = $this->context + ? $this->native('opendir', $path, $this->context) + : $this->native('opendir', $path); + return (bool) $this->handle; + } + + + public function dir_readdir() + { + return readdir($this->handle); + } + + + public function dir_rewinddir(): bool + { + return (bool) rewinddir($this->handle); + } + + + public function mkdir(string $path, int $mode, int $options): bool + { + $recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE); + return $this->context + ? $this->native('mkdir', $path, $mode, $recursive, $this->context) + : $this->native('mkdir', $path, $mode, $recursive); + } + + + public function rename(string $pathFrom, string $pathTo): bool + { + return $this->context + ? $this->native('rename', $pathFrom, $pathTo, $this->context) + : $this->native('rename', $pathFrom, $pathTo); + } + + + public function rmdir(string $path, int $options): bool + { + return $this->context + ? $this->native('rmdir', $path, $this->context) + : $this->native('rmdir', $path); + } + + + public function stream_cast(int $castAs) + { + return $this->handle; + } + + + public function stream_close(): void + { + fclose($this->handle); + } + + + public function stream_eof(): bool + { + return feof($this->handle); + } + + + public function stream_flush(): bool + { + return fflush($this->handle); + } + + + public function stream_lock(int $operation): bool + { + return $operation + ? flock($this->handle, $operation) + : true; + } + + + public function stream_metadata(string $path, int $option, $value): bool + { + switch ($option) { + case STREAM_META_TOUCH: + return $this->native('touch', $path, $value[0] ?? time(), $value[1] ?? time()); + case STREAM_META_OWNER_NAME: + case STREAM_META_OWNER: + return $this->native('chown', $path, $value); + case STREAM_META_GROUP_NAME: + case STREAM_META_GROUP: + return $this->native('chgrp', $path, $value); + case STREAM_META_ACCESS: + return $this->native('chmod', $path, $value); + } + + return false; + } + + + public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool + { + $usePath = (bool) ($options & STREAM_USE_PATH); + if ($mode === 'rb' && pathinfo($path, PATHINFO_EXTENSION) === 'php' && self::isPathInWhiteList($path)) { + if (self::$prevWrapper) { + $content = null; + self::$prevWrapper->stream_open($path, $mode, $options, $openedPath); + while (!self::$prevWrapper->stream_eof()) { + $content .= self::$prevWrapper->stream_read(8192); + } + + self::$prevWrapper->stream_close(); + } else { + $content = $this->native('file_get_contents', $path, $usePath, $this->context); + } + if (!is_string($content)) { + return false; + } + $modified = self::cachedRemoveFinals($content); + if ($modified !== $content) { + $this->handle = tmpfile(); + $this->native('fwrite', $this->handle, $modified); + $this->native('fseek', $this->handle, 0); + return true; + } + } + $this->handle = $this->context + ? $this->native('fopen', $path, $mode, $usePath, $this->context) + : $this->native('fopen', $path, $mode, $usePath); + return (bool) $this->handle; + } + + + public function stream_read(int $count) + { + return fread($this->handle, $count); + } + + + public function stream_seek(int $offset, int $whence = SEEK_SET): bool + { + return fseek($this->handle, $offset, $whence) === 0; + } + + + public function stream_set_option(int $option, int $arg1, ?int $arg2): bool + { + return false; + } + + + public function stream_stat() + { + return fstat($this->handle); + } + + + public function stream_tell() + { + return ftell($this->handle); + } + + + public function stream_truncate(int $newSize): bool + { + return ftruncate($this->handle, $newSize); + } + + + public function stream_write(string $data) + { + return fwrite($this->handle, $data); + } + + + public function unlink(string $path): bool + { + return $this->native('unlink', $path); + } + + + public function url_stat(string $path, int $flags) + { + $func = $flags & STREAM_URL_STAT_LINK ? 'lstat' : 'stat'; + return $flags & STREAM_URL_STAT_QUIET + ? @$this->native($func, $path) + : $this->native($func, $path); + } + + + private function native(string $func) + { + stream_wrapper_restore(self::PROTOCOL); + try { + return $func(...array_slice(func_get_args(), 1)); + } finally { + stream_wrapper_unregister(self::PROTOCOL); + stream_wrapper_register(self::PROTOCOL, self::class); + } + } + + + public static function cachedRemoveFinals(string $code): string + { + if (stripos($code, 'final') === false) { + // do nothing + + } elseif (self::$cacheDir) { + $hash = sha1($code); + if ($file = @fopen(self::$cacheDir . '/' . $hash, 'r')) { // @ may not exist + flock($file, LOCK_SH); + if ($res = stream_get_contents($file)) { + return $res; + } + } + $code = self::removeFinals($code); + if ($file = @fopen(self::$cacheDir . '/' . $hash, 'x')) { // @ may exist + flock($file, LOCK_EX); + fwrite($file, $code); + } + + } else { + $code = self::removeFinals($code); + } + + return $code; + } + + + public static function removeFinals(string $code): string + { + try { + $tokens = token_get_all($code, TOKEN_PARSE); + } catch (\ParseError $e) { + return $code; + } + + $code = ''; + foreach ($tokens as $token) { + $code .= is_array($token) + ? ($token[0] === T_FINAL ? '' : $token[1]) + : $token; + } + + return $code; + } + + + private static function isPathInWhiteList(string $path): bool + { + $path = strtr($path, '\\', '/'); + foreach (self::$pathWhitelist as $mask) { + if (fnmatch($mask, $path)) { + return true; + } + } + + return false; + } +}