Task to retroactively update Mural statuses (#4769)

* Task to retroactively update Mural statuses

* cargo sqlx prepare

* wip: add tests

* Prepare

* Fix up test

* start on muralpay mock

* Move mocking to muralpay crate
This commit is contained in:
aecsocket
2025-11-13 18:16:41 +00:00
committed by GitHub
parent 70e2138248
commit c27f787c91
24 changed files with 906 additions and 10 deletions

View File

@@ -9,6 +9,7 @@ keywords = []
categories = ["api-bindings"]
[dependencies]
arc-swap = { workspace = true, optional = true }
bytes = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
derive_more = { workspace = true, features = [
@@ -37,6 +38,7 @@ tokio = { workspace = true, features = ["full"] }
tracing-subscriber = { workspace = true }
[features]
mock = ["dep:arc-swap"]
utoipa = ["dep:utoipa"]
[lints]

View File

@@ -14,6 +14,8 @@ use crate::{
impl MuralPay {
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> {
mock!(self, get_all_accounts());
self.http_get(|base| format!("{base}/api/accounts"))
.send_mural()
.await
@@ -23,6 +25,8 @@ impl MuralPay {
&self,
id: AccountId,
) -> Result<Account, MuralError> {
mock!(self, get_account(id));
self.http_get(|base| format!("{base}/api/accounts/{id}"))
.send_mural()
.await
@@ -33,6 +37,14 @@ impl MuralPay {
name: impl AsRef<str>,
description: Option<impl AsRef<str>>,
) -> Result<Account, MuralError> {
mock!(
self,
create_account(
name.as_ref(),
description.as_ref().map(|x| x.as_ref()),
)
);
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {

View File

@@ -14,6 +14,8 @@ impl MuralPay {
&self,
params: Option<SearchParams<CounterpartyId>>,
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
mock!(self, search_counterparties(params));
self.http_post(|base| format!("{base}/api/counterparties/search"))
.query(&params.map(|p| p.to_query()).unwrap_or_default())
.send_mural()
@@ -24,6 +26,8 @@ impl MuralPay {
&self,
id: CounterpartyId,
) -> Result<Counterparty, MuralError> {
mock!(self, get_counterparty(id));
self.http_get(|base| {
format!("{base}/api/counterparties/counterparty/{id}")
})
@@ -35,6 +39,8 @@ impl MuralPay {
&self,
counterparty: &CreateCounterparty,
) -> Result<Counterparty, MuralError> {
mock!(self, create_counterparty(counterparty));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
@@ -54,6 +60,8 @@ impl MuralPay {
id: CounterpartyId,
counterparty: &UpdateCounterparty,
) -> Result<Counterparty, MuralError> {
mock!(self, update_counterparty(id, counterparty));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {

View File

@@ -1,5 +1,14 @@
#![doc = include_str!("../README.md")]
macro_rules! mock {
($self:expr, $fn:ident ( $($args:expr),* $(,)? )) => {
#[cfg(feature = "mock")]
if let Some(mock) = &*($self).mock.load() {
return (mock.$fn)($($args),*);
}
};
}
mod account;
mod counterparty;
mod error;
@@ -9,6 +18,9 @@ mod payout_method;
mod serde_iso3166;
mod util;
#[cfg(feature = "mock")]
pub mod mock;
pub use {
account::*, counterparty::*, error::*, organization::*, payout::*,
payout_method::*,
@@ -32,6 +44,8 @@ pub struct MuralPay {
pub api_url: String,
pub api_key: SecretString,
pub transfer_api_key: Option<SecretString>,
#[cfg(feature = "mock")]
mock: arc_swap::ArcSwapOption<mock::MuralPayMock>,
}
impl MuralPay {
@@ -45,6 +59,21 @@ impl MuralPay {
api_url: api_url.into(),
api_key: api_key.into(),
transfer_api_key: transfer_api_key.map(Into::into),
#[cfg(feature = "mock")]
mock: arc_swap::ArcSwapOption::empty(),
}
}
/// Creates a client which mocks responses.
#[cfg(feature = "mock")]
#[must_use]
pub fn from_mock(mock: mock::MuralPayMock) -> Self {
Self {
http: reqwest::Client::new(),
api_url: "".into(),
api_key: SecretString::from(String::new()),
transfer_api_key: None,
mock: arc_swap::ArcSwapOption::from_pointee(mock),
}
}
}

View File

@@ -0,0 +1,65 @@
//! See [`MuralPayMock`].
use std::fmt::{self, Debug};
use crate::{
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId,
CreateCounterparty, CreatePayout, FiatAndRailCode, FiatFeeRequest,
FiatPayoutFee, MuralError, Organization, OrganizationId, PayoutMethod,
PayoutMethodDetails, PayoutMethodId, PayoutRequest, PayoutRequestId,
PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
TokenFeeRequest, TokenPayoutFee, TransferError, UpdateCounterparty,
};
macro_rules! impl_mock {
(
$(fn $fn:ident ( $( $ty:ty ),* ) -> $ret:ty);* $(;)?
) => {
/// Mock data returned by [`crate::MuralPay`].
pub struct MuralPayMock {
$(
pub $fn: Box<dyn Fn($($ty),*) -> $ret + Send + Sync>,
)*
}
impl Default for MuralPayMock {
fn default() -> Self {
Self {
$(
$fn: Box::new(|$(_: $ty),*| panic!("missing mock for `{}`", stringify!($fn))),
)*
}
}
}
};
}
impl_mock! {
fn get_all_accounts() -> Result<Vec<Account>, MuralError>;
fn get_account(AccountId) -> Result<Account, MuralError>;
fn create_account(&str, Option<&str>) -> Result<Account, MuralError>;
fn search_payout_requests(Option<PayoutStatusFilter>, Option<SearchParams<PayoutRequestId>>) -> Result<SearchResponse<PayoutRequestId, PayoutRequest>, MuralError>;
fn get_payout_request(PayoutRequestId) -> Result<PayoutRequest, MuralError>;
fn get_fees_for_token_amount(&[TokenFeeRequest]) -> Result<Vec<TokenPayoutFee>, MuralError>;
fn get_fees_for_fiat_amount(&[FiatFeeRequest]) -> Result<Vec<FiatPayoutFee>, MuralError>;
fn create_payout_request(AccountId, Option<&str>, &[CreatePayout]) -> Result<PayoutRequest, MuralError>;
fn execute_payout_request(PayoutRequestId) -> Result<PayoutRequest, TransferError>;
fn cancel_payout_request(PayoutRequestId) -> Result<PayoutRequest, TransferError>;
fn get_bank_details(&[FiatAndRailCode]) -> Result<BankDetailsResponse, MuralError>;
fn search_payout_methods(CounterpartyId, Option<SearchParams<PayoutMethodId>>) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError>;
fn get_payout_method(CounterpartyId, PayoutMethodId) -> Result<PayoutMethod, MuralError>;
fn create_payout_method(CounterpartyId, &str, &PayoutMethodDetails) -> Result<PayoutMethod, MuralError>;
fn delete_payout_method(CounterpartyId, PayoutMethodId) -> Result<(), MuralError>;
fn search_organizations(SearchRequest) -> Result<SearchResponse<OrganizationId, Organization>, MuralError>;
fn get_organization(OrganizationId) -> Result<Organization, MuralError>;
fn search_counterparties(Option<SearchParams<CounterpartyId>>) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError>;
fn get_counterparty(CounterpartyId) -> Result<Counterparty, MuralError>;
fn create_counterparty(&CreateCounterparty) -> Result<Counterparty, MuralError>;
fn update_counterparty(CounterpartyId, &UpdateCounterparty) -> Result<Counterparty, MuralError>;
}
impl Debug for MuralPayMock {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("MuralPayMock").finish_non_exhaustive()
}
}

View File

@@ -15,6 +15,8 @@ impl MuralPay {
&self,
req: SearchRequest,
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> {
mock!(self, search_organizations(req.clone()));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
@@ -64,6 +66,8 @@ impl MuralPay {
&self,
id: OrganizationId,
) -> Result<Organization, MuralError> {
mock!(self, get_organization(id));
self.http_post(|base| format!("{base}/api/organizations/{id}"))
.send_mural()
.await

View File

@@ -29,6 +29,8 @@ impl MuralPay {
params: Option<SearchParams<PayoutRequestId>>,
) -> Result<SearchResponse<PayoutRequestId, PayoutRequest>, MuralError>
{
mock!(self, search_payout_requests(filter, params));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body {
@@ -50,6 +52,8 @@ impl MuralPay {
&self,
id: PayoutRequestId,
) -> Result<PayoutRequest, MuralError> {
mock!(self, get_payout_request(id));
self.http_get(|base| format!("{base}/api/payouts/payout/{id}"))
.send_mural()
.await
@@ -59,6 +63,8 @@ impl MuralPay {
&self,
token_fee_requests: &[TokenFeeRequest],
) -> Result<Vec<TokenPayoutFee>, MuralError> {
mock!(self, get_fees_for_token_amount(token_fee_requests));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
@@ -77,6 +83,8 @@ impl MuralPay {
&self,
fiat_fee_requests: &[FiatFeeRequest],
) -> Result<Vec<FiatPayoutFee>, MuralError> {
mock!(self, get_fees_for_fiat_amount(fiat_fee_requests));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
@@ -97,6 +105,8 @@ impl MuralPay {
memo: Option<impl AsRef<str>>,
payouts: &[CreatePayout],
) -> Result<PayoutRequest, MuralError> {
mock!(self, create_payout_request(source_account_id, memo.as_ref().map(|x| x.as_ref()), payouts));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
@@ -121,6 +131,8 @@ impl MuralPay {
&self,
id: PayoutRequestId,
) -> Result<PayoutRequest, TransferError> {
mock!(self, execute_payout_request(id));
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/execute"))
.transfer_auth(self)?
.send_mural()
@@ -132,6 +144,8 @@ impl MuralPay {
&self,
id: PayoutRequestId,
) -> Result<PayoutRequest, TransferError> {
mock!(self, cancel_payout_request(id));
self.http_post(|base| format!("{base}/api/payouts/payout/{id}/cancel"))
.transfer_auth(self)?
.send_mural()
@@ -143,6 +157,8 @@ impl MuralPay {
&self,
fiat_currency_and_rail: &[FiatAndRailCode],
) -> Result<BankDetailsResponse, MuralError> {
mock!(self, get_bank_details(fiat_currency_and_rail));
let query = fiat_currency_and_rail
.iter()
.map(|code| ("fiatCurrencyAndRail", code.to_string()))
@@ -207,7 +223,7 @@ impl FromStr for PayoutId {
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PayoutStatusFilter {
PayoutStatus { statuses: Vec<String> },
PayoutStatus { statuses: Vec<PayoutStatus> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -19,6 +19,8 @@ impl MuralPay {
counterparty_id: CounterpartyId,
params: Option<SearchParams<PayoutMethodId>>,
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
mock!(self, search_payout_methods(counterparty_id, params));
self.http_post(|base| {
format!(
"{base}/api/counterparties/{counterparty_id}/payout-methods/search"
@@ -34,6 +36,8 @@ impl MuralPay {
counterparty_id: CounterpartyId,
payout_method_id: PayoutMethodId,
) -> Result<PayoutMethod, MuralError> {
mock!(self, get_payout_method(counterparty_id, payout_method_id));
self.http_get(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
.send_mural()
.await
@@ -45,6 +49,8 @@ impl MuralPay {
alias: impl AsRef<str>,
payout_method: &PayoutMethodDetails,
) -> Result<PayoutMethod, MuralError> {
mock!(self, create_payout_method(counterparty_id, alias.as_ref(), payout_method));
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Body<'a> {
@@ -72,6 +78,8 @@ impl MuralPay {
counterparty_id: CounterpartyId,
payout_method_id: PayoutMethodId,
) -> Result<(), MuralError> {
mock!(self, delete_payout_method(counterparty_id, payout_method_id));
self.http_delete(|base| format!("{base}/api/counterparties/{counterparty_id}/payout-methods/{payout_method_id}"))
.send_mural()
.await