Ejegg has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/364143 )
Change subject: WIP Ingenico gateway mimics GlobalCollect ...................................................................... WIP Ingenico gateway mimics GlobalCollect Just swaps out the API calls and response parsing. TODO: actually parse stuff Change-Id: I4c02abd190b1ea27c1921f37196481d87cd0162a --- M gateway_common/donation.api.php A ingenico_gateway/ingenico.adapter.php A tests/phpunit/Adapter/Ingenico/IngenicoApiTest.php A tests/phpunit/Adapter/Ingenico/IngenicoFormLoadTest.php A tests/phpunit/Adapter/Ingenico/IngenicoOrphanAdapterTest.php A tests/phpunit/Adapter/Ingenico/IngenicoOrphanRectifierTest.php A tests/phpunit/Adapter/Ingenico/IngenicoTest.php A tests/phpunit/Adapter/Ingenico/RealTimeBankTransferIdealTest.php A tests/phpunit/Adapter/Ingenico/RecurringTest.php A tests/phpunit/Adapter/Ingenico/ResultSwitcherTest.php 10 files changed, 2,019 insertions(+), 11 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DonationInterface refs/changes/43/364143/1 diff --git a/gateway_common/donation.api.php b/gateway_common/donation.api.php index c90896b..2a4f908 100644 --- a/gateway_common/donation.api.php +++ b/gateway_common/donation.api.php @@ -37,17 +37,23 @@ return; } - if ( $this->gateway == 'globalcollect' ) { - switch ( $method ) { - // TODO: add other iframe payment methods - case 'cc': - $result = $gatewayObj->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); - break; - default: - $result = $gatewayObj->do_transaction( 'TEST_CONNECTION' ); - } - } elseif ( $this->gateway == 'adyen' ) { - $result = $gatewayObj->do_transaction( 'donate' ); + switch( $this->gateway ) { + case 'globalcollect': + switch ( $method ) { + // TODO: add other iframe payment methods + case 'cc': + $result = $gatewayObj->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); + break; + default: + $result = $gatewayObj->do_transaction( 'TEST_CONNECTION' ); + } + break; + case 'ingenico': + $result = $gatewayObj->do_transaction( 'createHostedCheckout' ); + break; + case 'adyen': + $result = $gatewayObj->do_transaction( 'donate' ); + break; } // $normalizedData = $gatewayObj->getData_Unstaged_Escaped(); diff --git a/ingenico_gateway/ingenico.adapter.php b/ingenico_gateway/ingenico.adapter.php new file mode 100644 index 0000000..1718bd9 --- /dev/null +++ b/ingenico_gateway/ingenico.adapter.php @@ -0,0 +1,68 @@ +<?php + +use SmashPig\PaymentProviders\PaymentProviderFactory; + +class IngenicoAdapter extends GlobalCollectAdapter { + const GATEWAY_NAME = 'Ingenico'; + const IDENTIFIER = 'ingenico'; + // For now, use the globals from the GlobalCollect adapter + + public function getCommunicationType() { + return 'namevalue'; + } + + // Not using this, so override parent + public function buildRequestNameValueString() {} + + public function getResponseType() { + return 'json'; + } + + public function curl_transaction( $data ) { + $email = $this->getData_Unstaged_Escaped( 'email' ); + $this->logger->info( "Making API call for donor $email" ); + + $filterResult = $this->runSessionVelocityFilter(); + if ( $filterResult == false ) { + return false; + } + + $provider = $this->getPaymentProvider(); + switch( $this->getCurrentTransaction() ) { + case 'createHostedCheckout': + $payment = new Payment( + $this->getData_Staged( 'amount' ), + $this->getData_Staged( 'currency' ), + $this->getData_Staged( 'order_id' ), + $this->getData_Staged( 'locale' ), + $this->getData_Staged( 'country' ), + array( + 'payment_product' => $this->getData_Staged( 'payment_product' ), + ) + ); + + $result = $provider->createHostedPayment( $payment ); + $this->transaction_response->setRawResponse( json_encode( $result ) ); + return true; + default: + return false; + } + } + + protected function getPaymentProvider() { + $method = $this->getData_Unstaged_Escaped( 'payment_method' ); + return PaymentProviderFactory::getProviderForMethod( $method ); + } + + public function parseResponseCommunicationStatus( $response ) { + return true; + } + + public function parseResponseErrors( $response ) { + return array(); + } + + public function parseResponseData( $response ) { + return $response; + } +} diff --git a/tests/phpunit/Adapter/Ingenico/IngenicoApiTest.php b/tests/phpunit/Adapter/Ingenico/IngenicoApiTest.php new file mode 100644 index 0000000..1d4f1c8 --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/IngenicoApiTest.php @@ -0,0 +1,30 @@ +<?php + +/** + * @group Fundraising + * @group DonationInterface + * @group Ingenico + * @group IngenicoApi + * @group medium + */ + +class IngenicoApiTest extends ApiTestCase { + + public function testGoodSubmit() { + $init = DonationInterfaceTestCase::getDonorTestData(); + $init['email'] = 'g...@innocent.com'; + $init['payment_method'] = 'cc'; + $init['gateway'] = 'ingenico'; + $init['action'] = 'donate'; + + $apiResult = $this->doApiRequest( $init ); + $result = $apiResult[0]['result']; + $orderId = $result['orderid']; + + $this->assertEquals( 'url_placeholder', $result['formaction'], 'GC API not setting formaction' ); + $this->assertTrue( is_numeric( $orderId ), 'Ingenico API not setting numeric order ID' ); + $this->assertTrue( $result['status'], 'Ingenico API result status should be true' ); + preg_match( "/Special:IngenicoGatewayResult\?order_id={$orderId}\$/", $result['returnurl'], $match ); + $this->assertNotEmpty( $match, 'Ingenico API not setting proper return url' ); + } +} diff --git a/tests/phpunit/Adapter/Ingenico/IngenicoFormLoadTest.php b/tests/phpunit/Adapter/Ingenico/IngenicoFormLoadTest.php new file mode 100644 index 0000000..9cb8bb4 --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/IngenicoFormLoadTest.php @@ -0,0 +1,270 @@ +<?php +/** + * Wikimedia Foundation + * + * LICENSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +/** + * + * @group Fundraising + * @group DonationInterface + * @group Ingenico + */ +class IngenicoFormLoadTest extends DonationInterfaceTestCase { + public function setUp() { + parent::setUp(); + + $vmad_countries = array( 'US', ); + $vmaj_countries = array( + 'AD', 'AT', 'AU', 'BE', 'BH', 'DE', 'EC', 'ES', 'FI', 'FR', 'GB', + 'GF', 'GR', 'HK', 'IE', 'IT', 'JP', 'KR', 'LU', 'MY', 'NL', 'PR', + 'PT', 'SG', 'SI', 'SK', 'TH', 'TW', + ); + $vma_countries = array( + 'AE', 'AL', 'AN', 'AR', 'BG', 'CA', 'CH', 'CN', 'CR', 'CY', 'CZ', 'DK', + 'DZ', 'EE', 'EG', 'JO', 'KE', 'HR', 'HU', 'IL', 'KW', 'KZ', 'LB', 'LI', + 'LK', 'LT', 'LV', 'MA', 'MT', 'NO', 'NZ', 'OM', 'PK', 'PL', 'QA', 'RO', + 'RU', 'SA', 'SE', 'TN', 'TR', 'UA', + ); + $this->setMwGlobals( array( + 'wgIngenicoGatewayEnabled' => true, + 'wgDonationInterfaceAllowedHtmlForms' => array( + 'cc-vmad' => array( + 'gateway' => 'ingenico', + 'payment_methods' => array('cc' => array( 'visa', 'mc', 'amex', 'discover' )), + 'countries' => array( + '+' => $vmad_countries, + ), + ), + 'cc-vmaj' => array( + 'gateway' => 'ingenico', + 'payment_methods' => array('cc' => array( 'visa', 'mc', 'amex', 'jcb' )), + 'countries' => array( + '+' => $vmaj_countries, + ), + ), + 'cc-vma' => array( + 'gateway' => 'ingenico', + 'payment_methods' => array('cc' => array( 'visa', 'mc', 'amex' )), + 'countries' => array( + // Array merge with cc-vmaj as fallback in case 'j' goes down + // Array merge with cc-vmad as fallback in case 'd' goes down + '+' => array_merge( + $vmaj_countries, + $vmad_countries, + $vma_countries + ), + ), + ), + 'rtbt-sofo' => array( + 'gateway' => 'ingenico', + 'countries' => array( + '+' => array( 'AT', 'BE', 'CH', 'DE' ), + '-' => 'GB' + ), + 'currencies' => array( '+' => 'EUR' ), + 'payment_methods' => array('rtbt' => 'rtbt_sofortuberweisung'), + ), + ), + ) ); + } + + public function testIngenicoFormLoad() { + $init = $this->getDonorTestData( 'US' ); + unset( $init['order_id'] ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['ffname'] = 'cc-vmad'; + + $assertNodes = array ( + 'submethod-mc' => array ( + 'nodename' => 'input' + ), + 'selected-amount' => array ( + 'nodename' => 'span', + 'innerhtmlmatches' => '/^\s*' . + str_replace( '$', '\$', + Amount::format( 1.55, 'USD', $init['language'] . '_' . $init['country'] ) + ). + '\s*$/', + ), + 'state_province' => array ( + 'nodename' => 'select', + 'selected' => 'CA', + ), + ); + + $this->verifyFormOutput( 'IngenicoGateway', $init, $assertNodes, true ); + } + + function testIngenicoFormLoad_FR() { + $init = $this->getDonorTestData( 'FR' ); + unset( $init['order_id'] ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['ffname'] = 'cc-vmaj'; + + $assertNodes = array ( + 'selected-amount' => array ( + 'nodename' => 'span', + 'innerhtmlmatches' => '/^\s*' . + Amount::format( 1.55, 'EUR', $init['language'] . '_' . $init['country'] ) . + '\s*$/', + ), + 'first_name' => array ( + 'nodename' => 'input', + 'value' => 'Prénom', + ), + 'last_name' => array ( + 'nodename' => 'input', + 'value' => 'Nom', + ), + 'country' => array ( + 'nodename' => 'input', + 'value' => 'FR', + ), + ); + + $this->verifyFormOutput( 'IngenicoGateway', $init, $assertNodes, true ); + } + + /** + * Ensure that form loads for Italy + */ + public function testIngenicoFormLoad_IT() { + $init = $this->getDonorTestData( 'IT' ); + unset( $init['order_id'] ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['ffname'] = 'cc-vmaj'; + + $assertNodes = array ( + 'selected-amount' => array ( + 'nodename' => 'span', + 'innerhtmlmatches' => '/^\s*' . + Amount::format( 1.55, 'EUR', $init['language'] . '_' . $init['country'] ) . + '\s*$/', + ), + 'first_name' => array ( + 'nodename' => 'input', + 'placeholder' => wfMessage( 'donate_interface-donor-first_name')->inLanguage( 'it' )->text(), + ), + 'last_name' => array ( + 'nodename' => 'input', + 'placeholder' => wfMessage( 'donate_interface-donor-last_name')->inLanguage( 'it' )->text(), + ), + 'informationsharing' => array ( + 'nodename' => 'p', + 'innerhtml' => wfMessage( 'donate_interface-informationsharing', '.*' )->inLanguage( 'it' )->text(), + ), + 'country' => array ( + 'nodename' => 'input', + 'value' => 'IT', + ), + ); + + $this->verifyFormOutput( 'IngenicoGateway', $init, $assertNodes, true ); + } + + /** + * Make sure Belgian form loads in all of that country's supported languages + * @dataProvider belgiumLanguageProvider + */ + public function testIngenicoFormLoad_BE( $language ) { + $init = $this->getDonorTestData( 'BE' ); + unset( $init['order_id'] ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['ffname'] = 'cc-vmaj'; + $init['language'] = $language; + + $assertNodes = array ( + 'selected-amount' => array ( + 'nodename' => 'span', + 'innerhtmlmatches' => '/^\s*' . + Amount::format( 1.55, 'EUR', $init['language'] . '_' . $init['country'] ) . + '\s*$/', + ), + 'first_name' => array ( + 'nodename' => 'input', + 'placeholder' => wfMessage( 'donate_interface-donor-first_name')->inLanguage( $language )->text(), + ), + 'last_name' => array ( + 'nodename' => 'input', + 'placeholder' => wfMessage( 'donate_interface-donor-last_name')->inLanguage( $language )->text(), + ), + 'informationsharing' => array ( + 'nodename' => 'p', + 'innerhtml' => wfMessage( 'donate_interface-informationsharing', '.*' )->inLanguage( $language )->text(), + ), + 'country' => array ( + 'nodename' => 'input', + 'value' => 'BE', + ), + ); + + $this->verifyFormOutput( 'IngenicoGateway', $init, $assertNodes, true ); + } + + /** + * Make sure Canadian CC form loads in English and French + * @dataProvider canadaLanguageProvider + */ + public function testIngenicoFormLoad_CA( $language ) { + $init = $this->getDonorTestData( 'CA' ); + unset( $init['order_id'] ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['ffname'] = 'cc-vma'; + $init['language'] = $language; + + $assertNodes = array ( + 'selected-amount' => array ( + 'nodename' => 'span', + 'innerhtmlmatches' => '/^\s*' . + str_replace( '$', '\$', + Amount::format( 1.55, 'CAD', $init['language'] . '_' . $init['country'] ) + ) . + '\s*$/', + ), + 'first_name' => array ( + 'nodename' => 'input', + 'placeholder' => wfMessage( 'donate_interface-donor-first_name')->inLanguage( $language )->text(), + ), + 'last_name' => array ( + 'nodename' => 'input', + 'placeholder' => wfMessage( 'donate_interface-donor-last_name')->inLanguage( $language )->text(), + ), + 'informationsharing' => array ( + 'nodename' => 'p', + 'innerhtml' => wfMessage( 'donate_interface-informationsharing', '.*' )->inLanguage( $language )->text(), + ), + 'state_province' => array ( + 'nodename' => 'select', + 'selected' => 'SK', + ), + 'postal_code' => array ( + 'nodename' => 'input', + 'value' => $init['postal_code'], + ), + 'country' => array ( + 'nodename' => 'input', + 'value' => 'CA', + ), + ); + + $this->verifyFormOutput( 'IngenicoGateway', $init, $assertNodes, true ); + } +} diff --git a/tests/phpunit/Adapter/Ingenico/IngenicoOrphanAdapterTest.php b/tests/phpunit/Adapter/Ingenico/IngenicoOrphanAdapterTest.php new file mode 100644 index 0000000..546e43e --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/IngenicoOrphanAdapterTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Wikimedia Foundation + * + * LICENSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +use Psr\Log\LogLevel; +use SmashPig\Core\Configuration; +use SmashPig\Core\Context; +use SmashPig\CrmLink\Messages\SourceFields; +use Wikimedia\TestingAccessWrapper; + +/** + * + * @group Fundraising + * @group DonationInterface + * @group Ingenico + * @group OrphanSlayer + */ +class DonationInterface_Adapter_Ingenico_Orphans_IngenicoTest extends DonationInterfaceTestCase { + public function setUp() { + parent::setUp(); + + $config = Configuration::createForView( 'ingenico' ); + Context::initWithLogger( $config ); + + $this->setMwGlobals( array( + 'wgIngenicoGatewayEnabled' => true, + 'wgDonationInterfaceAllowedHtmlForms' => array( + 'cc-vmad' => array( + 'gateway' => 'ingenico', + 'payment_methods' => array('cc' => array( 'visa', 'mc', 'amex', 'discover' )), + 'countries' => array( + '+' => array( 'US', ), + ), + ), + ), + ) ); + } + + /** + * @param $name string The name of the test case + * @param $data array Any parameters read from a dataProvider + * @param $dataName string|int The name or index of the data set + */ + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + $this->testAdapterClass = 'TestingIngenicoOrphanAdapter'; + $this->dummy_utm_data = array ( + 'utm_source' => 'dummy_source', + 'utm_campaign' => 'dummy_campaign', + 'utm_medium' => 'dummy_medium', + 'date' => time(), + ); + } + + public function testConstructor() { + + $options = $this->getDonorTestData(); + $class = $this->testAdapterClass; + + $gateway = $this->getFreshGatewayObject(); + + $this->assertInstanceOf( $class, $gateway ); + + $this->verifyNoLogErrors(); + } + + + public function testBatchOrderID_generate() { + + //no data on construct, generate Order IDs + $gateway = $this->getFreshGatewayObject( null, array ( 'order_id_meta' => array ( 'generate' => TRUE ) ) ); + $this->assertTrue( $gateway->getOrderIDMeta( 'generate' ), 'The order_id meta generate setting override is not working properly. Order_id generation may be broken.' ); + $this->assertNotNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed asserting that an absent order id is not left as null, when generating our own' ); + + $data = array_merge( $this->getDonorTestData(), $this->dummy_utm_data ); + $data['order_id'] = '55555'; + + //now, add data and check that we didn't kill the oid. Still generating. + $gateway->loadDataAndReInit( $data ); + $this->assertEquals( $gateway->getData_Unstaged_Escaped( 'order_id' ), '55555', 'loadDataAndReInit failed to stick OrderID' ); + + $data['order_id'] = '444444'; + $gateway->loadDataAndReInit( $data ); + $this->assertEquals( $gateway->getData_Unstaged_Escaped( 'order_id' ), '444444', 'loadDataAndReInit failed to stick OrderID' ); + + $this->verifyNoLogErrors(); + } + + public function testBatchOrderID_no_generate() { + + //no data on construct, do not generate Order IDs + $gateway = $this->getFreshGatewayObject( null, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); + $this->assertFalse( $gateway->getOrderIDMeta( 'generate' ), 'The order_id meta generate setting override is not working properly. Deferred order_id generation may be broken.' ); + $this->assertEmpty( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed asserting that an absent order id is left as null, when not generating our own' ); + + $data = array_merge( $this->getDonorTestData(), $this->dummy_utm_data ); + $data['order_id'] = '66666'; + + //now, add data and check that we didn't kill the oid. Still not generating + $gateway->loadDataAndReInit( $data ); + $this->assertEquals( $gateway->getData_Unstaged_Escaped( 'order_id' ), '66666', 'loadDataAndReInit failed to stick OrderID' ); + + $data['order_id'] = '777777'; + $gateway->loadDataAndReInit( $data ); + $this->assertEquals( $gateway->getData_Unstaged_Escaped( 'order_id' ), '777777', 'loadDataAndReInit failed to stick OrderID on second batch item' ); + + $this->verifyNoLogErrors(); + } + + /** + * Tests to make sure that certain error codes returned from GC will + * trigger order cancellation, even if retryable errors also exist. + * @dataProvider mcNoRetryCodeProvider + */ + public function testNoMastercardFinesForRepeatOnBadCodes( $code ) { + $gateway = $this->getFreshGatewayObject( null, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); + + //Toxic card should not retry, even if there's an order id collision + $init = array_merge( $this->getDonorTestData(), $this->dummy_utm_data ); + $init['ffname'] = 'cc-vmad'; + $init['order_id'] = '55555'; + $init['email'] = 'innoc...@clean.com'; + $init['contribution_tracking_id'] = mt_rand(); + $gateway->loadDataAndReInit( $init ); + + $gateway->setDummyGatewayResponseCode( $code ); + $result = $gateway->do_transaction( 'Confirm_CreditCard' ); + $this->assertEquals( 1, count( $gateway->curled ), "Gateway kept trying even with response code $code! MasterCard could fine us a thousand bucks for that!" ); + $this->assertEquals( false, $result->getCommunicationStatus(), "Error code $code should mean status of do_transaction is false" ); + $errors = $result->getErrors(); + $this->assertFalse( empty( $errors ), 'Orphan adapter needs to see the errors to consider it rectified' ); + $finder = function( $error ) { + return $error->getErrorCode() == '1000001'; + }; + $this->assertNotEmpty( array_filter( $errors, $finder ), 'Orphan adapter needs error 1000001 to consider it rectified' ); + $loglines = $this->getLogMatches( LogLevel::INFO, "/Got error code $code, not retrying to avoid MasterCard fines./" ); + $this->assertNotEmpty( $loglines, "GC Error $code is not generating the expected payments log error" ); + } + + /** + * Make sure we're incorporating GET_ORDERSTATUS AVS and CVV responses into + * fraud scores. + */ + function testGetOrderstatusPostProcessFraud() { + $this->setMwGlobals( array( + 'wgDonationInterfaceEnableCustomFilters' => true, + 'wgIngenicoGatewayCustomFiltersFunctions' => array( + 'getCVVResult' => 10, + 'getAVSResult' => 30, + ), + ) ); + $gateway = $this->getFreshGatewayObject( null, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); + + $init = array_merge( $this->getDonorTestData(), $this->dummy_utm_data ); + $init['ffname'] = 'cc-vmad'; + $init['order_id'] = '55555'; + $init['email'] = 'innoc...@manichean.com'; + $init['contribution_tracking_id'] = mt_rand(); + $init['payment_method'] = 'cc'; + + $gateway->loadDataAndReInit( $init ); + $gateway->setDummyGatewayResponseCode( '600_badCvv' ); + + $gateway->do_transaction( 'Confirm_CreditCard' ); + $action = $gateway->getValidationAction(); + $this->assertEquals( 'review', $action, + 'Orphan gateway should fraud fail on bad CVV and AVS' ); + + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $this->assertEquals( 40, $exposed->risk_score, + 'Risk score was incremented correctly.' ); + $message = DonationQueue::instance()->pop( 'payments-antifraud' ); + SourceFields::removeFromMessage( $message ); + $expected = array( + 'validation_action' => 'review', + 'risk_score' => 40, + 'score_breakdown' => array( + // FIXME: need to enable utm / email / country checks ??? + 'initial' => 0, + 'getCVVResult' => 10, + 'getAVSResult' => 30, + ), + 'user_ip' => null, // FIXME + 'gateway_txn_id' => '55555', + 'date' => $message['date'], + 'server' => gethostname(), + 'gateway' => 'ingenico', + 'contribution_tracking_id' => $gateway->getData_Unstaged_Escaped( 'contribution_tracking_id' ), + 'order_id' => $gateway->getData_Unstaged_Escaped( 'order_id' ), + 'payment_method' => 'cc', + ); + $this->assertEquals( $expected, $message ); + } +} diff --git a/tests/phpunit/Adapter/Ingenico/IngenicoOrphanRectifierTest.php b/tests/phpunit/Adapter/Ingenico/IngenicoOrphanRectifierTest.php new file mode 100644 index 0000000..ee27b85 --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/IngenicoOrphanRectifierTest.php @@ -0,0 +1,218 @@ +<?php + +/** + * Wikimedia Foundation + * + * LICENSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +use SmashPig\Core\Context; +use SmashPig\Core\DataStores\PendingDatabase; +use SmashPig\Tests\SmashPigDatabaseTestConfiguration; + +/** + * @covers IngenicoOrphanRectifier + * + * @group Fundraising + * @group DonationInterface + * @group Ingenico + * @group OrphanSlayer + */ +class DonationInterface_Adapter_Ingenico_Orphan_Rectifier_Test + extends DonationInterfaceTestCase +{ + // TODO: Give vulgar names. + // FIXME: Is 25 the normal unauthorized status? Use the common one, whatever that is. + const STATUS_PENDING = 25; + const STATUS_PENDING_POKE = 600; + const STATUS_COMPLETE = 800; + + // Arbitrary configuration for testing time logic. + const TIME_BUFFER = 60; + const TARGET_EXECUTE_TIME = 1200; + + public $pendingDb; + + public function setUp() { + parent::setUp(); + + $this->setMwGlobals( array( + 'wgDonationInterfaceOrphanCron' => array( + 'enable' => true, + 'target_execute_time' => self::TARGET_EXECUTE_TIME, + 'time_buffer' => self::TIME_BUFFER, + ), + 'wgIngenicoGatewayEnabled' => true, + 'wgDonationInterfaceGatewayAdapters' => array( + // We include the regular adapter in order to pass gateway validation D: + 'ingenico' => 'TestingIngenicoOrphanAdapter', + 'ingenico_orphan' => 'TestingIngenicoOrphanAdapter', + ), + ) ); + + $config = SmashPigDatabaseTestConfiguration::instance(); + Context::init( $config ); + + $this->pendingDb = PendingDatabase::get(); + + // Create the schema. + $this->pendingDb->createTable(); + } + + /** + * When leaving a message unprocessed and pending, don't try to process it + * again. + */ + public function testProcessOrphansStatusPending() { + $orphan_pending = $this->createOrphan(); + + $rectifier = new IngenicoOrphanRectifier(); + $this->gateway = $rectifier->getAdapter(); + $this->gateway->setDummyGatewayResponseCode( self::STATUS_PENDING ); + $rectifier->processOrphans(); + + $fetched = $this->pendingDb->fetchMessageByGatewayOrderId( + 'ingenico', $orphan_pending['order_id'] ); + $this->assertNull( $fetched, + 'Message was popped.' ); + + $this->assertGatewayCallsExactly( array( + 'GET_ORDERSTATUS' + ) ); + } + + /** + * If a message is waiting for the API kiss of death, perform it. + */ + public function testProcessOrphansStatusPendingPoke() { + $orphan_pending_poke = $this->createOrphan(); + + $rectifier = new IngenicoOrphanRectifier(); + $this->gateway = $rectifier->getAdapter(); + $this->gateway->setDummyGatewayResponseCode( self::STATUS_PENDING_POKE ); + $rectifier->processOrphans(); + + $fetched = $this->pendingDb->fetchMessageByGatewayOrderId( + 'ingenico', $orphan_pending_poke['order_id'] ); + $this->assertNull( $fetched, + 'Message was popped' ); + + $this->assertGatewayCallsExactly( array( + 'GET_ORDERSTATUS', + 'SET_PAYMENT', + ) ); + + // TODO: test that we sent a completion message + } + + /** + * Report a completed transaction. + */ + public function testProcessOrphansStatusComplete() { + + $orphan_complete = $this->createOrphan(); + + $rectifier = new IngenicoOrphanRectifier(); + $this->gateway = $rectifier->getAdapter(); + $this->gateway->setDummyGatewayResponseCode( self::STATUS_COMPLETE ); + $rectifier->processOrphans(); + + $fetched = $this->pendingDb->fetchMessageByGatewayOrderId( + 'ingenico', $orphan_complete['order_id'] ); + $this->assertNull( $fetched, + 'Message was popped' ); + + $this->assertGatewayCallsExactly( array( + 'GET_ORDERSTATUS', + ) ); + + // TODO: test that we sent a completion message + } + + /** + * Don't process recent messages. + */ + public function testTooRecentMessage() { + $orphan_complete = $this->createOrphan( array( + 'date' => time() - self::TIME_BUFFER + 30, + ) ); + + $rectifier = new IngenicoOrphanRectifier(); + $this->gateway = $rectifier->getAdapter(); + $rectifier->processOrphans(); + + $fetched = $this->pendingDb->fetchMessageByGatewayOrderId( + 'ingenico', $orphan_complete['order_id'] ); + $this->assertNotNull( $fetched, + 'Message was not popped' ); + + $this->assertGatewayCallsExactly( array() ); + + // TODO: Test that we: + // * Logged the "done with old messages" line. + } + + /** + * Create an orphaned tranaction and store it to the pending database. + * + * TODO: Reuse SmashPigBaseTest#createMessage + */ + public function createOrphan( $overrides = array() ) { + $uniq = mt_rand(); + $message = $overrides + array( + 'contribution_tracking_id' => $uniq, + 'first_name' => 'Flighty', + 'last_name' => 'Dono', + 'email' => 'test+...@eff.org', + 'gateway' => 'ingenico', + 'gateway_txn_id' => "txn-{$uniq}", + 'order_id' => "order-{$uniq}", + 'gateway_account' => 'default', + 'payment_method' => 'cc', + 'payment_submethod' => 'mc', + // Defaults to a magic 25 minutes ago, within the process window. + 'date' => time() - 25 * 60, + 'amount' => 123, + 'currency' => 'EUR', + ); + $this->pendingDb->storeMessage( $message ); + return $message; + } + + /** + * Assert whether we made exactly the expected gateway calls + * + * @param array $expected List of API action names, in the form they appear + * in the <ACTION> tag. + */ + protected function assertGatewayCallsExactly( $expected ) { + $expected_num_calls = count( $expected ); + $this->assertEquals( $expected_num_calls, count( $this->gateway->curled ), + "Ran exactly {$expected_num_calls} API calls" ); + foreach ( $expected as $index => $action ) { + $this->assertRegExp( '/\b' . $action . '\b/', $this->gateway->curled[$index], + "Call #" . ( $index + 1 ) . " was {$action}." ); + } + } + + /** + * Dump the entire database state, for debugging. + */ + protected function debugDbContents() { + $result = $this->pendingDb->getDatabase()->query( + "select * from pending" ); + $rows = $result->fetchAll( PDO::FETCH_ASSOC ); + var_export($rows); + } +} diff --git a/tests/phpunit/Adapter/Ingenico/IngenicoTest.php b/tests/phpunit/Adapter/Ingenico/IngenicoTest.php new file mode 100644 index 0000000..fc35370 --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/IngenicoTest.php @@ -0,0 +1,638 @@ +<?php +/** + * Wikimedia Foundation + * + * LICENSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +use Psr\Log\LogLevel; +use Wikimedia\TestingAccessWrapper; + +/** + * + * @group Fundraising + * @group DonationInterface + * @group Ingenico + */ +class DonationInterface_Adapter_Ingenico_IngenicoTest extends DonationInterfaceTestCase { + public function setUp() { + parent::setUp(); + + $this->setMwGlobals( array( + 'wgIngenicoGatewayEnabled' => true, + 'wgDonationInterfaceAllowedHtmlForms' => array( + 'cc-vmad' => array( + 'gateway' => 'ingenico', + 'payment_methods' => array('cc' => array( 'visa', 'mc', 'amex', 'discover' )), + 'countries' => array( + '+' => array( 'US', ), + ), + ), + ), + ) ); + } + + /** + * @param $name string The name of the test case + * @param $data array Any parameters read from a dataProvider + * @param $dataName string|int The name or index of the data set + */ + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + $this->testAdapterClass = 'TestingIngenicoAdapter'; + } + + /** + * testnormalizeOrderID + * Non-exhaustive integration tests to verify that order_id + * normalization works as expected with different settings and + * conditions in theIngenico adapter + * @covers GatewayAdapter::normalizeOrderID + */ + public function testNormalizeOrderID() { + $request = $this->getDonorTestData(); + $externalData = $this->getDonorTestData(); + $session = array( 'Donor' => $this->getDonorTestData() ); + + //no order_id from anywhere, explicit no generate + $gateway = $this->getFreshGatewayObject( $externalData, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); + $this->assertFalse( $gateway->getOrderIDMeta( 'generate' ), 'The order_id meta generate setting override is not working properly. Deferred order_id generation may be broken.' ); + $this->assertNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed asserting that an absent order id is left as null, when not generating our own' ); + + //no order_id from anywhere, explicit generate + $gateway = $this->getFreshGatewayObject( $externalData, array ( 'order_id_meta' => array ( 'generate' => TRUE ) ) ); + $this->assertTrue( $gateway->getOrderIDMeta( 'generate' ), 'The order_id meta generate setting override is not working properly. Self order_id generation may be broken.' ); + $this->assertInternalType( 'numeric', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Generated order_id is not numeric, which it should be for Ingenico' ); + + // conflicting order_id in request and session, default GC generation + $request['order_id'] = '55555'; + $session['Donor']['order_id'] = '44444'; + $this->setUpRequest( $request, $session ); + $gateway = new TestingIngenicoAdapter(); + $this->assertEquals( '55555', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Ingenico gateway is preferring session data over the request. Session should be secondary.' ); + + // conflicting order_id in request and session, garbage data in request, default GC generation + $request['order_id'] = 'nonsense!'; + $this->setUpRequest( $request, $session ); + $gateway = new TestingIngenicoAdapter(); + $this->assertEquals( '44444', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Ingenico gateway is not ignoring nonsensical order_id candidates' ); + + // order_id in session, default GC generation + unset( $request['order_id'] ); + $this->setUpRequest( $request, $session ); + $gateway = new TestingIngenicoAdapter(); + $this->assertEquals( '44444', $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Ingenico gateway is not recognizing the session order_id' ); + + // conflicting order_id in external data, request and session, explicit GC generation, batch mode + $request['order_id'] = '33333'; + $externalData['order_id'] = '22222'; + $this->setUpRequest( $request, $session ); + $gateway = $this->getFreshGatewayObject( $externalData, array ( 'order_id_meta' => array ( 'generate' => true ), 'batch_mode' => true ) ); + $this->assertEquals( $externalData['order_id'], $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed asserting that an extrenally provided order id is being honored in batch mode' ); + + //make sure that decimal numbers are rejected by GC. Should be a toss and regen + $externalData['order_id'] = '2143.0'; + unset( $request['order_id'] ); + unset( $session['Donor']['order_id'] ); + $this->setUpRequest( $request, $session ); + //conflicting order_id in external data, request and session, explicit GC generation, batch mode + $gateway = $this->getFreshGatewayObject( $externalData, array ( 'order_id_meta' => array ( 'generate' => true, 'disallow_decimals' => true ), 'batch_mode' => true ) ); + $this->assertNotEquals( $externalData['order_id'], $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Failed assering that a decimal order_id was regenerated, when disallow_decimals is true' ); + } + + /** + * Non-exhaustive integration tests to verify that order_id, when in + * self-generation mode, won't regenerate until it is told to. + * @covers GatewayAdapter::normalizeOrderID + * @covers GatewayAdapter::regenerateOrderID + */ + function testStickyGeneratedOrderID() { + $init = self::$initial_vars; + unset( $init['order_id'] ); + + //no order_id from anywhere, explicit generate + $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => TRUE ) ) ); + $this->assertNotNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Generated order_id is null. The rest of this test is broken.' ); + $original_order_id = $gateway->getData_Unstaged_Escaped( 'order_id' ); + + $gateway->normalizeOrderID(); + $this->assertEquals( $original_order_id, $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Re-normalized order_id has changed without explicit regeneration.' ); + + //this might look a bit strange, but we need to be able to generate valid order_ids without making them stick to anything. + $gateway->generateOrderID(); + $this->assertEquals( $original_order_id, $gateway->getData_Unstaged_Escaped( 'order_id' ), 'function generateOrderID auto-changed the selected order ID. Not cool.' ); + + $gateway->regenerateOrderID(); + $this->assertNotEquals( $original_order_id, $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Re-normalized order_id has not changed, after explicit regeneration.' ); + } + + /** + * Integration test to verify that order_id can be retrieved from + * performing an INSERT_ORDERWITHPAYMENT. + */ + function testOrderIDRetrieval() { + $init = $this->getDonorTestData(); + unset( $init['order_id'] ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + + //no order_id from anywhere, explicit generate + $gateway = $this->getFreshGatewayObject( $init, array ( 'order_id_meta' => array ( 'generate' => FALSE ) ) ); + $this->assertNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'Ungenerated order_id is not null. The rest of this test is broken.' ); + + $gateway->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); + + $this->assertNotNull( $gateway->getData_Unstaged_Escaped( 'order_id' ), 'No order_id was retrieved from INSERT_ORDERWITHPAYMENT' ); + } + + /** + * Just run the GET_ORDERSTATUS transaction and make sure we load the data + */ + function testGetOrderStatus() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@safedomain.org'; + + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->do_transaction( 'GET_ORDERSTATUS' ); + + $data = $gateway->getTransactionData(); + + $this->assertEquals( 'N', $data['CVVRESULT'], 'CVV Result not loaded from XML response' ); + } + + /** + * Don't fraud-fail someone for bad CVV if GET_ORDERSTATUS + * comes back with STATUSID 25 and no CVVRESULT + * @group CvvResult + */ + function testConfirmCreditCardStatus25() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@safedomain.org'; + + $this->setUpRequest( array( 'CVVRESULT' => 'M' ) ); + + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( '25' ); + + $gateway->do_transaction( 'Confirm_CreditCard' ); + $action = $gateway->getValidationAction(); + $this->assertEquals( 'process', $action, 'Gateway should not fraud fail on STATUSID 25' ); + } + + /** + * Make sure we're incorporating GET_ORDERSTATUS AVS and CVV responses into + * fraud scores. + */ + function testGetOrderstatusPostProcessFraud() { + $this->setMwGlobals( array( + 'wgDonationInterfaceEnableCustomFilters' => true, + 'wgIngenicoGatewayCustomFiltersFunctions' => array( + 'getCVVResult' => 10, + 'getAVSResult' => 30, + ), + ) ); + + $init = $this->getDonorTestData(); + $init['ffname'] = 'cc-vmad'; + $init['order_id'] = '55555'; + $init['email'] = 'innoc...@manichean.com'; + $init['contribution_tracking_id'] = mt_rand(); + $init['payment_method'] = 'cc'; + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->setDummyGatewayResponseCode( '600_badCvv' ); + + $gateway->do_transaction( 'Confirm_CreditCard' ); + $action = $gateway->getValidationAction(); + $this->assertEquals( 'review', $action, + 'Orphan gateway should fraud fail on bad CVV and AVS' ); + + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $this->assertEquals( 40, $exposed->risk_score, + 'Risk score was incremented correctly.' ); + } + + /** + * Ensure the Confirm_CreditCard transaction prefers CVVRESULT from the XML + * over any value from the querystring + */ + function testConfirmCreditCardPrefersXmlCvv() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@safedomain.org'; + + $this->setUpRequest( array( 'CVVRESULT' => 'M' ) ); + + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->do_transaction( 'Confirm_CreditCard' ); + + $this->assertEquals( 'N', $gateway->getData_Unstaged_Escaped('cvv_result'), 'CVV Result not taken from XML response' ); + } + + /** + * Make sure we record the actual amount charged, even if the donor has + * opened a new window and screwed up their session data. + */ + function testConfirmCreditCardUpdatesAmount() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@safedomain.org'; + // The values in session are not the values we originally used + // for INSERT_ORDERWITHPAYMENT + $init['amount'] = '12.50'; + $init['currency'] = 'USD'; + + $gateway = $this->getFreshGatewayObject( $init ); + + $amount = $gateway->getData_Unstaged_Escaped( 'amount' ); + $currency = $gateway->getData_Unstaged_Escaped( 'currency' ); + $this->assertEquals( '12.50', $amount ); + $this->assertEquals( 'USD', $currency ); + + $gateway->do_transaction( 'Confirm_CreditCard' ); + + $amount = $gateway->getData_Unstaged_Escaped( 'amount' ); + $currency = $gateway->getData_Unstaged_Escaped( 'currency' ); + $this->assertEquals( '23.45', $amount, 'Not recording correct amount' ); + $this->assertEquals( 'EUR', $currency, 'Not recording correct currency' ); + } + + /** + * testDefineVarMap + * + * This is tested with a bank transfer from Spain. + * + * @covers IngenicoAdapter::__construct + * @covers IngenicoAdapter::defineVarMap + */ + public function testDefineVarMap() { + + $gateway = $this->getFreshGatewayObject( self::$initial_vars ); + + $var_map = array( + 'ORDERID' => 'order_id', + 'AMOUNT' => 'amount', + 'CURRENCYCODE' => 'currency', + 'LANGUAGECODE' => 'language', + 'COUNTRYCODE' => 'country', + 'MERCHANTREFERENCE' => 'contribution_tracking_id', + 'RETURNURL' => 'returnto', + 'IPADDRESS' => 'server_ip', + 'ISSUERID' => 'issuer_id', + 'PAYMENTPRODUCTID' => 'payment_product', + 'CVV' => 'cvv', + 'EXPIRYDATE' => 'expiration', + 'CREDITCARDNUMBER' => 'card_num', + 'FIRSTNAME' => 'first_name', + 'SURNAME' => 'last_name', + 'STREET' => 'street_address', + 'CITY' => 'city', + 'STATE' => 'state_province', + 'ZIP' => 'postal_code', + 'EMAIL' => 'email', + 'ACCOUNTHOLDER' => 'account_holder', + 'ACCOUNTNAME' => 'account_name', + 'ACCOUNTNUMBER' => 'account_number', + 'ADDRESSLINE1E' => 'address_line_1e', + 'ADDRESSLINE2' => 'address_line_2', + 'ADDRESSLINE3' => 'address_line_3', + 'ADDRESSLINE4' => 'address_line_4', + 'ATTEMPTID' => 'attempt_id', + 'AUTHORISATIONID' => 'authorization_id', + 'BANKACCOUNTNUMBER' => 'bank_account_number', + 'BANKAGENZIA' => 'bank_agenzia', + 'BANKCHECKDIGIT' => 'bank_check_digit', + 'BANKCODE' => 'bank_code', + 'BANKFILIALE' => 'bank_filiale', + 'BANKNAME' => 'bank_name', + 'BRANCHCODE' => 'branch_code', + 'COUNTRYCODEBANK' => 'country_code_bank', + 'COUNTRYDESCRIPTION' => 'country_description', + 'CUSTOMERBANKCITY' => 'customer_bank_city', + 'CUSTOMERBANKSTREET' => 'customer_bank_street', + 'CUSTOMERBANKNUMBER' => 'customer_bank_number', + 'CUSTOMERBANKZIP' => 'customer_bank_zip', + 'DATECOLLECT' => 'date_collect', + 'DESCRIPTOR' => 'descriptor', + 'DIRECTDEBITTEXT' => 'direct_debit_text', + 'DOMICILIO' => 'domicilio', + 'EFFORTID' => 'effort_id', + 'IBAN' => 'iban', + 'IPADDRESSCUSTOMER' => 'user_ip', + 'PAYMENTREFERENCE' => 'payment_reference', + 'PROVINCIA' => 'provincia', + 'SPECIALID' => 'special_id', + 'SWIFTCODE' => 'swift_code', + 'TRANSACTIONTYPE' => 'transaction_type', + 'FISCALNUMBER' => 'fiscal_number', + 'AVSRESULT' => 'avs_result', + 'CVVRESULT' => 'cvv_result', + ); + + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $this->assertEquals( $var_map, $exposed->var_map ); + } + + public function testLanguageStaging() { + $options = $this->getDonorTestData( 'NO' ); + $options['payment_method'] = 'cc'; + $options['payment_submethod'] = 'visa'; + $gateway = $this->getFreshGatewayObject( $options ); + + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $exposed->stageData(); + + $this->assertEquals( $exposed->getData_Staged( 'language' ), 'no', "'NO' donor's language was inproperly set. Should be 'no'" ); + } + + public function testLanguageFallbackStaging() { + $options = $this->getDonorTestData( 'Catalonia' ); + $options['payment_method'] = 'cc'; + $options['payment_submethod'] = 'visa'; + $gateway = $this->getFreshGatewayObject( $options ); + + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $exposed->stageData(); + + // Requesting the fallback language from the gateway. + $this->assertEquals( 'en', $exposed->getData_Staged( 'language' ) ); + } + + /** + * Make sure unstaging functions don't overwrite core donor data. + */ + public function testAddResponseData_underzealous() { + $options = $this->getDonorTestData( 'Catalonia' ); + $options['payment_method'] = 'cc'; + $options['payment_submethod'] = 'visa'; + $gateway = $this->getFreshGatewayObject( $options ); + + // This will set staged_data['language'] = 'en'. + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $exposed->stageData(); + + $ctid = mt_rand(); + + $gateway->addResponseData( array( + 'contribution_tracking_id' => $ctid . '.1', + ) ); + + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + // Desired vars were written into normalized data. + $this->assertEquals( $ctid, $exposed->dataObj->getVal( 'contribution_tracking_id' ) ); + + // Language was not overwritten. + $this->assertEquals( 'ca', $exposed->dataObj->getVal( 'language' ) ); + } + + /** + * Tests to make sure that certain error codes returned from GC will or + * will not create payments error loglines. + */ + function testCCLogsOnGatewayError() { + $init = $this->getDonorTestData( 'US' ); + unset( $init['order_id'] ); + $init['ffname'] = 'cc-vmad'; + + //this should not throw any payments errors: Just an invalid card. + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( '430285' ); + $gateway->do_transaction( 'GET_ORDERSTATUS' ); + $this->verifyNoLogErrors(); + + //Now test one we want to throw a payments error + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( '21000050' ); + $gateway->do_transaction( 'GET_ORDERSTATUS' ); + $loglines = $this->getLogMatches( LogLevel::ERROR, '/Investigation required!/' ); + $this->assertNotEmpty( $loglines, 'GC Error 21000050 is not generating the expected payments log error' ); + + //Reset logs + $this->testLogger->messages = array(); + + //Most irritating version of 20001000 - They failed to enter an expiration date on GC's form. This should log some specific info, but not an error. + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( '20001000-expiry' ); + $gateway->do_transaction( 'GET_ORDERSTATUS' ); + $this->verifyNoLogErrors(); + $loglines = $this->getLogMatches( LogLevel::INFO, '/processResponse:.*EXPIRYDATE/' ); + $this->assertNotEmpty( $loglines, 'GC Error 20001000-expiry is not generating the expected payments log line' ); + } + + /** + * Tests to make sure that certain error codes returned from GC will + * trigger order cancellation, even if retryable errors also exist. + * @dataProvider mcNoRetryCodeProvider + */ + public function testNoMastercardFinesForRepeatOnBadCodes( $code ) { + $init = $this->getDonorTestData( 'US' ); + unset( $init['order_id'] ); + $init['ffname'] = 'cc-vmad'; + //Make it not look like an orphan + $this->setUpRequest( array( + 'CVVRESULT' => 'M', + 'AVSRESULT' => '0' + ) ); + + //Toxic card should not retry, even if there's an order id collision + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( $code ); + $gateway->do_transaction( 'Confirm_CreditCard' ); + $this->assertEquals( 1, count( $gateway->curled ), "Gateway kept trying even with response code $code! MasterCard could fine us a thousand bucks for that!" ); + } + + /** + * Tests that two API requests don't send the same order ID and merchant + * reference. This was the case when users doubleclicked and we were + * using the last 5 digits of time in seconds as a suffix. We want to see + * what happens when a 2nd request comes in while the 1st is still waiting + * for a CURL response, so here we fake that situation by having CURL throw + * an exception during the 1st response. + */ + public function testNoDupeOrderId( ) { + $this->setUpRequest( array( + 'action'=>'donate', + 'amount'=>'3.00', + 'card_type'=>'amex', + 'city'=>'Hollywood', + 'contribution_tracking_id'=>'22901382', + 'country'=>'US', + 'currency'=>'USD', + 'email'=>'faketyf...@gmail.com', + 'first_name'=>'Fakety', + 'format'=>'json', + 'gateway'=>'ingenico', + 'language'=>'en', + 'last_name'=>'Fake', + 'payment_method'=>'cc', + 'referrer'=>'http://en.wikipedia.org/wiki/Main_Page', + 'state_province'=>'MA', + 'street_address'=>'99 Fake St', + 'utm_campaign'=>'C14_en5C_dec_dsk_FR', + 'utm_medium'=>'sitenotice', + 'utm_source'=>'B14_120921_5C_lg_fnt_sans.no-LP.cc', + 'postal_code'=>'90210' + ) ); + + $gateway = new TestingIngenicoAdapter(); + $gateway->setDummyGatewayResponseCode( 'Exception' ); + try { + $gateway->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); + } + catch ( Exception $e ) { + // totally expected this + } + $first = $gateway->curled[0]; + //simulate another request coming in before we get anything back from GC + $anotherGateway = new TestingIngenicoAdapter(); + $anotherGateway->do_transaction( 'INSERT_ORDERWITHPAYMENT' ); + $second = $anotherGateway->curled[0]; + $this->assertFalse( $first == $second, 'Two calls to the api did the same thing'); + } + + /** + * Tests to see that we don't claim we're going to retry when we aren't + * going to. For GC, we really only want to retry on code 300620 + * @dataProvider benignNoRetryCodeProvider + */ + public function testNoClaimRetryOnBoringCodes( $code ) { + $init = $this->getDonorTestData( 'US' ); + unset( $init['order_id'] ); + $init['ffname'] = 'cc-vmad'; + //Make it not look like an orphan + $this->setUpRequest( array( + 'CVVRESULT' => 'M', + 'AVSRESULT' => '0' + ) ); + + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( $code ); + $exposed = TestingAccessWrapper::newFromObject( $gateway ); + $start_id = $exposed->getData_Staged( 'order_id' ); + $gateway->do_transaction( 'Confirm_CreditCard' ); + $finish_id = $exposed->getData_Staged( 'order_id' ); + $loglines = $this->getLogMatches( LogLevel::INFO, '/Repeating transaction on request for vars:/' ); + $this->assertEmpty( $loglines, "Log says we are going to repeat the transaction for code $code, but that is not true" ); + $this->assertEquals( $start_id, $finish_id, "Needlessly regenerated order id for code $code "); + } + + /** + * doPayment should return an iframe result with normal data + */ + function testDoPaymentSuccess() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@clean.com'; + $init['ffname'] = 'cc-vmad'; + unset( $init['order_id'] ); + + $gateway = $this->getFreshGatewayObject( $init ); + $result = $gateway->doPayment(); + $this->assertEmpty( $result->isFailed(), 'PaymentResult should not be failed' ); + $this->assertEmpty( $result->getErrors(), 'PaymentResult should have no errors' ); + $this->assertEquals( 'url_placeholder', $result->getIframe(), 'PaymentResult should have iframe set' ); + } + + /** + * doPayment should recover from an attempt to use a duplicate order ID. + */ + function testDuplicateOrderId() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@localhost.net'; + $init['ffname'] = 'cc-vmad'; + unset( $init['order_id'] ); + + $gateway = $this->getFreshGatewayObject( $init ); + $orig_id = $gateway->getData_Unstaged_Escaped( 'order_id' ); + $gateway->setDummyGatewayResponseCode( function ( $gateway ) use ( $orig_id ) { + if ( $gateway->getData_Unstaged_Escaped( 'order_id' ) === $orig_id ) { + return 'duplicate'; + } else { + return null; + } + } ); + $result = $gateway->doPayment(); + $this->assertEmpty( $result->isFailed(), 'PaymentResult should not be failed' ); + $this->assertEmpty( $result->getErrors(), 'PaymentResult should have no errors' ); + $this->assertNotEquals( $gateway->getData_Unstaged_Escaped( 'order_id' ), $orig_id, + 'Order ID regenerated in DonationData.' ); + $this->assertNotEquals( $gateway->session_getData( 'order_id' ), $orig_id, + 'Order ID regenerated in session.' ); + } + + /** + * doPayment should recover from Ingenico-side timeouts. + */ + function testTimeoutRecover() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@localhost.net'; + $init['ffname'] = 'cc-vmad'; + + $gateway = $this->getFreshGatewayObject( $init ); + $gateway->setDummyGatewayResponseCode( '11000400' ); + $gateway->do_transaction( 'SET_PAYMENT' ); + $loglines = $this->getLogMatches( LogLevel::INFO, '/Repeating transaction for timeout/' ); + $this->assertNotEmpty( $loglines, "Log does not say we retried for timeout." ); + } + + public function testDonorReturnSuccess() { + $init = $this->getDonorTestData( 'FR' ); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@localhost.net'; + $init['order_id'] = mt_rand(); + $session['Donor'] = $init; + $this->setUpRequest( $init, $session ); + $gateway = $this->getFreshGatewayObject( array() ); + $result = $gateway->processDonorReturn( array( + 'REF' => $init['order_id'], + 'CVVRESULT' => 'M', + 'AVSRESULT' => '0' + ) ); + $this->assertFalse( $result->isFailed() ); + $this->assertEmpty( $result->getErrors() ); + // TODO inspect the queue message + } + + public function testDonorReturnFailure() { + $init = $this->getDonorTestData(); + $init['payment_method'] = 'cc'; + $init['payment_submethod'] = 'visa'; + $init['email'] = 'innoc...@localhost.net'; + $init['order_id'] = mt_rand(); + $session['Donor'] = $init; + $this->setUpRequest( $init, $session ); + $gateway = $this->getFreshGatewayObject( array() ); + $gateway->setDummyGatewayResponseCode( '430285' ); // invalid card + $result = $gateway->processDonorReturn( array( + 'REF' => $init['order_id'], + 'CVVRESULT' => 'M', + 'AVSRESULT' => '0' + ) ); + $this->assertTrue( $result->isFailed() ); + } +} diff --git a/tests/phpunit/Adapter/Ingenico/RealTimeBankTransferIdealTest.php b/tests/phpunit/Adapter/Ingenico/RealTimeBankTransferIdealTest.php new file mode 100644 index 0000000..87297af --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/RealTimeBankTransferIdealTest.php @@ -0,0 +1,372 @@ +<?php +/** + * Wikimedia Foundation + * + * LICENSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ +use SmashPig\Core\Configuration; +use SmashPig\Core\Context; + +/** + * + * @group Fundraising + * @group DonationInterface + * @group Ingenico + * @group RealTimeBankTransfer + */ +class DonationInterface_Adapter_Ingenico_RealTimeBankTransferIdealTest extends DonationInterfaceTestCase { + /** + * @var PHPUnit_Framework_MockObject_MockObject + */ + protected $bankPaymentProvider; + + public function setUp() { + parent::setUp(); + + $config = Configuration::createForView( 'ingenico' ); + Context::initWithLogger( $config ); // gets torn down in parent + + $this->bankPaymentProvider = $this->getMockBuilder( + '\SmashPig\PaymentProviders\Ingenico\BankPaymentProvider' + )->disableOriginalConstructor()->getMock(); + + $config->overrideObjectInstance( 'payment-provider/rtbt', $this->bankPaymentProvider ); + + $this->bankPaymentProvider->method( 'getBankList' ) + ->willReturn( array( + 'Test1234' => 'Test Bank 1234', + 'Test5678' => 'Test Bank 5678', + ) ); + + + $this->setMwGlobals( array( + 'wgIngenicoGatewayEnabled' => true, + 'wgDonationInterfaceAllowedHtmlForms' => array( + 'rtbt-ideal' => array( + 'gateway' => 'ingenico', + 'payment_methods' => array('rtbt' => 'rtbt_ideal'), + 'countries' => array( '+' => 'NL' ), + 'currencies' => array( '+' => 'EUR' ), + ), + ), + ) ); + } + + /** + * Test for ideal form loading + */ + public function testIngenicoFormLoad_rtbt_Ideal() { + $init = $this->getDonorTestData( 'NL' ); + unset( $init['order_id'] ); + $init['payment_method'] = 'rtbt'; + $init['ffname'] = 'rtbt-ideal'; + + $assertNodes = array ( + 'amount' => array ( + 'nodename' => 'input', + 'value' => '1.55', + ), + 'currency' => array ( + 'nodename' => 'select', + 'selected' => 'EUR', + ), + 'country' => array ( + 'nodename' => 'input', + 'value' => 'NL', + ), + 'issuer_id' => array ( + 'innerhtmlmatches' => '/Test Bank 1234/' + ) + ); + + $this->verifyFormOutput( 'IngenicoGateway', $init, $assertNodes, true ); + } + + /** + * testBuildRequestXmlWithIssuerId21 + * + * Rabobank: 21 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId21() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 21, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId31 + * + * ABN AMRO: 31 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId31() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 31, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId91 + * + * Rabobank: 21 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId91() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 21, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId161 + * + * Van Lanschot Bankiers: 161 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId161() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 161, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId511 + * + * Triodos Bank: 511 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId511() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 511, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId721 + * + * ING: 721 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId721() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 721, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId751 + * + * SNS Bank: 751 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId751() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 751, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId761 + * + * ASN Bank: 761 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId761() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 761, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + /** + * testBuildRequestXmlWithIssuerId771 + * + * RegioBank: 771 + * + * @covers GatewayAdapter::__construct + * @covers GatewayAdapter::setCurrentTransaction + * @covers GatewayAdapter::buildRequestXML + * @covers GatewayAdapter::getData_Unstaged_Escaped + */ + public function testBuildRequestXmlWithIssuerId771() { + + $optionsForTestData = array( + 'form_name' => 'TwoStepAmount', + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'payment_product_id' => 809, + 'issuer_id' => 771, + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + unset( $options['payment_product_id'] ); + + $this->buildRequestXmlForIngenico( $optionsForTestData, $options ); + } + + public function testFormAction() { + + $optionsForTestData = array ( + 'payment_method' => 'rtbt', + 'payment_submethod' => 'rtbt_ideal', + 'issuer_id' => 771, + // Email is required for RTBT. + 'email' => 'nob...@wikimedia.org', + ); + + //somewhere else? + $options = $this->getDonorTestData( 'ES' ); + $options = array_merge( $options, $optionsForTestData ); + + $this->gatewayAdapter = $this->getFreshGatewayObject( $options ); + + $this->assertTrue( $this->gatewayAdapter->validatedOK() ); + + $this->gatewayAdapter->do_transaction( "INSERT_ORDERWITHPAYMENT" ); + $action = $this->gatewayAdapter->getTransactionDataFormAction(); + $this->assertEquals( "url_placeholder", $action, "The formaction was not populated as expected (ideal)." ); + } + +} + diff --git a/tests/phpunit/Adapter/Ingenico/RecurringTest.php b/tests/phpunit/Adapter/Ingenico/RecurringTest.php new file mode 100644 index 0000000..b40730f --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/RecurringTest.php @@ -0,0 +1,142 @@ +<?php +/** + * Wikimedia Foundation + * + * LICENSE + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +/** + * + * @group Fundraising + * @group DonationInterface + * @group Ingenico + * @group Recurring + */ +class DonationInterface_Adapter_Ingenico_RecurringTest extends DonationInterfaceTestCase { + + /** + * @param $name string The name of the test case + * @param $data array Any parameters read from a dataProvider + * @param $dataName string|int The name or index of the data set + */ + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + $this->testAdapterClass = 'TestingIngenicoAdapter'; + } + + public function setUp() { + parent::setUp(); + + $this->setMwGlobals( array( + 'wgIngenicoGatewayEnabled' => true, + ) ); + } + + /** + * Can make a recurring payment + * + * @covers IngenicoAdapter::transactionRecurring_Charge + */ + public function testRecurringCharge() { + $init = array( + 'amount' => '2345', + 'effort_id' => 2, + 'order_id' => '9998890004', + 'currency' => 'EUR', + 'payment_product' => '', + ); + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->setDummyGatewayResponseCode( 'recurring-OK' ); + + $result = $gateway->do_transaction( 'Recurring_Charge' ); + + $this->assertTrue( $result->getCommunicationStatus() ); + $this->assertRegExp( '/SET_PAYMENT/', $result->getRawResponse() ); + } + + /** + * Can make a recurring payment + * + * @covers IngenicoAdapter::transactionRecurring_Charge + */ + public function testDeclinedRecurringCharge() { + $init = array( + 'amount' => '2345', + 'effort_id' => 2, + 'order_id' => '9998890004', + 'currency' => 'EUR', + 'payment_product' => '', + ); + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->setDummyGatewayResponseCode( 'recurring-declined' ); + + $result = $gateway->do_transaction( 'Recurring_Charge' ); + + $this->assertRegExp( '/GET_ORDERSTATUS/', $result->getRawResponse(), + 'Stopped after GET_ORDERSTATUS.' ); + $this->assertEquals( 2, count( $gateway->curled ), + 'Expected 2 API calls' ); + $this->assertEquals( FinalStatus::FAILED, $gateway->getFinalStatus() ); + } + + /** + * Throw errors if the payment is incomplete + * + * @covers IngenicoAdapter::transactionRecurring_Charge + */ + public function testRecurringTimeout() { + $init = array( + 'amount' => '2345', + 'effort_id' => 2, + 'order_id' => '9998890004', + 'currency' => 'EUR', + 'payment_product' => '', + ); + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->setDummyGatewayResponseCode( 'recurring-timeout' ); + + $result = $gateway->do_transaction( 'Recurring_Charge' ); + + $this->assertFalse( $result->getCommunicationStatus() ); + $this->assertRegExp( '/GET_ORDERSTATUS/', $result->getRawResponse() ); + // FIXME: This is a little funky--the transaction is actually pending-poke. + $this->assertEquals( FinalStatus::FAILED, $gateway->getFinalStatus() ); + } + + /** + * Can resume a recurring payment + * + * @covers IngenicoAdapter::transactionRecurring_Charge + */ + public function testRecurringResume() { + $init = array( + 'amount' => '2345', + 'effort_id' => 2, + 'order_id' => '9998890004', + 'currency' => 'EUR', + 'payment_product' => '', + ); + $gateway = $this->getFreshGatewayObject( $init ); + + $gateway->setDummyGatewayResponseCode( 'recurring-resume' ); + + $result = $gateway->do_transaction( 'Recurring_Charge' ); + + $this->assertTrue( $result->getCommunicationStatus() ); + $this->assertRegExp( '/SET_PAYMENT/', $result->getRawResponse() ); + } +} diff --git a/tests/phpunit/Adapter/Ingenico/ResultSwitcherTest.php b/tests/phpunit/Adapter/Ingenico/ResultSwitcherTest.php new file mode 100644 index 0000000..803699c --- /dev/null +++ b/tests/phpunit/Adapter/Ingenico/ResultSwitcherTest.php @@ -0,0 +1,56 @@ +<?php +/** + * @group Fundraising + * @group DonationInterface + * @group Ingenico + */ +class DonationInterface_Adapter_Ingenico_ResultSwitcherTest extends DonationInterfaceTestCase { + protected $testAdapterClass = 'TestingIngenicoAdapter'; + + public function setUp() { + parent::setUp(); + + $this->setMwGlobals( + array( + 'wgIngenicoGatewayEnabled' => true, + ) + ); + } + + /** + * Assuming we've popped out of the frame, does processing succeed? + */ + public function testResultSwitcherLiberatedSuccess() { + $donorTestData = $this->getDonorTestData( 'FR' ); + $donorTestData['payment_method'] = 'cc'; + $donorTestData['payment_submethod'] = 'visa'; + $donorTestData['email'] = 'innoc...@localhost.net'; + $donorTestData['order_id'] = mt_rand(); + $session['Donor'] = $donorTestData; + // Mark the order as already popped out of the iframe + $session['order_status'][$donorTestData['order_id']] = 'liberated'; + $request = array( + 'REF' => $donorTestData['order_id'], + 'CVVRESULT' => 'M', + 'AVSRESULT' => '0', + 'language' => 'fr', // FIXME: verifyFormOutput conflates request with other stuff + ); + $assertNodes = array( + 'headers' => array( + 'Location' => function( $location ) use ( $donorTestData ) { + // Do this after the real processing to avoid side effects + $gateway = $this->getFreshGatewayObject( $donorTestData ); + $url = ResultPages::getThankYouPage( $gateway ); + $this->assertEquals( $url, $location ); + } + ) + ); + + $this->verifyFormOutput( 'IngenicoGatewayResult', $request, $assertNodes, false, $session ); + // Make sure we logged the expected cURL attempts + $messages = $this->getLogMatches( 'info', '/Preparing to send GET_ORDERSTATUS transaction to Global Collect/' ); + $this->assertNotEmpty( $messages ); + $messages = $this->getLogMatches( 'info', '/Preparing to send SET_PAYMENT transaction to Global Collect/' ); + $this->assertNotEmpty( $messages ); + } +} -- To view, visit https://gerrit.wikimedia.org/r/364143 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I4c02abd190b1ea27c1921f37196481d87cd0162a Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/DonationInterface Gerrit-Branch: master Gerrit-Owner: Ejegg <ej...@ejegg.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits