use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustlavel_payment::{
Channel, ChargeRequest, ChargeStatus, FakeGateway, Gateway, Handled, MemoryWebhookLog, Money,
Receiver, Wallet,
};
fn receiver(gateway: Arc<FakeGateway>, fail: Arc<AtomicUsize>) -> (Receiver, Arc<AtomicUsize>) {
let handled = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&handled);
let log = Arc::new(MemoryWebhookLog::new());
let receiver = Receiver::new(gateway, log, move |event| {
let counter = Arc::clone(&counter);
let fail = Arc::clone(&fail);
Box::pin(async move {
if fail.load(Ordering::SeqCst) > 0 {
fail.fetch_sub(1, Ordering::SeqCst);
return Err(rustlavel_core::Error::msg("the ledger is down"));
}
assert_eq!(event.charge.as_ref().map(|c| c.status), Some(ChargeStatus::Paid));
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
})
});
(receiver, handled)
}
#[tokio::test]
async fn a_charge_is_created_paid_and_credited_once() {
let gateway = Arc::new(FakeGateway::new("whsec_test"));
let (receiver, handled) = receiver(Arc::clone(&gateway), Arc::new(AtomicUsize::new(0)));
let charge = gateway
.create_charge(&ChargeRequest::new("order-7", Money::idr(150_000), Channel::Qris))
.await
.unwrap();
assert_eq!(charge.status, ChargeStatus::Pending);
assert!(charge.instructions.qr_string.is_some(), "a QRIS charge came with nothing to scan");
gateway.mark_paid(&charge.id).expect("pending, so payable");
let (headers, body) = gateway.webhook_for(&charge.id, "evt_1").unwrap();
assert_eq!(receiver.deliver(&headers, &body).await, Handled::Processed);
assert_eq!(handled.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn a_retried_delivery_is_acknowledged_and_not_handled_again() {
let gateway = Arc::new(FakeGateway::new("whsec_test"));
let (receiver, handled) = receiver(Arc::clone(&gateway), Arc::new(AtomicUsize::new(0)));
let charge = gateway.create_charge(&ChargeRequest::new("o", Money::idr(1000), Channel::Qris)).await.unwrap();
gateway.mark_paid(&charge.id);
let (headers, body) = gateway.webhook_for(&charge.id, "evt_1").unwrap();
assert_eq!(receiver.deliver(&headers, &body).await, Handled::Processed);
assert_eq!(receiver.deliver(&headers, &body).await, Handled::Duplicate);
assert_eq!(receiver.deliver(&headers, &body).await, Handled::Duplicate);
assert_eq!(handled.load(Ordering::SeqCst), 1, "a duplicate reached the handler");
}
#[tokio::test]
async fn a_tampered_or_unsigned_body_is_refused_and_never_handled() {
let gateway = Arc::new(FakeGateway::new("whsec_test"));
let (receiver, handled) = receiver(Arc::clone(&gateway), Arc::new(AtomicUsize::new(0)));
let charge = gateway.create_charge(&ChargeRequest::new("o", Money::idr(1000), Channel::Qris)).await.unwrap();
gateway.mark_paid(&charge.id);
let (headers, body) = gateway.webhook_for(&charge.id, "evt_1").unwrap();
let tampered = String::from_utf8(body.clone()).unwrap().replace("1000", "1000000").into_bytes();
assert_eq!(receiver.deliver(&headers, &tampered).await, Handled::Refused);
let other = FakeGateway::new("whsec_other");
assert_eq!(receiver.deliver(&other.sign(&body), &body).await, Handled::Refused);
assert_eq!(receiver.deliver(&rustlavel_http::Headers::new(), &body).await, Handled::Refused);
assert_eq!(handled.load(Ordering::SeqCst), 0, "a refused body reached the handler");
}
#[tokio::test]
async fn a_failed_handler_is_retried_and_then_succeeds_exactly_once() {
let gateway = Arc::new(FakeGateway::new("whsec_test"));
let fail_next = Arc::new(AtomicUsize::new(1));
let (receiver, handled) = receiver(Arc::clone(&gateway), fail_next);
let charge = gateway.create_charge(&ChargeRequest::new("o", Money::idr(1000), Channel::Qris)).await.unwrap();
gateway.mark_paid(&charge.id);
let (headers, body) = gateway.webhook_for(&charge.id, "evt_1").unwrap();
assert!(matches!(receiver.deliver(&headers, &body).await, Handled::HandlerFailed(_)));
assert_eq!(handled.load(Ordering::SeqCst), 0);
assert_eq!(receiver.deliver(&headers, &body).await, Handled::Processed, "the retry was dropped as a duplicate");
assert_eq!(handled.load(Ordering::SeqCst), 1);
assert_eq!(receiver.deliver(&headers, &body).await, Handled::Duplicate);
assert_eq!(handled.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn two_kinds_of_event_for_one_charge_are_both_delivered() {
let gateway = Arc::new(FakeGateway::new("whsec_test"));
let log = Arc::new(MemoryWebhookLog::new());
let receiver = Receiver::new(Arc::clone(&gateway) as Arc<dyn Gateway>, log, |_| Box::pin(async { Ok(()) }));
let a = gateway.create_charge(&ChargeRequest::new("a", Money::idr(1000), Channel::Qris)).await.unwrap();
let b = gateway.create_charge(&ChargeRequest::new("b", Money::idr(1000), Channel::EWallet(Wallet::Dana))).await.unwrap();
gateway.mark_paid(&a.id);
gateway.mark_expired(&b.id);
let (h1, b1) = gateway.webhook_for(&a.id, "evt_a").unwrap();
let (h2, b2) = gateway.webhook_for(&b.id, "evt_b").unwrap();
assert_eq!(receiver.deliver(&h1, &b1).await, Handled::Processed);
assert_eq!(receiver.deliver(&h2, &b2).await, Handled::Processed);
}
#[tokio::test]
async fn a_paid_charge_cannot_be_cancelled_and_a_pending_one_can() {
let gateway = FakeGateway::new("s");
let paid = gateway.create_charge(&ChargeRequest::new("p", Money::idr(1000), Channel::Qris)).await.unwrap();
gateway.mark_paid(&paid.id);
let error = gateway.cancel_charge(&paid.id).await.unwrap_err().to_string();
assert!(error.contains("refunded"), "{error}");
let pending = gateway.create_charge(&ChargeRequest::new("q", Money::idr(1000), Channel::Qris)).await.unwrap();
assert_eq!(gateway.cancel_charge(&pending.id).await.unwrap().status, ChargeStatus::Cancelled);
assert!(gateway.mark_paid(&pending.id).is_none(), "a cancelled charge was paid");
}
#[tokio::test]
async fn a_transfer_reference_is_used_once() {
use rustlavel_payment::{Account, Bank, TransferRequest};
let gateway = FakeGateway::new("s");
let to = Account::Bank { bank: Bank::Bca, number: "123".into(), holder: "Ada".into() };
let request = TransferRequest::new("payout-1", Money::idr(50_000), to);
assert!(gateway.transfer(&request).await.is_ok());
let error = gateway.transfer(&request).await.unwrap_err().to_string();
assert!(error.contains("already exists"), "a second transfer under one reference went through: {error}");
}