Skip to main content

winterbaume_core/protocol/
xml.rs

1//! XML protocol utilities for awsQuery and REST-XML services.
2
3use crate::service::MockResponse;
4
5/// Escape a string for safe inclusion in XML content.
6pub fn xml_escape(s: &str) -> String {
7    s.replace('&', "&")
8        .replace('<', "&lt;")
9        .replace('>', "&gt;")
10        .replace('"', "&quot;")
11        .replace('\'', "&apos;")
12}
13
14/// Wrap inner XML content in a standard awsQuery `<{Action}Response>` envelope.
15///
16/// Produces:
17/// ```xml
18/// <{action}Response xmlns="{namespace}">
19///   <{action}Result>
20///     {inner_xml}
21///   </{action}Result>
22///   <ResponseMetadata>
23///     <RequestId>{request_id}</RequestId>
24///   </ResponseMetadata>
25/// </{action}Response>
26/// ```
27pub fn aws_query_response(action: &str, namespace: &str, inner_xml: &str) -> MockResponse {
28    let request_id = uuid::Uuid::new_v4();
29    let xml = format!(
30        r#"<{action}Response xmlns="{namespace}">
31  <{action}Result>
32    {inner_xml}
33  </{action}Result>
34  <ResponseMetadata>
35    <RequestId>{request_id}</RequestId>
36  </ResponseMetadata>
37</{action}Response>"#,
38    );
39    MockResponse::xml(200, xml)
40}
41
42/// Create an awsQuery error response with a namespace.
43///
44/// Produces:
45/// ```xml
46/// <ErrorResponse xmlns="{namespace}">
47///   <Error>
48///     <Type>Sender</Type>
49///     <Code>{code}</Code>
50///     <Message>{message}</Message>
51///   </Error>
52///   <RequestId>{request_id}</RequestId>
53/// </ErrorResponse>
54/// ```
55pub fn aws_query_error_response(
56    status: u16,
57    code: &str,
58    message: &str,
59    namespace: &str,
60) -> MockResponse {
61    let request_id = uuid::Uuid::new_v4();
62    let xml = format!(
63        r#"<ErrorResponse xmlns="{namespace}">
64  <Error>
65    <Type>Sender</Type>
66    <Code>{code}</Code>
67    <Message>{message}</Message>
68  </Error>
69  <RequestId>{request_id}</RequestId>
70</ErrorResponse>"#,
71        namespace = xml_escape(namespace),
72        code = xml_escape(code),
73        message = xml_escape(message),
74    );
75    MockResponse::xml(status, xml)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_xml_escape() {
84        assert_eq!(xml_escape("a&b"), "a&amp;b");
85        assert_eq!(xml_escape("<tag>"), "&lt;tag&gt;");
86        assert_eq!(xml_escape(r#""quoted""#), "&quot;quoted&quot;");
87        assert_eq!(xml_escape("it's"), "it&apos;s");
88        assert_eq!(xml_escape("plain"), "plain");
89    }
90
91    #[test]
92    fn test_aws_query_response() {
93        let resp = aws_query_response(
94            "CreateUser",
95            "https://iam.amazonaws.com/doc/2010-05-08/",
96            "<User><UserName>test</UserName></User>",
97        );
98        assert_eq!(resp.status, 200);
99        let body = std::str::from_utf8(&resp.body).unwrap();
100        assert!(body.contains("<CreateUserResponse"));
101        assert!(body.contains("<CreateUserResult>"));
102        assert!(body.contains("<User><UserName>test</UserName></User>"));
103        assert!(body.contains("<RequestId>"));
104    }
105
106    #[test]
107    fn test_aws_query_error_response() {
108        let resp = aws_query_error_response(
109            400,
110            "EntityAlreadyExists",
111            "User already exists",
112            "https://iam.amazonaws.com/doc/2010-05-08/",
113        );
114        assert_eq!(resp.status, 400);
115        let body = std::str::from_utf8(&resp.body).unwrap();
116        assert!(body.contains("<Code>EntityAlreadyExists</Code>"));
117        assert!(body.contains("<Message>User already exists</Message>"));
118    }
119}