use axum::{Json, http::StatusCode, response::IntoResponse};
use crate::{
alert::{AlertDefinition, TicketSource},
http_server::types::{ValidationInfo, ValidationResponse},
};
pub async fn handle_validate(body: String) -> impl IntoResponse {
let deserializer = serde_yaml::Deserializer::from_str(&body);
let alert: AlertDefinition = match serde_path_to_error::deserialize(deserializer) {
Ok(alert) => alert,
Err(err) => {
let path = err.path().to_string();
let inner = err.into_inner();
let error_msg = format!("{}", inner);
let response = ValidationResponse {
valid: false,
error: Some(format!("Parse error at '{}': {}", path, error_msg)),
error_location: None, info: None,
};
return (StatusCode::OK, Json(response)).into_response();
}
};
if let Err(err) = validate_templates(&alert) {
let response = ValidationResponse {
valid: false,
error: Some(format!("Template validation error: {:#}", err)),
error_location: None,
info: None,
};
return (StatusCode::OK, Json(response)).into_response();
}
let external_targets = std::collections::HashMap::new();
match alert.normalise(&external_targets) {
Ok((alert, resolved_targets)) => {
let source_type = match &alert.source {
TicketSource::Sql { .. } => "sql",
TicketSource::Shell { .. } => "shell",
TicketSource::Event { .. } => "event",
TicketSource::None => "none",
}
.to_string();
let response = ValidationResponse {
valid: true,
error: None,
error_location: None,
info: Some(ValidationInfo {
enabled: alert.enabled,
interval: alert.interval.clone(),
source_type,
targets: resolved_targets.len(),
}),
};
(StatusCode::OK, Json(response)).into_response()
}
Err(err) => {
let response = ValidationResponse {
valid: false,
error: Some(format!("Validation error: {:#}", err)),
error_location: None,
info: None,
};
(StatusCode::OK, Json(response)).into_response()
}
}
}
fn validate_templates(alert: &AlertDefinition) -> miette::Result<()> {
use crate::templates;
use miette::Context as _;
for (idx, target) in alert.send.iter().enumerate() {
templates::load_templates(target.subject(), target.template())
.wrap_err_with(|| format!("validating templates for send target #{}", idx + 1))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use axum::{http::StatusCode, response::IntoResponse};
use super::*;
#[tokio::test]
async fn test_validate_valid_sql_alert() {
let yaml = r#"
sql: "SELECT 1"
send:
- id: test
subject: Test
template: Test
"#;
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(validation.valid);
assert!(validation.info.is_some());
}
#[tokio::test]
async fn test_validate_valid_shell_alert() {
let yaml = r#"
shell: uptime
run: uptime
send:
- id: test
subject: Test
template: Test
"#;
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(validation.valid);
assert!(validation.info.is_some());
}
#[tokio::test]
async fn test_validate_event_alert() {
let yaml = r#"
event: source-error
send:
- id: test
subject: Test
template: Test
"#;
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(validation.valid);
assert!(validation.info.is_some());
}
#[tokio::test]
async fn test_validate_invalid_yaml() {
let yaml = "this is: not: valid: yaml:";
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(!validation.valid);
assert!(validation.error.is_some());
}
#[tokio::test]
async fn test_validate_template_syntax_error() {
let yaml = r#"
sql: "SELECT 1"
send:
- id: test
subject: Test
template: "{{ unclosed tag"
"#;
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(!validation.valid);
assert!(validation.error.is_some());
assert!(validation.error.unwrap().contains("Template"));
}
#[tokio::test]
async fn test_validate_template_mismatched_tags() {
let yaml = r#"
sql: "SELECT 1"
send:
- id: test
subject: Test
template: "{% if foo %}bar"
"#;
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(!validation.valid);
assert!(validation.error.is_some());
}
#[tokio::test]
async fn test_validate_multiple_targets() {
let yaml = r#"
sql: "SELECT 1"
send:
- id: test1
subject: Test 1
template: Test 1
- id: test2
subject: Test 2
template: Test 2
"#;
let response = handle_validate(yaml.to_string()).await.into_response();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let validation: ValidationResponse = serde_json::from_slice(&body).unwrap();
assert!(validation.valid);
let info = validation.info.as_ref().unwrap();
assert_eq!(info.targets, 0);
}
}