Skip to main content

bpi_rs/dynamic/
detail.rs

1use crate::models::{Official, Pendant, Vip};
2use serde::{Deserialize, Serialize};
3// --- 动态详情 API 结构体 ---
4
5/// 动态详情响应数据
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct DynamicDetailData {
8    pub item: DynamicDetailItem,
9}
10
11/// 动态卡片内容,作为多个 API 的共享结构体
12#[derive(Debug, Clone, Deserialize, Serialize)]
13pub struct DynamicDetailItem {
14    pub id_str: String,
15    pub basic: DynamicBasic,
16
17    pub modules: serde_json::Value,
18
19    /// 当`type`字段的值为DYNAMIC_TYPE_FORWARD(转发动态)时,此字段不为null
20    pub orig: Option<Box<DynamicDetailItem>>,
21
22    pub r#type: String,
23
24    pub visible: bool,
25}
26
27/// 动态卡片内容,作为多个 API 的共享结构体
28#[derive(Debug, Clone, Deserialize, Serialize)]
29pub struct DynamicForwardItem {
30    pub desc: Desc,
31    pub id_str: String,
32    pub pub_time: String,
33    pub user: User,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Desc {
38    pub rich_text_nodes: Vec<RichTextNode>,
39    pub text: String,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RichTextNode {
44    pub orig_text: String,
45    pub text: String,
46    #[serde(rename = "type")]
47    pub type_field: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct User {
52    pub face: String,
53    pub face_nft: bool,
54    pub mid: i64,
55    pub name: String,
56    pub official: Official,
57    pub pendant: Pendant,
58    pub vip: Vip,
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct DynamicBasic {
63    pub comment_id_str: String,
64    pub comment_type: i64,
65    pub editable: Option<bool>,
66    pub jump_url: Option<String>,
67    pub like_icon: serde_json::Value,
68    pub rid_str: String,
69}
70
71// --- 动态点赞与转发列表 API 结构体 ---
72
73/// 点赞或转发的用户列表项
74#[derive(Debug, Clone, Deserialize, Serialize)]
75pub struct DynamicReactionItem {
76    pub action: String,
77    /// 1: 对方仅关注了发送者    2: 发送者关注了对方
78    pub attend: u8,
79    pub desc: String,
80    pub face: String,
81    pub mid: String,
82    pub name: String,
83}
84
85/// 动态点赞与转发列表响应数据
86#[derive(Debug, Clone, Deserialize, Serialize)]
87pub struct DynamicReactionData {
88    pub has_more: bool,
89    pub items: Vec<DynamicReactionItem>,
90    pub offset: String,
91    pub total: u64,
92}
93
94// --- 动态抽奖详情 API 结构体 ---
95
96/// 抽奖中奖用户。
97#[derive(Debug, Clone, Deserialize, Serialize)]
98pub struct LotteryWinner {
99    pub uid: u64,
100    pub name: String,
101    pub face: String,
102    pub hongbao_money: Option<f64>,
103}
104
105/// 动态抽奖结果。
106#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct DynamicLotteryResult {
108    #[serde(default)]
109    pub first_prize_result: Vec<LotteryWinner>,
110    #[serde(default)]
111    pub second_prize_result: Vec<LotteryWinner>,
112    #[serde(default)]
113    pub third_prize_result: Vec<LotteryWinner>,
114}
115
116/// 动态抽奖奖品类型。
117#[derive(Debug, Clone, Deserialize, Serialize)]
118pub struct DynamicLotteryPrizeType {
119    #[serde(rename = "type")]
120    pub type_field: u8,
121    pub value: DynamicLotteryPrizeTypeValue,
122}
123
124#[derive(Debug, Clone, Deserialize, Serialize)]
125pub struct DynamicLotteryPrizeTypeValue {
126    pub count: u64,
127    pub stype: u8,
128}
129
130/// 动态抽奖详情响应数据。
131#[derive(Debug, Clone, Deserialize, Serialize)]
132pub struct DynamicLotteryData {
133    pub lottery_id: u64,
134    pub sender_uid: u64,
135    pub business_type: u8,
136    pub business_id: u64,
137    pub status: u8,
138    pub lottery_time: u64,
139    pub participants: u64,
140    pub first_prize: u32,
141    pub first_prize_cmt: String,
142    pub first_prize_pic: String,
143    pub second_prize: u32,
144    #[serde(default)]
145    pub second_prize_cmt: Option<String>,
146    pub second_prize_pic: String,
147    pub third_prize: u32,
148    #[serde(default)]
149    pub third_prize_cmt: Option<String>,
150    pub third_prize_pic: String,
151    pub lottery_result: Option<DynamicLotteryResult>,
152    pub followed: bool,
153    pub has_charge_right: bool,
154    pub lottery_at_num: u32,
155    pub lottery_detail_url: String,
156    pub lottery_feed_limit: u32,
157    pub need_post: u8,
158    pub participated: bool,
159    #[serde(default)]
160    pub prize_type_first: Option<DynamicLotteryPrizeType>,
161    pub reposted: bool,
162    pub ts: u64,
163    pub upower_redirect_url: String,
164    pub vip_batch_sign: String,
165    pub vip_redirect_url: String,
166}
167
168// --- 动态转发列表 API 结构体 ---
169
170/// 动态转发列表响应数据
171#[derive(Debug, Clone, Deserialize, Serialize)]
172pub struct DynamicForwardData {
173    pub has_more: bool,
174    pub items: Vec<DynamicForwardItem>,
175    pub offset: String,
176    pub total: u64,
177}
178#[derive(Debug, Clone, Deserialize, Serialize)]
179pub struct DynamicForwardInfoData {
180    pub item: DynamicForwardItem,
181}
182
183// --- 获取动态图片 API 结构体 ---
184
185/// 动态图片信息
186#[derive(Debug, Clone, Deserialize, Serialize)]
187pub struct DynamicPic {
188    pub height: u64,
189    pub size: f64,
190    pub src: String,
191    pub width: u64,
192}
193
194/// 动态图片列表响应数据
195#[derive(Debug, Clone, Deserialize, Serialize)]
196pub struct DynamicPicsData {
197    pub data: Vec<DynamicPic>,
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::dynamic::{
204        DynamicDetailParams, DynamicForwardItemParams, DynamicForwardsParams,
205        DynamicLotteryNoticeParams, DynamicPicsParams, DynamicReactionsParams,
206    };
207    use crate::ids::DynamicId;
208    use crate::probe::contract::HttpMethod;
209    use crate::probe::endpoint_contract::EndpointContract;
210    use crate::{ApiEnvelope, BpiClient, BpiError, BpiResult};
211    use std::collections::BTreeMap;
212    use tracing::info;
213
214    fn parse_dynamic_id(value: &str) -> Result<DynamicId, BpiError> {
215        value.parse()
216    }
217
218    fn contract(endpoint: &str) -> BpiResult<EndpointContract> {
219        let bytes = match endpoint {
220            "detail" => include_bytes!("../../tests/contracts/dynamic/detail/detail/contract.json")
221                .as_slice(),
222            "reactions" => {
223                include_bytes!("../../tests/contracts/dynamic/detail/reactions/contract.json")
224                    .as_slice()
225            }
226            "forwards" => {
227                include_bytes!("../../tests/contracts/dynamic/detail/forwards/contract.json")
228                    .as_slice()
229            }
230            "pics" => {
231                include_bytes!("../../tests/contracts/dynamic/detail/pics/contract.json").as_slice()
232            }
233            "forward-item" => {
234                include_bytes!("../../tests/contracts/dynamic/detail/forward-item/contract.json")
235                    .as_slice()
236            }
237            "lottery-notice" => include_bytes!(
238                "../../tests/contracts/dynamic/lottery-notice-read/lottery-notice/contract.json"
239            )
240            .as_slice(),
241            _ => unreachable!("unknown dynamic detail endpoint"),
242        };
243
244        EndpointContract::from_slice(bytes)
245    }
246
247    fn query_map<I>(query: I) -> BTreeMap<String, String>
248    where
249        I: IntoIterator<Item = (&'static str, String)>,
250    {
251        query
252            .into_iter()
253            .map(|(key, value)| (key.to_string(), value))
254            .collect()
255    }
256
257    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
258    #[tokio::test]
259    async fn test_get_dynamic_detail() -> Result<(), BpiError> {
260        let bpi = BpiClient::new().expect("client should build");
261        let dynamic_id = "1099138163191840776";
262        let data = bpi
263            .dynamic()
264            .detail(DynamicDetailParams::new(parse_dynamic_id(dynamic_id)?))
265            .await?;
266
267        info!("动态详情: {:?}", data.item);
268        assert_eq!(data.item.id_str, dynamic_id);
269
270        let dynamic_id = "1152614216889270274"; // 此动态为陈叔叔的一条转发动态
271        let data = bpi
272            .dynamic()
273            .detail(DynamicDetailParams::new(parse_dynamic_id(dynamic_id)?))
274            .await?;
275        info!("动态详情: {:?}", data.item);
276        assert_eq!(data.item.id_str, dynamic_id);
277        assert!(data.item.orig.is_some());
278
279        Ok(())
280    }
281
282    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
283    #[tokio::test]
284    async fn test_get_dynamic_reactions() -> Result<(), BpiError> {
285        let bpi = BpiClient::new().expect("client should build");
286        let dynamic_id = "1099138163191840776";
287        let data = bpi
288            .dynamic()
289            .reactions(DynamicReactionsParams::new(parse_dynamic_id(dynamic_id)?))
290            .await?;
291
292        info!("点赞/转发总数: {}", data.total);
293        assert!(!data.items.is_empty());
294
295        Ok(())
296    }
297
298    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
299    #[tokio::test]
300    async fn test_get_lottery_notice() -> Result<(), BpiError> {
301        let bpi = BpiClient::new().expect("client should build");
302        let dynamic_id = "969916293954142214";
303        let data = bpi
304            .dynamic()
305            .lottery_notice(DynamicLotteryNoticeParams::new(parse_dynamic_id(
306                dynamic_id,
307            )?))
308            .await?;
309
310        info!("抽奖状态: {}", data.status);
311        assert_eq!(data.business_id.to_string(), dynamic_id);
312
313        Ok(())
314    }
315
316    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
317    #[tokio::test]
318    async fn test_get_dynamic_forwards() -> Result<(), BpiError> {
319        let bpi = BpiClient::new().expect("client should build");
320        let dynamic_id = "1099138163191840776";
321        let data = bpi
322            .dynamic()
323            .forwards(DynamicForwardsParams::new(parse_dynamic_id(dynamic_id)?))
324            .await?;
325
326        info!("转发总数: {}", data.total);
327        assert!(!data.items.is_empty());
328
329        Ok(())
330    }
331
332    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
333    #[tokio::test]
334    async fn test_get_dynamic_pics() -> Result<(), BpiError> {
335        let bpi = BpiClient::new().expect("client should build");
336        let dynamic_id = "1099138163191840776";
337        let data = bpi
338            .dynamic()
339            .pics(DynamicPicsParams::new(parse_dynamic_id(dynamic_id)?))
340            .await?;
341
342        info!("图片数量: {}", data.len());
343        assert!(!data.is_empty());
344
345        Ok(())
346    }
347
348    #[ignore = "legacy live API test; requires explicit BPI_LIVE_TEST review"]
349    #[tokio::test]
350    async fn test_get_forward_item() -> Result<(), BpiError> {
351        let bpi = BpiClient::new().expect("client should build");
352        let dynamic_id = "1110902525317349376";
353        let data = bpi
354            .dynamic()
355            .forward_item(DynamicForwardItemParams::new(parse_dynamic_id(dynamic_id)?))
356            .await?;
357
358        info!("转发动态详情: {:?}", data.item);
359        assert_eq!(data.item.id_str, dynamic_id);
360
361        Ok(())
362    }
363
364    #[test]
365    fn dynamic_detail_params_serializes_default_features() -> Result<(), BpiError> {
366        let params = DynamicDetailParams::new(parse_dynamic_id("1099138163191840776")?);
367
368        assert_eq!(
369            params.query_pairs(),
370            [
371                ("id", "1099138163191840776".to_string()),
372                (
373                    "features",
374                    "htmlNewStyle,itemOpusStyle,decorationCard".to_string()
375                ),
376            ]
377        );
378        Ok(())
379    }
380
381    #[test]
382    fn dynamic_detail_params_serializes_custom_features() -> Result<(), BpiError> {
383        let params = DynamicDetailParams::new(parse_dynamic_id("1099138163191840776")?)
384            .with_features("itemOpusStyle,opusBigCover")?;
385
386        assert_eq!(
387            params.query_pairs(),
388            [
389                ("id", "1099138163191840776".to_string()),
390                ("features", "itemOpusStyle,opusBigCover".to_string()),
391            ]
392        );
393        Ok(())
394    }
395
396    #[test]
397    fn dynamic_reactions_params_serializes_offset() -> Result<(), BpiError> {
398        let params = DynamicReactionsParams::new(parse_dynamic_id("1099138163191840776")?)
399            .with_offset("offset-token")?;
400
401        assert_eq!(
402            params.query_pairs(),
403            [
404                ("id", "1099138163191840776".to_string()),
405                ("offset", "offset-token".to_string()),
406            ]
407        );
408        Ok(())
409    }
410
411    #[test]
412    fn dynamic_lottery_notice_params_serializes_csrf_query() -> Result<(), BpiError> {
413        let params = DynamicLotteryNoticeParams::new(parse_dynamic_id("969916293954142214")?);
414
415        assert_eq!(
416            params.query_pairs("csrf-token"),
417            [
418                ("business_id", "969916293954142214".to_string()),
419                ("business_type", "1".to_string()),
420                ("csrf", "csrf-token".to_string()),
421            ]
422        );
423        Ok(())
424    }
425
426    #[test]
427    fn dynamic_pics_params_serializes_query() -> Result<(), BpiError> {
428        let params = DynamicPicsParams::new(parse_dynamic_id("1099138163191840776")?);
429
430        assert_eq!(
431            params.query_pairs(),
432            [("id", "1099138163191840776".to_string())]
433        );
434        Ok(())
435    }
436
437    #[test]
438    fn dynamic_forward_item_params_serializes_query() -> Result<(), BpiError> {
439        let params = DynamicForwardItemParams::new(parse_dynamic_id("1110902525317349376")?);
440
441        assert_eq!(
442            params.query_pairs(),
443            [("id", "1110902525317349376".to_string())]
444        );
445        Ok(())
446    }
447
448    #[test]
449    fn dynamic_forwards_params_rejects_blank_offset() -> Result<(), BpiError> {
450        let err = DynamicForwardsParams::new(parse_dynamic_id("1099138163191840776")?)
451            .with_offset("   ")
452            .unwrap_err();
453
454        assert!(matches!(
455            err,
456            BpiError::InvalidParameter {
457                field: "offset",
458                ..
459            }
460        ));
461        Ok(())
462    }
463
464    #[test]
465    fn dynamic_detail_read_contracts_match_endpoint_requests() -> BpiResult<()> {
466        let detail = contract("detail")?;
467        assert_eq!(detail.name, "dynamic.detail");
468        assert_eq!(detail.request.method, HttpMethod::Get);
469        assert_eq!(
470            detail.request.url.as_str(),
471            "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail"
472        );
473        assert_eq!(
474            detail.request.query,
475            query_map(
476                DynamicDetailParams::new(parse_dynamic_id("1099138163191840776")?).query_pairs()
477            )
478        );
479        assert_eq!(detail.cases.len(), 3);
480
481        let reactions = contract("reactions")?;
482        assert_eq!(reactions.name, "dynamic.detail_reaction");
483        assert_eq!(
484            reactions.request.url.as_str(),
485            "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/reaction"
486        );
487        assert_eq!(
488            reactions.request.query,
489            query_map(
490                DynamicReactionsParams::new(parse_dynamic_id("1099138163191840776")?).query_pairs()
491            )
492        );
493
494        let forwards = contract("forwards")?;
495        assert_eq!(forwards.name, "dynamic.detail_forward");
496        assert_eq!(
497            forwards.request.url.as_str(),
498            "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/forward"
499        );
500        assert_eq!(
501            forwards.request.query,
502            query_map(
503                DynamicForwardsParams::new(parse_dynamic_id("1099138163191840776")?).query_pairs()
504            )
505        );
506
507        let pics = contract("pics")?;
508        assert_eq!(pics.name, "dynamic.detail_pic");
509        assert_eq!(
510            pics.request.url.as_str(),
511            "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/pic"
512        );
513        assert_eq!(
514            pics.request.query,
515            query_map(
516                DynamicPicsParams::new(parse_dynamic_id("1099138163191840776")?).query_pairs()
517            )
518        );
519
520        let forward_item = contract("forward-item")?;
521        let forward_item_id = parse_dynamic_id("1110902525317349376")?;
522        assert_eq!(forward_item.name, "dynamic.detail_forward_item");
523        assert_eq!(
524            forward_item.request.url.as_str(),
525            "https://api.bilibili.com/x/polymer/web-dynamic/v1/detail/forward/item"
526        );
527        assert_eq!(
528            forward_item.request.query,
529            query_map(DynamicForwardItemParams::new(forward_item_id).query_pairs())
530        );
531        assert_eq!(
532            forward_item.cases[0].response.error.as_deref(),
533            Some("requires_login")
534        );
535
536        let lottery_notice = contract("lottery-notice")?;
537        assert_eq!(lottery_notice.name, "dynamic.lottery_notice");
538        assert_eq!(
539            lottery_notice.request.url.as_str(),
540            "https://api.vc.bilibili.com/lottery_svr/v1/lottery_svr/lottery_notice"
541        );
542        assert_eq!(
543            lottery_notice.request.query,
544            query_map(
545                DynamicLotteryNoticeParams::new(parse_dynamic_id("969916293954142214")?)
546                    .query_pairs("${csrf}")
547            )
548        );
549        assert_eq!(lottery_notice.cases.len(), 3);
550        Ok(())
551    }
552
553    #[test]
554    fn dynamic_detail_read_response_fixtures_parse_declared_models() -> BpiResult<()> {
555        for bytes in [
556            include_bytes!(
557                "../../tests/contracts/dynamic/detail/detail/responses/anonymous.success.json"
558            )
559            .as_slice(),
560            include_bytes!(
561                "../../tests/contracts/dynamic/detail/detail/responses/normal.success.json"
562            )
563            .as_slice(),
564            include_bytes!(
565                "../../tests/contracts/dynamic/detail/detail/responses/vip.success.json"
566            )
567            .as_slice(),
568        ] {
569            let payload = ApiEnvelope::<DynamicDetailData>::from_slice(bytes)?.into_payload()?;
570            assert_eq!(payload.item.id_str, "1099138163191840776");
571        }
572
573        for bytes in [
574            include_bytes!(
575                "../../tests/contracts/dynamic/detail/reactions/responses/anonymous.success.json"
576            )
577            .as_slice(),
578            include_bytes!(
579                "../../tests/contracts/dynamic/detail/reactions/responses/normal.success.json"
580            )
581            .as_slice(),
582            include_bytes!(
583                "../../tests/contracts/dynamic/detail/reactions/responses/vip.success.json"
584            )
585            .as_slice(),
586        ] {
587            let payload = ApiEnvelope::<DynamicReactionData>::from_slice(bytes)?.into_payload()?;
588            let _ = payload.total;
589        }
590
591        for bytes in [
592            include_bytes!(
593                "../../tests/contracts/dynamic/detail/forwards/responses/anonymous.success.json"
594            )
595            .as_slice(),
596            include_bytes!(
597                "../../tests/contracts/dynamic/detail/forwards/responses/normal.success.json"
598            )
599            .as_slice(),
600            include_bytes!(
601                "../../tests/contracts/dynamic/detail/forwards/responses/vip.success.json"
602            )
603            .as_slice(),
604        ] {
605            let payload = ApiEnvelope::<DynamicForwardData>::from_slice(bytes)?.into_payload()?;
606            assert_eq!(payload.items.len(), 1);
607        }
608
609        for bytes in [
610            include_bytes!(
611                "../../tests/contracts/dynamic/detail/pics/responses/anonymous.success.json"
612            )
613            .as_slice(),
614            include_bytes!(
615                "../../tests/contracts/dynamic/detail/pics/responses/normal.success.json"
616            )
617            .as_slice(),
618            include_bytes!("../../tests/contracts/dynamic/detail/pics/responses/vip.success.json")
619                .as_slice(),
620        ] {
621            let payload = ApiEnvelope::<Vec<DynamicPic>>::from_slice(bytes)?.into_payload()?;
622            assert_eq!(payload.len(), 1);
623        }
624
625        for bytes in [
626            include_bytes!(
627                "../../tests/contracts/dynamic/detail/forward-item/responses/normal.success.json"
628            )
629            .as_slice(),
630            include_bytes!(
631                "../../tests/contracts/dynamic/detail/forward-item/responses/vip.success.json"
632            )
633            .as_slice(),
634        ] {
635            let payload =
636                ApiEnvelope::<DynamicForwardInfoData>::from_slice(bytes)?.into_payload()?;
637            assert_eq!(payload.item.id_str, "1110902525317349376");
638        }
639
640        let payload = ApiEnvelope::<DynamicLotteryData>::from_slice(include_bytes!(
641            "../../tests/contracts/dynamic/lottery-notice-read/lottery-notice/responses/success.json"
642        ))?
643        .into_payload()?;
644        assert_eq!(payload.business_id, 969916293954142214);
645        assert_eq!(
646            payload
647                .lottery_result
648                .as_ref()
649                .map(|result| result.first_prize_result.len()),
650            Some(1)
651        );
652        Ok(())
653    }
654
655    #[test]
656    fn dynamic_forward_item_anonymous_fixture_records_login_error() -> BpiResult<()> {
657        let err = ApiEnvelope::<serde_json::Value>::from_slice(include_bytes!(
658            "../../tests/contracts/dynamic/detail/forward-item/responses/anonymous.requires_login.json"
659        ))?
660        .ensure_success()
661        .unwrap_err();
662
663        assert_eq!(err.code(), Some(-101));
664        Ok(())
665    }
666
667    fn local_probe_body(endpoint: &str, profile: &str) -> Option<serde_json::Value> {
668        let batch = if endpoint == "lottery-notice" {
669            "lottery-notice-read"
670        } else {
671            "detail-readonly"
672        };
673        let path =
674            format!("target/bpi-probe-runs/dynamic/{batch}/{endpoint}/{profile}.response.json");
675        let bytes = std::fs::read(path).ok()?;
676        let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
677        value
678            .get("response")
679            .and_then(|response| response.get("body"))
680            .cloned()
681    }
682
683    #[test]
684    fn dynamic_detail_read_models_match_local_probe_outputs_when_available() -> BpiResult<()> {
685        for profile in ["anonymous", "normal", "vip"] {
686            if let Some(body) = local_probe_body("detail", profile) {
687                let payload = serde_json::from_value::<ApiEnvelope<DynamicDetailData>>(body)?
688                    .into_payload()?;
689                assert_eq!(payload.item.id_str, "1099138163191840776");
690            }
691
692            if let Some(body) = local_probe_body("reactions", profile) {
693                let _ = serde_json::from_value::<ApiEnvelope<DynamicReactionData>>(body)?
694                    .into_payload()?;
695            }
696
697            if let Some(body) = local_probe_body("forwards", profile) {
698                let payload = serde_json::from_value::<ApiEnvelope<DynamicForwardData>>(body)?
699                    .into_payload()?;
700                assert!(!payload.items.is_empty());
701            }
702
703            if let Some(body) = local_probe_body("pics", profile) {
704                let payload =
705                    serde_json::from_value::<ApiEnvelope<Vec<DynamicPic>>>(body)?.into_payload()?;
706                assert!(!payload.is_empty());
707            }
708        }
709
710        if let Some(body) = local_probe_body("forward-item", "anonymous") {
711            let err = serde_json::from_value::<ApiEnvelope<serde_json::Value>>(body)?
712                .ensure_success()
713                .unwrap_err();
714            assert_eq!(err.code(), Some(-101));
715        }
716
717        for profile in ["normal", "vip"] {
718            if let Some(body) = local_probe_body("forward-item", profile) {
719                let payload = serde_json::from_value::<ApiEnvelope<DynamicForwardInfoData>>(body)?
720                    .into_payload()?;
721                assert_eq!(payload.item.id_str, "1110902525317349376");
722            }
723        }
724
725        for profile in ["anonymous", "normal", "vip"] {
726            if let Some(body) = local_probe_body("lottery-notice", profile) {
727                let payload = serde_json::from_value::<ApiEnvelope<DynamicLotteryData>>(body)?
728                    .into_payload()?;
729                assert_eq!(payload.business_id, 969916293954142214);
730            }
731        }
732        Ok(())
733    }
734}