Skip to main content

bpi_rs/dynamic/
banner.rs

1use serde::{Deserialize, Serialize};
2
3/// 动态首页公告栏响应数据
4#[derive(Debug, Clone, Deserialize, Serialize)]
5pub struct DynamicBannerData {
6    /// 横幅列表
7    pub banners: Vec<DynamicBanner>,
8}
9
10/// 动态横幅数据
11#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct DynamicBanner {
13    /// 横幅 ID
14    pub banner_id: u64,
15    /// 结束时间(UNIX 秒级时间戳)
16    pub end_time: u64,
17    /// 图片 URL
18    pub img_url: String,
19    /// 跳转链接
20    pub link: String,
21    /// 平台
22    pub platform: u64,
23    /// 位置
24    pub position: String,
25    /// 开始时间(UNIX 秒级时间戳)
26    pub start_time: u64,
27    /// 标题
28    pub title: String,
29    /// 权重
30    pub weight: u64,
31}
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36    use crate::probe::contract::HttpMethod;
37    use crate::probe::endpoint_contract::EndpointContract;
38    use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
39    use tracing::info;
40
41    fn contract() -> BpiResult<EndpointContract> {
42        EndpointContract::from_slice(include_bytes!(
43            "../../tests/contracts/dynamic/feed/banner/contract.json"
44        ))
45    }
46
47    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
48    #[tokio::test]
49    async fn test_dynamic_feed_banner() -> Result<(), BpiError> {
50        let bpi = BpiClient::new().expect("client should build");
51        let data = bpi.dynamic().feed_banner().await?;
52
53        info!("成功获取到 {} 条公告", data.banners.len());
54        assert!(!data.banners.is_empty());
55
56        Ok(())
57    }
58
59    #[test]
60    fn dynamic_feed_banner_contract_matches_endpoint_request() -> BpiResult<()> {
61        let contract = contract()?;
62
63        assert_eq!(contract.name, "dynamic.feed_banner");
64        assert_eq!(contract.request.method, HttpMethod::Get);
65        assert_eq!(
66            contract.request.url.as_str(),
67            "https://api.bilibili.com/x/dynamic/feed/dyn/banner"
68        );
69        assert_eq!(
70            contract.request.query.get("platform").map(String::as_str),
71            Some("1")
72        );
73        assert_eq!(
74            contract.request.query.get("position").map(String::as_str),
75            Some("web动态")
76        );
77        assert_eq!(
78            contract
79                .request
80                .query
81                .get("web_location")
82                .map(String::as_str),
83            Some("333.1365")
84        );
85        assert_eq!(contract.cases.len(), 3);
86        assert_eq!(
87            contract.cases[0].response.rust_model.as_deref(),
88            Some("DynamicBannerData")
89        );
90        Ok(())
91    }
92
93    #[test]
94    fn dynamic_feed_banner_response_fixtures_parse_declared_model() -> BpiResult<()> {
95        for bytes in [
96            include_bytes!(
97                "../../tests/contracts/dynamic/feed/banner/responses/anonymous.success.json"
98            )
99            .as_slice(),
100            include_bytes!(
101                "../../tests/contracts/dynamic/feed/banner/responses/normal.success.json"
102            )
103            .as_slice(),
104            include_bytes!("../../tests/contracts/dynamic/feed/banner/responses/vip.success.json")
105                .as_slice(),
106        ] {
107            let payload = ApiEnvelope::<DynamicBannerData>::from_slice(bytes)?.into_payload()?;
108            assert!(!payload.banners.is_empty());
109        }
110        Ok(())
111    }
112
113    fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
114        let path = format!(
115            "target/bpi-probe-runs/dynamic/feed-readonly/feed-banner/{profile}.response.json"
116        );
117        let bytes = std::fs::read(path).ok()?;
118        let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
119        value
120            .get("response")
121            .and_then(|response| response.get("body"))
122            .cloned()
123    }
124
125    #[test]
126    fn dynamic_feed_banner_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
127        for profile in ["anonymous", "normal", "vip"] {
128            let Some(body) = local_probe_body(profile) else {
129                continue;
130            };
131            let payload =
132                serde_json::from_value::<ApiEnvelope<DynamicBannerData>>(body)?.into_payload()?;
133            assert!(!payload.banners.is_empty());
134        }
135        Ok(())
136    }
137}