1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Serialize, Clone, Deserialize)]
6pub struct GiftItem {
7 pub id: i64,
9 pub name: String,
11 pub price: i64,
13 pub r#type: i32,
15 pub coin_type: String,
17 pub effect: i32,
19 pub stay_time: i32,
21 pub animation_frame_num: i32,
23 pub desc: String,
25 pub img_basic: String,
27 pub gif: String,
29}
30
31#[derive(Debug, Serialize, Clone, Deserialize)]
32pub struct GiftConfig {
33 pub list: Vec<GiftItem>,
35}
36
37#[derive(Debug, Serialize, Clone, Deserialize)]
38pub struct GiftBaseConfig {
39 pub base_config: GiftConfig,
41}
42
43#[derive(Debug, Serialize, Clone, Deserialize)]
44pub struct RoomGiftData {
45 pub gift_config: Option<GiftBaseConfig>,
47 pub gift_data: Option<serde_json::Value>,
49 pub global_config: Option<serde_json::Value>,
51}
52
53#[derive(Debug, Serialize, Clone, Deserialize)]
54pub struct BlindGiftItem {
55 pub gift_id: i64,
57 pub price: i64,
59 pub gift_name: String,
61 pub gift_img: String,
63 pub chance: String,
65}
66
67#[derive(Debug, Serialize, Clone, Deserialize)]
68pub struct BlindGiftData {
69 pub note_text: String,
71 pub blind_price: i64,
73 pub blind_gift_name: String,
75 pub gifts: Vec<BlindGiftItem>,
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::probe::contract::HttpMethod;
83 use crate::probe::endpoint_contract::EndpointContract;
84 use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
85
86 fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
87 let bytes = match endpoint {
88 "room-gift-list" => {
89 include_bytes!("../../tests/contracts/live/gift-read/room-gift-list/contract.json")
90 .as_slice()
91 }
92 "blind-gift-info" => {
93 include_bytes!("../../tests/contracts/live/gift-read/blind-gift-info/contract.json")
94 .as_slice()
95 }
96 _ => unreachable!("unknown live gift contract endpoint"),
97 };
98
99 EndpointContract::from_slice(bytes)
100 }
101
102 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
103 #[tokio::test]
104 async fn test_get_room_gift_list() -> Result<(), Box<BpiError>> {
105 let bpi = BpiClient::new().expect("client should build");
106 let data = bpi.live().room_gift_list(23174842, None, None).await?;
107
108 if let Some(gift_config) = data.gift_config {
109 assert!(!gift_config.base_config.list.is_empty());
110 } else {
111 assert!(data.gift_data.is_some());
112 }
113 Ok(())
114 }
115
116 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
117 #[tokio::test]
118 async fn test_get_blind_gift_info() -> Result<(), Box<BpiError>> {
119 let bpi = BpiClient::new().expect("client should build");
120 let data = bpi.live().blind_gift_info(32251).await?;
121
122 assert!(!data.gifts.is_empty());
123 Ok(())
124 }
125
126 #[test]
127 fn live_room_gift_list_contract_matches_endpoint_request() -> BpiResult<()> {
128 let contract = contract("room-gift-list")?;
129
130 assert_eq!(contract.name, "live.room_gift_list");
131 assert_eq!(contract.request.method, HttpMethod::Get);
132 assert_eq!(
133 contract.request.url.as_str(),
134 "https://api.live.bilibili.com/xlive/web-room/v1/giftPanel/roomGiftList"
135 );
136 assert_eq!(
137 contract.request.query.get("room_id").map(String::as_str),
138 Some("23174842")
139 );
140 assert_eq!(
141 contract.request.query.get("platform").map(String::as_str),
142 Some("web")
143 );
144 assert_eq!(contract.cases.len(), 3);
145 assert_eq!(
146 contract.cases[0].response.rust_model.as_deref(),
147 Some("RoomGiftData")
148 );
149 Ok(())
150 }
151
152 #[test]
153 fn live_blind_gift_info_contract_matches_endpoint_request() -> BpiResult<()> {
154 let contract = contract("blind-gift-info")?;
155
156 assert_eq!(contract.name, "live.blind_gift_info");
157 assert_eq!(contract.request.method, HttpMethod::Get);
158 assert_eq!(
159 contract.request.url.as_str(),
160 "https://api.live.bilibili.com/xlive/general-interface/v1/blindFirstWin/getInfo"
161 );
162 assert_eq!(
163 contract.request.query.get("gift_id").map(String::as_str),
164 Some("32251")
165 );
166 assert_eq!(contract.cases.len(), 3);
167 assert_eq!(contract.cases[0].response.api_code, Some(-101));
168 assert_eq!(
169 contract.cases[1].response.rust_model.as_deref(),
170 Some("BlindGiftData")
171 );
172 Ok(())
173 }
174
175 #[test]
176 fn live_gift_response_fixtures_parse_declared_models() -> BpiResult<()> {
177 let room_gift = ApiEnvelope::<RoomGiftData>::from_slice(include_bytes!(
178 "../../tests/contracts/live/gift-read/room-gift-list/responses/success.json"
179 ))?
180 .into_payload()?;
181 assert!(room_gift.gift_config.is_none());
182 assert!(room_gift.gift_data.is_some());
183
184 let err = ApiEnvelope::<serde_json::Value>::from_slice(include_bytes!(
185 "../../tests/contracts/live/gift-read/blind-gift-info/responses/anonymous.requires_login.json"
186 ))?
187 .ensure_success()
188 .unwrap_err();
189 assert!(err.requires_login());
190
191 let blind = ApiEnvelope::<BlindGiftData>::from_slice(include_bytes!(
192 "../../tests/contracts/live/gift-read/blind-gift-info/responses/authenticated.success.json"
193 ))?
194 .into_payload()?;
195 assert_eq!(blind.gifts.len(), 1);
196 Ok(())
197 }
198
199 fn local_probe_body(endpoint: &str, profile: &str) -> Option<serde_json::Value> {
200 let path =
201 format!("target/bpi-probe-runs/live/gift-read/{endpoint}/{profile}.response.json");
202 let bytes = std::fs::read(path).ok()?;
203 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
204 value
205 .get("response")
206 .and_then(|response| response.get("body"))
207 .cloned()
208 }
209
210 #[test]
211 fn live_gift_models_match_local_probe_outputs_when_available() -> BpiResult<()> {
212 for profile in ["anonymous", "normal", "vip"] {
213 if let Some(body) = local_probe_body("room-gift-list", profile) {
214 let payload =
215 serde_json::from_value::<ApiEnvelope<RoomGiftData>>(body)?.into_payload()?;
216 assert!(payload.gift_config.is_some() || payload.gift_data.is_some());
217 }
218
219 if let Some(body) = local_probe_body("blind-gift-info", profile) {
220 let envelope = serde_json::from_value::<ApiEnvelope<BlindGiftData>>(body)?;
221 if profile == "anonymous" {
222 let err = envelope.ensure_success().unwrap_err();
223 assert!(err.requires_login());
224 } else {
225 let payload = envelope.into_payload()?;
226 assert!(!payload.gifts.is_empty());
227 }
228 }
229 }
230 Ok(())
231 }
232}