bpi_rs/audio/
status_number.rs1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AudioStatusNumberData {
8 pub sid: i64,
9 pub play: i64,
10 pub collect: i64,
11 pub comment: i64,
12 pub share: i64,
13}
14
15#[cfg(test)]
16mod tests {
17 use super::*;
18 use crate::audio::params::AudioSongParams;
19 use crate::ids::AudioId;
20 use crate::probe::contract::HttpMethod;
21 use crate::probe::endpoint_contract::EndpointContract;
22 use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
23
24 const TEST_SID: u64 = 13603;
25
26 fn contract() -> BpiResult<EndpointContract> {
27 EndpointContract::from_slice(include_bytes!(
28 "../../tests/contracts/audio/status-number/contract.json"
29 ))
30 }
31
32 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
33 #[tokio::test]
34 async fn test_audio_status_number() -> Result<(), Box<BpiError>> {
35 let bpi = BpiClient::new().expect("client should build");
36 let data = bpi
37 .audio()
38 .status_number(AudioSongParams::new(AudioId::new(TEST_SID)?))
39 .await?;
40 tracing::info!("{:#?}", data);
41
42 assert_eq!(data.sid, TEST_SID as i64);
43 assert!(data.play >= 0);
44 assert!(data.collect >= 0);
45 assert!(data.comment >= 0);
46 assert!(data.share >= 0);
47
48 Ok(())
49 }
50
51 #[test]
52 fn audio_status_number_contract_matches_endpoint_request() -> BpiResult<()> {
53 let contract = contract()?;
54 let params = AudioSongParams::new(AudioId::new(TEST_SID)?);
55
56 assert_eq!(contract.name, "audio.status_number");
57 assert_eq!(contract.request.method, HttpMethod::Get);
58 assert_eq!(
59 contract.request.url.as_str(),
60 "https://www.bilibili.com/audio/music-service-c/web/stat/song"
61 );
62 assert_eq!(
63 contract.request.query.get("sid").map(String::as_str),
64 Some("13603")
65 );
66 assert_eq!(params.query_pairs(), vec![("sid", "13603".to_string())]);
67 assert_eq!(contract.cases.len(), 3);
68 assert_eq!(
69 contract.cases[0].response.rust_model.as_deref(),
70 Some("AudioStatusNumberData")
71 );
72 Ok(())
73 }
74
75 #[test]
76 fn audio_status_number_response_fixtures_parse_declared_model() -> BpiResult<()> {
77 for bytes in [
78 include_bytes!(
79 "../../tests/contracts/audio/status-number/responses/anonymous.success.json"
80 )
81 .as_slice(),
82 include_bytes!(
83 "../../tests/contracts/audio/status-number/responses/normal.success.json"
84 )
85 .as_slice(),
86 include_bytes!("../../tests/contracts/audio/status-number/responses/vip.success.json")
87 .as_slice(),
88 ] {
89 let payload =
90 ApiEnvelope::<AudioStatusNumberData>::from_slice(bytes)?.into_payload()?;
91
92 assert_eq!(payload.sid, TEST_SID as i64);
93 }
94 Ok(())
95 }
96
97 fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
98 let path = format!(
99 "target/bpi-probe-runs/audio/public-read/status-number/{profile}.response.json"
100 );
101 let bytes = std::fs::read(path).ok()?;
102 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
103 value
104 .get("response")
105 .and_then(|response| response.get("body"))
106 .cloned()
107 }
108
109 #[test]
110 fn audio_status_number_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
111 for profile in ["anonymous", "normal", "vip"] {
112 let Some(body) = local_probe_body(profile) else {
113 continue;
114 };
115 let payload = serde_json::from_value::<ApiEnvelope<AudioStatusNumberData>>(body)?
116 .into_payload()?;
117
118 assert_eq!(payload.sid, TEST_SID as i64);
119 }
120 Ok(())
121 }
122}