jenkins-bot has submitted this change and it was merged.
Change subject: Mock Adyen API and queue, test capture job
......................................................................
Mock Adyen API and queue, test capture job
In which we plumb the depths of the conflict between static
untestibility and configurable dependency injection.
Nothing was using Context::set, so I changed the behavior to allow
resetting the instance to null.
Bug: T127880
Change-Id: I4aae0d4fce2275920597867bf2e56d569ce6738d
---
M Core/Context.php
M PaymentProviders/Adyen/AdyenPaymentsAPI.php
A PaymentProviders/Adyen/AdyenPaymentsInterface.php
M PaymentProviders/Adyen/Jobs/ProcessCaptureRequestJob.php
A PaymentProviders/Adyen/Tests/Data/auth.json
A PaymentProviders/Adyen/Tests/Data/pending.json
A PaymentProviders/Adyen/Tests/MockAdyenPaymentsAPI.php
A PaymentProviders/Adyen/Tests/config_test_failure.php
A PaymentProviders/Adyen/Tests/config_test_success.php
A PaymentProviders/Adyen/Tests/phpunit/CaptureJobTest.php
M PaymentProviders/Amazon/Tests/phpunit/ApiTest.php
M Tests/BaseSmashPigUnitTestCase.php
A Tests/MockDataStore.php
M Tests/bootstrap-phpunit.php
M config_defaults.php
15 files changed, 401 insertions(+), 31 deletions(-)
Approvals:
Awight: Looks good to me, approved
jenkins-bot: Verified
diff --git a/Core/Context.php b/Core/Context.php
index f36cdf3..ec0bafd 100644
--- a/Core/Context.php
+++ b/Core/Context.php
@@ -48,7 +48,7 @@
*/
public static function set( Context $c = null ) {
$old = Context::$instance;
- Context::$instance = ( $c === null ) ? new Context() : $c;
+ Context::$instance = $c;
return $old;
}
diff --git a/PaymentProviders/Adyen/AdyenPaymentsAPI.php
b/PaymentProviders/Adyen/AdyenPaymentsAPI.php
index a61b43d..9a1ec66 100644
--- a/PaymentProviders/Adyen/AdyenPaymentsAPI.php
+++ b/PaymentProviders/Adyen/AdyenPaymentsAPI.php
@@ -1,16 +1,15 @@
<?php namespace SmashPig\PaymentProviders\Adyen;
use SmashPig\Core\Context;
-use SmashPig\Core\Configuration;
use SmashPig\Core\Logging\Logger;
use SmashPig\Core\Logging\TaggedLogger;
-class AdyenPaymentsAPI {
+class AdyenPaymentsAPI implements AdyenPaymentsInterface {
protected $soapClient = null;
protected $account = '';
- public function __construct( $account ) {
+ public function setAccount( $account ) {
require_once( 'WSDL/Payment.php' );
$this->account = $account;
@@ -26,16 +25,6 @@
);
}
- /**
- * Performs a Capture modification to a given Adyen transaction.
- *
- * @param string $currency Original currency of the request
- * @param int $amount Amount to be captured. Less than or
equal to the original request
- * @param string $pspReference Original pspReference of the request
- *
- * @returns bool|string Result will be false on SOAP exception or
remote request denial. If request was successful
- * the return result will be a pspReference string to this modification.
- */
public function capture( $currency, $amount, $pspReference ) {
$data = new WSDL\capture();
$data->modificationRequest = new WSDL\ModificationRequest();
@@ -64,14 +53,6 @@
}
}
- /**
- * Cancels an Adyen authorization
- *
- * @param string $pspReference Original pspReference of the request
- *
- * @returns bool|string Result will be false on SOAP exception or
remote request denial.
- * If request was successful the return result will be a pspReference
string to this modification.
- */
public function cancel( $pspReference ) {
$data = new WSDL\cancel();
$data->modificationRequest = new WSDL\ModificationRequest();
diff --git a/PaymentProviders/Adyen/AdyenPaymentsInterface.php
b/PaymentProviders/Adyen/AdyenPaymentsInterface.php
new file mode 100644
index 0000000..4750d35
--- /dev/null
+++ b/PaymentProviders/Adyen/AdyenPaymentsInterface.php
@@ -0,0 +1,32 @@
+<?php namespace SmashPig\PaymentProviders\Adyen;
+
+interface AdyenPaymentsInterface {
+
+ /**
+ * Indicate which merchant account to use for API calls
+ * @param string $account the merchant account code
+ */
+ public function setAccount( $account );
+
+ /**
+ * Performs a Capture modification to a given Adyen transaction.
+ *
+ * @param string $currency Original currency of the request
+ * @param int $amount Amount to be captured. Less than or equal to the
original request
+ * @param string $pspReference Original pspReference of the request
+ *
+ * @return bool|string Result will be false on SOAP exception or remote
request denial.
+ * If request was successful the return result will be a pspReference
string to this modification.
+ */
+ public function capture( $currency, $amount, $pspReference );
+
+ /**
+ * Cancels an Adyen authorization
+ *
+ * @param string $pspReference Original pspReference of the request
+ *
+ * @return bool|string Result will be false on SOAP exception or remote
request denial.
+ * If request was successful the return result will be a pspReference
string to this modification.
+ */
+ public function cancel( $pspReference );
+}
diff --git a/PaymentProviders/Adyen/Jobs/ProcessCaptureRequestJob.php
b/PaymentProviders/Adyen/Jobs/ProcessCaptureRequestJob.php
index d0cc7e6..d6d5987 100644
--- a/PaymentProviders/Adyen/Jobs/ProcessCaptureRequestJob.php
+++ b/PaymentProviders/Adyen/Jobs/ProcessCaptureRequestJob.php
@@ -56,6 +56,9 @@
// Determine if a message exists in the pending queue; if it
does not then
// this payment has already been sent to the verified queue.
Logger::debug( 'Attempting to locate associated message in
pending queue.' );
+ /**
+ * @var \SmashPig\Core\DataStores\KeyedOpaqueDataStore
+ */
$pendingQueue = Configuration::getDefaultConfig()->object(
'data-store/pending' );
$queueMessage = $pendingQueue->queueGetObject( null,
$this->correlationId );
$success = true;
@@ -66,7 +69,7 @@
$pendingQueue->queueIgnoreObject();
// Attempt to capture the payment
- $api = new AdyenPaymentsAPI( $this->account );
+ $api = $this->getApi();
Logger::info(
"Attempting capture API call for currency
'{$this->currency}', " .
"amount '{$this->amount}', reference
'{$this->pspReference}'."
@@ -91,7 +94,7 @@
}
} else if ( $action == self::ACTION_REJECT ) {
Logger::debug( "Cancelling authorization with reference
'{$this->pspReference}'" );
- $api = new AdyenPaymentsAPI( $this->account );
+ $api = $this->getApi();
$result = $api->cancel( $this->pspReference );
if ( $result ) {
Logger::debug( "Successfully cancelled
authorization" );
@@ -166,4 +169,13 @@
Logger::debug( "Sending antifraud message with risk score
$riskScore and action $action." );
Configuration::getDefaultConfig()->object(
'data-store/antifraud' )->addObject( $antifraudMessage );
}
+
+ /**
+ * @return \SmashPig\PaymentProviders\Adyen\AdyenPaymentsInterface
+ */
+ protected function getApi() {
+ $api = Configuration::getDefaultConfig()->object(
'payment-provider/adyen/api' );
+ $api->setAccount( $this->account );
+ return $api;
+ }
}
diff --git a/PaymentProviders/Adyen/Tests/Data/auth.json
b/PaymentProviders/Adyen/Tests/Data/auth.json
new file mode 100644
index 0000000..7e98f63
--- /dev/null
+++ b/PaymentProviders/Adyen/Tests/Data/auth.json
@@ -0,0 +1,24 @@
+{
+ "paymentMethod": "amex",
+ "operations": [
+ "CANCEL",
+ "CAPTURE",
+ "REFUND"
+ ],
+ "reason": "27533:0003:6\/2016",
+ "cvvResult": "1",
+ "avsResult": "7",
+ "parentPspReference": null,
+ "pspReference": "762895314225",
+ "merchantReference": "119223.0",
+ "merchantAccountCode": "WikimediaCOM",
+ "currency": "USD",
+ "amount": 10,
+ "eventDate": "2016-01-25T17:07:36.862+01:00",
+ "success": true,
+ "correlationId": "adyen-119223.0",
+ "propertiesExportedAsKeys": [
+ "correlationId"
+ ],
+ "propertiesExcludedFromExport": []
+}
diff --git a/PaymentProviders/Adyen/Tests/Data/pending.json
b/PaymentProviders/Adyen/Tests/Data/pending.json
new file mode 100644
index 0000000..0a16bc9
--- /dev/null
+++ b/PaymentProviders/Adyen/Tests/Data/pending.json
@@ -0,0 +1,48 @@
+{
+ "city": "Columbus",
+ "city_2": "",
+ "comment": "",
+ "contribution_tracking_id": "119223",
+ "country": "US",
+ "country_2": "",
+ "currency": "USD",
+ "date": 1458060070,
+ "email": "[email protected]",
+ "fee": "0",
+ "first_name": "Testy119223",
+ "first_name_2": "",
+ "gateway": "adyen",
+ "gateway_account": "WikimediaCOM",
+ "gateway_txn_id": false,
+ "gross": "10.00",
+ "language": "en",
+ "last_name": "Testerson119223",
+ "last_name_2": "",
+ "middle_name": "",
+ "net": "",
+ "payment_method": "cc",
+ "payment_submethod": "amex",
+ "postal_code": "12345",
+ "postal_code_2": "",
+ "premium_language": "",
+ "recurring": "",
+ "referrer": "https:\/\/mediawiki.dev\/index.php\/Main_Page",
+ "response": false,
+ "risk_score": 10,
+ "size": "",
+ "state_province": "OH",
+ "state_province_2": "",
+ "street_address": "123 Fake St",
+ "street_address_2": "",
+ "supplemental_address_1": "",
+ "supplemental_address_2": "",
+ "user_ip": "127.0.0.1",
+ "utm_campaign": "",
+ "utm_medium": "",
+ "utm_source": "..cc",
+ "correlationId": "adyen-119223.0",
+ "propertiesExportedAsKeys": [
+ "correlationId"
+ ],
+ "propertiesExcludedFromExport": []
+}
diff --git a/PaymentProviders/Adyen/Tests/MockAdyenPaymentsAPI.php
b/PaymentProviders/Adyen/Tests/MockAdyenPaymentsAPI.php
new file mode 100644
index 0000000..4c44a92
--- /dev/null
+++ b/PaymentProviders/Adyen/Tests/MockAdyenPaymentsAPI.php
@@ -0,0 +1,41 @@
+<?php namespace SmashPig\PaymentProviders\Adyen\Tests;
+
+use SmashPig\PaymentProviders\Adyen\AdyenPaymentsInterface;
+
+class MockAdyenPaymentsAPI implements AdyenPaymentsInterface {
+
+ protected $account = '';
+ protected $returnCode = false;
+
+ public function __construct( $returnCode ) {
+ $this->returnCode = $returnCode;
+ }
+
+ public function setAccount( $account ) {
+ $this->account = $account;
+ }
+
+ /**
+ * Fakes a Capture modification to a given Adyen transaction.
+ *
+ * @param string $currency Original currency of the request
+ * @param int $amount Amount to be captured. Less than or
equal to the original request
+ * @param string $pspReference Original pspReference of the request
+ *
+ * @returns bool|string The return code set in the constructor.
+ */
+ public function capture( $currency, $amount, $pspReference ) {
+ return $this->returnCode;
+ }
+
+ /**
+ * Pretends to cancel an Adyen authorization
+ *
+ * @param string $pspReference Original pspReference of the request
+ *
+ * @returns bool|string The return code set in the constructor.
+ */
+ public function cancel( $pspReference ) {
+ return $this->returnCode;
+ }
+}
diff --git a/PaymentProviders/Adyen/Tests/config_test_failure.php
b/PaymentProviders/Adyen/Tests/config_test_failure.php
new file mode 100644
index 0000000..47c32dc
--- /dev/null
+++ b/PaymentProviders/Adyen/Tests/config_test_failure.php
@@ -0,0 +1,24 @@
+<?php
+
+$config = array(
+ 'adyen' => array(
+ 'data-store' => array(
+ 'pending' => array(
+ 'class' => 'MockDataStore',
+ 'inst-args' => array(),
+ ),
+ 'antifraud' => array(
+ 'class' => 'MockDataStore',
+ 'inst-args' => array(),
+ ),
+ ),
+ 'payment-provider' => array(
+ 'adyen' => array(
+ 'api' => array(
+ 'class' =>
'SmashPig\PaymentProviders\Adyen\Tests\MockAdyenPaymentsAPI',
+ 'inst-args' => array( false ),
+ ),
+ ),
+ )
+ )
+);
diff --git a/PaymentProviders/Adyen/Tests/config_test_success.php
b/PaymentProviders/Adyen/Tests/config_test_success.php
new file mode 100644
index 0000000..efd7931
--- /dev/null
+++ b/PaymentProviders/Adyen/Tests/config_test_success.php
@@ -0,0 +1,24 @@
+<?php
+
+$config = array(
+ 'adyen' => array(
+ 'data-store' => array(
+ 'pending' => array(
+ 'class' => 'MockDataStore',
+ 'inst-args' => array(),
+ ),
+ 'antifraud' => array(
+ 'class' => 'MockDataStore',
+ 'inst-args' => array(),
+ ),
+ ),
+ 'payment-provider' => array(
+ 'adyen' => array(
+ 'api' => array(
+ 'class' =>
'SmashPig\PaymentProviders\Adyen\Tests\MockAdyenPaymentsAPI',
+ 'inst-args' => array( 'Success!' ),
+ ),
+ ),
+ )
+ )
+);
diff --git a/PaymentProviders/Adyen/Tests/phpunit/CaptureJobTest.php
b/PaymentProviders/Adyen/Tests/phpunit/CaptureJobTest.php
new file mode 100644
index 0000000..499ab9e
--- /dev/null
+++ b/PaymentProviders/Adyen/Tests/phpunit/CaptureJobTest.php
@@ -0,0 +1,43 @@
+<?php namespace SmashPig\PaymentProviders\Adyen\Test;
+
+use SmashPig\Core\Configuration;
+use SmashPig\Core\DataStores\KeyedOpaqueStorableObject;
+use SmashPig\PaymentProviders\Adyen\Jobs\ProcessCaptureRequestJob;
+
+/**
+ * Verify Adyen Capture job functions
+ */
+class CaptureJobTest extends \BaseSmashPigUnitTestCase {
+
+ /**
+ * For a legit donation, ProcessCaptureJob should leave donor data
+ * on the pending queue, add an antifraud message, and return true.
+ */
+ public function testSuccessfulCapture() {
+ $this->setConfig( __DIR__ . '/../config_test_success.php',
'adyen' );
+ $antifraudQueue = Configuration::getDefaultConfig()->object(
'data-store/antifraud', true );
+ $pendingQueue = Configuration::getDefaultConfig()->object(
'data-store/pending', true );
+ $pendingQueue->addObject(
+ KeyedOpaqueStorableObject::fromJsonProxy(
+
'SmashPig\CrmLink\Messages\DonationInterfaceMessage',
+ file_get_contents( __DIR__ .
'/../Data/pending.json' )
+ )
+ );
+ $auth = KeyedOpaqueStorableObject::fromJsonProxy(
+
'SmashPig\PaymentProviders\Adyen\ExpatriatedMessages\Authorisation',
+ file_get_contents( __DIR__ . '/../Data/auth.json' )
+ );
+ $job = ProcessCaptureRequestJob::factory( $auth );
+ $this->assertTrue( $job->execute() );
+ $this->assertNotNull(
+ $pendingQueue->queueGetObject( null,
$auth->correlationId ),
+ 'RequestCaptureJob did not leave donor data on pending
queue'
+ );
+ $this->assertNotNull(
+ // Blank correlation ID on antifraud messages
+ $antifraudQueue->queueGetObject( null, "" ),
+ 'RequestCaptureJob did not send antifraud message'
+ );
+ }
+
+}
diff --git a/PaymentProviders/Amazon/Tests/phpunit/ApiTest.php
b/PaymentProviders/Amazon/Tests/phpunit/ApiTest.php
index 9f908f1..1107383 100644
--- a/PaymentProviders/Amazon/Tests/phpunit/ApiTest.php
+++ b/PaymentProviders/Amazon/Tests/phpunit/ApiTest.php
@@ -10,13 +10,7 @@
public function setUp() {
parent::setUp();
- $config = new Configuration(
- __DIR__ . '/../../../../config_defaults.php',
- __DIR__ . '/../config_test.php',
- 'amazon',
- true
- );
- Context::init( $config );
+ $this->setConfig( __DIR__ . '/../config_test.php', 'amazon' );
$this->mockClient = Context::get()->getConfiguration()->object(
'payments-client', true );
$this->mockClient->calls = array();
$this->mockClient->returns = array();
diff --git a/Tests/BaseSmashPigUnitTestCase.php
b/Tests/BaseSmashPigUnitTestCase.php
index e25fd43..8bb5b3e 100644
--- a/Tests/BaseSmashPigUnitTestCase.php
+++ b/Tests/BaseSmashPigUnitTestCase.php
@@ -1,13 +1,38 @@
<?php
+use SmashPig\Core\Context;
+use SmashPig\Core\Configuration;
+use SmashPig\Core\Logging\Logger;
class BaseSmashPigUnitTestCase extends PHPUnit_Framework_TestCase {
+ protected static $loggerCreated = false; // Aaargh!
+
function setUp() {
parent::setUp();
require_once __DIR__ . '/../vendor/autoload.php';
}
+ function tearDown() {
+ Context::set(); // Nullify the context for next run.
+ }
+
function loadJson( $path ) {
return json_decode( file_get_contents( $path ), true );
}
+
+ function setConfig( $configPath = null, $configNode = 'default' ) {
+ $defaultConfig = __DIR__ . '/../config_defaults.php';
+ $config = new Configuration(
+ $defaultConfig,
+ $configPath,
+ $configNode,
+ true
+ );
+ Context::init( $config );
+ if ( !self::$loggerCreated ) {
+ // Don't care which config the logger gets, let's just
not explode
+ Logger::init( 'test', 'debug', $config );
+ self::$loggerCreated = true;
+ }
+ }
}
diff --git a/Tests/MockDataStore.php b/Tests/MockDataStore.php
new file mode 100644
index 0000000..49be5cc
--- /dev/null
+++ b/Tests/MockDataStore.php
@@ -0,0 +1,116 @@
+<?php
+use SmashPig\Core\DataStores\DataStoreException;
+use SmashPig\Core\DataStores\DataStoreTransactionException;
+use SmashPig\Core\DataStores\KeyedOpaqueDataStore;
+use SmashPig\Core\DataStores\KeyedOpaqueStorableObject;
+use SmashPig\Core\SmashPigException;
+
+/**
+ * Fakes some data store behavior for tests
+ * TODO: anything that cares about message type
+ * Class MockDataStore
+ */
+class MockDataStore extends KeyedOpaqueDataStore {
+
+ protected $messages = array();
+
+ protected $currentQueue = null;
+
+ public function __construct() {}
+
+ /**
+ * Adds an object to the persistent data store.
+ *
+ * @param \SmashPig\Core\DataStores\KeyedOpaqueStorableObject $obj
+ *
+ * @throws \SmashPig\Core\DataStores\DataStoreException if the message
could not be stored.
+ * @return null
+ */
+ public function addObject( KeyedOpaqueStorableObject $obj ) {
+ $keys = $obj->getObjectKeys();
+ if ( !array_key_exists( 'correlationId', $keys ) ) {
+ throw new DataStoreException(
+ "Required property correlationId was not
exposed."
+ );
+ }
+ $corrId = $keys['correlationId'];
+ if ( !isset( $this->messages[$corrId] ) ) {
+ $this->messages[$corrId] = array();
+ }
+ array_push( $this->messages[$corrId], $obj );
+ }
+
+ /**
+ * Remove objects with the same serialization type and correlation ID
from the
+ * persistent store.
+ * @param \SmashPig\Core\DataStores\KeyedOpaqueStorableObject $protoObj
Prototype to remove.
+ *
+ * @return int Count of messages removed.
+ */
+ public function removeObjects( KeyedOpaqueStorableObject $protoObj ) {
+ // TODO
+ }
+
+ /**
+ * Remove objects with a given correlation ID from the store.
+ *
+ * @param string $id Correlation ID of messages to remove
+ *
+ * @return int Count of messages removed.
+ */
+ public function removeObjectsById( $id ) {
+ if ( isset( $this->messages[$id] ) ) {
+ unset( $this->messages[$id] );
+ }
+ }
+
+ /**
+ *
+ * If a object has not yet been completely acked when this function
gets called,
+ * it will throw a DataStoreTransactionException exception.
+ *
+ * If there were no objects fitting the filter, null will be returned.
+ *
+ * @param string|null $type ignored
+ * @param null|string $id The correlation ID of the message
+ *
+ * @throws \SmashPig\Core\DataStores\DataStoreTransactionException
+ * @return KeyedOpaqueStorableObject|null
+ */
+ public function queueGetObject( $type = null, $id = null ) {
+ if ( $id === null ) {
+ throw new SmashPigException( 'Need id for mock queue' );
+ }
+ if ( empty( $this->messages[$id] ) ) {
+ return null;
+ }
+ $this->currentQueue = & $this->messages[$id];
+ return $this->currentQueue[0];
+ }
+
+ /**
+ * Acknowledges and removes from the backing data store the current
queue object
+ */
+ public function queueAckObject() {
+ if ( !$this->currentQueue ) {
+ throw new DataStoreTransactionException(
+ "No STOMP transaction currently in progress.
Cannot ACK a non-existent message!"
+ );
+ }
+ array_shift( $this->currentQueue );
+ }
+
+ /**
+ * Acknowledges and replaces into the backing data store the current
queue object
+ */
+ public function queueIgnoreObject() {
+ if ( !$this->currentQueue ) {
+ throw new DataStoreTransactionException(
+ "No STOMP transaction currently in progress.
Cannot ACK a non-existent message!"
+ );
+ }
+ $currentMessage = $this->currentQueue[0];
+ $this->queueAckObject();
+ $this->addObject( $currentMessage );
+ }
+}
diff --git a/Tests/bootstrap-phpunit.php b/Tests/bootstrap-phpunit.php
index ed0032a..386c9f9 100644
--- a/Tests/bootstrap-phpunit.php
+++ b/Tests/bootstrap-phpunit.php
@@ -2,3 +2,4 @@
require_once( 'vendor/autoload.php' );
require_once( 'Tests/BaseSmashPigUnitTestCase.php' );
+require_once( 'Tests/MockDataStore.php' );
diff --git a/config_defaults.php b/config_defaults.php
index d21c326..5bbf536 100644
--- a/config_defaults.php
+++ b/config_defaults.php
@@ -117,6 +117,11 @@
'payment-provider' => array(
'adyen' => array(
+ 'api' => array(
+ 'class' =>
'SmashPig\PaymentProviders\Adyen\AdyenPaymentsAPI',
+ 'inst-args' => array(),
+ ),
+
'payments-wsdl' =>
'https://pal-live.adyen.com/pal/Payment.wsdl',
'accounts' => array(
--
To view, visit https://gerrit.wikimedia.org/r/277554
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I4aae0d4fce2275920597867bf2e56d569ce6738d
Gerrit-PatchSet: 8
Gerrit-Project: wikimedia/fundraising/SmashPig
Gerrit-Branch: master
Gerrit-Owner: Ejegg <[email protected]>
Gerrit-Reviewer: Awight <[email protected]>
Gerrit-Reviewer: Cdentinger <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits