Skip to main content

bpi_rs/dynamic/
content.rs

1use serde::{Deserialize, Serialize};
2
3/// 直播的已关注者列表项
4#[derive(Debug, Clone, Deserialize, Serialize)]
5pub struct LiveUser {
6    /// 直播者头像 URL
7    pub face: String,
8    /// 直播链接
9    pub link: String,
10    /// 直播标题
11    pub title: String,
12    /// 直播者 ID
13    pub uid: u64,
14    /// 直播者昵称
15    pub uname: String,
16}
17
18/// 正在直播的已关注者响应数据
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct LiveUsersData {
21    /// 直播者数量
22    pub count: u64,
23    /// 作用尚不明确
24    pub group: String,
25    /// 直播者列表
26    pub items: Vec<LiveUser>,
27}
28
29/// 发布新动态的已关注者列表项
30#[derive(Debug, Clone, Deserialize, Serialize)]
31pub struct DynUpUser {
32    pub user_profile: UserProfile,
33}
34#[derive(Debug, Clone, Deserialize, Serialize)]
35pub struct UserProfile {
36    pub info: UserInfo,
37}
38
39#[derive(Debug, Clone, Deserialize, Serialize)]
40pub struct UserInfo {
41    pub uid: u64,
42    pub uname: String,
43    pub face: String,
44}
45
46/// 发布新动态的已关注者响应数据
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct DynUpUsersData {
49    /// 作用尚不明确
50    pub button_statement: String,
51    /// 更新者列表
52    pub items: Vec<DynUpUser>,
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::dynamic::params::{DynamicLiveUsersParams, DynamicUpUsersParams};
59    use crate::probe::contract::HttpMethod;
60    use crate::probe::endpoint_contract::EndpointContract;
61    use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
62    use std::collections::BTreeMap;
63    use tracing::info;
64
65    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
66        let bytes = match endpoint {
67            "live-users" => {
68                include_bytes!("../../tests/contracts/dynamic/content/live-users/contract.json")
69                    .as_slice()
70            }
71            "up-users" => {
72                include_bytes!("../../tests/contracts/dynamic/content/up-users/contract.json")
73                    .as_slice()
74            }
75            _ => unreachable!("unknown dynamic content endpoint"),
76        };
77
78        EndpointContract::from_slice(bytes)
79    }
80
81    fn query_map(query: Vec<(&'static str, String)>) -> BTreeMap<String, String> {
82        query
83            .into_iter()
84            .map(|(key, value)| (key.to_string(), value))
85            .collect()
86    }
87
88    // 您需要在 `Cargo.toml` 中添加 `dotenvy` 和 `tracing` 依赖,并在 `main.rs` 或测试入口处初始化日志
89    // 例如: tracing_subscriber::fmt::init();
90
91    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
92    #[tokio::test]
93    async fn test_get_live_users() -> Result<(), BpiError> {
94        let bpi = BpiClient::new().expect("client should build");
95        let data = bpi
96            .dynamic()
97            .live_users(DynamicLiveUsersParams::new().with_size(1)?)
98            .await?;
99
100        info!("直播中的关注者数量: {}", data.count);
101        info!("第一位直播中的关注者: {:?}", data.items.first());
102
103        Ok(())
104    }
105
106    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
107    #[tokio::test]
108    async fn test_get_dyn_up_users() -> Result<(), BpiError> {
109        let bpi = BpiClient::new().expect("client should build");
110        let data = bpi.dynamic().up_users(DynamicUpUsersParams::new()).await?;
111
112        info!("发布新动态的关注者列表: {:?}", data.items);
113        assert!(!data.items.is_empty());
114
115        Ok(())
116    }
117
118    #[test]
119    fn dynamic_content_contracts_match_endpoint_requests() -> BpiResult<()> {
120        let live_users = contract("live-users")?;
121        assert_eq!(live_users.name, "dynamic.live_users");
122        assert_eq!(live_users.request.method, HttpMethod::Get);
123        assert_eq!(
124            live_users.request.url.as_str(),
125            "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/w_live_users"
126        );
127        assert_eq!(
128            live_users.request.query,
129            query_map(DynamicLiveUsersParams::new().with_size(1)?.query_pairs())
130        );
131        assert_eq!(live_users.cases.len(), 3);
132        assert_eq!(
133            live_users.cases[0].response.error.as_deref(),
134            Some("requires_login")
135        );
136
137        let up_users = contract("up-users")?;
138        assert_eq!(up_users.name, "dynamic.up_users");
139        assert_eq!(up_users.request.method, HttpMethod::Get);
140        assert_eq!(
141            up_users.request.url.as_str(),
142            "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/w_dyn_uplist"
143        );
144        assert_eq!(
145            up_users.request.query,
146            query_map(DynamicUpUsersParams::new().query_pairs())
147        );
148        assert_eq!(up_users.cases.len(), 3);
149        assert_eq!(
150            up_users.cases[0].response.error.as_deref(),
151            Some("requires_login")
152        );
153        Ok(())
154    }
155
156    #[test]
157    fn dynamic_content_response_fixtures_parse_declared_models() -> BpiResult<()> {
158        for bytes in [
159            include_bytes!(
160                "../../tests/contracts/dynamic/content/live-users/responses/normal.success.json"
161            )
162            .as_slice(),
163            include_bytes!(
164                "../../tests/contracts/dynamic/content/live-users/responses/vip.success.json"
165            )
166            .as_slice(),
167        ] {
168            let payload = ApiEnvelope::<LiveUsersData>::from_slice(bytes)?.into_payload()?;
169            assert_eq!(payload.group, "default");
170        }
171
172        for bytes in [
173            include_bytes!(
174                "../../tests/contracts/dynamic/content/up-users/responses/normal.success.json"
175            )
176            .as_slice(),
177            include_bytes!(
178                "../../tests/contracts/dynamic/content/up-users/responses/vip.success.json"
179            )
180            .as_slice(),
181        ] {
182            let _ = ApiEnvelope::<DynUpUsersData>::from_slice(bytes)?.into_payload()?;
183        }
184        Ok(())
185    }
186
187    #[test]
188    fn dynamic_content_anonymous_fixtures_record_login_errors() -> BpiResult<()> {
189        for bytes in [
190            include_bytes!(
191                "../../tests/contracts/dynamic/content/live-users/responses/anonymous.requires_login.json"
192            )
193            .as_slice(),
194            include_bytes!(
195                "../../tests/contracts/dynamic/content/up-users/responses/anonymous.requires_login.json"
196            )
197            .as_slice(),
198        ] {
199            let err = ApiEnvelope::<serde_json::Value>::from_slice(bytes)?
200                .ensure_success()
201                .unwrap_err();
202            assert_eq!(err.code(), Some(4100000));
203        }
204        Ok(())
205    }
206
207    fn local_probe_body(endpoint: &str, profile: &str) -> Option<serde_json::Value> {
208        let path = format!(
209            "target/bpi-probe-runs/dynamic/content-readonly/{endpoint}/{profile}.response.json"
210        );
211        let bytes = std::fs::read(path).ok()?;
212        let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
213        value
214            .get("response")
215            .and_then(|response| response.get("body"))
216            .cloned()
217    }
218
219    #[test]
220    fn dynamic_content_models_match_local_probe_outputs_when_available() -> BpiResult<()> {
221        for profile in ["normal", "vip"] {
222            if let Some(body) = local_probe_body("live-users", profile) {
223                let payload =
224                    serde_json::from_value::<ApiEnvelope<LiveUsersData>>(body)?.into_payload()?;
225                assert_eq!(payload.group, "default");
226            }
227
228            if let Some(body) = local_probe_body("up-users", profile) {
229                let _ =
230                    serde_json::from_value::<ApiEnvelope<DynUpUsersData>>(body)?.into_payload()?;
231            }
232        }
233
234        for endpoint in ["live-users", "up-users"] {
235            if let Some(body) = local_probe_body(endpoint, "anonymous") {
236                let err = serde_json::from_value::<ApiEnvelope<serde_json::Value>>(body)?
237                    .ensure_success()
238                    .unwrap_err();
239                assert_eq!(err.code(), Some(4100000));
240            }
241        }
242        Ok(())
243    }
244}