bestool_alertd/http_server/endpoints/
validate.rs1use axum::{Json, http::StatusCode, response::IntoResponse};
2
3use crate::{
4 alert::{AlertDefinition, TicketSource},
5 http_server::types::{ValidationInfo, ValidationResponse},
6};
7
8pub async fn handle_validate(body: String) -> impl IntoResponse {
9 let deserializer = serde_yaml::Deserializer::from_str(&body);
11 let alert: AlertDefinition = match serde_path_to_error::deserialize(deserializer) {
12 Ok(alert) => alert,
13 Err(err) => {
14 let path = err.path().to_string();
16 let inner = err.into_inner();
17 let error_msg = format!("{}", inner);
18
19 let response = ValidationResponse {
22 valid: false,
23 error: Some(format!("Parse error at '{}': {}", path, error_msg)),
24 error_location: None, info: None,
26 };
27
28 return (StatusCode::OK, Json(response)).into_response();
29 }
30 };
31
32 if let Err(err) = validate_templates(&alert) {
34 let response = ValidationResponse {
35 valid: false,
36 error: Some(format!("Template validation error: {:#}", err)),
37 error_location: None,
38 info: None,
39 };
40
41 return (StatusCode::OK, Json(response)).into_response();
42 }
43
44 let external_targets = std::collections::HashMap::new();
46 match alert.normalise(&external_targets) {
47 Ok((alert, resolved_targets)) => {
48 let source_type = match &alert.source {
49 TicketSource::Sql { .. } => "sql",
50 TicketSource::Shell { .. } => "shell",
51 TicketSource::Event { .. } => "event",
52 TicketSource::None => "none",
53 }
54 .to_string();
55
56 let response = ValidationResponse {
57 valid: true,
58 error: None,
59 error_location: None,
60 info: Some(ValidationInfo {
61 enabled: alert.enabled,
62 interval: alert.interval.clone(),
63 source_type,
64 targets: resolved_targets.len(),
65 }),
66 };
67
68 (StatusCode::OK, Json(response)).into_response()
69 }
70 Err(err) => {
71 let response = ValidationResponse {
73 valid: false,
74 error: Some(format!("Validation error: {:#}", err)),
75 error_location: None,
76 info: None,
77 };
78
79 (StatusCode::OK, Json(response)).into_response()
80 }
81 }
82}
83
84fn validate_templates(alert: &AlertDefinition) -> miette::Result<()> {
85 use crate::templates;
86 use miette::Context as _;
87
88 for (idx, target) in alert.send.iter().enumerate() {
93 templates::load_templates(target.subject(), target.template())
96 .wrap_err_with(|| format!("validating templates for send target #{}", idx + 1))?;
97 }
98
99 Ok(())
100}
101
102#[cfg(test)]
103mod tests {
104 use axum::{http::StatusCode, response::IntoResponse};
105
106 use super::*;
107
108 #[tokio::test]
109 async fn test_validate_valid_sql_alert() {
110 let yaml = r#"
111sql: "SELECT 1"
112send:
113 - id: test
114 subject: Test
115 template: Test
116"#;
117
118 let response = handle_validate(yaml.to_string()).await.into_response();
119
120 assert_eq!(response.status(), StatusCode::OK);
121 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
122 .await
123 .unwrap();
124 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
125
126 assert!(validation.valid);
127 assert!(validation.info.is_some());
128 }
129
130 #[tokio::test]
131 async fn test_validate_valid_shell_alert() {
132 let yaml = r#"
133shell: uptime
134run: uptime
135send:
136 - id: test
137 subject: Test
138 template: Test
139"#;
140
141 let response = handle_validate(yaml.to_string()).await.into_response();
142
143 assert_eq!(response.status(), StatusCode::OK);
144 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
145 .await
146 .unwrap();
147 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
148
149 assert!(validation.valid);
150 assert!(validation.info.is_some());
151 }
152
153 #[tokio::test]
154 async fn test_validate_event_alert() {
155 let yaml = r#"
156event: source-error
157send:
158 - id: test
159 subject: Test
160 template: Test
161"#;
162
163 let response = handle_validate(yaml.to_string()).await.into_response();
164
165 assert_eq!(response.status(), StatusCode::OK);
166 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
167 .await
168 .unwrap();
169 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
170
171 assert!(validation.valid);
172 assert!(validation.info.is_some());
173 }
174
175 #[tokio::test]
176 async fn test_validate_invalid_yaml() {
177 let yaml = "this is: not: valid: yaml:";
178
179 let response = handle_validate(yaml.to_string()).await.into_response();
180
181 assert_eq!(response.status(), StatusCode::OK);
182 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
183 .await
184 .unwrap();
185 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
186
187 assert!(!validation.valid);
188 assert!(validation.error.is_some());
189 }
190
191 #[tokio::test]
192 async fn test_validate_template_syntax_error() {
193 let yaml = r#"
194sql: "SELECT 1"
195send:
196 - id: test
197 subject: Test
198 template: "{{ unclosed tag"
199"#;
200
201 let response = handle_validate(yaml.to_string()).await.into_response();
202
203 assert_eq!(response.status(), StatusCode::OK);
204 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
205 .await
206 .unwrap();
207 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
208
209 assert!(!validation.valid);
210 assert!(validation.error.is_some());
211 assert!(validation.error.unwrap().contains("Template"));
212 }
213
214 #[tokio::test]
215 async fn test_validate_template_mismatched_tags() {
216 let yaml = r#"
217sql: "SELECT 1"
218send:
219 - id: test
220 subject: Test
221 template: "{% if foo %}bar"
222"#;
223
224 let response = handle_validate(yaml.to_string()).await.into_response();
225
226 assert_eq!(response.status(), StatusCode::OK);
227 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
228 .await
229 .unwrap();
230 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
231
232 assert!(!validation.valid);
233 assert!(validation.error.is_some());
234 }
235
236 #[tokio::test]
237 async fn test_validate_multiple_targets() {
238 let yaml = r#"
239sql: "SELECT 1"
240send:
241 - id: test1
242 subject: Test 1
243 template: Test 1
244 - id: test2
245 subject: Test 2
246 template: Test 2
247"#;
248
249 let response = handle_validate(yaml.to_string()).await.into_response();
250
251 assert_eq!(response.status(), StatusCode::OK);
252 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
253 .await
254 .unwrap();
255 let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
256
257 assert!(validation.valid);
258 let info = validation.info.as_ref().unwrap();
259 assert_eq!(info.targets, 0);
261 }
262}