use rsoap::{SoapClient, SoapOperation, SoapVersion};
#[test]
fn creates_client_with_valid_url() {
let client = SoapClient::new("https://example.com/soap").unwrap();
assert_eq!(client.endpoint(), "https://example.com/soap");
}
#[test]
fn rejects_invalid_url() {
SoapClient::new("not-a-url").unwrap_err();
}
#[test]
fn parses_soap_fault() {
let fault_xml = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>Server</faultcode>
<faultstring>Invalid credentials</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>"#;
let (code, message) = rsoap::envelope::parse_soap_fault(fault_xml).unwrap();
assert_eq!(code, "Server");
assert_eq!(message, "Invalid credentials");
}
#[test]
fn extracts_body_from_envelope() {
let xml = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetWeatherResponse>
<temperature>72</temperature>
</GetWeatherResponse>
</soap:Body>
</soap:Envelope>"#;
let body = rsoap::envelope::extract_body(xml).unwrap();
assert!(body.contains("GetWeatherResponse"));
assert!(body.contains("72"));
}
#[test]
fn empty_soap_fault_defaults() {
let fault_xml = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<faultstring>Generic error</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>"#;
let (code, message) = rsoap::envelope::parse_soap_fault(fault_xml).unwrap();
assert_eq!(code, "unknown");
assert_eq!(message, "Generic error");
}
#[test]
fn client_with_headers() {
let client = SoapClient::new("https://example.com")
.unwrap()
.with_header("X-Auth", "token123")
.with_header("X-Tenant", "acme");
let debug = format!("{client:?}");
assert!(debug.contains("SoapClient"));
assert!(debug.contains("X-Auth"));
assert!(debug.contains("X-Tenant"));
assert!(debug.contains("token123"));
assert!(debug.contains("acme"));
}
#[test]
fn full_serialize_deserialize_round_trip() {
#[derive(Debug, serde::Serialize)]
struct WeatherReq {
zip: String,
}
#[derive(Debug, serde::Deserialize)]
struct WeatherRsp {
temp: Option<f64>,
}
let req = WeatherReq {
zip: "90210".into(),
};
let xml = rsoap::quick_xml::se::to_string_with_root("GetWeather", &req).unwrap();
assert!(xml.contains("90210"));
let resp_xml = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetWeather><temp>72.5</temp>
</GetWeather></soap:Body>
</soap:Envelope>"#;
let rsp: WeatherRsp = rsoap::envelope::deserialize_response(resp_xml).unwrap();
assert_eq!(rsp.temp, Some(72.5));
}
#[test]
fn soap_operation_trait_exists() {
use rsoap::SoapOperation;
struct DummyOp;
impl SoapOperation for DummyOp {
type Request = ();
type Response = ();
const ACTION: &'static str = "http://example.com/DummyOp";
const ENDPOINT: &'static str = "http://localhost:8080/dummy";
const BODY_ELEMENT: &'static str = "DummyRequest";
fn build_request_body(
&self,
_request: &Self::Request,
) -> Result<(String, String), quick_xml::se::SeError> {
Ok((Self::ACTION.into(), Self::BODY_ELEMENT.into()))
}
fn parse_response(&self, _response_xml: &str) -> Result<Self::Response, rsoap::SoapError>
where
Self::Response: serde::de::DeserializeOwned,
{
Ok(())
}
}
let _op = DummyOp;
assert_eq!(
<DummyOp as SoapOperation>::ACTION,
"http://example.com/DummyOp"
);
}
#[derive(Debug)]
struct TestOp;
impl SoapOperation for TestOp {
type Request = WeatherReqE2e;
type Response = WeatherRspE2e;
const ACTION: &'static str = "http://example.com/GetWeather";
const ENDPOINT: &'static str = "http://127.0.0.1:0/mock-soap"; const BODY_ELEMENT: &'static str = "GetWeather";
}
#[derive(Debug, serde::Serialize)]
struct WeatherReqE2e {
zip_code: String,
}
#[derive(Debug, PartialEq, serde::Deserialize)]
struct WeatherRspE2e {
temperature: f64,
}
#[tokio::test]
async fn e2e_successful_call() {
let mock_server = wiremock::MockServer::start().await;
let soap_response = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetWeatherResponse>
<temperature>72.5</temperature>
</GetWeatherResponse>
</soap:Body>
</soap:Envelope>"#;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string(soap_response))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp,
&WeatherReqE2e {
zip_code: "90210".into(),
},
)
.await;
assert!(
result.is_ok(),
"expected successful call, got error: {:?}",
result.err()
);
let rsp = result.unwrap();
assert_eq!(rsp.temperature, 72.5);
}
#[tokio::test]
async fn e2e_soap_fault() {
let mock_server = wiremock::MockServer::start().await;
let soap_fault = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>Client</faultcode>
<faultstring>Invalid API key</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>"#;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string(soap_fault))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp,
&WeatherReqE2e {
zip_code: "10001".into(),
},
)
.await;
assert!(result.is_err(), "expected SoapFault error");
match result.unwrap_err() {
rsoap::SoapError::SoapFault { code, message } => {
assert_eq!(code, "Client");
assert_eq!(message, "Invalid API key");
}
other => panic!("expected SoapFault, got {:?}", other),
}
}
#[tokio::test]
async fn e2e_http_error() {
let mock_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.respond_with(wiremock::ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp,
&WeatherReqE2e {
zip_code: "0".into(),
},
)
.await;
assert!(result.is_err(), "expected HTTP error");
match result.unwrap_err() {
rsoap::SoapError::HttpStatus { code, .. } => assert_eq!(code, 500),
other => panic!("expected SoapError::HttpStatus, got {:?}", other),
}
}
#[tokio::test]
async fn e2e_request_body_check() {
let mock_server = wiremock::MockServer::start().await;
let soap_response = r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetWeatherResponse><temperature>68.0</temperature></GetWeatherResponse>
</soap:Body>
</soap:Envelope>"#;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::header(
"Content-Type",
"text/xml; charset=utf-8",
))
.and(|req: &wiremock::Request| {
String::from_utf8(req.body.clone())
.unwrap_or_default()
.contains("90210")
})
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string(soap_response))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp,
&WeatherReqE2e {
zip_code: "90210".into(),
},
)
.await;
assert!(
result.is_ok(),
"expected successful call with body check, got error: {:?}",
result.err()
);
assert_eq!(result.unwrap().temperature, 68.0);
}
#[tokio::test]
async fn e2e_custom_headers_sent() {
let mock_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::header("Authorization", "Bearer mytoken123"))
.respond_with(wiremock::ResponseTemplate::new(200)
.set_body_string(r#"<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body><GetWeatherResponse><temperature>80.0</temperature></GetWeatherResponse></soap:Body>
</soap:Envelope>"#))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri())
.unwrap()
.with_header("Authorization", "Bearer mytoken123");
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp,
&WeatherReqE2e {
zip_code: "30301".into(),
},
)
.await;
assert!(
result.is_ok(),
"expected successful call with auth header, got error: {:?}",
result.err()
);
}
#[derive(Debug)]
struct TestOp12;
impl SoapOperation for TestOp12 {
type Request = WeatherReqE2e;
type Response = WeatherRspE2e;
const ACTION: &'static str = "http://example.com/GetWeather12";
const ENDPOINT: &'static str = "http://127.0.0.1:0/mock-soap12";
const BODY_ELEMENT: &'static str = "GetWeather";
const VERSION: SoapVersion = SoapVersion::V12;
}
#[tokio::test]
async fn e2e_soap12_content_type_carries_action() {
let mock_server = wiremock::MockServer::start().await;
let soap_response = r#"<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<GetWeatherResponse>
<temperature>65.0</temperature>
</GetWeatherResponse>
</env:Body>
</env:Envelope>"#;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::header_regex(
"Content-Type",
r#"^application/soap\+xml; charset=utf-8; action="http://example.com/GetWeather12""#,
))
.and(|req: &wiremock::Request| {
!req.headers
.iter()
.any(|(k, _)| k.as_str().eq_ignore_ascii_case("SOAPAction"))
})
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string(soap_response))
.expect(1)
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp12,
&WeatherReqE2e {
zip_code: "20001".into(),
},
)
.await;
assert!(
result.is_ok(),
"expected successful 1.2 call, got error: {:?}",
result.err()
);
assert_eq!(result.unwrap().temperature, 65.0);
}
#[tokio::test]
async fn e2e_soap12_envelope_uses_env_namespace() {
let mock_server = wiremock::MockServer::start().await;
let soap_response = r#"<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<GetWeatherResponse>
<temperature>70.0</temperature>
</GetWeatherResponse>
</env:Body>
</env:Envelope>"#;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(|req: &wiremock::Request| {
String::from_utf8(req.body.clone())
.unwrap_or_default()
.contains("<env:Envelope")
&& String::from_utf8(req.body.clone())
.unwrap_or_default()
.contains("http://www.w3.org/2003/05/soap-envelope")
})
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string(soap_response))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp12,
&WeatherReqE2e {
zip_code: "94101".into(),
},
)
.await;
assert!(
result.is_ok(),
"expected successful 1.2 envelope call, got error: {:?}",
result.err()
);
}
#[tokio::test]
async fn e2e_soap12_fault_detected() {
let mock_server = wiremock::MockServer::start().await;
let soap_fault = r#"<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<env:Fault>
<Code><Value>env:Sender</Value></Code>
<Reason><Text xml:lang="en">Invalid zip code</Text></Reason>
</env:Fault>
</env:Body>
</env:Envelope>"#;
wiremock::Mock::given(wiremock::matchers::method("POST"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_string(soap_fault))
.mount(&mock_server)
.await;
let client = SoapClient::new(mock_server.uri()).unwrap();
let result: Result<WeatherRspE2e, _> = client
.call(
&TestOp12,
&WeatherReqE2e {
zip_code: "00000".into(),
},
)
.await;
assert!(result.is_err(), "expected SoapFault error for 1.2");
match result.unwrap_err() {
rsoap::SoapError::SoapFault { code, message } => {
assert_eq!(code, "env:Sender");
assert_eq!(message, "Invalid zip code");
}
other => panic!("expected SoapFault, got {:?}", other),
}
}