#![cfg(all(
feature = "iyzico",
feature = "mollie",
feature = "paypal",
feature = "paytr",
feature = "stripe"
))]
#![allow(
clippy::expect_used,
reason = "a fixture that cannot be built is a failed test"
)]
use kasapay::{
ChargeRequest, Currency, ErrorKind, IdempotencyKey, Money, OrderRef, PaymentId, Provider,
Secret,
};
use serde_json::json;
use wiremock::matchers::{any, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
struct Subject {
label: &'static str,
provider: Box<dyn Provider>,
server: MockServer,
currency: Currency,
baseline: usize,
}
impl Subject {
async fn reached(&self) -> usize {
self.server
.received_requests()
.await
.expect("the mock server records requests")
.len()
- self.baseline
}
}
async fn dead_server() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/oauth2/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"access_token": "A21AA",
"token_type": "Bearer",
"expires_in": 31_668,
})))
.mount(&server)
.await;
Mock::given(any())
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
server
}
async fn subject(
label: &'static str,
provider: Box<dyn Provider>,
server: MockServer,
currency: Currency,
) -> Subject {
let baseline = server
.received_requests()
.await
.expect("the mock server records requests")
.len();
Subject {
label,
provider,
server,
currency,
baseline,
}
}
async fn every_adapter() -> Vec<Subject> {
let mut subjects = Vec::new();
let server = dead_server().await;
let config = kasapay::iyzico::classic::Config::new(
&server.uri(),
kasapay::iyzico::Credentials::new("api-key", "secret-key"),
)
.expect("valid base");
subjects.push(
subject(
"iyzico classic",
Box::new(kasapay::iyzico::classic::Client::new(config).expect("client builds")),
server,
Currency::Try,
)
.await,
);
let server = dead_server().await;
let config = kasapay::iyzico::in_store::Config::new(
&format!("{}/v3/in-store/", server.uri()),
"api-key",
"secret-key",
"merchant-id",
)
.expect("valid base");
subjects.push(
subject(
"iyzico in_store",
Box::new(kasapay::iyzico::in_store::Client::new(config).expect("client builds")),
server,
Currency::Try,
)
.await,
);
let server = dead_server().await;
let config = kasapay::mollie::Config::at(&server.uri(), Secret::new("test_kasapay"))
.expect("valid base");
subjects.push(
subject(
"mollie",
Box::new(kasapay::mollie::Mollie::new(config).expect("client builds")),
server,
Currency::Eur,
)
.await,
);
let server = dead_server().await;
let config = kasapay::paypal::Config::at(
&server.uri(),
Secret::new("client-id"),
Secret::new("client-secret"),
)
.expect("valid base");
subjects.push(
subject(
"paypal",
Box::new(kasapay::paypal::PayPal::new(config).expect("client builds")),
server,
Currency::Eur,
)
.await,
);
let server = dead_server().await;
let config = kasapay::paytr::Config::at(
&server.uri(),
kasapay::paytr::Credentials::new("merchant-1", "merchant-key", "merchant-salt"),
)
.expect("valid base")
.test_mode();
subjects.push(
subject(
"paytr",
Box::new(kasapay::paytr::PayTr::new(config).expect("client builds")),
server,
Currency::Try,
)
.await,
);
let server = dead_server().await;
let stripe = kasapay::stripe::Stripe::at(&server.uri(), &Secret::new("sk_test_kasapay"))
.expect("client builds");
subjects.push(subject("stripe", Box::new(stripe), server, Currency::Eur).await);
subjects
}
fn a_payment() -> PaymentId {
PaymentId::issued("pay-1")
}
#[tokio::test]
async fn capture_answers_the_flag_that_describes_it() {
for subject in every_adapter().await {
let who = subject.label;
let capable = subject.provider.capabilities().separate_capture;
let error = subject
.provider
.capture(&a_payment(), None, None)
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
if capable {
assert_ne!(
error.kind(),
ErrorKind::Unsupported,
"{who} says it captures separately and then refuses to"
);
assert!(
subject.reached().await > 0,
"{who} says it captures separately and never asked"
);
} else {
assert_eq!(
error.kind(),
ErrorKind::Unsupported,
"{who} has no capture step and did not say so"
);
assert_eq!(
subject.reached().await,
0,
"{who} refused a capture only after sending it"
);
}
}
}
#[tokio::test]
async fn a_capture_refuses_an_idempotency_key_before_it_sends_anything() {
let key = IdempotencyKey::new("idem-1");
for subject in every_adapter().await {
let who = subject.label;
let error = subject
.provider
.capture(&a_payment(), None, Some(&key))
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
if error.kind() == ErrorKind::Unsupported {
assert_eq!(
subject.reached().await,
0,
"{who} refused the key only after sending the capture"
);
}
}
}
#[tokio::test]
async fn lookup_answers_the_flag_that_describes_it() {
for subject in every_adapter().await {
let who = subject.label;
let capable = subject.provider.capabilities().lookup_by_order;
let error = subject
.provider
.lookup(&OrderRef::new("ord-1"))
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
if capable {
assert_ne!(
error.kind(),
ErrorKind::Unsupported,
"{who} says it looks up by order and then refuses to"
);
assert!(
subject.reached().await > 0,
"{who} says it looks up by order and never asked"
);
} else {
assert_eq!(
error.kind(),
ErrorKind::Unsupported,
"{who} cannot look up by order and did not say so"
);
assert_eq!(
subject.reached().await,
0,
"{who} refused a lookup only after sending it"
);
}
}
}
#[tokio::test]
async fn resume_answers_the_flag_that_describes_it() {
for subject in every_adapter().await {
let who = subject.label;
let capable = subject.provider.capabilities().resume_by_continuation;
let error = subject
.provider
.resume("continuation-1")
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
if capable {
assert_ne!(
error.kind(),
ErrorKind::Unsupported,
"{who} says it resumes by continuation and then refuses to"
);
assert!(
subject.reached().await > 0,
"{who} says it resumes by continuation and never asked"
);
} else {
assert_eq!(
error.kind(),
ErrorKind::Unsupported,
"{who} cannot resume by continuation and did not say so"
);
assert_eq!(
subject.reached().await,
0,
"{who} refused a resume only after sending it"
);
}
}
}
#[tokio::test]
async fn a_refused_cancel_never_reaches_the_network() {
for subject in every_adapter().await {
let who = subject.label;
let error = subject
.provider
.cancel(&a_payment())
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
if error.kind() == ErrorKind::Unsupported {
assert_eq!(
subject.reached().await,
0,
"{who} refused a cancel only after sending it"
);
}
}
}
#[tokio::test]
async fn a_refused_instrument_list_never_reaches_the_network() {
for subject in every_adapter().await {
let who = subject.label;
let error = subject
.provider
.instruments("cus-1")
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
if error.kind() == ErrorKind::Unsupported {
assert_eq!(
subject.reached().await,
0,
"{who} refused an instrument list only after sending it"
);
}
}
}
#[tokio::test]
async fn every_currency_is_either_settled_or_refused_before_the_wire() {
for subject in every_adapter().await {
let who = subject.label;
let mut settled = 0_usize;
let mut refused = 0_usize;
let mut before = subject.reached().await;
for currency in Currency::KNOWN.iter().copied() {
let request = ChargeRequest::builder(
OrderRef::new("ord-1"),
Money::from_minor_units(1000, currency),
)
.description("Order #1")
.customer("cus-1")
.return_url("https://merchant.test/ok".parse().expect("valid url"))
.failure_url("https://merchant.test/no".parse().expect("valid url"))
.buyer(
kasapay::Buyer::new("Ayse", "ayse@example.test")
.surname("Yilmaz")
.identity_number("11111111111")
.phone("+905350000000")
.ip("203.0.113.7")
.address(kasapay::Address::new("Bagdat Cad. 1", "Istanbul", "Turkey")),
)
.item(
kasapay::BasketItem::new("sku-1", "Kahve", Money::from_minor_units(1000, currency))
.category("Icecek"),
)
.build()
.expect("valid request");
let error = subject
.provider
.charge(&request)
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
let after = subject.reached().await;
if error.kind() == ErrorKind::Unsupported {
assert_eq!(
after, before,
"{who} refused {currency} only after sending it"
);
refused += 1;
} else {
settled += 1;
}
before = after;
}
assert!(settled > 0, "{who} refuses every currency kasapay names");
assert!(refused > 0, "{who} settles in every currency kasapay names");
}
}
#[tokio::test]
async fn no_capability_contradicts_another() {
for subject in every_adapter().await {
let who = subject.label;
let capabilities = subject.provider.capabilities();
assert!(
capabilities.separate_capture || !capabilities.partial_capture,
"{who} captures part of an amount it never authorises separately"
);
assert!(
capabilities.partial_refund || !capabilities.repeated_refund,
"{who} refunds repeatedly but never for part of an amount"
);
}
}
#[tokio::test]
async fn a_charge_missing_fields_is_an_invalid_request_rather_than_an_unsupported_one() {
for subject in every_adapter().await {
let who = subject.label;
let request = ChargeRequest::builder(
OrderRef::new("ord-1"),
Money::parse("10.00", subject.currency).expect("valid amount"),
)
.build()
.expect("valid request");
let error = subject
.provider
.charge(&request)
.await
.expect_err("the server answers 500 to everything it is asked");
assert_eq!(
error.provider(),
subject.provider.id(),
"{who} answered for somebody else"
);
assert_ne!(
error.kind(),
ErrorKind::Unsupported,
"{who} cannot start a payment at all through the trait"
);
}
}