#[cfg(test)]
mod tests {
use crate::records::{AddRecordRequest, RemoveRecordRequest, UpdateRecordRequest};
#[test]
fn test_add_record_request_serialization() {
let request = AddRecordRequest {
name: "www".to_string(),
record_type: "A".to_string(),
value: "192.0.2.1".to_string(),
ttl: 3600,
priority: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"name\":\"www\""));
assert!(json.contains("\"type\":\"A\""));
assert!(json.contains("\"value\":\"192.0.2.1\""));
assert!(json.contains("\"ttl\":3600"));
}
#[test]
fn test_remove_record_request_serialization() {
let request = RemoveRecordRequest {
name: "www".to_string(),
record_type: "A".to_string(),
value: Some("192.0.2.1".to_string()),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"name\":\"www\""));
assert!(json.contains("\"type\":\"A\""));
assert!(json.contains("\"value\":\"192.0.2.1\""));
}
#[test]
fn test_remove_record_request_without_value() {
let request = RemoveRecordRequest {
name: "www".to_string(),
record_type: "A".to_string(),
value: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"name\":\"www\""));
assert!(json.contains("\"type\":\"A\""));
assert!(!json.contains("\"value\""));
}
#[test]
fn test_update_record_request_serialization() {
let request = UpdateRecordRequest {
name: "www".to_string(),
record_type: "A".to_string(),
current_value: "192.0.2.1".to_string(),
new_value: "192.0.2.2".to_string(),
ttl: 7200,
priority: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"name\":\"www\""));
assert!(json.contains("\"type\":\"A\""));
assert!(json.contains("\"currentValue\":\"192.0.2.1\""));
assert!(json.contains("\"newValue\":\"192.0.2.2\""));
assert!(json.contains("\"ttl\":7200"));
}
#[test]
fn test_add_record_request_with_priority() {
let request = AddRecordRequest {
name: "@".to_string(),
record_type: "MX".to_string(),
value: "mail.example.com.".to_string(),
ttl: 3600,
priority: Some(10),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"priority\":10"));
}
}
#[cfg(test)]
mod validation_security_tests {
use crate::records::{validate_record_name, validate_record_value};
#[test]
fn test_validate_record_value_rejects_control_chars_for_txt() {
for value in ["v=spf1\nupdate add evil", "a\rb", "x\0y"] {
assert!(
validate_record_value("TXT", value).is_err(),
"expected TXT value {value:?} to be rejected"
);
}
}
#[test]
fn test_validate_record_value_rejects_control_chars_for_caa_srv() {
assert!(validate_record_value("CAA", "0 issue \"ca\"\nx").is_err());
assert!(validate_record_value("SRV", "0 5 443 host.\ninject").is_err());
}
#[test]
fn test_validate_record_value_accepts_clean_txt() {
assert!(validate_record_value("TXT", "v=spf1 -all").is_ok());
assert!(validate_record_value("CAA", "0 issue \"letsencrypt.org\"").is_ok());
}
#[test]
fn test_validate_record_value_still_rejects_empty() {
assert!(validate_record_value("TXT", "").is_err());
}
#[test]
fn test_validate_record_name_rejects_control_chars() {
for name in ["www\nupdate add evil", "a\rb", "x\0y"] {
assert!(
validate_record_name(name).is_err(),
"expected name {name:?} to be rejected"
);
}
}
#[test]
fn test_validate_record_name_accepts_clean_names() {
for name in [
"www",
"@",
"sub.example.com.",
"_dmarc",
"*",
"*.wildcard",
"_sip._tcp",
"host-1",
] {
assert!(
validate_record_name(name).is_ok(),
"expected name {name:?} to be accepted"
);
}
}
#[test]
fn test_validate_record_name_rejects_zone_file_directives_without_control_chars() {
for name in [
"$INCLUDE /etc/bind/rndc.key ;",
"$GENERATE 1-16777215 host$",
"www example", "a;comment", "name\"quoted\"", "name(paren)", "$ORIGIN evil.", "", ] {
assert!(
validate_record_name(name).is_err(),
"expected name {name:?} to be rejected"
);
}
}
}