1use crate::xml_escape::escape_text;
3use thiserror::Error;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum FaultCode {
7 VersionMismatch,
8 MustUnderstand,
9 DataEncodingUnknown,
10 Sender,
11 Receiver,
12}
13
14impl FaultCode {
15 pub fn as_soap12_str(&self) -> &'static str {
18 match self {
19 FaultCode::VersionMismatch => "env:VersionMismatch",
20 FaultCode::MustUnderstand => "env:MustUnderstand",
21 FaultCode::DataEncodingUnknown => "env:DataEncodingUnknown",
22 FaultCode::Sender => "env:Sender",
23 FaultCode::Receiver => "env:Receiver",
24 }
25 }
26
27 pub fn as_soap11_str(&self) -> &'static str {
30 match self {
31 FaultCode::VersionMismatch => "env:VersionMismatch",
32 FaultCode::MustUnderstand => "env:MustUnderstand",
33 FaultCode::DataEncodingUnknown => "env:Server",
35 FaultCode::Sender => "env:Client",
36 FaultCode::Receiver => "env:Server",
37 }
38 }
39}
40
41#[derive(Debug, Clone, Error)]
42#[error("SOAP Fault: {code:?} — {reason}")]
43pub struct SoapFault {
44 pub code: FaultCode,
45 pub reason: String,
46 pub detail: Option<String>,
47 pub detail_xml: Option<String>,
50}
51
52impl SoapFault {
53 pub fn new(code: FaultCode, reason: impl Into<String>, detail: Option<String>) -> Self {
54 Self {
55 code,
56 reason: reason.into(),
57 detail,
58 detail_xml: None,
59 }
60 }
61
62 pub fn with_detail_xml(mut self, xml: impl Into<String>) -> Self {
66 self.detail_xml = Some(xml.into());
67 self
68 }
69
70 pub fn sender(reason: impl Into<String>) -> Self {
71 Self::new(FaultCode::Sender, reason, None)
72 }
73
74 pub fn receiver(reason: impl Into<String>) -> Self {
75 Self::new(FaultCode::Receiver, reason, None)
76 }
77
78 pub fn version_mismatch() -> Self {
79 Self::new(FaultCode::VersionMismatch, "SOAP version mismatch", None)
80 }
81
82 pub fn must_understand(header: &str) -> Self {
83 Self::new(
84 FaultCode::MustUnderstand,
85 format!("Header not understood: {header}"),
86 None,
87 )
88 }
89
90 pub fn action_not_supported(action: &str) -> Self {
91 Self::new(
92 FaultCode::Sender,
93 format!("Action not supported: {action}"),
94 None,
95 )
96 }
97
98 pub fn to_xml_bytes_versioned(
102 &self,
103 version: &crate::wsdl::definitions::SoapVersion,
104 ) -> Vec<u8> {
105 match version {
106 crate::wsdl::definitions::SoapVersion::Soap12 => self.to_xml_bytes(),
107 crate::wsdl::definitions::SoapVersion::Soap11 => self.to_xml_bytes_v11(),
108 }
109 }
110
111 fn to_xml_bytes_v11(&self) -> Vec<u8> {
112 let ns = "http://schemas.xmlsoap.org/soap/envelope/";
113 let faultcode = match &self.code {
114 FaultCode::Sender => "SOAP-ENV:Client",
115 FaultCode::Receiver => "SOAP-ENV:Server",
116 FaultCode::VersionMismatch => "SOAP-ENV:VersionMismatch",
117 FaultCode::MustUnderstand => "SOAP-ENV:MustUnderstand",
118 FaultCode::DataEncodingUnknown => "SOAP-ENV:Server",
120 };
121 let reason = escape_text(&self.reason);
122 let detail_xml = if let Some(raw) = &self.detail_xml {
123 format!("<detail>{raw}</detail>")
124 } else {
125 match &self.detail {
126 Some(detail) => format!("<detail>{}</detail>", escape_text(detail)),
127 None => String::new(),
128 }
129 };
130 format!(
131 r#"<SOAP-ENV:Envelope xmlns:SOAP-ENV="{ns}"><SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>{faultcode}</faultcode><faultstring>{reason}</faultstring>{detail_xml}</SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>"#
132 ).into_bytes()
133 }
134
135 pub fn to_xml_bytes(&self) -> Vec<u8> {
138 let code = self.code.as_soap12_str();
139 let reason = escape_text(&self.reason);
140
141 let detail_xml = if let Some(raw) = &self.detail_xml {
145 format!("<env:Detail>{raw}</env:Detail>")
146 } else {
147 String::new()
148 };
149
150 let xml = format!(
151 r#"<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Body><env:Fault><env:Code><env:Value>{code}</env:Value></env:Code><env:Reason><env:Text xml:lang="en">{reason}</env:Text></env:Reason>{detail_xml}</env:Fault></env:Body></env:Envelope>"#
152 );
153
154 xml.into_bytes()
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn fault_code_sender_as_soap12_str() {
164 assert_eq!(FaultCode::Sender.as_soap12_str(), "env:Sender");
165 }
166
167 #[test]
168 fn fault_code_receiver_as_soap12_str() {
169 assert_eq!(FaultCode::Receiver.as_soap12_str(), "env:Receiver");
170 }
171
172 #[test]
173 fn fault_code_version_mismatch_as_soap12_str() {
174 assert_eq!(
175 FaultCode::VersionMismatch.as_soap12_str(),
176 "env:VersionMismatch"
177 );
178 }
179
180 #[test]
181 fn fault_code_must_understand_as_soap12_str() {
182 assert_eq!(
183 FaultCode::MustUnderstand.as_soap12_str(),
184 "env:MustUnderstand"
185 );
186 }
187
188 #[test]
189 fn fault_code_data_encoding_unknown_as_soap12_str() {
190 assert_eq!(
191 FaultCode::DataEncodingUnknown.as_soap12_str(),
192 "env:DataEncodingUnknown"
193 );
194 }
195
196 #[test]
197 fn fault_code_sender_as_soap11_str() {
198 assert_eq!(FaultCode::Sender.as_soap11_str(), "env:Client");
199 }
200
201 #[test]
202 fn fault_code_receiver_as_soap11_str() {
203 assert_eq!(FaultCode::Receiver.as_soap11_str(), "env:Server");
204 }
205
206 #[test]
207 fn fault_code_version_mismatch_as_soap11_str() {
208 assert_eq!(
209 FaultCode::VersionMismatch.as_soap11_str(),
210 "env:VersionMismatch"
211 );
212 }
213
214 #[test]
215 fn fault_code_must_understand_as_soap11_str() {
216 assert_eq!(
217 FaultCode::MustUnderstand.as_soap11_str(),
218 "env:MustUnderstand"
219 );
220 }
221
222 #[test]
223 fn fault_code_data_encoding_unknown_as_soap11_str() {
224 assert_eq!(FaultCode::DataEncodingUnknown.as_soap11_str(), "env:Server");
226 }
227
228 #[test]
229 fn serialize_sender_fault_contains_env_sender() {
230 let fault = SoapFault::sender("Bad request");
231 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
232 assert!(
233 xml.contains("<env:Value>env:Sender</env:Value>"),
234 "Expected env:Sender in XML, got: {xml}"
235 );
236 }
237
238 #[test]
239 fn serialize_receiver_fault_contains_env_receiver() {
240 let fault = SoapFault::receiver("Internal error");
241 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
242 assert!(
243 xml.contains("<env:Value>env:Receiver</env:Value>"),
244 "Expected env:Receiver in XML, got: {xml}"
245 );
246 }
247
248 #[test]
249 fn serialize_fault_wraps_in_soap12_envelope() {
250 let fault = SoapFault::sender("test");
251 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
252 assert!(
253 xml.contains(r#"xmlns:env="http://www.w3.org/2003/05/soap-envelope""#),
254 "Expected SOAP 1.2 namespace in XML, got: {xml}"
255 );
256 assert!(xml.starts_with("<env:Envelope"), "Expected envelope root");
257 assert!(xml.contains("<env:Body>"), "Expected Body element");
258 assert!(xml.contains("<env:Fault>"), "Expected Fault element");
259 }
260
261 #[test]
262 fn serialize_fault_reason_included() {
263 let fault = SoapFault::sender("Custom reason text");
264 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
265 assert!(
266 xml.contains("Custom reason text"),
267 "Expected reason in XML, got: {xml}"
268 );
269 assert!(
270 xml.contains(r#"<env:Text xml:lang="en">"#),
271 "Expected Text element with lang"
272 );
273 }
274
275 #[test]
276 fn serialize_fault_no_detail_when_none() {
277 let fault = SoapFault::sender("test");
278 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
279 assert!(
280 !xml.contains("<env:Detail>"),
281 "Expected no Detail element when detail is None, got: {xml}"
282 );
283 }
284
285 #[test]
286 fn serialize_fault_with_detail() {
287 let fault = SoapFault::new(
291 FaultCode::Receiver,
292 "Internal error",
293 Some("extra info".to_string()),
294 );
295 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
296 assert!(
297 !xml.contains("<env:Detail>extra info</env:Detail>"),
298 "SOAP 1.2 must not emit char-data in env:Detail (F-3): {xml}"
299 );
300 assert!(
301 !xml.contains("<env:Detail>"),
302 "No env:Detail at all for plain-text detail (F-3): {xml}"
303 );
304 assert!(
305 xml.contains("Internal error"),
306 "Reason must still be in Reason/Text: {xml}"
307 );
308 }
309
310 #[test]
311 fn version_mismatch_convenience() {
312 let fault = SoapFault::version_mismatch();
313 assert_eq!(fault.code, FaultCode::VersionMismatch);
314 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
315 assert!(xml.contains("<env:Value>env:VersionMismatch</env:Value>"));
316 }
317
318 #[test]
319 fn must_understand_convenience() {
320 let fault = SoapFault::must_understand("MyHeader");
321 assert_eq!(fault.code, FaultCode::MustUnderstand);
322 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
323 assert!(xml.contains("<env:Value>env:MustUnderstand</env:Value>"));
324 assert!(xml.contains("MyHeader"));
325 }
326
327 #[test]
328 fn action_not_supported_is_sender_fault() {
329 let fault = SoapFault::action_not_supported("urn:SomeAction");
330 assert_eq!(fault.code, FaultCode::Sender);
331 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
332 assert!(xml.contains("<env:Value>env:Sender</env:Value>"));
333 assert!(xml.contains("urn:SomeAction"));
334 }
335
336 #[test]
339 fn fault_soap11_structure() {
340 let fault = SoapFault::sender("bad");
341 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
342 assert!(
343 xml.contains("<faultcode>"),
344 "Expected <faultcode>, got: {xml}"
345 );
346 assert!(
347 xml.contains("<faultstring>"),
348 "Expected <faultstring>, got: {xml}"
349 );
350 assert!(
351 xml.contains("SOAP-ENV:Fault"),
352 "Expected SOAP-ENV:Fault, got: {xml}"
353 );
354 assert!(
355 !xml.contains("<env:Code>"),
356 "Should NOT contain <env:Code>, got: {xml}"
357 );
358 assert!(
359 !xml.contains("<env:Reason>"),
360 "Should NOT contain <env:Reason>, got: {xml}"
361 );
362 }
363
364 #[test]
365 fn fault_soap11_namespace() {
366 let fault = SoapFault::sender("bad");
367 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
368 assert!(
369 xml.contains(r#"xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/""#),
370 "Expected SOAP 1.1 namespace on Envelope, got: {xml}"
371 );
372 }
373
374 #[test]
375 fn fault_soap11_wraps_in_envelope() {
376 let fault = SoapFault::sender("bad");
377 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
378 assert!(
379 xml.starts_with("<SOAP-ENV:Envelope"),
380 "Expected SOAP-ENV:Envelope root, got: {xml}"
381 );
382 assert!(
383 xml.contains("<SOAP-ENV:Body>"),
384 "Expected <SOAP-ENV:Body>, got: {xml}"
385 );
386 }
387
388 #[test]
389 fn fault_code_sender_maps_to_client() {
390 let fault = SoapFault::sender("bad");
391 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
392 assert!(
393 xml.contains("<faultcode>SOAP-ENV:Client</faultcode>"),
394 "Expected SOAP-ENV:Client, got: {xml}"
395 );
396 }
397
398 #[test]
399 fn fault_code_receiver_maps_to_server() {
400 let fault = SoapFault::receiver("internal");
401 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
402 assert!(
403 xml.contains("<faultcode>SOAP-ENV:Server</faultcode>"),
404 "Expected SOAP-ENV:Server, got: {xml}"
405 );
406 }
407
408 #[test]
409 fn fault_code_version_mismatch_soap11() {
410 let fault = SoapFault::version_mismatch();
411 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
412 assert!(
413 xml.contains("<faultcode>SOAP-ENV:VersionMismatch</faultcode>"),
414 "Expected SOAP-ENV:VersionMismatch, got: {xml}"
415 );
416 }
417
418 #[test]
419 fn fault_code_must_understand_soap11() {
420 let fault = SoapFault::must_understand("MyHeader");
421 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
422 assert!(
423 xml.contains("<faultcode>SOAP-ENV:MustUnderstand</faultcode>"),
424 "Expected SOAP-ENV:MustUnderstand, got: {xml}"
425 );
426 }
427
428 #[test]
429 fn fault_code_data_encoding_unknown_soap11() {
430 let fault = SoapFault::new(FaultCode::DataEncodingUnknown, "enc error", None);
431 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
432 assert!(
433 xml.contains("<faultcode>SOAP-ENV:Server</faultcode>"),
434 "Expected SOAP-ENV:Server (no 1.1 equivalent for DataEncodingUnknown), got: {xml}"
435 );
436 }
437
438 #[test]
439 fn fault_soap11_with_detail() {
440 let fault = SoapFault::new(
441 FaultCode::Receiver,
442 "Internal error",
443 Some("extra info".to_string()),
444 );
445 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
446 assert!(
447 xml.contains("<detail>extra info</detail>"),
448 "Expected <detail> element with content, got: {xml}"
449 );
450 }
451
452 #[test]
453 fn fault_soap11_no_detail_when_none() {
454 let fault = SoapFault::sender("test");
455 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
456 assert!(
457 !xml.contains("<detail>"),
458 "Expected no <detail> when detail is None, got: {xml}"
459 );
460 }
461
462 #[test]
463 fn to_xml_bytes_versioned_soap12_calls_existing() {
464 use crate::wsdl::definitions::SoapVersion;
465 let fault = SoapFault::sender("test");
466 let v12 = fault.to_xml_bytes_versioned(&SoapVersion::Soap12);
467 let existing = fault.to_xml_bytes();
468 assert_eq!(
469 v12, existing,
470 "Soap12 path should produce identical output to to_xml_bytes()"
471 );
472 }
473
474 #[test]
475 fn to_xml_bytes_versioned_soap11_calls_v11() {
476 use crate::wsdl::definitions::SoapVersion;
477 let fault = SoapFault::sender("test");
478 let v11_versioned = fault.to_xml_bytes_versioned(&SoapVersion::Soap11);
479 let v11_direct = fault.to_xml_bytes_v11();
480 assert_eq!(
481 v11_versioned, v11_direct,
482 "Soap11 path should produce identical output to to_xml_bytes_v11()"
483 );
484 }
485
486 #[test]
493 fn soap12_fault_special_chars_in_reason_and_detail_are_escaped() {
494 use quick_xml::events::Event;
495 use quick_xml::Reader;
496
497 let special = r#"& < > " '"#;
498 let fault = SoapFault::new(FaultCode::Sender, special, Some(special.to_string()));
499 let xml_bytes = fault.to_xml_bytes();
500 let xml_str = String::from_utf8(xml_bytes.clone()).unwrap();
501
502 assert!(
506 !xml_str.contains("& "),
507 "Unescaped '& ' found in SOAP 1.2 fault XML: {xml_str}"
508 );
509 assert!(xml_str.contains("&"), "Expected &: {xml_str}");
511 assert!(xml_str.contains("<"), "Expected <: {xml_str}");
512 assert!(xml_str.contains(">"), "Expected >: {xml_str}");
513 assert!(xml_str.contains("""), "Expected ": {xml_str}");
514 assert!(xml_str.contains("'"), "Expected ': {xml_str}");
515
516 let mut reader = Reader::from_str(&xml_str);
522 reader.config_mut().trim_text(true);
523 let mut buf = Vec::new();
524 let mut event_count = 0usize;
525 loop {
526 let is_eof = {
527 let ev = reader
528 .read_event_into(&mut buf)
529 .expect("XML must parse as well-formed");
530 matches!(ev, Event::Eof)
531 };
532 buf.clear();
533 if is_eof {
534 break;
535 }
536 event_count += 1;
537 }
538 assert!(event_count > 0, "Expected at least one XML event");
539 }
540
541 #[test]
544 fn serialize_fault_with_raw_xml_detail_soap12() {
545 let fault = SoapFault::sender("bad")
546 .with_detail_xml(r#"<c:Info xmlns:c="urn:x"><c:Code>42</c:Code></c:Info>"#);
547 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
548 assert!(
550 xml.contains(
551 r#"<env:Detail><c:Info xmlns:c="urn:x"><c:Code>42</c:Code></c:Info></env:Detail>"#
552 ),
553 "got: {xml}"
554 );
555 assert!(
556 !xml.contains("<c:Info"),
557 "detail must not be escaped: {xml}"
558 );
559 }
560
561 #[test]
562 fn serialize_fault_with_raw_xml_detail_soap11() {
563 let fault =
564 SoapFault::sender("bad").with_detail_xml(r#"<c:Info xmlns:c="urn:x">x</c:Info>"#);
565 let xml = String::from_utf8(fault.to_xml_bytes_v11()).unwrap();
566 assert!(
567 xml.contains(r#"<detail><c:Info xmlns:c="urn:x">x</c:Info></detail>"#),
568 "got: {xml}"
569 );
570 assert!(
571 !xml.contains("<c:Info"),
572 "detail must not be escaped: {xml}"
573 );
574 }
575
576 #[test]
577 fn detail_xml_takes_precedence_over_text_detail() {
578 let fault = SoapFault::new(FaultCode::Sender, "r", Some("plaintext".into()))
579 .with_detail_xml("<x:Y xmlns:x=\"urn:z\"/>");
580 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
581 assert!(
582 xml.contains("<x:Y xmlns:x=\"urn:z\"/>"),
583 "raw xml should win: {xml}"
584 );
585 assert!(
586 !xml.contains("plaintext"),
587 "text detail should be suppressed when detail_xml set: {xml}"
588 );
589 }
590
591 #[test]
592 fn existing_text_detail_still_escaped_when_no_xml() {
593 let fault = SoapFault::new(FaultCode::Sender, "r", Some("a < b & c".into()));
597 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
598 assert!(
599 !xml.contains("<env:Detail>"),
600 "SOAP 1.2: env:Detail must not be emitted for plain-text detail (F-3): {xml}"
601 );
602 }
603
604 #[test]
610 fn soap12_text_detail_does_not_emit_env_detail_chardata() {
611 let fault = SoapFault::new(
612 FaultCode::Receiver,
613 "Internal error",
614 Some("plain text detail".to_string()),
615 );
616 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
617 assert!(
619 !xml.contains("<env:Detail>plain text detail</env:Detail>"),
620 "SOAP 1.2 env:Detail must not contain character data (F-3): {xml}"
621 );
622 assert!(
624 !xml.contains("<env:Detail>"),
625 "SOAP 1.2 env:Detail must not be emitted for plain-text detail (F-3): {xml}"
626 );
627 assert!(
629 xml.contains("Internal error"),
630 "Reason must still appear in Reason/Text: {xml}"
631 );
632 }
633
634 #[test]
637 fn soap12_detail_xml_element_still_emits_env_detail() {
638 let fault = SoapFault::new(
639 FaultCode::Receiver,
640 "Internal error",
641 Some("plain text".to_string()),
642 )
643 .with_detail_xml("<c:Child xmlns:c=\"urn:x\"/>");
644 let xml = String::from_utf8(fault.to_xml_bytes()).unwrap();
645 assert!(
646 xml.contains("<env:Detail><c:Child xmlns:c=\"urn:x\"/></env:Detail>"),
647 "detail_xml element path must still emit env:Detail: {xml}"
648 );
649 }
650
651 #[test]
652 fn soap11_fault_special_chars_in_reason_and_detail_are_escaped() {
653 use quick_xml::events::Event;
654 use quick_xml::Reader;
655
656 let special = r#"& < > " '"#;
657 let fault = SoapFault::new(FaultCode::Sender, special, Some(special.to_string()));
658 let xml_bytes = fault.to_xml_bytes_v11();
659 let xml_str = String::from_utf8(xml_bytes).unwrap();
660
661 assert!(
662 !xml_str.contains("& "),
663 "Unescaped '& ' found in SOAP 1.1 fault XML: {xml_str}"
664 );
665 assert!(xml_str.contains("&"), "Expected &: {xml_str}");
666 assert!(xml_str.contains("<"), "Expected <: {xml_str}");
667
668 let mut reader = Reader::from_str(&xml_str);
670 reader.config_mut().trim_text(true);
671 let mut buf = Vec::new();
672 loop {
673 let is_eof = {
674 let ev = reader.read_event_into(&mut buf).expect("XML must parse");
675 matches!(ev, Event::Eof)
676 };
677 buf.clear();
678 if is_eof {
679 break;
680 }
681 }
682 }
683}