Skip to main content

bpi_rs/wallet/
info.rs

1use serde::{Deserialize, Serialize};
2
3/// 用户钱包数据
4#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
5#[serde(rename_all = "camelCase")]
6pub struct UserWallet {
7    /// 用户 mid
8    pub mid: i64,
9    /// 总计 B 币
10    pub total_bp: f64,
11    /// 默认 B 币
12    pub default_bp: f64,
13    /// iOS B 币
14    pub ios_bp: f64,
15    /// 优惠券余额
16    pub coupon_balance: f64,
17    /// 可用 B 币
18    pub available_bp: f64,
19    /// 不可用 B 币
20    pub unavailable_bp: f64,
21    /// 不可用原因
22    pub unavailable_reason: String,
23    /// 提示信息
24    pub tip: String,
25    /// 需要显示类余额, 1
26    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}