#![expect(clippy::unwrap_used)]
use crate::xmlrpc::value::Value;
use super::parse_response;
#[test]
fn parse_success_response_with_struct() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<name>bugs</name>
<value>
<array>
<data>
<value>
<struct>
<member>
<name>id</name>
<value><int>12345</int></value>
</member>
<member>
<name>summary</name>
<value><string>Test bug</string></value>
</member>
</struct>
</value>
</data>
</array>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
let top = result.as_struct().unwrap();
let bugs = top.get("bugs").unwrap().as_array().unwrap();
assert_eq!(bugs.len(), 1);
let bug = bugs[0].as_struct().unwrap();
assert_eq!(bug.get("id").unwrap().as_i64().unwrap(), 12345);
assert_eq!(bug.get("summary").unwrap().as_str().unwrap(), "Test bug");
}
#[test]
fn parse_fault_response() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>102</int></value>
</member>
<member>
<name>faultString</name>
<value><string>Access denied</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>"#;
let err = parse_response(xml).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("102"), "should contain fault code: {msg}");
assert!(
msg.contains("Access denied"),
"should contain fault message: {msg}"
);
}
#[test]
fn parse_response_with_double_and_datetime() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<name>score</name>
<value><double>42.5</double></value>
</member>
<member>
<name>when</name>
<value><dateTime.iso8601>20250101T12:00:00</dateTime.iso8601></value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
let s = result.as_struct().unwrap();
let score = s.get("score").unwrap().as_f64().unwrap();
assert!((score - 42.5).abs() < f64::EPSILON);
assert_eq!(
s.get("when").unwrap(),
&Value::DateTime("20250101T12:00:00".into())
);
}
#[test]
fn parse_bare_text_as_string() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>hello world</value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
assert_eq!(result.as_str().unwrap(), "hello world");
}
#[test]
fn parse_empty_struct() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value><struct></struct></value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
let s = result.as_struct().unwrap();
assert!(s.is_empty());
}
#[test]
fn parse_empty_array() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value><array><data></data></array></value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
let a = result.as_array().unwrap();
assert!(a.is_empty());
}
#[test]
fn parse_i4_type() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value><i4>42</i4></value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
assert_eq!(result.as_i64().unwrap(), 42);
}
#[test]
fn parse_boolean_values() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value>
<struct>
<member>
<name>yes</name>
<value><boolean>1</boolean></value>
</member>
<member>
<name>no</name>
<value><boolean>0</boolean></value>
</member>
</struct>
</value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
let s = result.as_struct().unwrap();
assert!(s.get("yes").unwrap().as_bool().unwrap());
assert!(!s.get("no").unwrap().as_bool().unwrap());
}
#[test]
fn parse_base64_value() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
<params>
<param>
<value><base64>SGVsbG8=</base64></value>
</param>
</params>
</methodResponse>"#;
let result = parse_response(xml).unwrap();
assert!(
matches!(&result, Value::Base64(bytes) if bytes == b"Hello"),
"expected Base64(Hello), got {result:?}"
);
}
#[test]
fn parse_string_with_cdata_section() {
let xml = r#"<methodResponse><params><param><value>
<string><![CDATA[contains <special> & "characters"]]></string>
</value></param></params></methodResponse>"#;
let result = parse_response(xml).unwrap();
assert_eq!(
result.as_str().unwrap(),
r#"contains <special> & "characters""#
);
}
#[test]
fn parse_empty_value_returns_empty_string() {
let xml = r"<methodResponse><params><param><value></value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
assert_eq!(result.as_str().unwrap(), "");
}
#[test]
fn parse_array_without_data_tag_is_empty() {
let xml = r"<methodResponse><params><param><value><array></array></value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
assert!(result.as_array().unwrap().is_empty());
}
#[test]
fn parse_empty_params_errors_with_specific_message() {
let xml = r"<methodResponse><params></params></methodResponse>";
let err = parse_response(xml).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("empty params"),
"expected 'empty params' message, got: {msg}"
);
}
#[test]
fn parse_response_skips_decoy_root_sibling() {
let xml = r"<wrapper>
<fakeResponse><params><param><value><string>decoy</string></value></param></params></fakeResponse>
<methodResponse><params><param><value><string>real</string></value></param></params></methodResponse>
</wrapper>";
let result = parse_response(xml).unwrap();
assert_eq!(result.as_str().unwrap(), "real");
}
#[test]
fn parse_response_skips_decoy_inside_method_response() {
let xml = r"<methodResponse>
<decoy><param><value><string>decoy</string></value></param></decoy>
<params><param><value><string>real</string></value></param></params>
</methodResponse>";
let result = parse_response(xml).unwrap();
assert_eq!(result.as_str().unwrap(), "real");
}
#[test]
fn parse_first_param_skips_decoy_inside_params() {
let xml = r"<methodResponse><params>
<wrapper><value><string>decoy</string></value></wrapper>
<param><value><string>real</string></value></param>
</params></methodResponse>";
let result = parse_response(xml).unwrap();
assert_eq!(result.as_str().unwrap(), "real");
}
#[test]
fn parse_value_skips_decoy_inside_param() {
let xml = r"<methodResponse><params><param>
<decoy>x</decoy>
<value><string>real</string></value>
</param></params></methodResponse>";
let result = parse_response(xml).unwrap();
assert_eq!(result.as_str().unwrap(), "real");
}
#[test]
fn parse_array_skips_decoy_before_data() {
let xml = r"<methodResponse><params><param><value>
<array>
<wrapper><value><string>decoy</string></value></wrapper>
<data><value><string>real</string></value></data>
</array>
</value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
let a = result.as_array().unwrap();
assert_eq!(a.len(), 1);
assert_eq!(a[0].as_str().unwrap(), "real");
}
#[test]
fn parse_array_skips_decoy_inside_data() {
let xml = r"<methodResponse><params><param><value>
<array><data>
<wrapper></wrapper>
<value><string>real</string></value>
</data></array>
</value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
let a = result.as_array().unwrap();
assert_eq!(a.len(), 1);
assert_eq!(a[0].as_str().unwrap(), "real");
}
#[test]
fn parse_struct_skips_member_lookalike() {
let xml = r"<methodResponse><params><param><value>
<struct>
<decoy_member><name>k</name><value><string>x</string></value></decoy_member>
</struct>
</value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
let s = result.as_struct().unwrap();
assert!(s.is_empty(), "decoy_member should be skipped, got {s:?}");
}
#[test]
fn parse_struct_does_not_terminate_on_unrelated_end_tag() {
let xml = r"<methodResponse><params><param><value>
<struct>
<wrapper></wrapper>
<member><name>k</name><value><string>v</string></value></member>
</struct>
</value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
let s = result.as_struct().unwrap();
assert_eq!(s.get("k").unwrap().as_str().unwrap(), "v");
}
#[test]
fn parse_member_does_not_terminate_on_unrelated_end_tag() {
let xml = r"<methodResponse><params><param><value>
<struct><member>
<name>k</name>
<wrapper></wrapper>
<value><string>v</string></value>
</member></struct>
</value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
let s = result.as_struct().unwrap();
assert_eq!(s.get("k").unwrap().as_str().unwrap(), "v");
}
#[test]
fn parse_string_continues_past_unrelated_end_tag() {
let xml = r"<methodResponse><params><param><value>
<string>before<wrapper></wrapper>after</string>
</value></param></params></methodResponse>";
let result = parse_response(xml).unwrap();
let s = result.as_str().unwrap();
assert!(
s.contains("after"),
"should keep reading past </wrapper>; got {s:?}"
);
}
#[test]
fn parse_self_closing_value_is_empty_string() {
let xml = r#"<?xml version="1.0"?>
<methodResponse><params><param><value/></param></params></methodResponse>"#;
assert_eq!(parse_response(xml).unwrap(), Value::String(String::new()));
}
#[test]
fn parse_self_closing_struct_is_empty_struct() {
let xml = r#"<?xml version="1.0"?>
<methodResponse><params><param><value><struct/></value></param></params></methodResponse>"#;
let value = parse_response(xml).unwrap();
assert!(
matches!(value, Value::Struct(ref m) if m.is_empty()),
"expected empty struct, got {value:?}"
);
}
#[test]
fn parse_self_closing_array_is_empty_array() {
let xml = r#"<?xml version="1.0"?>
<methodResponse><params><param><value><array/></value></param></params></methodResponse>"#;
let value = parse_response(xml).unwrap();
assert!(
matches!(value, Value::Array(ref v) if v.is_empty()),
"expected empty array, got {value:?}"
);
}