Skip to main content

aster/auto_reply/
webhook.rs

1//! Webhook 触发处理模块
2//!
3//! 实现 Webhook 触发器的签名验证和请求解析功能。
4//!
5//! # 功能
6//!
7//! - HMAC-SHA256 签名验证
8//! - 请求体解析
9//! - 可配置端点路径
10//!
11//! # 示例
12//!
13//! ```rust,ignore
14//! use aster::auto_reply::webhook::{WebhookHandler, WebhookResult};
15//!
16//! let handler = WebhookHandler::new("my-secret".to_string(), "/webhook".to_string());
17//!
18//! // 验证并处理请求
19//! match handler.handle_request(body, signature) {
20//!     WebhookResult::Triggered { request } => {
21//!         println!("Webhook triggered: {}", request.content);
22//!     }
23//!     WebhookResult::InvalidSignature => {
24//!         println!("Invalid signature");
25//!     }
26//!     WebhookResult::ParseError(err) => {
27//!         println!("Parse error: {}", err);
28//!     }
29//! }
30//! ```
31
32use hmac::{Hmac, Mac};
33use serde::{Deserialize, Serialize};
34use sha2::Sha256;
35use std::collections::HashMap;
36
37/// HMAC-SHA256 类型别名
38type HmacSha256 = Hmac<Sha256>;
39
40/// Webhook 处理器
41///
42/// 负责验证 Webhook 请求签名和解析请求体。
43#[derive(Debug, Clone)]
44pub struct WebhookHandler {
45    /// 验证密钥
46    secret: String,
47    /// 端点路径
48    path: String,
49}
50
51impl WebhookHandler {
52    /// 创建新的 Webhook 处理器
53    ///
54    /// # 参数
55    ///
56    /// * `secret` - 用于签名验证的密钥
57    /// * `path` - Webhook 端点路径
58    ///
59    /// # 示例
60    ///
61    /// ```rust
62    /// use aster::auto_reply::webhook::WebhookHandler;
63    ///
64    /// let handler = WebhookHandler::new(
65    ///     "my-secret".to_string(),
66    ///     "/webhook/auto-reply".to_string(),
67    /// );
68    /// ```
69    pub fn new(secret: String, path: String) -> Self {
70        Self { secret, path }
71    }
72
73    /// 获取端点路径
74    pub fn path(&self) -> &str {
75        &self.path
76    }
77
78    /// 验证请求签名
79    ///
80    /// 使用 HMAC-SHA256 验证签名。签名格式支持:
81    /// - 纯 hex 字符串
82    /// - `sha256=<hex>` 格式(GitHub 风格)
83    ///
84    /// # 参数
85    ///
86    /// * `payload` - 请求体原始字节
87    /// * `signature` - 请求头中的签名
88    ///
89    /// # 返回
90    ///
91    /// 签名验证通过返回 `true`,否则返回 `false`
92    ///
93    /// # Requirements
94    ///
95    /// - 9.1: THE Webhook_Trigger SHALL validate request signature using secret
96    /// - 9.2: WHEN signature validation fails, THE Webhook_Trigger SHALL reject the request
97    pub fn verify_signature(&self, payload: &[u8], signature: &str) -> bool {
98        // 支持 "sha256=<hex>" 格式(GitHub 风格)
99        let signature_hex = signature.strip_prefix("sha256=").unwrap_or(signature);
100
101        // 解码签名
102        let expected_signature = match hex::decode(signature_hex) {
103            Ok(sig) => sig,
104            Err(_) => return false,
105        };
106
107        // 创建 HMAC 实例
108        let mut mac = match HmacSha256::new_from_slice(self.secret.as_bytes()) {
109            Ok(mac) => mac,
110            Err(_) => return false,
111        };
112
113        // 计算 HMAC
114        mac.update(payload);
115
116        // 使用常量时间比较验证签名
117        mac.verify_slice(&expected_signature).is_ok()
118    }
119
120    /// 解析请求体
121    ///
122    /// 将 JSON 请求体解析为 `WebhookRequest` 结构体。
123    ///
124    /// # 参数
125    ///
126    /// * `body` - 请求体原始字节
127    ///
128    /// # 返回
129    ///
130    /// 解析成功返回 `Ok(WebhookRequest)`,失败返回错误信息
131    ///
132    /// # Requirements
133    ///
134    /// - 9.3: THE Webhook_Trigger SHALL extract message content from request body
135    pub fn parse_request(&self, body: &[u8]) -> Result<WebhookRequest, String> {
136        serde_json::from_slice(body).map_err(|e| format!("Failed to parse request body: {}", e))
137    }
138
139    /// 处理 Webhook 请求
140    ///
141    /// 验证签名并解析请求体,返回处理结果。
142    ///
143    /// # 参数
144    ///
145    /// * `body` - 请求体原始字节
146    /// * `signature` - 请求头中的签名
147    ///
148    /// # 返回
149    ///
150    /// 返回 `WebhookResult` 表示处理结果
151    ///
152    /// # Requirements
153    ///
154    /// - 9.1: THE Webhook_Trigger SHALL validate request signature using secret
155    /// - 9.2: WHEN signature validation fails, THE Webhook_Trigger SHALL reject the request
156    /// - 9.3: THE Webhook_Trigger SHALL extract message content from request body
157    /// - 9.5: THE Webhook_Trigger SHALL return trigger result in response
158    pub fn handle_request(&self, body: &[u8], signature: &str) -> WebhookResult {
159        // 验证签名
160        if !self.verify_signature(body, signature) {
161            return WebhookResult::InvalidSignature;
162        }
163
164        // 解析请求体
165        match self.parse_request(body) {
166            Ok(request) => WebhookResult::Triggered { request },
167            Err(err) => WebhookResult::ParseError(err),
168        }
169    }
170
171    /// 计算 payload 的签名
172    ///
173    /// 用于生成测试签名或客户端签名。
174    ///
175    /// # 参数
176    ///
177    /// * `payload` - 要签名的数据
178    ///
179    /// # 返回
180    ///
181    /// 返回 hex 编码的签名字符串
182    pub fn compute_signature(&self, payload: &[u8]) -> String {
183        let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes())
184            .expect("HMAC can take key of any size");
185        mac.update(payload);
186        let result = mac.finalize();
187        hex::encode(result.into_bytes())
188    }
189}
190
191/// Webhook 请求体
192///
193/// 表示从 Webhook 请求中解析出的数据。
194///
195/// # Requirements
196///
197/// - 9.3: THE Webhook_Trigger SHALL extract message content from request body
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199pub struct WebhookRequest {
200    /// 消息内容
201    pub content: String,
202    /// 发送者 ID(可选)
203    #[serde(default)]
204    pub sender_id: Option<String>,
205    /// 附加数据
206    #[serde(default)]
207    pub metadata: HashMap<String, serde_json::Value>,
208}
209
210impl WebhookRequest {
211    /// 创建新的 Webhook 请求
212    pub fn new(content: String) -> Self {
213        Self {
214            content,
215            sender_id: None,
216            metadata: HashMap::new(),
217        }
218    }
219
220    /// 设置发送者 ID
221    pub fn with_sender_id(mut self, sender_id: String) -> Self {
222        self.sender_id = Some(sender_id);
223        self
224    }
225
226    /// 添加元数据
227    pub fn with_metadata(mut self, key: String, value: serde_json::Value) -> Self {
228        self.metadata.insert(key, value);
229        self
230    }
231}
232
233/// Webhook 处理结果
234///
235/// 表示 Webhook 请求的处理结果。
236///
237/// # Requirements
238///
239/// - 9.2: WHEN signature validation fails, THE Webhook_Trigger SHALL reject the request
240/// - 9.5: THE Webhook_Trigger SHALL return trigger result in response
241#[derive(Debug, Clone, PartialEq)]
242pub enum WebhookResult {
243    /// 成功触发
244    Triggered {
245        /// 解析后的请求
246        request: WebhookRequest,
247    },
248    /// 签名验证失败
249    InvalidSignature,
250    /// 请求体解析失败
251    ParseError(String),
252}
253
254impl WebhookResult {
255    /// 检查是否触发成功
256    pub fn is_triggered(&self) -> bool {
257        matches!(self, WebhookResult::Triggered { .. })
258    }
259
260    /// 检查是否签名无效
261    pub fn is_invalid_signature(&self) -> bool {
262        matches!(self, WebhookResult::InvalidSignature)
263    }
264
265    /// 检查是否解析错误
266    pub fn is_parse_error(&self) -> bool {
267        matches!(self, WebhookResult::ParseError(_))
268    }
269
270    /// 获取触发的请求(如果成功)
271    pub fn into_request(self) -> Option<WebhookRequest> {
272        match self {
273            WebhookResult::Triggered { request } => Some(request),
274            _ => None,
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use proptest::prelude::*;
283
284    /// 创建测试用的 WebhookHandler
285    fn create_test_handler() -> WebhookHandler {
286        WebhookHandler::new("test-secret".to_string(), "/webhook/test".to_string())
287    }
288
289    /// 创建有效的测试请求体
290    fn create_test_body() -> Vec<u8> {
291        r#"{"content":"Hello, World!","sender_id":"user-123"}"#
292            .as_bytes()
293            .to_vec()
294    }
295
296    // ==================== 签名验证测试 ====================
297
298    /// 测试:有效签名应该通过验证
299    /// Validates: Requirement 9.1
300    #[test]
301    fn test_verify_signature_valid() {
302        let handler = create_test_handler();
303        let body = create_test_body();
304
305        // 计算正确的签名
306        let signature = handler.compute_signature(&body);
307
308        assert!(handler.verify_signature(&body, &signature));
309    }
310
311    /// 测试:带 sha256= 前缀的签名应该通过验证
312    /// Validates: Requirement 9.1
313    #[test]
314    fn test_verify_signature_with_prefix() {
315        let handler = create_test_handler();
316        let body = create_test_body();
317
318        let signature = handler.compute_signature(&body);
319        let prefixed_signature = format!("sha256={}", signature);
320
321        assert!(handler.verify_signature(&body, &prefixed_signature));
322    }
323
324    /// 测试:无效签名应该被拒绝
325    /// Validates: Requirement 9.2
326    #[test]
327    fn test_verify_signature_invalid() {
328        let handler = create_test_handler();
329        let body = create_test_body();
330
331        // 使用错误的签名
332        let invalid_signature = "0000000000000000000000000000000000000000000000000000000000000000";
333
334        assert!(!handler.verify_signature(&body, invalid_signature));
335    }
336
337    /// 测试:非 hex 格式的签名应该被拒绝
338    /// Validates: Requirement 9.2
339    #[test]
340    fn test_verify_signature_invalid_hex() {
341        let handler = create_test_handler();
342        let body = create_test_body();
343
344        // 非 hex 格式
345        assert!(!handler.verify_signature(&body, "not-a-hex-string"));
346        assert!(!handler.verify_signature(&body, "zzzz"));
347    }
348
349    /// 测试:空签名应该被拒绝
350    /// Validates: Requirement 9.2
351    #[test]
352    fn test_verify_signature_empty() {
353        let handler = create_test_handler();
354        let body = create_test_body();
355
356        assert!(!handler.verify_signature(&body, ""));
357    }
358
359    /// 测试:修改后的 payload 签名应该失败
360    /// Validates: Requirement 9.1, 9.2
361    #[test]
362    fn test_verify_signature_tampered_payload() {
363        let handler = create_test_handler();
364        let body = create_test_body();
365
366        // 计算原始 body 的签名
367        let signature = handler.compute_signature(&body);
368
369        // 修改 body
370        let tampered_body = r#"{"content":"Tampered!","sender_id":"user-123"}"#.as_bytes();
371
372        // 使用原始签名验证修改后的 body 应该失败
373        assert!(!handler.verify_signature(tampered_body, &signature));
374    }
375
376    // ==================== 请求解析测试 ====================
377
378    /// 测试:解析有效的请求体
379    /// Validates: Requirement 9.3
380    #[test]
381    fn test_parse_request_valid() {
382        let handler = create_test_handler();
383        let body = create_test_body();
384
385        let result = handler.parse_request(&body);
386        assert!(result.is_ok());
387
388        let request = result.unwrap();
389        assert_eq!(request.content, "Hello, World!");
390        assert_eq!(request.sender_id, Some("user-123".to_string()));
391    }
392
393    /// 测试:解析只有 content 的请求体
394    /// Validates: Requirement 9.3
395    #[test]
396    fn test_parse_request_minimal() {
397        let handler = create_test_handler();
398        let body = r#"{"content":"Minimal message"}"#.as_bytes();
399
400        let result = handler.parse_request(body);
401        assert!(result.is_ok());
402
403        let request = result.unwrap();
404        assert_eq!(request.content, "Minimal message");
405        assert_eq!(request.sender_id, None);
406        assert!(request.metadata.is_empty());
407    }
408
409    /// 测试:解析带 metadata 的请求体
410    /// Validates: Requirement 9.3
411    #[test]
412    fn test_parse_request_with_metadata() {
413        let handler = create_test_handler();
414        let body = r#"{
415            "content": "Message with metadata",
416            "sender_id": "user-456",
417            "metadata": {
418                "source": "github",
419                "priority": 1
420            }
421        }"#
422        .as_bytes();
423
424        let result = handler.parse_request(body);
425        assert!(result.is_ok());
426
427        let request = result.unwrap();
428        assert_eq!(request.content, "Message with metadata");
429        assert_eq!(request.sender_id, Some("user-456".to_string()));
430        assert_eq!(
431            request.metadata.get("source"),
432            Some(&serde_json::json!("github"))
433        );
434        assert_eq!(
435            request.metadata.get("priority"),
436            Some(&serde_json::json!(1))
437        );
438    }
439
440    /// 测试:解析无效 JSON 应该失败
441    #[test]
442    fn test_parse_request_invalid_json() {
443        let handler = create_test_handler();
444        let body = b"not valid json";
445
446        let result = handler.parse_request(body);
447        assert!(result.is_err());
448    }
449
450    /// 测试:解析缺少 content 字段应该失败
451    #[test]
452    fn test_parse_request_missing_content() {
453        let handler = create_test_handler();
454        let body = r#"{"sender_id":"user-123"}"#.as_bytes();
455
456        let result = handler.parse_request(body);
457        assert!(result.is_err());
458    }
459
460    // ==================== handle_request 集成测试 ====================
461
462    /// 测试:有效请求应该成功触发
463    /// Validates: Requirements 9.1, 9.3, 9.5
464    #[test]
465    fn test_handle_request_success() {
466        let handler = create_test_handler();
467        let body = create_test_body();
468        let signature = handler.compute_signature(&body);
469
470        let result = handler.handle_request(&body, &signature);
471
472        assert!(result.is_triggered());
473        let request = result.into_request().unwrap();
474        assert_eq!(request.content, "Hello, World!");
475    }
476
477    /// 测试:无效签名应该返回 InvalidSignature
478    /// Validates: Requirements 9.2, 9.5
479    #[test]
480    fn test_handle_request_invalid_signature() {
481        let handler = create_test_handler();
482        let body = create_test_body();
483        let invalid_signature = "invalid";
484
485        let result = handler.handle_request(&body, invalid_signature);
486
487        assert!(result.is_invalid_signature());
488        assert_eq!(result, WebhookResult::InvalidSignature);
489    }
490
491    /// 测试:有效签名但无效 JSON 应该返回 ParseError
492    /// Validates: Requirements 9.1, 9.5
493    #[test]
494    fn test_handle_request_parse_error() {
495        let handler = create_test_handler();
496        let body = b"not valid json";
497        let signature = handler.compute_signature(body);
498
499        let result = handler.handle_request(body, &signature);
500
501        assert!(result.is_parse_error());
502    }
503
504    // ==================== WebhookRequest 测试 ====================
505
506    /// 测试:WebhookRequest builder 模式
507    #[test]
508    fn test_webhook_request_builder() {
509        let request = WebhookRequest::new("Test content".to_string())
510            .with_sender_id("sender-1".to_string())
511            .with_metadata("key".to_string(), serde_json::json!("value"));
512
513        assert_eq!(request.content, "Test content");
514        assert_eq!(request.sender_id, Some("sender-1".to_string()));
515        assert_eq!(
516            request.metadata.get("key"),
517            Some(&serde_json::json!("value"))
518        );
519    }
520
521    /// 测试:WebhookRequest 序列化/反序列化
522    #[test]
523    fn test_webhook_request_serde() {
524        let request = WebhookRequest::new("Test".to_string()).with_sender_id("user".to_string());
525
526        let json = serde_json::to_string(&request).unwrap();
527        let parsed: WebhookRequest = serde_json::from_str(&json).unwrap();
528
529        assert_eq!(request, parsed);
530    }
531
532    // ==================== 端点路径测试 ====================
533
534    /// 测试:可配置端点路径
535    /// Validates: Requirement 9.4
536    #[test]
537    fn test_configurable_path() {
538        let handler1 = WebhookHandler::new("secret".to_string(), "/api/webhook".to_string());
539        let handler2 = WebhookHandler::new("secret".to_string(), "/custom/path".to_string());
540
541        assert_eq!(handler1.path(), "/api/webhook");
542        assert_eq!(handler2.path(), "/custom/path");
543    }
544
545    // ==================== 不同密钥测试 ====================
546
547    /// 测试:不同密钥产生不同签名
548    #[test]
549    fn test_different_secrets_different_signatures() {
550        let handler1 = WebhookHandler::new("secret1".to_string(), "/webhook".to_string());
551        let handler2 = WebhookHandler::new("secret2".to_string(), "/webhook".to_string());
552        let body = create_test_body();
553
554        let sig1 = handler1.compute_signature(&body);
555        let sig2 = handler2.compute_signature(&body);
556
557        assert_ne!(sig1, sig2);
558
559        // handler1 的签名不能通过 handler2 的验证
560        assert!(!handler2.verify_signature(&body, &sig1));
561    }
562
563    // =========================================================================
564    // Property-Based Tests - Property 9: Webhook 签名验证
565    // =========================================================================
566
567    /// 生成随机 secret 字符串
568    fn arb_secret() -> impl Strategy<Value = String> {
569        // 生成 8-64 字符的 ASCII 字符串作为 secret
570        prop::string::string_regex("[a-zA-Z0-9_-]{8,64}")
571            .unwrap()
572            .prop_filter("Secret must not be empty", |s| !s.is_empty())
573    }
574
575    /// 生成随机 payload 字节
576    fn arb_payload() -> impl Strategy<Value = Vec<u8>> {
577        // 生成 1-1024 字节的随机数据
578        prop::collection::vec(any::<u8>(), 1..1024)
579    }
580
581    /// 生成随机 WebhookRequest 内容
582    fn arb_content() -> impl Strategy<Value = String> {
583        // 生成 1-256 字符的 ASCII 字符串作为消息内容
584        prop::string::string_regex("[a-zA-Z0-9 .,!?]{1,256}")
585            .unwrap()
586            .prop_filter("Content must not be empty", |s| !s.is_empty())
587    }
588
589    /// 生成随机 sender_id
590    fn arb_sender_id() -> impl Strategy<Value = Option<String>> {
591        prop::option::of(prop::string::string_regex("[a-zA-Z0-9_-]{1,32}").unwrap())
592    }
593
594    /// 生成随机 WebhookRequest
595    fn arb_webhook_request() -> impl Strategy<Value = WebhookRequest> {
596        (arb_content(), arb_sender_id()).prop_map(|(content, sender_id)| {
597            let mut request = WebhookRequest::new(content);
598            if let Some(id) = sender_id {
599                request = request.with_sender_id(id);
600            }
601            request
602        })
603    }
604
605    /// 生成随机端点路径
606    fn arb_path() -> impl Strategy<Value = String> {
607        prop::string::string_regex("/[a-z0-9/_-]{1,64}")
608            .unwrap()
609            .prop_filter("Path must start with /", |s| s.starts_with('/'))
610    }
611
612    /// 生成篡改后的 payload(确保与原始不同)
613    fn arb_tampered_payload(original: &[u8]) -> impl Strategy<Value = Vec<u8>> {
614        let original_len = original.len();
615        let original_clone = original.to_vec();
616
617        prop::strategy::Union::new_weighted(vec![
618            // 策略 1: 修改一个字节
619            (3, {
620                let orig = original_clone.clone();
621                any::<prop::sample::Index>()
622                    .prop_flat_map(move |idx| {
623                        let orig = orig.clone();
624                        let pos = idx.index(orig.len().max(1));
625                        any::<u8>().prop_map(move |new_byte| {
626                            let mut result = orig.clone();
627                            if !result.is_empty() {
628                                // 确保修改后的字节与原始不同
629                                result[pos] = if result[pos] == new_byte {
630                                    new_byte.wrapping_add(1)
631                                } else {
632                                    new_byte
633                                };
634                            }
635                            result
636                        })
637                    })
638                    .boxed()
639            }),
640            // 策略 2: 添加字节
641            (2, {
642                let orig = original_clone.clone();
643                prop::collection::vec(any::<u8>(), 1..10)
644                    .prop_map(move |extra| {
645                        let mut result = orig.clone();
646                        result.extend(extra);
647                        result
648                    })
649                    .boxed()
650            }),
651            // 策略 3: 删除字节(如果长度 > 1)
652            (1, {
653                let orig = original_clone.clone();
654                if original_len > 1 {
655                    any::<prop::sample::Index>()
656                        .prop_map(move |idx| {
657                            let mut result = orig.clone();
658                            let pos = idx.index(result.len());
659                            result.remove(pos);
660                            result
661                        })
662                        .boxed()
663                } else {
664                    // 如果只有一个字节,添加一个字节
665                    any::<u8>()
666                        .prop_map(move |extra| {
667                            let mut result = orig.clone();
668                            result.push(extra);
669                            result
670                        })
671                        .boxed()
672                }
673            }),
674        ])
675    }
676
677    /// 生成无效的签名(非 hex 或错误的 hex)
678    fn arb_invalid_signature() -> impl Strategy<Value = String> {
679        prop::strategy::Union::new_weighted(vec![
680            // 策略 1: 非 hex 字符串
681            (
682                3,
683                prop::string::string_regex("[g-z]{32,64}").unwrap().boxed(),
684            ),
685            // 策略 2: 空字符串
686            (1, Just("".to_string()).boxed()),
687            // 策略 3: 太短的 hex
688            (
689                2,
690                prop::string::string_regex("[0-9a-f]{1,10}")
691                    .unwrap()
692                    .boxed(),
693            ),
694            // 策略 4: 包含非 hex 字符
695            (
696                2,
697                prop::string::string_regex("[0-9a-f]{20}[xyz]{5}[0-9a-f]{20}")
698                    .unwrap()
699                    .boxed(),
700            ),
701        ])
702    }
703
704    proptest! {
705        #![proptest_config(ProptestConfig::with_cases(100))]
706
707        /// Property 9.1: 有效签名始终通过验证
708        /// **Validates: Requirement 9.1**
709        ///
710        /// THE Webhook_Trigger SHALL validate request signature using secret
711        /// 对于任意 secret 和 payload,使用正确的签名应该始终通过验证
712        #[test]
713        fn prop_valid_signature_always_passes(
714            secret in arb_secret(),
715            payload in arb_payload(),
716            path in arb_path()
717        ) {
718            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
719            // **Validates: Requirements 9.1, 9.2**
720
721            let handler = WebhookHandler::new(secret, path);
722
723            // 计算正确的签名
724            let signature = handler.compute_signature(&payload);
725
726            // 验证签名应该通过
727            prop_assert!(
728                handler.verify_signature(&payload, &signature),
729                "Valid signature should always pass verification"
730            );
731        }
732
733        /// Property 9.2: 有效签名带 sha256= 前缀也通过验证
734        /// **Validates: Requirement 9.1**
735        ///
736        /// 支持 GitHub 风格的签名格式
737        #[test]
738        fn prop_valid_signature_with_prefix_passes(
739            secret in arb_secret(),
740            payload in arb_payload(),
741            path in arb_path()
742        ) {
743            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
744            // **Validates: Requirements 9.1, 9.2**
745
746            let handler = WebhookHandler::new(secret, path);
747
748            // 计算正确的签名并添加前缀
749            let signature = handler.compute_signature(&payload);
750            let prefixed_signature = format!("sha256={}", signature);
751
752            // 验证带前缀的签名应该通过
753            prop_assert!(
754                handler.verify_signature(&payload, &prefixed_signature),
755                "Valid signature with sha256= prefix should pass verification"
756            );
757        }
758
759        /// Property 9.3: 无效/篡改的签名始终被拒绝
760        /// **Validates: Requirement 9.2**
761        ///
762        /// WHEN signature validation fails, THE Webhook_Trigger SHALL reject the request
763        #[test]
764        fn prop_invalid_signature_always_fails(
765            secret in arb_secret(),
766            payload in arb_payload(),
767            path in arb_path(),
768            invalid_sig in arb_invalid_signature()
769        ) {
770            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
771            // **Validates: Requirements 9.1, 9.2**
772
773            let handler = WebhookHandler::new(secret, path);
774
775            // 无效签名应该被拒绝
776            prop_assert!(
777                !handler.verify_signature(&payload, &invalid_sig),
778                "Invalid signature '{}' should be rejected",
779                invalid_sig
780            );
781        }
782
783        /// Property 9.4: 篡改后的 payload 使用原始签名会失败
784        /// **Validates: Requirements 9.1, 9.2**
785        ///
786        /// 确保签名与 payload 绑定,任何修改都会导致验证失败
787        #[test]
788        fn prop_tampered_payload_fails_verification(
789            secret in arb_secret(),
790            payload in arb_payload().prop_filter("Need non-empty payload", |p| !p.is_empty()),
791            path in arb_path()
792        ) {
793            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
794            // **Validates: Requirements 9.1, 9.2**
795
796            let handler = WebhookHandler::new(secret, path);
797
798            // 计算原始 payload 的签名
799            let signature = handler.compute_signature(&payload);
800
801            // 生成篡改后的 payload
802            let tampered = arb_tampered_payload(&payload);
803
804            // 使用 proptest runner 测试篡改后的 payload
805            proptest!(|(tampered_payload in tampered)| {
806                // 只有当篡改后的 payload 与原始不同时才测试
807                if tampered_payload != payload {
808                    prop_assert!(
809                        !handler.verify_signature(&tampered_payload, &signature),
810                        "Tampered payload should fail verification with original signature"
811                    );
812                }
813            });
814        }
815
816        /// Property 9.5: 不同 secret 产生不同签名
817        /// **Validates: Requirement 9.1**
818        ///
819        /// 确保不同的 secret 会产生不同的签名,防止跨账户攻击
820        #[test]
821        fn prop_different_secrets_produce_different_signatures(
822            secret1 in arb_secret(),
823            secret2 in arb_secret().prop_filter("Secrets must be different", |s| !s.is_empty()),
824            payload in arb_payload(),
825            path in arb_path()
826        ) {
827            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
828            // **Validates: Requirements 9.1, 9.2**
829
830            // 只有当两个 secret 不同时才测试
831            prop_assume!(secret1 != secret2);
832
833            let handler1 = WebhookHandler::new(secret1, path.clone());
834            let handler2 = WebhookHandler::new(secret2, path);
835
836            let sig1 = handler1.compute_signature(&payload);
837            let sig2 = handler2.compute_signature(&payload);
838
839            // 不同 secret 应该产生不同签名
840            prop_assert_ne!(
841                &sig1, &sig2,
842                "Different secrets should produce different signatures"
843            );
844
845            // handler1 的签名不能通过 handler2 的验证
846            prop_assert!(
847                !handler2.verify_signature(&payload, &sig1),
848                "Signature from secret1 should not pass verification with secret2"
849            );
850
851            // handler2 的签名不能通过 handler1 的验证
852            prop_assert!(
853                !handler1.verify_signature(&payload, &sig2),
854                "Signature from secret2 should not pass verification with secret1"
855            );
856        }
857
858        /// Property 9.6: 相同 payload + 相同 secret = 相同签名(确定性)
859        /// **Validates: Requirement 9.1**
860        ///
861        /// 签名算法应该是确定性的,相同输入产生相同输出
862        #[test]
863        fn prop_same_payload_same_secret_same_signature(
864            secret in arb_secret(),
865            payload in arb_payload(),
866            path in arb_path()
867        ) {
868            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
869            // **Validates: Requirements 9.1, 9.2**
870
871            let handler = WebhookHandler::new(secret, path);
872
873            // 多次计算签名
874            let sig1 = handler.compute_signature(&payload);
875            let sig2 = handler.compute_signature(&payload);
876            let sig3 = handler.compute_signature(&payload);
877
878            // 所有签名应该相同
879            prop_assert_eq!(
880                &sig1, &sig2,
881                "Same payload and secret should produce same signature (1 vs 2)"
882            );
883            prop_assert_eq!(
884                &sig2, &sig3,
885                "Same payload and secret should produce same signature (2 vs 3)"
886            );
887        }
888
889        /// Property 9.7: handle_request 正确处理有效请求
890        /// **Validates: Requirements 9.1, 9.3, 9.5**
891        ///
892        /// 完整的请求处理流程:签名验证 + 请求解析
893        #[test]
894        fn prop_handle_request_with_valid_signature_succeeds(
895            secret in arb_secret(),
896            request in arb_webhook_request(),
897            path in arb_path()
898        ) {
899            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
900            // **Validates: Requirements 9.1, 9.2, 9.3, 9.5**
901
902            let handler = WebhookHandler::new(secret, path);
903
904            // 序列化请求为 JSON
905            let body = serde_json::to_vec(&request).unwrap();
906
907            // 计算正确的签名
908            let signature = handler.compute_signature(&body);
909
910            // 处理请求
911            let result = handler.handle_request(&body, &signature);
912
913            // 应该成功触发
914            prop_assert!(
915                result.is_triggered(),
916                "Valid request with valid signature should trigger"
917            );
918
919            // 解析后的请求应该与原始请求一致
920            if let WebhookResult::Triggered { request: parsed } = result {
921                prop_assert_eq!(
922                    parsed.content, request.content,
923                    "Parsed content should match original"
924                );
925                prop_assert_eq!(
926                    parsed.sender_id, request.sender_id,
927                    "Parsed sender_id should match original"
928                );
929            }
930        }
931
932        /// Property 9.8: handle_request 拒绝无效签名
933        /// **Validates: Requirements 9.2, 9.5**
934        ///
935        /// 无效签名应该导致请求被拒绝
936        #[test]
937        fn prop_handle_request_with_invalid_signature_fails(
938            secret in arb_secret(),
939            request in arb_webhook_request(),
940            path in arb_path(),
941            invalid_sig in arb_invalid_signature()
942        ) {
943            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
944            // **Validates: Requirements 9.1, 9.2, 9.5**
945
946            let handler = WebhookHandler::new(secret, path);
947
948            // 序列化请求为 JSON
949            let body = serde_json::to_vec(&request).unwrap();
950
951            // 使用无效签名处理请求
952            let result = handler.handle_request(&body, &invalid_sig);
953
954            // 应该返回 InvalidSignature
955            prop_assert!(
956                result.is_invalid_signature(),
957                "Request with invalid signature should return InvalidSignature"
958            );
959        }
960
961        /// Property 9.9: 签名长度固定为 64 字符(SHA256 hex)
962        /// **Validates: Requirement 9.1**
963        ///
964        /// HMAC-SHA256 产生 32 字节 = 64 hex 字符的签名
965        #[test]
966        fn prop_signature_length_is_fixed(
967            secret in arb_secret(),
968            payload in arb_payload(),
969            path in arb_path()
970        ) {
971            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
972            // **Validates: Requirements 9.1, 9.2**
973
974            let handler = WebhookHandler::new(secret, path);
975            let signature = handler.compute_signature(&payload);
976
977            // SHA256 产生 32 字节 = 64 hex 字符
978            prop_assert_eq!(
979                signature.len(), 64,
980                "Signature should be 64 hex characters (SHA256), got {} characters",
981                signature.len()
982            );
983
984            // 验证是有效的 hex 字符串
985            prop_assert!(
986                signature.chars().all(|c| c.is_ascii_hexdigit()),
987                "Signature should only contain hex characters"
988            );
989        }
990
991        /// Property 9.10: 空 payload 也能正确签名和验证
992        /// **Validates: Requirement 9.1**
993        ///
994        /// 边界情况:空 payload 应该能正常处理
995        #[test]
996        fn prop_empty_payload_signature_works(
997            secret in arb_secret(),
998            path in arb_path()
999        ) {
1000            // Feature: auto-reply-mechanism, Property 9: Webhook 签名验证
1001            // **Validates: Requirements 9.1, 9.2**
1002
1003            let handler = WebhookHandler::new(secret, path);
1004            let empty_payload: &[u8] = &[];
1005
1006            // 计算空 payload 的签名
1007            let signature = handler.compute_signature(empty_payload);
1008
1009            // 签名应该是有效的 64 字符 hex
1010            prop_assert_eq!(signature.len(), 64);
1011
1012            // 验证应该通过
1013            prop_assert!(
1014                handler.verify_signature(empty_payload, &signature),
1015                "Empty payload signature should verify correctly"
1016            );
1017        }
1018    }
1019}