worldinterface_http_trigger/
validation.rs1use crate::error::WebhookError;
4
5pub fn validate_webhook_path(path: &str) -> Result<(), WebhookError> {
14 if path.is_empty() {
15 return Err(WebhookError::InvalidPath("path must not be empty".into()));
16 }
17 if path.starts_with('/') || path.ends_with('/') {
18 return Err(WebhookError::InvalidPath("path must not start or end with '/'".into()));
19 }
20 if path.contains("..") {
21 return Err(WebhookError::InvalidPath("path must not contain '..'".into()));
22 }
23 if path.len() > 256 {
24 return Err(WebhookError::InvalidPath("path must be 256 characters or fewer".into()));
25 }
26 let valid = path
27 .chars()
28 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/');
29 if !valid {
30 return Err(WebhookError::InvalidPath(
31 "path contains invalid characters (allowed: alphanumeric, -, _, ., /)".into(),
32 ));
33 }
34 Ok(())
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 #[test]
42 fn valid_simple_path() {
43 assert!(validate_webhook_path("github/push").is_ok());
44 }
45
46 #[test]
47 fn valid_nested_path() {
48 assert!(validate_webhook_path("org/repo/branch").is_ok());
49 }
50
51 #[test]
52 fn valid_path_with_dots() {
53 assert!(validate_webhook_path("api.v1.hook").is_ok());
54 }
55
56 #[test]
57 fn valid_path_with_hyphens() {
58 assert!(validate_webhook_path("my-webhook").is_ok());
59 }
60
61 #[test]
62 fn valid_path_with_underscores() {
63 assert!(validate_webhook_path("my_webhook").is_ok());
64 }
65
66 #[test]
67 fn rejects_empty_path() {
68 let err = validate_webhook_path("").unwrap_err();
69 assert!(matches!(err, WebhookError::InvalidPath(_)));
70 }
71
72 #[test]
73 fn rejects_leading_slash() {
74 let err = validate_webhook_path("/github/push").unwrap_err();
75 assert!(matches!(err, WebhookError::InvalidPath(_)));
76 }
77
78 #[test]
79 fn rejects_trailing_slash() {
80 let err = validate_webhook_path("github/push/").unwrap_err();
81 assert!(matches!(err, WebhookError::InvalidPath(_)));
82 }
83
84 #[test]
85 fn rejects_directory_traversal() {
86 let err = validate_webhook_path("../secret").unwrap_err();
87 assert!(matches!(err, WebhookError::InvalidPath(_)));
88 }
89
90 #[test]
91 fn rejects_special_characters() {
92 let err = validate_webhook_path("hook?q=1").unwrap_err();
93 assert!(matches!(err, WebhookError::InvalidPath(_)));
94 }
95
96 #[test]
97 fn rejects_too_long_path() {
98 let long = "a".repeat(257);
99 let err = validate_webhook_path(&long).unwrap_err();
100 assert!(matches!(err, WebhookError::InvalidPath(_)));
101 }
102
103 #[test]
104 fn accepts_max_length_path() {
105 let max = "a".repeat(256);
106 assert!(validate_webhook_path(&max).is_ok());
107 }
108}