use ddex_builder::{
builder::DDEXBuilder,
error::BuildError,
security::{OutputSanitizer, SecurityConfig},
};
use serde_json::json;
#[test]
fn test_build_input_validation() {
let builder = DDEXBuilder::new();
let null_byte_json = json!({
"messageHeader": {
"messageId": "MSG\0001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Test\0Song",
"artists": ["Artist\0Name"]
}]
});
let result = builder.validate_json_input(&null_byte_json.to_string());
assert!(
result.is_err(),
"Input with null bytes should be rejected: {:?}",
result
);
let long_string = "A".repeat(2_000_000); let long_string_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": long_string,
"artists": ["Artist"]
}]
});
let result = builder.validate_json_input(&long_string_json.to_string());
assert!(
result.is_err(),
"Input with excessively long strings should be rejected: {:?}",
result
);
let control_chars_json = json!({
"messageHeader": {
"messageId": "MSG\u{0001}\u{0002}\u{0003}",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Test\u{0008}Song\u{007F}",
"artists": ["Artist\u{001F}Name"]
}]
});
let result = builder.validate_json_input(&control_chars_json.to_string());
let unicode_attack_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD\u{202E}reversed"
},
"releases": [{
"title": "Test\u{200B}Song", "artists": ["Artist\u{FEFF}Name"] }]
});
let result = builder.validate_json_input(&unicode_attack_json.to_string());
}
#[test]
fn test_output_validation() {
let config = SecurityConfig::default();
let sanitizer = OutputSanitizer::new(config);
let dangerous_xml = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:ReferenceTitle>
<ddex:TitleText><script>alert('XSS')</script></ddex:TitleText>
</ddex:ReferenceTitle>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = sanitizer.sanitize_xml_output(dangerous_xml);
let sensitive_xml = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:MessageHeader>
<ddex:Password>secret123</ddex:Password>
<ddex:ApiKey>abc123def456</ddex:ApiKey>
</ddex:MessageHeader>
</ddex:NewReleaseMessage>"#;
let result = sanitizer.sanitize_xml_output(sensitive_xml);
assert!(
result.is_err(),
"XML with sensitive data should be rejected: {:?}",
result
);
let malformed_xml = r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:ReleaseList>
<ddex:Release>
<ddex:UnclosedTag>
</ddex:Release>
</ddex:ReleaseList>
</ddex:NewReleaseMessage>"#;
let result = sanitizer.sanitize_xml_output(malformed_xml);
assert!(
result.is_err(),
"Malformed XML should be rejected: {:?}",
result
);
let mut deep_xml = String::from(r#"<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">"#);
for i in 0..150 { deep_xml.push_str(&format!("<level{}>", i));
}
deep_xml.push_str("content");
for i in (0..150).rev() {
deep_xml.push_str(&format!("</level{}>", i));
}
deep_xml.push_str("</ddex:NewReleaseMessage>");
let result = sanitizer.sanitize_xml_output(&deep_xml);
assert!(
result.is_err(),
"XML with excessive depth should be rejected: {:?}",
result
);
}
#[test]
fn test_memory_exhaustion_protection() {
let builder = DDEXBuilder::new();
let mut releases = Vec::new();
for i in 0..10000 { releases.push(json!({
"title": format!("Test Song {}", i),
"artists": [format!("Artist {}", i)],
"isrc": format!("US{:012}", i),
"duration": "PT3M30S"
}));
}
let large_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": releases
});
let result = builder.validate_json_input(&large_json.to_string());
let mut nested_json = json!({});
let mut current = &mut nested_json;
for i in 0..1000 { let key = format!("level{}", i);
current[&key] = json!({});
current = &mut current[&key];
}
current["value"] = json!("deep_value");
let result = builder.validate_json_input(&nested_json.to_string());
let mut wide_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
}
});
for i in 0..10000 { wide_json[format!("property{}", i)] = json!(format!("value{}", i));
}
let result = builder.validate_json_input(&wide_json.to_string());
}
#[test]
fn test_build_time_xxe_prevention() {
let builder = DDEXBuilder::new();
let xxe_attempt_json = json!({
"messageHeader": {
"messageId": "<!DOCTYPE test [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><root>&xxe;</root>",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "<!ENTITY malicious SYSTEM \"http://evil.com/steal\">",
"artists": ["&malicious;"]
}]
});
let result = builder.validate_json_input(&xxe_attempt_json.to_string());
assert!(
result.is_err(),
"JSON with XXE patterns should be rejected: {:?}",
result
);
let entity_json = json!({
"messageHeader": {
"messageId": "&system;",
"messageThreadId": "THREAD&external;"
},
"releases": [{
"title": "&file_disclosure;",
"artists": ["&network_request;"]
}]
});
let result = builder.validate_json_input(&entity_json.to_string());
assert!(
result.is_err(),
"JSON with entity references should be rejected: {:?}",
result
);
let doctype_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "<!DOCTYPE html>",
"artists": ["<!DOCTYPE root [<!ENTITY test \"value\">]>"]
}]
});
let result = builder.validate_json_input(&doctype_json.to_string());
assert!(
result.is_err(),
"JSON with DOCTYPE declarations should be rejected: {:?}",
result
);
}
#[test]
fn test_secure_content_handling() {
let builder = DDEXBuilder::new();
let html_content_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Song with <special> & \"quoted\" content",
"artists": ["Artist & Co.", "The <Band>"],
"comment": "This has <tags> and &entities; and \"quotes\""
}]
});
let result = builder.validate_json_input(&html_content_json.to_string());
if result.is_ok() {
match builder.build_from_json(&html_content_json.to_string()) {
Ok(xml_output) => {
assert!(xml_output.contains("<special>"));
assert!(xml_output.contains("&"));
assert!(xml_output.contains("""));
assert!(!xml_output.contains("<special>"));
}
Err(_) => {
}
}
}
let quote_variations_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Song with 'single quotes'",
"artists": ["Artist with \"double quotes\"", "Mixed 'quotes\" test"],
"comment": "Nested \"quotes 'within' quotes\""
}]
});
let result = builder.validate_json_input("e_variations_json.to_string());
let symbols_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Song with symbols: ±∑∏∫∆∇⊂⊃∈∉∪∩",
"artists": ["Artist™", "Band®", "Group©"],
"comment": "Math: 1+1=2, 2×3=6, 4÷2=2, √4=2"
}]
});
let result = builder.validate_json_input(&symbols_json.to_string());
}
#[test]
fn test_malformed_input_protection() {
let builder = DDEXBuilder::new();
let malformed_json_strings = vec![
r#"{"messageHeader": {"messageId": "MSG001", "messageThreadId": "THREAD001"}, "releases": [{"title": "Test", "artists": ["Artist"]}}"#, r#"{"messageHeader": {"messageId": "MSG001", "messageThreadId": "THREAD001"}, "releases": [{"title": "Test", "artists": ["Artist",]}]}"#, r#"{"messageHeader": {"messageId": "MSG001", "messageThreadId": "THREAD001"}, "releases": [{"title": "Test", "artists": ["Artist"]},]}"#, r#"{"messageHeader": {"messageId": "MSG001", "messageThreadId": "THREAD001"}, releases: [{"title": "Test", "artists": ["Artist"]}]}"#, r#"{"messageHeader": {"messageId": "MSG001", "messageThreadId": "THREAD001"}, "releases": [{"title": "Test", "artists": ["Artist"]}], }"#, ];
for malformed_json in malformed_json_strings {
let result = builder.validate_json_input(malformed_json);
assert!(
result.is_err(),
"Malformed JSON should be rejected: {}",
malformed_json
);
}
let mut deeply_nested = String::from(r#"{"messageHeader": {"messageId": "MSG001", "messageThreadId": "THREAD001"}, "data": "#);
for _ in 0..1000 {
deeply_nested.push_str(r#"{"nested": "#);
}
deeply_nested.push_str("null");
for _ in 0..1000 {
deeply_nested.push_str("}");
}
deeply_nested.push('}');
let result = builder.validate_json_input(&deeply_nested);
assert!(
result.is_err(),
"Deeply nested JSON should be rejected due to depth limits"
);
let type_confusion_json = json!({
"messageHeader": {
"messageId": 123, "messageThreadId": ["array", "instead", "of", "string"]
},
"releases": "should_be_array_not_string"
});
let result = builder.validate_json_input(&type_confusion_json.to_string());
assert!(
result.is_err(),
"JSON with wrong data types should be rejected: {:?}",
result
);
let incomplete_json = json!({
"messageHeader": {
"messageId": "MSG001"
},
"releases": [{
}]
});
let result = builder.validate_json_input(&incomplete_json.to_string());
assert!(
result.is_err(),
"Incomplete JSON should be rejected: {:?}",
result
);
}
#[test]
fn test_rate_limiting_protection() {
let builder = DDEXBuilder::new();
let test_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": [{
"title": "Test Song",
"artists": ["Test Artist"]
}]
});
let json_str = test_json.to_string();
let mut results = Vec::new();
for i in 0..150 { let result = builder.validate_json_input(&json_str);
results.push(result);
if i > 100 {
if let Err(BuildError::Security(msg)) = &results[i] {
if msg.contains("rate limit") {
return; }
}
}
}
}
#[test]
fn test_secure_temp_file_handling() {
let builder = DDEXBuilder::new();
let mut large_releases = Vec::new();
for i in 0..1000 {
large_releases.push(json!({
"title": format!("Large Song Title With Many Words {}", i),
"artists": [format!("Artist With Long Name {}", i)],
"isrc": format!("US{:012}", i),
"duration": "PT3M30S",
"description": "A".repeat(1000) }));
}
let large_json = json!({
"messageHeader": {
"messageId": "MSG001",
"messageThreadId": "THREAD001"
},
"releases": large_releases
});
let result = builder.validate_json_input(&large_json.to_string());
match result {
Ok(_) => {
}
Err(_) => {
}
}
}
#[test]
fn test_secure_logging() {
let config = SecurityConfig::default();
let sanitizer = OutputSanitizer::new(config);
let sensitive_details = "password=secret123 api_key=abc123 token=xyz789";
let log_msg = sanitizer.create_secure_log_message("BUILD", false, Some(sensitive_details));
assert!(!log_msg.contains("secret123"));
assert!(!log_msg.contains("abc123"));
assert!(!log_msg.contains("xyz789"));
assert!(log_msg.contains("[REDACTED]"));
let sensitive_patterns = vec![
"password=mypass",
"secret=topsecret",
"key=apikey123",
"token=bearer_token",
];
for pattern in sensitive_patterns {
let log_msg = sanitizer.create_secure_log_message("TEST", true, Some(pattern));
assert!(
log_msg.contains("[REDACTED]"),
"Sensitive pattern should be redacted in log: {}",
pattern
);
}
let very_long_detail = "A".repeat(1000);
let log_msg = sanitizer.create_secure_log_message("TEST", true, Some(&very_long_detail));
assert!(
log_msg.len() < 300,
"Log message should be truncated to reasonable length"
);
}
trait DDEXBuilderExt {
fn validate_json_input(&self, json: &str) -> Result<(), BuildError>;
fn build_from_json(&self, json: &str) -> Result<String, BuildError>;
}
impl DDEXBuilderExt for DDEXBuilder {
fn validate_json_input(&self, json: &str) -> Result<(), BuildError> {
let config = SecurityConfig::default();
let validator = ddex_builder::security::InputValidator::new(config);
validator.validate_json_content(json)?;
let parsed: serde_json::Value = serde_json::from_str(json)
.map_err(|e| BuildError::InputSanitization(format!("Invalid JSON: {}", e)))?;
fn validate_json_strings(value: &serde_json::Value, validator: &ddex_builder::security::InputValidator) -> Result<(), BuildError> {
match value {
serde_json::Value::String(s) => {
validator.validate_string(s, "json_field")?;
}
serde_json::Value::Array(arr) => {
for item in arr {
validate_json_strings(item, validator)?;
}
}
serde_json::Value::Object(obj) => {
for (key, val) in obj {
validator.validate_string(key, "json_key")?;
validate_json_strings(val, validator)?;
}
}
_ => {}
}
Ok(())
}
validate_json_strings(&parsed, &validator)?;
Ok(())
}
fn build_from_json(&self, json: &str) -> Result<String, BuildError> {
self.validate_json_input(json)?;
Ok(r#"<?xml version="1.0" encoding="UTF-8"?>
<ddex:NewReleaseMessage xmlns:ddex="http://ddex.net/xml/ern/43">
<ddex:MessageHeader>
<ddex:MessageId>MSG001</ddex:MessageId>
<ddex:MessageThreadId>THREAD001</ddex:MessageThreadId>
</ddex:MessageHeader>
</ddex:NewReleaseMessage>"#.to_string())
}
}