bpi_rs/dynamic/
content.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Deserialize, Serialize)]
5pub struct LiveUser {
6 pub face: String,
8 pub link: String,
10 pub title: String,
12 pub uid: u64,
14 pub uname: String,
16}
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct LiveUsersData {
21 pub count: u64,
23 pub group: String,
25 pub items: Vec<LiveUser>,
27}
28
29#[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#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct DynUpUsersData {
49 pub button_statement: String,
51 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 #[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}