use faucet_core::FaucetError;
use faucet_source_xml::{SoapConfig, SoapVersion, XmlAuth, XmlStream, XmlStreamConfig};
use reqwest::Method;
use wiremock::matchers::{body_string_contains, header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn soap_users_response(users: &str) -> String {
format!(
"<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\"><Body>\
<GetUsersResponse xmlns=\"urn:example\"><Users>{users}</Users></GetUsersResponse>\
</Body></Envelope>"
)
}
#[tokio::test]
async fn soap11_injects_soapaction_header_and_text_xml_content_type() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/ws"))
.and(header("soapaction", "\"urn:example:GetUsers\""))
.and(header("content-type", "text/xml; charset=utf-8"))
.and(body_string_contains(
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">",
))
.and(body_string_contains(
"<soap:Body><GetUsers xmlns=\"urn:example\"/></soap:Body>",
))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(soap_users_response(
"<User><Name>Alice</Name></User><User><Name>Bob</Name></User>",
)),
)
.expect(1)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.records_element_path("GetUsersResponse.Users.User")
.with_soap(SoapConfig {
version: SoapVersion::Soap11,
action: Some("urn:example:GetUsers".into()),
body_inner: Some("<GetUsers xmlns=\"urn:example\"/>".into()),
..Default::default()
});
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0]["Name"], "Alice");
assert_eq!(records[1]["Name"], "Bob");
}
#[tokio::test]
async fn soap12_carries_action_in_content_type_and_sends_no_soapaction_header() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/ws"))
.and(header(
"content-type",
"application/soap+xml; charset=utf-8; action=\"urn:example:GetUsers\"",
))
.and(body_string_contains(
"<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">",
))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "application/soap+xml")
.set_body_string(soap_users_response("<User><Name>Carol</Name></User>")),
)
.expect(1)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.records_element_path("GetUsersResponse.Users.User")
.with_soap(SoapConfig {
version: SoapVersion::Soap12,
action: Some("urn:example:GetUsers".into()),
body_inner: Some("<GetUsers xmlns=\"urn:example\"/>".into()),
..Default::default()
});
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0]["Name"], "Carol");
}
#[tokio::test]
async fn soap_namespace_prefixed_record_element_is_extracted() {
let server = MockServer::start().await;
let response = "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\"><Body>\
<GetUsersResponse xmlns:usr=\"urn:example:users\"><Users>\
<usr:User><usr:Name>Dana</usr:Name></usr:User>\
<usr:User><usr:Name>Erin</usr:Name></usr:User>\
</Users></GetUsersResponse></Body></Envelope>";
Mock::given(method("POST"))
.and(path("/ws"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(response),
)
.expect(1)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.records_element_path("GetUsersResponse.Users.usr:User")
.with_soap(SoapConfig {
body_inner: Some("<GetUsers/>".into()),
..Default::default()
});
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0]["usr:Name"], "Dana");
assert_eq!(records[1]["usr:Name"], "Erin");
}
#[tokio::test]
async fn soap_absolute_path_override_bypasses_body_prepend() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/ws"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(soap_users_response("<User><Name>Fred</Name></User>")),
)
.expect(1)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.records_element_path("Envelope.Body.GetUsersResponse.Users.User")
.with_soap(SoapConfig {
body_inner: Some("<GetUsers/>".into()),
path_relative_to_body: false,
..Default::default()
});
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0]["Name"], "Fred");
}
#[tokio::test]
async fn soap_fault_as_error_surfaces_source_error() {
let server = MockServer::start().await;
let fault = "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\"><Body>\
<Fault><faultcode>Server</faultcode>\
<faultstring>Account not found</faultstring></Fault></Body></Envelope>";
Mock::given(method("POST"))
.and(path("/ws"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(fault),
)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.records_element_path("GetUsersResponse.Users.User")
.with_soap(SoapConfig {
body_inner: Some("<GetUsers/>".into()),
..Default::default()
});
let err = XmlStream::new(config).fetch_all().await.unwrap_err();
assert!(
matches!(&err, FaucetError::Source(m) if m.contains("SOAP fault") && m.contains("Account not found")),
"got {err:?}"
);
}
#[tokio::test]
async fn soap_fault_not_error_yields_zero_records() {
let server = MockServer::start().await;
let fault = "<Envelope xmlns=\"http://schemas.xmlsoap.org/soap/envelope/\"><Body>\
<Fault><faultstring>transient</faultstring></Fault></Body></Envelope>";
Mock::given(method("POST"))
.and(path("/ws"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(fault),
)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.records_element_path("GetUsersResponse.Users.User")
.with_soap(SoapConfig {
body_inner: Some("<GetUsers/>".into()),
fault_as_error: false,
..Default::default()
});
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert!(records.is_empty());
}
#[tokio::test]
async fn soap_headers_injected_alongside_real_bearer_auth() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/ws"))
.and(header("authorization", "Bearer tok-123"))
.and(header("soapaction", "\"urn:Op\""))
.and(header("content-type", "text/xml; charset=utf-8"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(soap_users_response("<User><Name>Gwen</Name></User>")),
)
.expect(1)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.auth(XmlAuth::Bearer {
token: "tok-123".into(),
})
.records_element_path("GetUsersResponse.Users.User")
.with_soap(SoapConfig {
action: Some("urn:Op".into()),
body_inner: Some("<Op/>".into()),
..Default::default()
});
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0]["Name"], "Gwen");
}
#[tokio::test]
async fn raw_body_path_is_unchanged_when_soap_absent() {
let server = MockServer::start().await;
let raw_envelope =
"<soap:Envelope><soap:Body><GetUsers/></soap:Body></soap:Envelope>".to_string();
Mock::given(method("POST"))
.and(path("/ws"))
.and(header("content-type", "text/xml; charset=utf-8"))
.and(body_string_contains(&raw_envelope))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Type", "text/xml")
.set_body_string(
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\
<soap:Body><GetUsersResponse><User><Name>Ivy</Name></User>\
</GetUsersResponse></soap:Body></soap:Envelope>",
),
)
.expect(1)
.mount(&server)
.await;
let config = XmlStreamConfig::new(server.uri(), "/ws")
.method(Method::POST)
.body(raw_envelope.clone())
.records_element_path("soap:Envelope.soap:Body.GetUsersResponse.User");
let records = XmlStream::new(config).fetch_all().await.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0]["Name"], "Ivy");
}