mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-03 16:07:46 +08:00
fix(admin): classify bulk wallet adjustment failures
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
use super::{
|
use super::{
|
||||||
build_admin_users_bad_request_response, build_admin_users_permission_denied_response,
|
build_admin_users_bad_request_response, build_admin_users_permission_denied_response,
|
||||||
build_admin_users_read_only_response, disabled_user_policy_detail, disabled_user_policy_field,
|
build_admin_users_read_only_response, build_admin_users_wallet_permission_denied_response,
|
||||||
|
disabled_user_policy_detail, disabled_user_policy_field,
|
||||||
management_token_may_adjust_admin_wallet_balance,
|
management_token_may_adjust_admin_wallet_balance,
|
||||||
management_token_may_administer_user_accounts, normalize_admin_user_role,
|
management_token_may_administer_user_accounts, normalize_admin_user_role,
|
||||||
};
|
};
|
||||||
@@ -111,6 +112,11 @@ enum AdminUserWalletBalanceOperation {
|
|||||||
Deduct,
|
Deduct,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AdminBatchWalletBalanceAdjustmentError {
|
||||||
|
WalletLookup,
|
||||||
|
BalanceAdjustment,
|
||||||
|
}
|
||||||
|
|
||||||
pub(in super::super) async fn build_admin_resolve_user_selection_response(
|
pub(in super::super) async fn build_admin_resolve_user_selection_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
_request_context: &AdminRequestContext<'_>,
|
_request_context: &AdminRequestContext<'_>,
|
||||||
@@ -177,7 +183,7 @@ pub(in super::super) async fn build_admin_user_batch_action_response(
|
|||||||
if mutation.wallet_balance_adjustment.is_some()
|
if mutation.wallet_balance_adjustment.is_some()
|
||||||
&& !management_token_may_adjust_admin_wallet_balance(request_context)
|
&& !management_token_may_adjust_admin_wallet_balance(request_context)
|
||||||
{
|
{
|
||||||
return Ok(build_admin_users_permission_denied_response(
|
return Ok(build_admin_users_wallet_permission_denied_response(
|
||||||
request_context,
|
request_context,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -290,7 +296,20 @@ pub(in super::super) async fn build_admin_user_batch_action_response(
|
|||||||
}));
|
}));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(AdminBatchWalletBalanceAdjustmentError::WalletLookup) => {
|
||||||
|
record_batch_action_interruption(
|
||||||
|
&resolved.items,
|
||||||
|
item_index,
|
||||||
|
false,
|
||||||
|
"读取用户钱包失败,批次已中止,该用户未执行",
|
||||||
|
&mut failures,
|
||||||
|
&mut uncertain_user_ids,
|
||||||
|
&mut unprocessed_user_ids,
|
||||||
|
);
|
||||||
|
interrupted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(AdminBatchWalletBalanceAdjustmentError::BalanceAdjustment) => {
|
||||||
record_batch_action_interruption(
|
record_batch_action_interruption(
|
||||||
&resolved.items,
|
&resolved.items,
|
||||||
item_index,
|
item_index,
|
||||||
@@ -903,13 +922,14 @@ async fn apply_batch_user_wallet_balance_adjustment(
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
adjustment: AdminUserWalletBalanceAdjustment,
|
adjustment: AdminUserWalletBalanceAdjustment,
|
||||||
operator_id: Option<&str>,
|
operator_id: Option<&str>,
|
||||||
) -> Result<bool, GatewayError> {
|
) -> Result<bool, AdminBatchWalletBalanceAdjustmentError> {
|
||||||
// Resolve only the wallet ID; the repository clamps the deduction under its row lock.
|
// Resolve only the wallet ID; the repository clamps the deduction under its row lock.
|
||||||
let Some(wallet) = state
|
let Some(wallet) = state
|
||||||
.find_wallet(aether_data::repository::wallet::WalletLookupKey::UserId(
|
.find_wallet(aether_data::repository::wallet::WalletLookupKey::UserId(
|
||||||
user_id,
|
user_id,
|
||||||
))
|
))
|
||||||
.await?
|
.await
|
||||||
|
.map_err(|_| AdminBatchWalletBalanceAdjustmentError::WalletLookup)?
|
||||||
else {
|
else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
@@ -918,7 +938,7 @@ async fn apply_batch_user_wallet_balance_adjustment(
|
|||||||
AdminUserWalletBalanceOperation::Deduct => -adjustment.amount,
|
AdminUserWalletBalanceOperation::Deduct => -adjustment.amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(state
|
state
|
||||||
.admin_adjust_wallet_balance(
|
.admin_adjust_wallet_balance(
|
||||||
&wallet.id,
|
&wallet.id,
|
||||||
amount,
|
amount,
|
||||||
@@ -927,8 +947,9 @@ async fn apply_batch_user_wallet_balance_adjustment(
|
|||||||
Some("管理员批量调整用户余额"),
|
Some("管理员批量调整用户余额"),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?
|
.await
|
||||||
.is_some())
|
.map_err(|_| AdminBatchWalletBalanceAdjustmentError::BalanceAdjustment)
|
||||||
|
.map(|result| result.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_admin_user_batch_bad_request_response(detail: String) -> Response<Body> {
|
fn build_admin_user_batch_bad_request_response(detail: String) -> Response<Body> {
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ use self::shared::AdminUpdateUserPatch;
|
|||||||
use self::shared::{
|
use self::shared::{
|
||||||
admin_default_user_initial_gift, build_admin_users_bad_request_response,
|
admin_default_user_initial_gift, build_admin_users_bad_request_response,
|
||||||
build_admin_users_data_unavailable_response, build_admin_users_permission_denied_response,
|
build_admin_users_data_unavailable_response, build_admin_users_permission_denied_response,
|
||||||
build_admin_users_read_only_response, disabled_user_policy_detail, disabled_user_policy_field,
|
build_admin_users_read_only_response, build_admin_users_wallet_permission_denied_response,
|
||||||
format_optional_datetime_iso8601, legacy_admin_list_policy_mode,
|
disabled_user_policy_detail, disabled_user_policy_field, format_optional_datetime_iso8601,
|
||||||
legacy_admin_rate_limit_policy_mode, management_token_may_adjust_admin_wallet_balance,
|
legacy_admin_list_policy_mode, legacy_admin_rate_limit_policy_mode,
|
||||||
|
management_token_may_adjust_admin_wallet_balance,
|
||||||
management_token_may_administer_user_accounts, normalize_admin_optional_user_email,
|
management_token_may_administer_user_accounts, normalize_admin_optional_user_email,
|
||||||
normalize_admin_user_group_ids, normalize_admin_user_role, normalize_admin_username,
|
normalize_admin_user_group_ids, normalize_admin_user_role, normalize_admin_username,
|
||||||
validate_admin_user_password, AdminCreateUserApiKeyRequest, AdminCreateUserRequest,
|
validate_admin_user_password, AdminCreateUserApiKeyRequest, AdminCreateUserRequest,
|
||||||
|
|||||||
@@ -204,6 +204,34 @@ pub(super) fn build_admin_users_permission_denied_response(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn build_admin_users_wallet_permission_denied_response(
|
||||||
|
request_context: &crate::handlers::admin::request::AdminRequestContext<'_>,
|
||||||
|
) -> Response<Body> {
|
||||||
|
let actor_id = request_context
|
||||||
|
.decision()
|
||||||
|
.and_then(|decision| decision.admin_principal.as_ref())
|
||||||
|
.and_then(|principal| principal.management_token_id.as_deref())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
crate::handlers::admin::shared::attach_admin_audit_response(
|
||||||
|
(
|
||||||
|
http::StatusCode::FORBIDDEN,
|
||||||
|
Json(json!({
|
||||||
|
"detail": "management token permission denied",
|
||||||
|
"required_permissions": ["admin:wallets:write", "admin:wallets:admin"],
|
||||||
|
"permission_mode": "any_of",
|
||||||
|
"route_family": request_context.route_family(),
|
||||||
|
"route_kind": request_context.route_kind(),
|
||||||
|
"request_path": request_context.path(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
"admin_user_wallet_balance_permission_denied",
|
||||||
|
"permission_denied",
|
||||||
|
"admin_user_wallet_balance",
|
||||||
|
actor_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn normalize_admin_optional_user_email(
|
pub(super) fn normalize_admin_optional_user_email(
|
||||||
value: Option<&str>,
|
value: Option<&str>,
|
||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
@@ -409,9 +437,52 @@ pub(super) fn format_optional_datetime_iso8601(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{normalize_admin_user_api_formats, AdminUpdateUserApiKeyRequest};
|
use super::{
|
||||||
|
build_admin_users_wallet_permission_denied_response, normalize_admin_user_api_formats,
|
||||||
|
AdminUpdateUserApiKeyRequest,
|
||||||
|
};
|
||||||
|
use crate::control::{GatewayControlDecision, GatewayPublicRequestContext};
|
||||||
|
use crate::handlers::admin::request::AdminRequestContext;
|
||||||
|
use axum::http::{HeaderMap, Method, Uri};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wallet_permission_denial_uses_wallet_audit_category() {
|
||||||
|
let uri: Uri = "/api/admin/users/batch-action"
|
||||||
|
.parse()
|
||||||
|
.expect("uri should parse");
|
||||||
|
let method = Method::POST;
|
||||||
|
let headers = HeaderMap::new();
|
||||||
|
let decision = GatewayControlDecision::synthetic(
|
||||||
|
uri.path(),
|
||||||
|
Some("admin_proxy".to_string()),
|
||||||
|
Some("users_manage".to_string()),
|
||||||
|
Some("batch_user_action".to_string()),
|
||||||
|
Some("admin:users".to_string()),
|
||||||
|
);
|
||||||
|
let context = GatewayPublicRequestContext::from_request_parts(
|
||||||
|
"trace-wallet-permission-denied",
|
||||||
|
&method,
|
||||||
|
&uri,
|
||||||
|
&headers,
|
||||||
|
Some(decision),
|
||||||
|
);
|
||||||
|
let request_context = AdminRequestContext::new(&context);
|
||||||
|
|
||||||
|
let response = build_admin_users_wallet_permission_denied_response(&request_context);
|
||||||
|
let event = response
|
||||||
|
.extensions()
|
||||||
|
.get::<crate::audit::AdminAuditEvent>()
|
||||||
|
.expect("wallet denial should attach an audit event");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
event.event_name,
|
||||||
|
"admin_user_wallet_balance_permission_denied"
|
||||||
|
);
|
||||||
|
assert_eq!(event.action, "permission_denied");
|
||||||
|
assert_eq!(event.target_type, "admin_user_wallet_balance");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn admin_user_api_formats_accept_current_canonical_signatures() {
|
fn admin_user_api_formats_accept_current_canonical_signatures() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -494,6 +494,8 @@ pub struct AppState {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) auth_wallet_adjustment_error_for_tests: Option<String>,
|
pub(crate) auth_wallet_adjustment_error_for_tests: Option<String>,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
pub(crate) auth_wallet_lookup_error_for_tests: Option<String>,
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) admin_wallet_payment_order_store:
|
pub(crate) admin_wallet_payment_order_store:
|
||||||
Option<Arc<StdMutex<HashMap<String, AdminWalletPaymentOrderRecord>>>>,
|
Option<Arc<StdMutex<HashMap<String, AdminWalletPaymentOrderRecord>>>>,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -468,6 +468,8 @@ impl AppState {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
auth_wallet_adjustment_error_for_tests: None,
|
auth_wallet_adjustment_error_for_tests: None,
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
auth_wallet_lookup_error_for_tests: None,
|
||||||
|
#[cfg(test)]
|
||||||
admin_wallet_payment_order_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
admin_wallet_payment_order_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
admin_payment_callback_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
admin_payment_callback_store: Some(Arc::new(StdMutex::new(HashMap::new()))),
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ impl AppState {
|
|||||||
&self,
|
&self,
|
||||||
lookup: aether_data::repository::wallet::WalletLookupKey<'_>,
|
lookup: aether_data::repository::wallet::WalletLookupKey<'_>,
|
||||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||||
|
#[cfg(test)]
|
||||||
|
if let Some(failed_user_id) = self.auth_wallet_lookup_error_for_tests.as_deref() {
|
||||||
|
let lookup_user_id = match &lookup {
|
||||||
|
aether_data::repository::wallet::WalletLookupKey::UserId(user_id) => Some(*user_id),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if lookup_user_id == Some(failed_user_id) {
|
||||||
|
return Err(GatewayError::Internal(
|
||||||
|
"injected test wallet lookup failure".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||||
let wallet = {
|
let wallet = {
|
||||||
|
|||||||
@@ -480,6 +480,11 @@ impl AppState {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fail_auth_wallet_lookup_for_tests(mut self, user_id: impl Into<String>) -> Self {
|
||||||
|
self.auth_wallet_lookup_error_for_tests = Some(user_id.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn with_admin_wallet_payment_orders_for_tests<I>(mut self, orders: I) -> Self
|
pub(crate) fn with_admin_wallet_payment_orders_for_tests<I>(mut self, orders: I) -> Self
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = crate::AdminWalletPaymentOrderRecord>,
|
I: IntoIterator<Item = crate::AdminWalletPaymentOrderRecord>,
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ async fn gateway_batches_wallet_addition_deduction_and_clamped_deduction_per_use
|
|||||||
async fn gateway_requires_wallet_write_permission_for_batch_balance_adjustments() {
|
async fn gateway_requires_wallet_write_permission_for_batch_balance_adjustments() {
|
||||||
let users_write_token = "ae-batch-users-write-only";
|
let users_write_token = "ae-batch-users-write-only";
|
||||||
let wallet_write_token = "ae-batch-users-wallet-write";
|
let wallet_write_token = "ae-batch-users-wallet-write";
|
||||||
|
let wallet_admin_token = "ae-batch-users-wallet-admin";
|
||||||
let token_owner = sample_user_with_role("token-owner", "admin");
|
let token_owner = sample_user_with_role("token-owner", "admin");
|
||||||
let target_user = sample_user("user-1");
|
let target_user = sample_user("user-1");
|
||||||
let mut users_only = sample_management_token(
|
let mut users_only = sample_management_token(
|
||||||
@@ -194,8 +195,16 @@ async fn gateway_requires_wallet_write_permission_for_batch_balance_adjustments(
|
|||||||
);
|
);
|
||||||
users_and_wallets.token.allowed_ips = None;
|
users_and_wallets.token.allowed_ips = None;
|
||||||
users_and_wallets.token.permissions = Some(json!(["admin:users:write", "admin:wallets:write"]));
|
users_and_wallets.token.permissions = Some(json!(["admin:users:write", "admin:wallets:write"]));
|
||||||
|
let mut wallets_admin = sample_management_token(
|
||||||
|
"token-users-wallet-admin",
|
||||||
|
&token_owner.id,
|
||||||
|
&token_owner.username,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
wallets_admin.token.allowed_ips = None;
|
||||||
|
wallets_admin.token.permissions = Some(json!(["admin:users:write", "admin:wallets:admin"]));
|
||||||
let token_repository = Arc::new(InMemoryManagementTokenRepository::seed_with_hashes(
|
let token_repository = Arc::new(InMemoryManagementTokenRepository::seed_with_hashes(
|
||||||
vec![users_only, users_and_wallets],
|
vec![users_only, users_and_wallets, wallets_admin],
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
hash_management_token(users_write_token),
|
hash_management_token(users_write_token),
|
||||||
@@ -205,6 +214,10 @@ async fn gateway_requires_wallet_write_permission_for_batch_balance_adjustments(
|
|||||||
hash_management_token(wallet_write_token),
|
hash_management_token(wallet_write_token),
|
||||||
"token-users-and-wallet-write".to_string(),
|
"token-users-and-wallet-write".to_string(),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
hash_management_token(wallet_admin_token),
|
||||||
|
"token-users-wallet-admin".to_string(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
||||||
@@ -235,6 +248,12 @@ async fn gateway_requires_wallet_write_permission_for_batch_balance_adjustments(
|
|||||||
.await
|
.await
|
||||||
.expect("users-only management token request should complete");
|
.expect("users-only management token request should complete");
|
||||||
assert_eq!(denied.status(), StatusCode::FORBIDDEN);
|
assert_eq!(denied.status(), StatusCode::FORBIDDEN);
|
||||||
|
let denied_payload: Value = denied.json().await.expect("response should parse");
|
||||||
|
assert_eq!(
|
||||||
|
denied_payload["required_permissions"],
|
||||||
|
json!(["admin:wallets:write", "admin:wallets:admin"])
|
||||||
|
);
|
||||||
|
assert_eq!(denied_payload["permission_mode"], "any_of");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
wallet_detail(&client, &gateway_url, "user-1").await["balance"],
|
wallet_detail(&client, &gateway_url, "user-1").await["balance"],
|
||||||
10.0
|
10.0
|
||||||
@@ -256,6 +275,25 @@ async fn gateway_requires_wallet_write_permission_for_batch_balance_adjustments(
|
|||||||
15.0
|
15.0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let allowed_with_wallet_admin = client
|
||||||
|
.post(format!("{gateway_url}/api/admin/users/batch-action"))
|
||||||
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.bearer_auth(wallet_admin_token)
|
||||||
|
.json(&payload)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("wallet-admin management token request should complete");
|
||||||
|
assert_eq!(allowed_with_wallet_admin.status(), StatusCode::OK);
|
||||||
|
let result: Value = allowed_with_wallet_admin
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("response should parse");
|
||||||
|
assert_eq!(result["success"], 1);
|
||||||
|
assert_eq!(
|
||||||
|
wallet_detail(&client, &gateway_url, "user-1").await["balance"],
|
||||||
|
20.0
|
||||||
|
);
|
||||||
|
|
||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +351,56 @@ async fn gateway_reports_completed_uncertain_and_unprocessed_users_after_adjustm
|
|||||||
gateway_handle.abort();
|
gateway_handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn gateway_reports_wallet_lookup_failure_as_unprocessed() {
|
||||||
|
let state = AppState::new()
|
||||||
|
.expect("gateway should build")
|
||||||
|
.with_auth_users_for_tests([
|
||||||
|
sample_user("user-1"),
|
||||||
|
sample_user("user-2"),
|
||||||
|
sample_user("user-3"),
|
||||||
|
])
|
||||||
|
.with_auth_wallets_for_tests([
|
||||||
|
sample_wallet("user-1", 10.0, 0.0),
|
||||||
|
sample_wallet("user-2", 20.0, 0.0),
|
||||||
|
sample_wallet("user-3", 30.0, 0.0),
|
||||||
|
])
|
||||||
|
.fail_auth_wallet_lookup_for_tests("user-2");
|
||||||
|
let (gateway_url, gateway_handle) = start_server(build_router_with_state(state)).await;
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let response = post_batch_action(
|
||||||
|
&client,
|
||||||
|
&gateway_url,
|
||||||
|
json!({
|
||||||
|
"selection": { "user_ids": ["user-1", "user-2", "user-3"] },
|
||||||
|
"action": "adjust_wallet_balance",
|
||||||
|
"payload": { "operation": "add", "amount": 5.0 }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
let result: Value = response.json().await.expect("response should parse");
|
||||||
|
assert_eq!(result["interrupted"], true);
|
||||||
|
assert_eq!(result["completed_user_ids"], json!(["user-1"]));
|
||||||
|
assert_eq!(result["uncertain_user_ids"], json!([]));
|
||||||
|
assert_eq!(result["unprocessed_user_ids"], json!(["user-2", "user-3"]));
|
||||||
|
assert_eq!(
|
||||||
|
wallet_detail(&client, &gateway_url, "user-1").await["balance"],
|
||||||
|
15.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wallet_detail(&client, &gateway_url, "user-2").await["balance"],
|
||||||
|
20.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
wallet_detail(&client, &gateway_url, "user-3").await["balance"],
|
||||||
|
30.0
|
||||||
|
);
|
||||||
|
|
||||||
|
gateway_handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gateway_reports_missing_wallet_and_skips_zero_delta_for_non_positive_balance() {
|
async fn gateway_reports_missing_wallet_and_skips_zero_delta_for_non_positive_balance() {
|
||||||
let state = AppState::new()
|
let state = AppState::new()
|
||||||
|
|||||||
Reference in New Issue
Block a user