1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
5#[serde(rename_all = "camelCase")]
6pub struct UserWallet {
7 pub mid: i64,
9 pub total_bp: f64,
11 pub default_bp: f64,
13 pub ios_bp: f64,
15 pub coupon_balance: f64,
17 pub available_bp: f64,
19 pub unavailable_bp: f64,
21 pub unavailable_reason: String,
23 pub tip: String,
25 pub need_show_class_balance: i64,
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use crate::ApiEnvelope;
33 use crate::probe::contract::HttpMethod;
34 use crate::probe::endpoint_contract::EndpointContract;
35 use crate::wallet::params::WalletInfoParams;
36 use crate::{BpiClient, BpiError};
37 use tracing::info;
38
39 const WALLET_INFO_ENDPOINT: &str = "https://pay.bilibili.com/paywallet/wallet/getUserWallet";
40
41 fn local_wallet_probe_body(profile: &str) -> Option<serde_json::Value> {
42 let path = format!("target/bpi-probe-runs/wallet/read/info/{profile}.response.json");
43 let bytes = std::fs::read(path).ok()?;
44 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
45 value
46 .get("response")
47 .and_then(|response| response.get("body"))
48 .cloned()
49 }
50
51 #[test]
52 fn wallet_info_contract_matches_endpoint_request() -> Result<(), BpiError> {
53 let contract = EndpointContract::from_slice(include_bytes!(
54 "../../tests/contracts/wallet/read/info/contract.json"
55 ))?;
56
57 assert_eq!(contract.name, "wallet.info");
58 assert_eq!(contract.request.method, HttpMethod::Post);
59 assert_eq!(contract.request.url.as_str(), WALLET_INFO_ENDPOINT);
60 assert!(contract.request.query.is_empty());
61 assert_eq!(contract.cases.len(), 2);
62
63 let body = contract
64 .request
65 .body
66 .as_ref()
67 .ok_or_else(|| BpiError::unsupported_response("missing wallet contract body"))?;
68 assert_eq!(body["csrf"], "${csrf}");
69 assert_eq!(body["platformType"], 3);
70 assert_eq!(body["timestamp"], 1_700_000_000_000_i64);
71 assert_eq!(body["traceId"], 1_700_000_000_000_i64);
72 assert_eq!(body["version"], "1.0");
73 Ok(())
74 }
75
76 #[test]
77 fn wallet_info_contract_covers_authenticated_profiles() -> Result<(), BpiError> {
78 let contract = EndpointContract::from_slice(include_bytes!(
79 "../../tests/contracts/wallet/read/info/contract.json"
80 ))?;
81
82 for case in &contract.cases {
83 assert!(matches!(case.name.as_str(), "normal" | "vip"));
84 assert!(case.auth.requires_cookie());
85 assert!(case.auth.requires_csrf());
86 assert_eq!(case.response.api_code, Some(0));
87 assert_eq!(case.response.rust_model.as_deref(), Some("UserWallet"));
88 assert_eq!(
89 case.response.fixture.as_deref(),
90 Some("responses/authenticated.success.json")
91 );
92 }
93 Ok(())
94 }
95
96 #[test]
97 fn wallet_response_fixture_parses_declared_model() -> Result<(), BpiError> {
98 let wallet = ApiEnvelope::<UserWallet>::from_slice(include_bytes!(
99 "../../tests/contracts/wallet/read/info/responses/authenticated.success.json"
100 ))?
101 .into_payload()?;
102
103 assert_eq!(wallet.mid, 1_000_001);
104 assert_eq!(wallet.need_show_class_balance, 1);
105 Ok(())
106 }
107
108 #[test]
109 fn wallet_model_matches_local_probe_outputs_when_available() -> Result<(), BpiError> {
110 for profile in ["normal", "vip"] {
111 let Some(body) = local_wallet_probe_body(profile) else {
112 continue;
113 };
114
115 let wallet = serde_json::from_value::<ApiEnvelope<UserWallet>>(body)?.into_payload()?;
116
117 assert!(wallet.mid > 0);
118 assert_eq!(wallet.need_show_class_balance, 1);
119 }
120 Ok(())
121 }
122
123 #[test]
124 fn wallet_anonymous_local_probe_preserves_login_error_when_available() -> Result<(), BpiError> {
125 let Some(body) = local_wallet_probe_body("anonymous") else {
126 return Ok(());
127 };
128
129 let err = serde_json::from_value::<ApiEnvelope<serde_json::Value>>(body)?
130 .ensure_success()
131 .unwrap_err();
132
133 assert!(err.requires_login());
134 assert_eq!(err.code(), Some(800501007));
135 Ok(())
136 }
137
138 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
139 #[tokio::test]
140 async fn test_get_user_wallet() {
141 let bpi = BpiClient::new().expect("client should build");
142 let resp = bpi.wallet().info(WalletInfoParams::new()).await;
143 info!("响应: {:?}", resp);
144 assert!(resp.is_ok());
145 if let Ok(wallet) = resp {
146 info!("用户mid: {}", wallet.mid);
147 }
148 }
149}