Skip to main content

fakecloud_cloudformation/
xml_responses.rs

1use crate::state::{Stack, StackResource};
2
3use fakecloud_aws::xml::xml_escape;
4
5pub fn create_stack_response(stack_id: &str, request_id: &str) -> String {
6    format!(
7        r#"<CreateStackResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
8  <CreateStackResult>
9    <StackId>{stack_id}</StackId>
10  </CreateStackResult>
11  <ResponseMetadata>
12    <RequestId>{request_id}</RequestId>
13  </ResponseMetadata>
14</CreateStackResponse>"#,
15        stack_id = xml_escape(stack_id),
16        request_id = xml_escape(request_id),
17    )
18}
19
20pub fn update_stack_response(stack_id: &str, request_id: &str) -> String {
21    format!(
22        r#"<UpdateStackResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
23  <UpdateStackResult>
24    <StackId>{stack_id}</StackId>
25  </UpdateStackResult>
26  <ResponseMetadata>
27    <RequestId>{request_id}</RequestId>
28  </ResponseMetadata>
29</UpdateStackResponse>"#,
30        stack_id = xml_escape(stack_id),
31        request_id = xml_escape(request_id),
32    )
33}
34
35pub fn delete_stack_response(request_id: &str) -> String {
36    format!(
37        r#"<DeleteStackResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
38  <ResponseMetadata>
39    <RequestId>{request_id}</RequestId>
40  </ResponseMetadata>
41</DeleteStackResponse>"#,
42        request_id = xml_escape(request_id),
43    )
44}
45
46pub fn describe_stacks_response(stacks: &[Stack], request_id: &str) -> String {
47    let members: String = stacks
48        .iter()
49        .map(stack_member_xml)
50        .collect::<Vec<_>>()
51        .join("\n");
52
53    format!(
54        r#"<DescribeStacksResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
55  <DescribeStacksResult>
56    <Stacks>
57{members}
58    </Stacks>
59  </DescribeStacksResult>
60  <ResponseMetadata>
61    <RequestId>{request_id}</RequestId>
62  </ResponseMetadata>
63</DescribeStacksResponse>"#,
64        request_id = xml_escape(request_id),
65    )
66}
67
68fn stack_member_xml(stack: &Stack) -> String {
69    let tags_xml = if stack.tags.is_empty() {
70        String::new()
71    } else {
72        let tags: String = stack
73            .tags
74            .iter()
75            .map(|(k, v)| {
76                format!(
77                    "          <member>\n            <Key>{}</Key>\n            <Value>{}</Value>\n          </member>",
78                    xml_escape(k),
79                    xml_escape(v),
80                )
81            })
82            .collect::<Vec<_>>()
83            .join("\n");
84        format!("\n        <Tags>\n{tags}\n        </Tags>")
85    };
86
87    let params_xml = if stack.parameters.is_empty() {
88        String::new()
89    } else {
90        let params: String = stack
91            .parameters
92            .iter()
93            .map(|(k, v)| {
94                format!(
95                    "          <member>\n            <ParameterKey>{}</ParameterKey>\n            <ParameterValue>{}</ParameterValue>\n          </member>",
96                    xml_escape(k),
97                    xml_escape(v),
98                )
99            })
100            .collect::<Vec<_>>()
101            .join("\n");
102        format!("\n        <Parameters>\n{params}\n        </Parameters>")
103    };
104
105    let description_xml = stack
106        .description
107        .as_ref()
108        .map(|d| format!("\n        <Description>{}</Description>", xml_escape(d)))
109        .unwrap_or_default();
110
111    let notification_arns_xml = if stack.notification_arns.is_empty() {
112        String::new()
113    } else {
114        let members: String = stack
115            .notification_arns
116            .iter()
117            .map(|arn| format!("          <member>{}</member>", xml_escape(arn)))
118            .collect::<Vec<_>>()
119            .join("\n");
120        format!("\n        <NotificationARNs>\n{members}\n        </NotificationARNs>")
121    };
122
123    format!(
124        r#"      <member>
125        <StackName>{name}</StackName>
126        <StackId>{id}</StackId>
127        <StackStatus>{status}</StackStatus>
128        <CreationTime>{created}</CreationTime>{description_xml}{tags_xml}{params_xml}{notification_arns_xml}
129      </member>"#,
130        name = xml_escape(&stack.name),
131        id = xml_escape(&stack.stack_id),
132        status = xml_escape(&stack.status),
133        created = stack.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
134    )
135}
136
137pub fn list_stacks_response(stacks: &[Stack], request_id: &str) -> String {
138    let summaries: String = stacks
139        .iter()
140        .map(|s| {
141            format!(
142                r#"      <member>
143        <StackName>{name}</StackName>
144        <StackId>{id}</StackId>
145        <StackStatus>{status}</StackStatus>
146        <CreationTime>{created}</CreationTime>
147      </member>"#,
148                name = xml_escape(&s.name),
149                id = xml_escape(&s.stack_id),
150                status = xml_escape(&s.status),
151                created = s.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
152            )
153        })
154        .collect::<Vec<_>>()
155        .join("\n");
156
157    format!(
158        r#"<ListStacksResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
159  <ListStacksResult>
160    <StackSummaries>
161{summaries}
162    </StackSummaries>
163  </ListStacksResult>
164  <ResponseMetadata>
165    <RequestId>{request_id}</RequestId>
166  </ResponseMetadata>
167</ListStacksResponse>"#,
168        request_id = xml_escape(request_id),
169    )
170}
171
172pub fn list_stack_resources_response(resources: &[StackResource], request_id: &str) -> String {
173    let summaries: String = resources
174        .iter()
175        .map(stack_resource_summary_xml)
176        .collect::<Vec<_>>()
177        .join("\n");
178
179    format!(
180        r#"<ListStackResourcesResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
181  <ListStackResourcesResult>
182    <StackResourceSummaries>
183{summaries}
184    </StackResourceSummaries>
185  </ListStackResourcesResult>
186  <ResponseMetadata>
187    <RequestId>{request_id}</RequestId>
188  </ResponseMetadata>
189</ListStackResourcesResponse>"#,
190        request_id = xml_escape(request_id),
191    )
192}
193
194fn stack_resource_summary_xml(resource: &StackResource) -> String {
195    format!(
196        r#"      <member>
197        <LogicalResourceId>{logical_id}</LogicalResourceId>
198        <PhysicalResourceId>{physical_id}</PhysicalResourceId>
199        <ResourceType>{resource_type}</ResourceType>
200        <ResourceStatus>{status}</ResourceStatus>
201      </member>"#,
202        logical_id = xml_escape(&resource.logical_id),
203        physical_id = xml_escape(&resource.physical_id),
204        resource_type = xml_escape(&resource.resource_type),
205        status = xml_escape(&resource.status),
206    )
207}
208
209pub fn describe_stack_resources_response(
210    resources: &[StackResource],
211    stack_name: &str,
212    request_id: &str,
213) -> String {
214    let members: String = resources
215        .iter()
216        .map(|r| {
217            format!(
218                r#"      <member>
219        <StackName>{stack_name}</StackName>
220        <LogicalResourceId>{logical_id}</LogicalResourceId>
221        <PhysicalResourceId>{physical_id}</PhysicalResourceId>
222        <ResourceType>{resource_type}</ResourceType>
223        <ResourceStatus>{status}</ResourceStatus>
224      </member>"#,
225                stack_name = xml_escape(stack_name),
226                logical_id = xml_escape(&r.logical_id),
227                physical_id = xml_escape(&r.physical_id),
228                resource_type = xml_escape(&r.resource_type),
229                status = xml_escape(&r.status),
230            )
231        })
232        .collect::<Vec<_>>()
233        .join("\n");
234
235    format!(
236        r#"<DescribeStackResourcesResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
237  <DescribeStackResourcesResult>
238    <StackResources>
239{members}
240    </StackResources>
241  </DescribeStackResourcesResult>
242  <ResponseMetadata>
243    <RequestId>{request_id}</RequestId>
244  </ResponseMetadata>
245</DescribeStackResourcesResponse>"#,
246        request_id = xml_escape(request_id),
247    )
248}
249
250pub fn get_template_response(template_body: &str, request_id: &str) -> String {
251    format!(
252        r#"<GetTemplateResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
253  <GetTemplateResult>
254    <TemplateBody>{template_body}</TemplateBody>
255  </GetTemplateResult>
256  <ResponseMetadata>
257    <RequestId>{request_id}</RequestId>
258  </ResponseMetadata>
259</GetTemplateResponse>"#,
260        template_body = xml_escape(template_body),
261        request_id = xml_escape(request_id),
262    )
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use chrono::Utc;
269    use std::collections::HashMap;
270
271    fn make_stack(name: &str) -> Stack {
272        Stack {
273            name: name.to_string(),
274            stack_id: format!("arn:aws:cloudformation:us-east-1:123456789012:stack/{name}/abc123"),
275            template: "{}".to_string(),
276            status: "CREATE_COMPLETE".to_string(),
277            resources: vec![],
278            parameters: HashMap::new(),
279            tags: HashMap::new(),
280            created_at: Utc::now(),
281            updated_at: None,
282            description: None,
283            notification_arns: vec![],
284        }
285    }
286
287    fn make_resource(logical_id: &str, resource_type: &str) -> StackResource {
288        StackResource {
289            logical_id: logical_id.to_string(),
290            physical_id: format!("phys-{logical_id}"),
291            resource_type: resource_type.to_string(),
292            status: "CREATE_COMPLETE".to_string(),
293            service_token: None,
294        }
295    }
296
297    #[test]
298    fn create_stack_response_contains_stack_id() {
299        let xml = create_stack_response("stack-123", "req-1");
300        assert!(xml.contains("<StackId>stack-123</StackId>"));
301        assert!(xml.contains("<RequestId>req-1</RequestId>"));
302        assert!(xml.contains("CreateStackResponse"));
303    }
304
305    #[test]
306    fn update_stack_response_contains_stack_id() {
307        let xml = update_stack_response("stack-456", "req-2");
308        assert!(xml.contains("<StackId>stack-456</StackId>"));
309        assert!(xml.contains("UpdateStackResponse"));
310    }
311
312    #[test]
313    fn delete_stack_response_format() {
314        let xml = delete_stack_response("req-3");
315        assert!(xml.contains("<RequestId>req-3</RequestId>"));
316        assert!(xml.contains("DeleteStackResponse"));
317    }
318
319    #[test]
320    fn describe_stacks_response_lists_stacks() {
321        let s1 = make_stack("my-stack");
322        let xml = describe_stacks_response(&[s1], "req-4");
323        assert!(xml.contains("<StackName>my-stack</StackName>"));
324        assert!(xml.contains("CREATE_COMPLETE"));
325        assert!(xml.contains("DescribeStacksResponse"));
326    }
327
328    #[test]
329    fn describe_stacks_with_tags_and_params() {
330        let mut stack = make_stack("tagged-stack");
331        stack.tags.insert("env".to_string(), "prod".to_string());
332        stack
333            .parameters
334            .insert("Param1".to_string(), "Value1".to_string());
335        stack.description = Some("My stack desc".to_string());
336
337        let xml = describe_stacks_response(&[stack], "req-5");
338        assert!(xml.contains("<Key>env</Key>"));
339        assert!(xml.contains("<Value>prod</Value>"));
340        assert!(xml.contains("<ParameterKey>Param1</ParameterKey>"));
341        assert!(xml.contains("<Description>My stack desc</Description>"));
342    }
343
344    #[test]
345    fn list_stacks_response_lists_summaries() {
346        let stacks = vec![make_stack("s1"), make_stack("s2")];
347        let xml = list_stacks_response(&stacks, "req-6");
348        assert!(xml.contains("<StackName>s1</StackName>"));
349        assert!(xml.contains("<StackName>s2</StackName>"));
350        assert!(xml.contains("ListStacksResponse"));
351    }
352
353    #[test]
354    fn list_stack_resources_response_format() {
355        let resources = vec![
356            make_resource("MyBucket", "AWS::S3::Bucket"),
357            make_resource("MyTable", "AWS::DynamoDB::Table"),
358        ];
359        let xml = list_stack_resources_response(&resources, "req-7");
360        assert!(xml.contains("<LogicalResourceId>MyBucket</LogicalResourceId>"));
361        assert!(xml.contains("<ResourceType>AWS::S3::Bucket</ResourceType>"));
362        assert!(xml.contains("<LogicalResourceId>MyTable</LogicalResourceId>"));
363        assert!(xml.contains("ListStackResourcesResponse"));
364    }
365
366    #[test]
367    fn describe_stack_resources_response_includes_stack_name() {
368        let resources = vec![make_resource("Fn", "AWS::Lambda::Function")];
369        let xml = describe_stack_resources_response(&resources, "my-stack", "req-8");
370        assert!(xml.contains("<StackName>my-stack</StackName>"));
371        assert!(xml.contains("<LogicalResourceId>Fn</LogicalResourceId>"));
372        assert!(xml.contains("DescribeStackResourcesResponse"));
373    }
374
375    #[test]
376    fn get_template_response_contains_body() {
377        let xml = get_template_response(r#"{"AWSTemplateFormatVersion":"2010-09-09"}"#, "req-9");
378        assert!(xml.contains("AWSTemplateFormatVersion"));
379        assert!(xml.contains("GetTemplateResponse"));
380    }
381
382    #[test]
383    fn xml_escaping_works() {
384        let xml = create_stack_response("stack<>\"&'123", "req");
385        assert!(xml.contains("&lt;"));
386        assert!(xml.contains("&gt;"));
387        assert!(xml.contains("&amp;"));
388    }
389
390    #[test]
391    fn describe_stacks_with_notification_arns() {
392        let mut stack = make_stack("notif-stack");
393        stack
394            .notification_arns
395            .push("arn:aws:sns:us-east-1:123456789012:my-topic".to_string());
396        let xml = describe_stacks_response(&[stack], "req-10");
397        assert!(xml.contains("<NotificationARNs>"));
398        assert!(xml.contains("my-topic"));
399    }
400
401    #[test]
402    fn empty_stacks_produces_valid_xml() {
403        let xml = describe_stacks_response(&[], "req-11");
404        assert!(xml.contains("<Stacks>"));
405        assert!(xml.contains("</Stacks>"));
406    }
407}