1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Clone, Deserialize)]
4pub struct UserInfo {
5 pub uid: i64,
7 pub base: UserBaseInfo,
9 pub medal: UserMedalInfo,
11 pub wealth: Option<serde_json::Value>,
13 pub title: Option<serde_json::Value>,
15 pub guard: UserGuardInfo,
17 pub uhead_frame: Option<serde_json::Value>,
19 pub guard_leader: Option<serde_json::Value>,
21}
22
23#[derive(Debug, Serialize, Clone, Deserialize)]
24pub struct GuardTabInfo {
25 pub num: i32,
27 pub page: i32,
29 pub now: i32,
31 pub achievement_level: i32,
33 pub anchor_guard_achieve_level: i32,
35 pub achievement_icon_src: String,
37 pub buy_guard_icon_src: String,
39 pub rule_doc_src: String,
41 pub ex_background_src: String,
43 pub color_start: String,
45 pub color_end: String,
47 pub tab_color: Vec<String>,
49 pub title_color: Vec<String>,
51}
52
53#[derive(Debug, Serialize, Clone, Deserialize)]
54pub struct UserOriginInfo {
55 pub name: String,
57 pub face: String,
59}
60
61#[derive(Debug, Serialize, Clone, Deserialize)]
62pub struct UserOfficialInfo {
63 pub role: i32,
65 pub title: String,
67 pub desc: String,
69 pub r#type: i32,
71}
72
73#[derive(Debug, Serialize, Clone, Deserialize)]
74pub struct UserBaseInfo {
75 pub name: String,
77 pub face: String,
79 pub name_color: i32,
81 pub is_mystery: bool,
83 pub risk_ctrl_info: Option<serde_json::Value>,
85 pub origin_info: UserOriginInfo,
87 pub official_info: UserOfficialInfo,
89 pub name_color_str: String,
91}
92
93#[derive(Debug, Serialize, Clone, Deserialize)]
94pub struct UserMedalInfo {
95 pub name: String,
97 pub level: i32,
99 pub color_start: i32,
101 pub color_end: i32,
103 pub color_border: i32,
105 pub color: i32,
107 pub id: i32,
109 pub typ: i32,
111 pub is_light: i32,
113 pub ruid: i64,
115 pub guard_level: i32,
117 pub score: i32,
119 pub guard_icon: String,
121 pub honor_icon: String,
123 pub v2_medal_color_start: String,
125 pub v2_medal_color_end: String,
127 pub v2_medal_color_border: String,
129 pub v2_medal_color_text: String,
131 pub v2_medal_color_level: String,
133 pub user_receive_count: i32,
135}
136
137#[derive(Debug, Serialize, Clone, Deserialize)]
138pub struct UserGuardInfo {
139 pub level: i32,
141 pub expired_str: String,
143}
144
145#[derive(Debug, Serialize, Clone, Deserialize)]
146pub struct GuardMember {
147 pub ruid: i64,
149 pub rank: i32,
151 pub accompany: i32,
153 pub uinfo: UserInfo,
155 pub score: i32,
157}
158
159#[derive(Debug, Serialize, Clone, Deserialize)]
160pub struct GuardListData {
161 pub info: GuardTabInfo,
163 pub top3: Vec<GuardMember>,
165 pub list: Vec<GuardMember>,
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use crate::probe::contract::HttpMethod;
173 use crate::probe::endpoint_contract::EndpointContract;
174 use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
175
176 fn contract() -> BpiResult<EndpointContract> {
177 EndpointContract::from_slice(include_bytes!(
178 "../../tests/contracts/live/guard-read/guard-list/contract.json"
179 ))
180 }
181
182 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
183 #[tokio::test]
184 async fn test_get_guard_list() -> Result<(), Box<BpiError>> {
185 let bpi = BpiClient::new().expect("client should build");
186 let data = bpi
187 .live()
188 .guard_list(23174842, 504140200, None, None, None)
189 .await?;
190
191 assert!(!data.list.is_empty());
192 Ok(())
193 }
194
195 #[test]
196 fn live_guard_list_contract_matches_endpoint_request() -> BpiResult<()> {
197 let contract = contract()?;
198 let params: Vec<(&str, String)> = vec![
199 ("roomid", 23174842_i64.to_string()),
200 ("ruid", 504140200_i64.to_string()),
201 ("page", 1_i32.to_string()),
202 ("page_size", 20_i32.to_string()),
203 ("typ", 5_i32.to_string()),
204 ];
205
206 assert_eq!(contract.name, "live.guard_list");
207 assert_eq!(contract.request.method, HttpMethod::Get);
208 assert_eq!(
209 contract.request.url.as_str(),
210 "https://api.live.bilibili.com/xlive/app-room/v2/guardTab/topListNew"
211 );
212 assert_eq!(
213 contract.request.query.get("roomid").map(String::as_str),
214 Some("23174842")
215 );
216 assert_eq!(
217 contract.request.query.get("ruid").map(String::as_str),
218 Some("504140200")
219 );
220 assert_eq!(
221 params,
222 vec![
223 ("roomid", "23174842".to_string()),
224 ("ruid", "504140200".to_string()),
225 ("page", "1".to_string()),
226 ("page_size", "20".to_string()),
227 ("typ", "5".to_string()),
228 ]
229 );
230 assert_eq!(contract.cases.len(), 3);
231 assert_eq!(
232 contract.cases[0].response.rust_model.as_deref(),
233 Some("GuardListData")
234 );
235 Ok(())
236 }
237
238 #[test]
239 fn live_guard_list_response_fixture_parses_declared_model() -> BpiResult<()> {
240 let payload = ApiEnvelope::<GuardListData>::from_slice(include_bytes!(
241 "../../tests/contracts/live/guard-read/guard-list/responses/success.json"
242 ))?
243 .into_payload()?;
244
245 assert_eq!(payload.info.now, 1);
246 assert_eq!(payload.top3.len(), 1);
247 assert_eq!(payload.list.len(), 1);
248 Ok(())
249 }
250
251 fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
252 let path =
253 format!("target/bpi-probe-runs/live/guard-read/guard-list/{profile}.response.json");
254 let bytes = std::fs::read(path).ok()?;
255 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
256 value
257 .get("response")
258 .and_then(|response| response.get("body"))
259 .cloned()
260 }
261
262 #[test]
263 fn live_guard_list_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
264 for profile in ["anonymous", "normal", "vip"] {
265 let Some(body) = local_probe_body(profile) else {
266 continue;
267 };
268 let payload =
269 serde_json::from_value::<ApiEnvelope<GuardListData>>(body)?.into_payload()?;
270
271 assert!(!payload.list.is_empty() || !payload.top3.is_empty());
272 }
273 Ok(())
274 }
275}