use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct FollowUpLiveItem {
pub roomid: i64,
pub uid: i64,
pub uname: String,
pub title: String,
pub face: String,
pub live_status: i32,
pub record_live_time: i64,
pub area_name_v2: String,
pub room_news: String,
pub text_small: String,
pub room_cover: String,
pub parent_area_id: i32,
pub area_id: i32,
}
#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct FollowUpLiveData {
pub title: String,
#[serde(rename = "pageSize")]
pub page_size: i32,
#[serde(rename = "totalPage")]
pub total_page: i32,
pub list: Vec<FollowUpLiveItem>,
pub count: i32,
pub never_lived_count: i32,
pub live_count: i32,
pub never_lived_faces: Vec<String>,
}
#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct LiveRoom {
pub title: String,
pub room_id: i64,
pub uid: i64,
pub online: i32,
pub live_time: i64,
pub live_status: i32,
pub short_id: i32,
pub area: i32,
pub area_name: String,
pub area_v2_id: i32,
pub area_v2_name: String,
pub area_v2_parent_name: String,
pub area_v2_parent_id: i32,
pub uname: String,
pub face: String,
pub tag_name: String,
pub tags: String,
pub cover_from_user: String,
pub keyframe: String,
pub lock_till: String,
pub hidden_till: String,
pub broadcast_type: i32,
pub is_encrypt: bool,
pub link: String,
pub nickname: String,
pub roomname: String,
pub roomid: i64,
}
#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct LiveWebListData {
pub rooms: Vec<LiveRoom>,
pub list: Vec<LiveRoom>,
pub count: i32,
pub not_living_num: i32,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::probe::contract::HttpMethod;
use crate::probe::endpoint_contract::EndpointContract;
use crate::{ApiEnvelope, BpiClient, BpiResult};
fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
let bytes = match endpoint {
"follow-up-list" => include_bytes!(
"../../tests/contracts/live/account-private-read/follow-up-list/contract.json"
)
.as_slice(),
"follow-up-web-list" => include_bytes!(
"../../tests/contracts/live/account-private-read/follow-up-web-list/contract.json"
)
.as_slice(),
_ => unreachable!("unknown live follow-up contract endpoint"),
};
EndpointContract::from_slice(bytes)
}
#[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
#[tokio::test]
async fn test_get_follow_up_live_list() {
let bpi = BpiClient::new().expect("client should build");
let data = bpi
.live()
.follow_up_list(Some(1), Some(2), Some(1), Some(true))
.await
.unwrap();
tracing::info!("{:?}", data);
}
#[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
#[tokio::test]
async fn test_get_follow_up_live_web_list() {
let bpi = BpiClient::new().expect("client should build");
let data = bpi.live().follow_up_web_list(Some(false)).await.unwrap();
tracing::info!("{:?}", data);
}
#[test]
fn live_follow_up_list_contract_matches_endpoint_request() -> BpiResult<()> {
let contract = contract("follow-up-list")?;
let params = [
("page", 1_i32.to_string()),
("page_size", 2_i32.to_string()),
("ignoreRecord", 1_i32.to_string()),
("hit_ab", true.to_string()),
];
assert_eq!(contract.name, "live.follow_up_list");
assert_eq!(contract.request.method, HttpMethod::Get);
assert_eq!(
contract.request.url.as_str(),
"https://api.live.bilibili.com/xlive/web-ucenter/user/following"
);
assert_eq!(
contract
.request
.query
.get("ignoreRecord")
.map(String::as_str),
Some("1")
);
assert_eq!(
contract.request.query.get("hit_ab").map(String::as_str),
Some("true")
);
assert_eq!(
params,
[
("page", "1".to_string()),
("page_size", "2".to_string()),
("ignoreRecord", "1".to_string()),
("hit_ab", "true".to_string())
]
);
assert_eq!(contract.cases.len(), 3);
assert_eq!(
contract.cases[1].response.rust_model.as_deref(),
Some("FollowUpLiveData")
);
Ok(())
}
#[test]
fn live_follow_up_web_list_contract_matches_endpoint_request() -> BpiResult<()> {
let contract = contract("follow-up-web-list")?;
let params = [("hit_ab", false.to_string())];
assert_eq!(contract.name, "live.follow_up_web_list");
assert_eq!(contract.request.method, HttpMethod::Get);
assert_eq!(
contract.request.url.as_str(),
"https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/GetWebList"
);
assert_eq!(
contract.request.query.get("hit_ab").map(String::as_str),
Some("false")
);
assert_eq!(params, [("hit_ab", "false".to_string())]);
assert_eq!(contract.cases.len(), 3);
assert_eq!(
contract.cases[2].response.rust_model.as_deref(),
Some("LiveWebListData")
);
Ok(())
}
#[test]
fn live_follow_up_response_fixtures_parse_declared_models() -> BpiResult<()> {
let err = ApiEnvelope::<serde_json::Value>::from_slice(include_bytes!(
"../../tests/contracts/live/account-private-read/follow-up-list/responses/anonymous.requires_login.json"
))?
.ensure_success()
.unwrap_err();
assert!(err.requires_login());
let follow_up = ApiEnvelope::<FollowUpLiveData>::from_slice(include_bytes!(
"../../tests/contracts/live/account-private-read/follow-up-list/responses/authenticated.success.json"
))?
.into_payload()?;
assert_eq!(follow_up.list.len(), 1);
let empty_web = ApiEnvelope::<LiveWebListData>::from_slice(include_bytes!(
"../../tests/contracts/live/account-private-read/follow-up-web-list/responses/normal.empty.success.json"
))?
.into_payload()?;
assert!(empty_web.rooms.is_empty());
let vip_web = ApiEnvelope::<LiveWebListData>::from_slice(include_bytes!(
"../../tests/contracts/live/account-private-read/follow-up-web-list/responses/vip.sample.success.json"
))?
.into_payload()?;
assert_eq!(vip_web.rooms.len(), 1);
Ok(())
}
fn local_probe_body(endpoint: &str, profile: &str) -> Option<serde_json::Value> {
let path = format!(
"target/bpi-probe-runs/live/account-private-read/{endpoint}/{profile}.response.json"
);
let bytes = std::fs::read(path).ok()?;
let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
value
.get("response")
.and_then(|response| response.get("body"))
.cloned()
}
#[test]
fn live_follow_up_models_match_local_probe_outputs_when_available() -> BpiResult<()> {
for profile in ["anonymous", "normal", "vip"] {
if let Some(body) = local_probe_body("follow-up-list", profile) {
let envelope = serde_json::from_value::<ApiEnvelope<FollowUpLiveData>>(body)?;
if profile == "anonymous" {
assert!(envelope.ensure_success().unwrap_err().requires_login());
} else {
let payload = envelope.into_payload()?;
assert!(payload.count >= 0);
}
}
if let Some(body) = local_probe_body("follow-up-web-list", profile) {
let envelope = serde_json::from_value::<ApiEnvelope<LiveWebListData>>(body)?;
if profile == "anonymous" {
assert!(envelope.ensure_success().unwrap_err().requires_login());
} else {
let payload = envelope.into_payload()?;
assert!(payload.count >= 0);
}
}
}
Ok(())
}
}