1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Deserialize, Serialize)]
5pub struct DynamicBannerData {
6 pub banners: Vec<DynamicBanner>,
8}
9
10#[derive(Debug, Clone, Deserialize, Serialize)]
12pub struct DynamicBanner {
13 pub banner_id: u64,
15 pub end_time: u64,
17 pub img_url: String,
19 pub link: String,
21 pub platform: u64,
23 pub position: String,
25 pub start_time: u64,
27 pub title: String,
29 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}