test(stripe): add unit tests for stripe connector (#473)

This commit is contained in:
Jagan
2023-01-30 15:29:27 +05:30
committed by GitHub
parent 9e420d511d
commit d3ef24e8e9
4 changed files with 509 additions and 33 deletions

View File

@ -12,6 +12,7 @@ pub(crate) struct ConnectorAuthentication {
pub payu: Option<BodyKey>,
pub rapyd: Option<BodyKey>,
pub shift4: Option<HeaderKey>,
pub stripe: Option<HeaderKey>,
pub worldpay: Option<HeaderKey>,
pub worldline: Option<SignatureKey>,
}

View File

@ -10,6 +10,7 @@ mod globalpay;
mod payu;
mod rapyd;
mod shift4;
mod stripe;
mod utils;
mod worldline;
mod worldpay;

View File

@ -0,0 +1,337 @@
use std::time::Duration;
use masking::Secret;
use router::types::{self, api, storage::enums};
use crate::{
connector_auth,
utils::{self, ConnectorActions},
};
struct Stripe;
impl ConnectorActions for Stripe {}
impl utils::Connector for Stripe {
fn get_data(&self) -> types::api::ConnectorData {
use router::connector::Stripe;
types::api::ConnectorData {
connector: Box::new(&Stripe),
connector_name: types::Connector::Stripe,
get_token: types::api::GetToken::Connector,
}
}
fn get_auth_token(&self) -> types::ConnectorAuthType {
types::ConnectorAuthType::from(
connector_auth::ConnectorAuthentication::new()
.stripe
.expect("Missing connector authentication configuration"),
)
}
fn get_name(&self) -> String {
"stripe".to_string()
}
}
fn get_payment_authorize_data() -> Option<types::PaymentsAuthorizeData> {
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_number: Secret::new("4242424242424242".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
})
}
#[actix_web::test]
async fn should_only_authorize_payment() {
let response = Stripe {}
.authorize_payment(get_payment_authorize_data(), None)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Authorized);
}
#[actix_web::test]
async fn should_authorize_and_capture_payment() {
let response = Stripe {}
.make_payment(get_payment_authorize_data(), None)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_capture_already_authorized_payment() {
let connector = Stripe {};
let response = connector
.authorize_and_capture_payment(get_payment_authorize_data(), None, None)
.await;
assert_eq!(response.unwrap().status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_partially_capture_already_authorized_payment() {
let connector = Stripe {};
let response = connector
.authorize_and_capture_payment(
get_payment_authorize_data(),
Some(types::PaymentsCaptureData {
amount_to_capture: Some(50),
..utils::PaymentCaptureType::default().0
}),
None,
)
.await;
assert_eq!(response.unwrap().status, enums::AttemptStatus::Charged);
}
#[actix_web::test]
async fn should_sync_payment() {
let connector = Stripe {};
let authorize_response = connector
.authorize_payment(get_payment_authorize_data(), None)
.await
.unwrap();
let txn_id = utils::get_connector_transaction_id(authorize_response);
let response = connector
.psync_retry_till_status_matches(
enums::AttemptStatus::Authorized,
Some(types::PaymentsSyncData {
connector_transaction_id: router::types::ResponseId::ConnectorTransactionId(
txn_id.unwrap(),
),
encoded_data: None,
capture_method: None,
}),
None,
)
.await
.unwrap();
assert_eq!(response.status, enums::AttemptStatus::Authorized,);
}
#[actix_web::test]
async fn should_void_already_authorized_payment() {
let connector = Stripe {};
let response = connector
.authorize_and_void_payment(
get_payment_authorize_data(),
Some(types::PaymentsCancelData {
connector_transaction_id: "".to_string(),
cancellation_reason: Some("requested_by_customer".to_string()),
}),
None,
)
.await;
assert_eq!(response.unwrap().status, enums::AttemptStatus::Voided);
}
#[actix_web::test]
async fn should_fail_payment_for_incorrect_card_number() {
let response = Stripe {}
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_number: Secret::new("4024007134364842".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(
x.message,
"Your card was declined. Your request was in test mode, but used a non test (live) card. For a list of valid test cards, visit: https://stripe.com/docs/testing.",
);
}
#[actix_web::test]
async fn should_fail_payment_for_no_card_number() {
let response = Stripe {}
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_number: Secret::new("".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(
x.message,
"You passed an empty string for 'payment_method_data[card][number]'. We assume empty values are an attempt to unset a parameter; however 'payment_method_data[card][number]' cannot be unset. You should remove 'payment_method_data[card][number]' from your request or supply a non-empty value.",
);
}
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_month() {
let response = Stripe {}
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_exp_month: Secret::new("13".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Your card's expiration month is invalid.",);
}
#[actix_web::test]
async fn should_fail_payment_for_invalid_exp_year() {
let response = Stripe {}
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_exp_year: Secret::new("2022".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Your card's expiration year is invalid.",);
}
#[actix_web::test]
async fn should_fail_payment_for_invalid_card_cvc() {
let response = Stripe {}
.make_payment(
Some(types::PaymentsAuthorizeData {
payment_method_data: types::api::PaymentMethod::Card(api::CCard {
card_cvc: Secret::new("12".to_string()),
..utils::CCardType::default().0
}),
..utils::PaymentAuthorizeType::default().0
}),
None,
)
.await
.unwrap();
let x = response.response.unwrap_err();
assert_eq!(x.message, "Your card's security code is invalid.",);
}
#[actix_web::test]
async fn should_fail_capture_for_invalid_payment() {
let connector = Stripe {};
let authorize_response = connector
.authorize_payment(get_payment_authorize_data(), None)
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
tokio::time::sleep(Duration::from_secs(5)).await; // to avoid 404 error as stripe takes some time to process the new transaction
let response = connector
.capture_payment("12345".to_string(), None, None)
.await
.unwrap();
let err = response.response.unwrap_err();
assert_eq!(err.message, "No such payment_intent: '12345'".to_string());
assert_eq!(err.code, "resource_missing".to_string());
}
#[actix_web::test]
async fn should_refund_succeeded_payment() {
let connector = Stripe {};
let response = connector
.make_payment_and_refund(get_payment_authorize_data(), None, None)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_partially_refund_succeeded_payment() {
let connector = Stripe {};
let refund_response = connector
.make_payment_and_refund(
get_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
#[actix_web::test]
async fn should_refund_succeeded_payment_multiple_times() {
let connector = Stripe {};
connector
.make_payment_and_multiple_refund(
get_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0
}),
None,
)
.await;
}
#[actix_web::test]
async fn should_fail_refund_for_invalid_amount() {
let connector = Stripe {};
let response = connector
.make_payment_and_refund(
get_payment_authorize_data(),
Some(types::RefundsData {
refund_amount: 150,
..utils::PaymentRefundType::default().0
}),
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap_err().message,
"Refund amount ($1.50) is greater than charge amount ($1.00)",
);
}
#[actix_web::test]
async fn should_sync_refund() {
let connector = Stripe {};
let refund_response = connector
.make_payment_and_refund(get_payment_authorize_data(), None, None)
.await
.unwrap();
let response = connector
.rsync_retry_till_status_matches(
enums::RefundStatus::Success,
refund_response.response.unwrap().connector_refund_id,
None,
None,
)
.await
.unwrap();
assert_eq!(
response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}

View File

@ -1,4 +1,4 @@
use std::{fmt::Debug, marker::PhantomData, thread::sleep, time::Duration};
use std::{fmt::Debug, marker::PhantomData, time::Duration};
use async_trait::async_trait;
use error_stack::Report;
@ -18,6 +18,10 @@ pub trait Connector {
fn get_connector_meta(&self) -> Option<serde_json::Value> {
None
}
/// interval in seconds to be followed when making the subsequent request whenever needed
fn get_request_interval(&self) -> u64 {
5
}
}
#[derive(Debug, Default, Clone)]
@ -36,14 +40,16 @@ pub trait ConnectorActions: Connector {
) -> Result<types::PaymentsAuthorizeRouterData, Report<ConnectorError>> {
let integration = self.get_data().connector.get_connector_integration();
let request = self.generate_data(
payment_data.unwrap_or_else(|| types::PaymentsAuthorizeData {
types::PaymentsAuthorizeData {
confirm: false,
capture_method: Some(storage_models::enums::CaptureMethod::Manual),
..PaymentAuthorizeType::default().0
}),
..(payment_data.unwrap_or(PaymentAuthorizeType::default().0))
},
payment_info,
);
call_connector(request, integration).await
}
async fn make_payment(
&self,
payment_data: Option<types::PaymentsAuthorizeData>,
@ -70,25 +76,23 @@ pub trait ConnectorActions: Connector {
call_connector(request, integration).await
}
/// will retry the psync till the given status matches or retry max 3 times in a 10secs interval
/// will retry the psync till the given status matches or retry max 3 times
async fn psync_retry_till_status_matches(
&self,
status: enums::AttemptStatus,
payment_data: Option<types::PaymentsSyncData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::PaymentsSyncRouterData, Report<ConnectorError>> {
let max_try = 3;
let mut curr_try = 1;
while curr_try <= max_try {
let max_tries = 3;
for curr_try in 0..max_tries {
let sync_res = self
.sync_payment(payment_data.clone(), payment_info.clone())
.await
.unwrap();
if (sync_res.status == status) || (curr_try == max_try) {
if (sync_res.status == status) || (curr_try == max_tries - 1) {
return Ok(sync_res);
}
sleep(Duration::from_secs(10));
curr_try += 1;
tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await;
}
Err(errors::ConnectorError::ProcessingStepFailed(None).into())
}
@ -101,17 +105,34 @@ pub trait ConnectorActions: Connector {
) -> Result<types::PaymentsCaptureRouterData, Report<ConnectorError>> {
let integration = self.get_data().connector.get_connector_integration();
let request = self.generate_data(
payment_data.unwrap_or(types::PaymentsCaptureData {
amount_to_capture: Some(100),
currency: enums::Currency::USD,
types::PaymentsCaptureData {
connector_transaction_id: transaction_id,
amount: 100,
}),
..payment_data.unwrap_or(PaymentCaptureType::default().0)
},
payment_info,
);
call_connector(request, integration).await
}
async fn authorize_and_capture_payment(
&self,
authorize_data: Option<types::PaymentsAuthorizeData>,
capture_data: Option<types::PaymentsCaptureData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::PaymentsCaptureRouterData, Report<ConnectorError>> {
let authorize_response = self
.authorize_payment(authorize_data, payment_info.clone())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
let txn_id = get_connector_transaction_id(authorize_response);
let response = self
.capture_payment(txn_id.unwrap(), capture_data, payment_info)
.await
.unwrap();
return Ok(response);
}
async fn void_payment(
&self,
transaction_id: String,
@ -120,15 +141,35 @@ pub trait ConnectorActions: Connector {
) -> Result<types::PaymentsCancelRouterData, Report<ConnectorError>> {
let integration = self.get_data().connector.get_connector_integration();
let request = self.generate_data(
payment_data.unwrap_or(types::PaymentsCancelData {
types::PaymentsCancelData {
connector_transaction_id: transaction_id,
cancellation_reason: Some("Test cancellation".to_string()),
}),
..payment_data.unwrap_or(PaymentCancelType::default().0)
},
payment_info,
);
call_connector(request, integration).await
}
async fn authorize_and_void_payment(
&self,
authorize_data: Option<types::PaymentsAuthorizeData>,
void_data: Option<types::PaymentsCancelData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::PaymentsCancelRouterData, Report<ConnectorError>> {
let authorize_response = self
.authorize_payment(authorize_data, payment_info.clone())
.await
.unwrap();
assert_eq!(authorize_response.status, enums::AttemptStatus::Authorized);
let txn_id = get_connector_transaction_id(authorize_response);
tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error
let response = self
.void_payment(txn_id.unwrap(), void_data, payment_info)
.await
.unwrap();
return Ok(response);
}
async fn refund_payment(
&self,
transaction_id: String,
@ -137,24 +178,70 @@ pub trait ConnectorActions: Connector {
) -> Result<types::RefundExecuteRouterData, Report<ConnectorError>> {
let integration = self.get_data().connector.get_connector_integration();
let request = self.generate_data(
payment_data.unwrap_or_else(|| types::RefundsData {
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
types::RefundsData {
connector_transaction_id: transaction_id,
refund_amount: 100,
connector_metadata: None,
connector_refund_id: None,
reason: Some("Customer returned product".to_string()),
}),
..payment_data.unwrap_or(PaymentRefundType::default().0)
},
payment_info,
);
call_connector(request, integration).await
}
async fn make_payment_and_refund(
&self,
authorize_data: Option<types::PaymentsAuthorizeData>,
refund_data: Option<types::RefundsData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::RefundExecuteRouterData, Report<ConnectorError>> {
//make a successful payment
let response = self
.make_payment(authorize_data, payment_info.clone())
.await
.unwrap();
//try refund for previous payment
let transaction_id = get_connector_transaction_id(response).unwrap();
tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error
Ok(self
.refund_payment(transaction_id, refund_data, payment_info)
.await
.unwrap())
}
async fn make_payment_and_multiple_refund(
&self,
authorize_data: Option<types::PaymentsAuthorizeData>,
refund_data: Option<types::RefundsData>,
payment_info: Option<PaymentInfo>,
) {
//make a successful payment
let response = self
.make_payment(authorize_data, payment_info.clone())
.await
.unwrap();
//try refund for previous payment
let transaction_id = get_connector_transaction_id(response).unwrap();
for _x in 0..2 {
tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await; // to avoid 404 error
let refund_response = self
.refund_payment(
transaction_id.clone(),
refund_data.clone(),
payment_info.clone(),
)
.await
.unwrap();
assert_eq!(
refund_response.response.unwrap().refund_status,
enums::RefundStatus::Success,
);
}
}
async fn sync_refund(
&self,
transaction_id: String,
refund_id: String,
payment_data: Option<types::RefundsData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::RefundSyncRouterData, Report<ConnectorError>> {
@ -164,17 +251,45 @@ pub trait ConnectorActions: Connector {
amount: 1000,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
connector_transaction_id: transaction_id,
connector_transaction_id: "".to_string(),
refund_amount: 100,
connector_metadata: None,
reason: None,
connector_refund_id: None,
connector_refund_id: Some(refund_id),
}),
payment_info,
);
call_connector(request, integration).await
}
/// will retry the rsync till the given status matches or retry max 3 times
async fn rsync_retry_till_status_matches(
&self,
status: enums::RefundStatus,
refund_id: String,
payment_data: Option<types::RefundsData>,
payment_info: Option<PaymentInfo>,
) -> Result<types::RefundSyncRouterData, Report<ConnectorError>> {
let max_tries = 3;
for curr_try in 0..max_tries {
let sync_res = self
.sync_refund(
refund_id.clone(),
payment_data.clone(),
payment_info.clone(),
)
.await
.unwrap();
if (sync_res.clone().response.unwrap().refund_status == status)
|| (curr_try == max_tries - 1)
{
return Ok(sync_res);
}
tokio::time::sleep(Duration::from_secs(self.get_request_interval())).await;
}
Err(errors::ConnectorError::ProcessingStepFailed(None).into())
}
fn generate_data<Flow, Req: From<Req>, Res>(
&self,
req: Req,
@ -258,6 +373,8 @@ pub trait LocalMock {
}
pub struct PaymentAuthorizeType(pub types::PaymentsAuthorizeData);
pub struct PaymentCaptureType(pub types::PaymentsCaptureData);
pub struct PaymentCancelType(pub types::PaymentsCancelData);
pub struct PaymentSyncType(pub types::PaymentsSyncData);
pub struct PaymentRefundType(pub types::RefundsData);
pub struct CCardType(pub api::CCard);
@ -296,6 +413,26 @@ impl Default for PaymentAuthorizeType {
}
}
impl Default for PaymentCaptureType {
fn default() -> Self {
Self(types::PaymentsCaptureData {
amount_to_capture: Some(100),
currency: enums::Currency::USD,
connector_transaction_id: "".to_string(),
amount: 100,
})
}
}
impl Default for PaymentCancelType {
fn default() -> Self {
Self(types::PaymentsCancelData {
cancellation_reason: Some("requested_by_customer".to_string()),
connector_transaction_id: "".to_string(),
})
}
}
impl Default for BrowserInfoType {
fn default() -> Self {
let data = types::BrowserInformation {
@ -330,13 +467,13 @@ impl Default for PaymentSyncType {
impl Default for PaymentRefundType {
fn default() -> Self {
let data = types::RefundsData {
amount: 1000,
amount: 100,
currency: enums::Currency::USD,
refund_id: uuid::Uuid::new_v4().to_string(),
connector_transaction_id: String::new(),
refund_amount: 100,
connector_metadata: None,
reason: None,
reason: Some("Customer returned product".to_string()),
connector_refund_id: None,
};
Self(data)