#![allow(dead_code)] use actix_http::{body::MessageBody, Request}; use actix_web::{ dev::{Service, ServiceResponse}, test::{call_and_read_body_json, TestRequest}, }; use derive_deref::Deref; use router::{configs::settings::Settings, routes::AppState, start_server}; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::{json, Value}; use tokio::sync::OnceCell; static SERVER: OnceCell = OnceCell::const_new(); async fn spawn_server() -> bool { let conf = Settings::new().expect("invalid settings"); let (server, _state) = start_server(conf).await.expect("failed to create server"); let _server = tokio::spawn(server); true } pub async fn setup() { SERVER.get_or_init(spawn_server).await; } const STRIPE_MOCK: &str = "http://localhost:12111/"; async fn stripemock() -> Option { // not working: https://github.com/stripe/stripe-mock/issues/231 None } pub async fn mk_service() -> impl actix_web::dev::Service< actix_http::Request, Response = actix_web::dev::ServiceResponse, Error = actix_web::Error, > { let mut conf = Settings::new().unwrap(); let request_body_limit = conf.server.request_body_limit; if let Some(url) = stripemock().await { conf.connectors.stripe.base_url = url; } let app_state = AppState::with_storage(conf, router::db::StorageImpl::Mock).await; actix_web::test::init_service(router::mk_app(app_state, request_body_limit)).await } pub struct Guest; pub struct Admin { authkey: String, } pub struct User { authkey: String, } #[allow(dead_code)] pub struct AppClient { state: T, } impl AppClient { pub fn guest() -> AppClient { AppClient { state: Guest } } } impl AppClient { pub async fn create_merchant_account( &self, app: &S, merchant_id: impl Into>, ) -> T where S: Service, Error = actix_web::Error>, B: MessageBody, { let request = TestRequest::post() .uri("/accounts") .append_header(("api-key".to_owned(), self.state.authkey.clone())) .set_json(mk_merchant_account(merchant_id.into())) .to_request(); call_and_read_body_json(app, request).await } pub async fn create_connector( &self, app: &S, merchant_id: &str, connector_name: &str, api_key: &str, ) -> T where S: Service, Error = actix_web::Error>, B: MessageBody, { let request = TestRequest::post() .uri(&format!("/account/{merchant_id}/connectors")) .append_header(("api-key".to_owned(), self.state.authkey.clone())) .set_json(mk_connector(connector_name, api_key)) .to_request(); call_and_read_body_json(app, request).await } } impl AppClient { pub async fn create_payment( &self, app: &S, amount: i32, amount_to_capture: i32, ) -> T where S: Service, Error = actix_web::Error>, B: MessageBody, { let request = TestRequest::post() .uri("/payments") .append_header(("api-key".to_owned(), self.state.authkey.clone())) .set_json(mk_payment(amount, amount_to_capture)) .to_request(); call_and_read_body_json(app, request).await } pub async fn create_refund( &self, app: &S, payment_id: &str, amount: usize, ) -> T where S: Service, Error = actix_web::Error>, B: MessageBody, { let request = TestRequest::post() .uri("/refunds") .append_header(("api-key".to_owned(), self.state.authkey.clone())) .set_json(mk_refund(payment_id, amount)) .to_request(); call_and_read_body_json(app, request).await } } impl AppClient { pub fn admin(&self, authkey: &str) -> AppClient { AppClient { state: Admin { authkey: authkey.to_string(), }, } } pub fn user(&self, authkey: &str) -> AppClient { AppClient { state: User { authkey: authkey.to_string(), }, } } pub async fn health(&self, app: &S) -> String where S: Service, Error = actix_web::Error>, B: MessageBody, { let request = TestRequest::get().uri("/health").to_request(); let bytes = actix_web::test::call_and_read_body(app, request).await; String::from_utf8(bytes.to_vec()).unwrap() } } fn mk_merchant_account(merchant_id: Option) -> Value { let merchant_id = merchant_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string()); json!({ "merchant_id": merchant_id, "merchant_name": "NewAge Retailer", "merchant_details": { "primary_contact_person": "John Test", "primary_email": "JohnTest@test.com", "primary_phone": "sunt laborum", "secondary_contact_person": "John Test2", "secondary_email": "JohnTest2@test.com", "secondary_phone": "cillum do dolor id", "website": "www.example.com", "about_business": "Online Retail with a wide selection of organic products for North America", "address": { "line1": "Juspay Router", "line2": "Koramangala", "line3": "Stallion", "city": "Bangalore", "state": "Karnataka", "zip": "560095", "country": "IN" } }, "return_url": "www.example.com/success", "webhook_details": { "webhook_version": "1.0.1", "webhook_username": "ekart_retail", "webhook_password": "password_ekart@123", "payment_created_enabled": true, "payment_succeeded_enabled": true, "payment_failed_enabled": true }, "routing_algorithm": "custom", "custom_routing_rules": [ { "payment_methods_incl": [ "card", "card" ], "payment_methods_excl": [ "card", "card" ], "payment_method_types_incl": [ "credit", "credit" ], "payment_method_types_excl": [ "credit", "credit" ], "payment_method_issuers_incl": [ "Citibank", "JPMorgan" ], "payment_method_issuers_excl": [ "Citibank", "JPMorgan" ], "countries_incl": [ "US", "UK", "IN" ], "countries_excl": [ "US", "UK", "IN" ], "currencies_incl": [ "USD", "EUR" ], "currencies_excl": [ "AED", "SGD" ], "metadata_filters_keys": [ "payments.udf1", "payments.udf2" ], "metadata_filters_values": [ "android", "Category_Electronics" ], "connectors_pecking_order": [ "stripe", "adyen", "brain_tree" ], "connectors_traffic_weightage_key": [ "stripe", "adyen", "brain_tree" ], "connectors_traffic_weightage_value": [ 50, 30, 20 ] }, { "payment_methods_incl": [ "card", "card" ], "payment_methods_excl": [ "card", "card" ], "payment_method_types_incl": [ "credit", "credit" ], "payment_method_types_excl": [ "credit", "credit" ], "payment_method_issuers_incl": [ "Citibank", "JPMorgan" ], "payment_method_issuers_excl": [ "Citibank", "JPMorgan" ], "countries_incl": [ "US", "UK", "IN" ], "countries_excl": [ "US", "UK", "IN" ], "currencies_incl": [ "USD", "EUR" ], "currencies_excl": [ "AED", "SGD" ], "metadata_filters_keys": [ "payments.udf1", "payments.udf2" ], "metadata_filters_values": [ "android", "Category_Electronics" ], "connectors_pecking_order": [ "stripe", "adyen", "brain_tree" ], "connectors_traffic_weightage_key": [ "stripe", "adyen", "brain_tree" ], "connectors_traffic_weightage_value": [ 50, 30, 20 ] } ], "sub_merchants_enabled": false, "metadata": { "city": "NY", "unit": "245" } }) } fn mk_payment(amount: i32, amount_to_capture: i32) -> Value { json!({ "amount": amount, "currency": "USD", "confirm": true, "capture_method": "automatic", "capture_on": "2022-10-10T10:11:12Z", "amount_to_capture": amount_to_capture, "customer_id": "cus_udst2tfldj6upmye2reztkmm4i", "email": "guest@example.com", "name": "John Doe", "phone": "999999999", "phone_country_code": "+65", "description": "Its my first payment request", "authentication_type": "no_three_ds", "payment_method": "card", "payment_method_data": { "card": { "card_number": "4242424242424242", "card_exp_month": "10", "card_exp_year": "35", "card_holder_name": "John Doe", "card_cvc": "123" } }, "statement_descriptor_name": "Juspay", "statement_descriptor_suffix": "Router", "metadata": { "udf1": "value1", "new_customer": "true", "login_date": "2019-09-10T10:11:12Z" } }) } fn mk_connector(connector_name: &str, api_key: &str) -> Value { json!({ "connector_type": "fiz_operations", "connector_name": connector_name, "connector_account_details": { "auth_type": "HeaderKey", "api_key": api_key, }, "test_mode": false, "disabled": false, "payment_methods_enabled": [ { "payment_method": "wallet", "payment_method_types": [ "upi_collect", "upi_intent" ], "payment_method_issuers": [ "labore magna ipsum", "aute" ], "payment_schemes": [ "Discover", "Discover" ], "accepted_currencies": [ "AED", "AED" ], "accepted_countries": [ "in", "us" ], "minimum_amount": 1, "maximum_amount": 68607706, "recurring_enabled": true, "installment_payment_enabled": true } ], "metadata": { "city": "NY", "unit": "245" } }) } fn _mk_payment_confirm() -> Value { json!({ "return_url": "http://example.com/payments", "setup_future_usage": "on_session", "authentication_type": "no_three_ds", "payment_method": "card", "payment_method_data": { "card": { "card_number": "4242424242424242", "card_exp_month": "10", "card_exp_year": "35", "card_holder_name": "John Doe", "card_cvc": "123" } }, "shipping": {}, "billing": {} }) } fn mk_refund(payment_id: &str, amount: usize) -> Value { let timestamp = common_utils::date_time::now().to_string(); json!({ "payment_id": payment_id, "refund_id": timestamp.get(23..), "amount": amount, "reason": "Customer returned product", "metadata": { "udf1": "value1", "new_customer": "true", "login_date": "2019-09-10T10:11:12Z" } }) } pub struct HNil; impl<'de> Deserialize<'de> for HNil { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { deserializer.deserialize_any(serde::de::IgnoredAny)?; Ok(Self) } } #[derive(Deserialize)] pub struct HCons { #[serde(flatten)] pub head: H, #[serde(flatten)] pub tail: T, } #[macro_export] macro_rules! HList { () => { $crate::utils::HNil }; ($head:ty $(, $rest:ty)* $(,)?) => { $crate::utils::HCons<$head, HList![$($rest),*]> }; } #[macro_export] macro_rules! hlist_pat { () => { $crate::utils::HNil }; ($head:pat $(, $rest:pat)* $(,)?) => { $crate::utils::HCons { head: $head, tail: hlist_pat!($($rest),*) } }; } #[derive(Deserialize, Deref)] pub struct MerchantId { merchant_id: String, } #[derive(Deserialize, Deref)] pub struct ApiKey { api_key: String, } #[derive(Deserialize)] pub struct Error { pub message: Message, } #[derive(Deserialize, Deref)] pub struct Message { message: String, } #[derive(Deserialize, Deref)] pub struct PaymentId { payment_id: String, } #[derive(Deserialize, Deref)] pub struct Status { status: String, }