bpi_rs/article/
articles.rs1use crate::article::models::{ArticleAuthor, ArticleCategory, ArticleStats};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ArticlesData {
11 pub list: ArticleList,
13 pub articles: Vec<ArticleItem>,
15 pub author: ArticleAuthor,
17 pub last: ArticleItem,
19 pub attention: bool,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ArticleList {
26 pub id: i64,
28 pub mid: i64,
30 pub name: String,
32 pub image_url: String,
34 pub update_time: i64,
36 pub ctime: i64,
38 pub publish_time: i64,
40 pub summary: String,
42 pub words: i64,
44 pub read: i64,
46 pub articles_count: i32,
48 pub state: i32,
50 pub reason: String,
52 pub apply_time: String,
54 pub check_time: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ArticleItem {
61 pub id: i64,
63 pub title: String,
65 pub state: i32,
67 pub publish_time: i64,
69 pub words: i64,
71 pub image_urls: Vec<String>,
73 pub category: ArticleCategory,
75 pub categories: Vec<ArticleCategory>,
77 pub summary: String,
79 pub stats: Option<ArticleStats>,
81 pub like_state: Option<i32>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct AuthorVip {
88 pub r#type: i32,
90 pub status: i32,
92 pub due_date: i64,
94 pub vip_pay_type: i32,
96 pub theme_type: i32,
98 pub label: Option<serde_json::Value>,
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::article::params::ArticleArticlesInfoParams;
106 use crate::probe::contract::HttpMethod;
107 use crate::probe::endpoint_contract::EndpointContract;
108 use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
109
110 const TEST_LIST_ID: i64 = 207146;
111
112 fn contract() -> BpiResult<EndpointContract> {
113 EndpointContract::from_slice(include_bytes!(
114 "../../tests/contracts/article/articles/contract.json"
115 ))
116 }
117
118 #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
119 #[tokio::test]
120 async fn test_get_articles_info() -> Result<(), Box<BpiError>> {
121 let bpi = BpiClient::new().expect("client should build");
122
123 let params = ArticleArticlesInfoParams::new(TEST_LIST_ID)?;
124
125 let data = bpi.article().articles(params).await?;
126 tracing::info!("{:#?}", data);
127
128 assert!(!data.list.name.is_empty());
129 assert!(!data.articles.is_empty());
130 assert!(!data.author.name.is_empty());
131
132 Ok(())
133 }
134
135 #[test]
136 fn article_articles_contract_matches_endpoint_request() -> BpiResult<()> {
137 let contract = contract()?;
138 let params = ArticleArticlesInfoParams::new(TEST_LIST_ID)?;
139
140 assert_eq!(contract.name, "article.articles_info");
141 assert_eq!(contract.request.method, HttpMethod::Get);
142 assert_eq!(
143 contract.request.url.as_str(),
144 "https://api.bilibili.com/x/article/list/web/articles"
145 );
146 assert_eq!(
147 contract.request.query.get("id").map(String::as_str),
148 Some("207146")
149 );
150 assert_eq!(params.query_pairs(), vec![("id", "207146".to_string())]);
151 assert_eq!(contract.cases.len(), 3);
152 assert_eq!(
153 contract.cases[0].response.rust_model.as_deref(),
154 Some("ArticlesData")
155 );
156 Ok(())
157 }
158
159 #[test]
160 fn article_articles_response_fixtures_parse_declared_model() -> BpiResult<()> {
161 for bytes in [
162 include_bytes!(
163 "../../tests/contracts/article/articles/responses/anonymous.success.json"
164 )
165 .as_slice(),
166 include_bytes!("../../tests/contracts/article/articles/responses/normal.success.json")
167 .as_slice(),
168 include_bytes!("../../tests/contracts/article/articles/responses/vip.success.json")
169 .as_slice(),
170 ] {
171 let payload = ApiEnvelope::<ArticlesData>::from_slice(bytes)?.into_payload()?;
172
173 assert_eq!(payload.list.id, TEST_LIST_ID);
174 assert!(!payload.articles.is_empty());
175 }
176 Ok(())
177 }
178
179 fn local_probe_body(profile: &str) -> Option<serde_json::Value> {
180 let path = format!("target/bpi-probe-runs/article/read/articles/{profile}.response.json");
181 let bytes = std::fs::read(path).ok()?;
182 let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
183 value
184 .get("response")
185 .and_then(|response| response.get("body"))
186 .cloned()
187 }
188
189 #[test]
190 fn article_articles_model_matches_local_probe_outputs_when_available() -> BpiResult<()> {
191 for profile in ["anonymous", "normal", "vip"] {
192 let Some(body) = local_probe_body(profile) else {
193 continue;
194 };
195 let payload =
196 serde_json::from_value::<ApiEnvelope<ArticlesData>>(body)?.into_payload()?;
197
198 assert_eq!(payload.list.id, TEST_LIST_ID);
199 }
200 Ok(())
201 }
202}