fakecloud_cloudformation/
xml_responses.rs1use 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 let outputs_xml = if stack.outputs.is_empty() {
124 String::new()
125 } else {
126 let members: String = stack
127 .outputs
128 .iter()
129 .map(|o| {
130 let desc = o
131 .description
132 .as_ref()
133 .map(|d| format!("\n <Description>{}</Description>", xml_escape(d)))
134 .unwrap_or_default();
135 let export = o
136 .export_name
137 .as_ref()
138 .map(|n| format!("\n <ExportName>{}</ExportName>", xml_escape(n)))
139 .unwrap_or_default();
140 format!(
141 " <member>\n <OutputKey>{}</OutputKey>\n <OutputValue>{}</OutputValue>{}{}\n </member>",
142 xml_escape(&o.key),
143 xml_escape(&o.value),
144 desc,
145 export,
146 )
147 })
148 .collect::<Vec<_>>()
149 .join("\n");
150 format!("\n <Outputs>\n{members}\n </Outputs>")
151 };
152
153 format!(
154 r#" <member>
155 <StackName>{name}</StackName>
156 <StackId>{id}</StackId>
157 <StackStatus>{status}</StackStatus>
158 <CreationTime>{created}</CreationTime>{description_xml}{tags_xml}{params_xml}{notification_arns_xml}{outputs_xml}
159 </member>"#,
160 name = xml_escape(&stack.name),
161 id = xml_escape(&stack.stack_id),
162 status = xml_escape(&stack.status),
163 created = stack.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
164 )
165}
166
167pub fn list_stacks_response(stacks: &[Stack], request_id: &str) -> String {
168 let summaries: String = stacks
169 .iter()
170 .map(|s| {
171 format!(
172 r#" <member>
173 <StackName>{name}</StackName>
174 <StackId>{id}</StackId>
175 <StackStatus>{status}</StackStatus>
176 <CreationTime>{created}</CreationTime>
177 </member>"#,
178 name = xml_escape(&s.name),
179 id = xml_escape(&s.stack_id),
180 status = xml_escape(&s.status),
181 created = s.created_at.format("%Y-%m-%dT%H:%M:%SZ"),
182 )
183 })
184 .collect::<Vec<_>>()
185 .join("\n");
186
187 format!(
188 r#"<ListStacksResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
189 <ListStacksResult>
190 <StackSummaries>
191{summaries}
192 </StackSummaries>
193 </ListStacksResult>
194 <ResponseMetadata>
195 <RequestId>{request_id}</RequestId>
196 </ResponseMetadata>
197</ListStacksResponse>"#,
198 request_id = xml_escape(request_id),
199 )
200}
201
202pub fn list_stack_resources_response(resources: &[StackResource], request_id: &str) -> String {
203 let summaries: String = resources
204 .iter()
205 .map(stack_resource_summary_xml)
206 .collect::<Vec<_>>()
207 .join("\n");
208
209 format!(
210 r#"<ListStackResourcesResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
211 <ListStackResourcesResult>
212 <StackResourceSummaries>
213{summaries}
214 </StackResourceSummaries>
215 </ListStackResourcesResult>
216 <ResponseMetadata>
217 <RequestId>{request_id}</RequestId>
218 </ResponseMetadata>
219</ListStackResourcesResponse>"#,
220 request_id = xml_escape(request_id),
221 )
222}
223
224fn stack_resource_summary_xml(resource: &StackResource) -> String {
225 format!(
226 r#" <member>
227 <LogicalResourceId>{logical_id}</LogicalResourceId>
228 <PhysicalResourceId>{physical_id}</PhysicalResourceId>
229 <ResourceType>{resource_type}</ResourceType>
230 <ResourceStatus>{status}</ResourceStatus>
231 </member>"#,
232 logical_id = xml_escape(&resource.logical_id),
233 physical_id = xml_escape(&resource.physical_id),
234 resource_type = xml_escape(&resource.resource_type),
235 status = xml_escape(&resource.status),
236 )
237}
238
239pub fn describe_stack_resources_response(
240 resources: &[StackResource],
241 stack_name: &str,
242 request_id: &str,
243) -> String {
244 let members: String = resources
245 .iter()
246 .map(|r| {
247 format!(
248 r#" <member>
249 <StackName>{stack_name}</StackName>
250 <LogicalResourceId>{logical_id}</LogicalResourceId>
251 <PhysicalResourceId>{physical_id}</PhysicalResourceId>
252 <ResourceType>{resource_type}</ResourceType>
253 <ResourceStatus>{status}</ResourceStatus>
254 </member>"#,
255 stack_name = xml_escape(stack_name),
256 logical_id = xml_escape(&r.logical_id),
257 physical_id = xml_escape(&r.physical_id),
258 resource_type = xml_escape(&r.resource_type),
259 status = xml_escape(&r.status),
260 )
261 })
262 .collect::<Vec<_>>()
263 .join("\n");
264
265 format!(
266 r#"<DescribeStackResourcesResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
267 <DescribeStackResourcesResult>
268 <StackResources>
269{members}
270 </StackResources>
271 </DescribeStackResourcesResult>
272 <ResponseMetadata>
273 <RequestId>{request_id}</RequestId>
274 </ResponseMetadata>
275</DescribeStackResourcesResponse>"#,
276 request_id = xml_escape(request_id),
277 )
278}
279
280pub fn get_template_response(template_body: &str, request_id: &str) -> String {
281 format!(
282 r#"<GetTemplateResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
283 <GetTemplateResult>
284 <TemplateBody>{template_body}</TemplateBody>
285 </GetTemplateResult>
286 <ResponseMetadata>
287 <RequestId>{request_id}</RequestId>
288 </ResponseMetadata>
289</GetTemplateResponse>"#,
290 template_body = xml_escape(template_body),
291 request_id = xml_escape(request_id),
292 )
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use chrono::Utc;
299 use fakecloud_aws::arn::Arn;
300 use std::collections::BTreeMap;
301
302 fn make_stack(name: &str) -> Stack {
303 Stack {
304 name: name.to_string(),
305 stack_id: Arn::new(
306 "cloudformation",
307 "us-east-1",
308 "123456789012",
309 &format!("stack/{name}/abc123"),
310 )
311 .to_string(),
312 template: "{}".to_string(),
313 status: "CREATE_COMPLETE".to_string(),
314 resources: vec![],
315 parameters: BTreeMap::new(),
316 tags: BTreeMap::new(),
317 created_at: Utc::now(),
318 updated_at: None,
319 description: None,
320 notification_arns: vec![],
321 outputs: vec![],
322 }
323 }
324
325 fn make_resource(logical_id: &str, resource_type: &str) -> StackResource {
326 StackResource {
327 logical_id: logical_id.to_string(),
328 physical_id: format!("phys-{logical_id}"),
329 resource_type: resource_type.to_string(),
330 status: "CREATE_COMPLETE".to_string(),
331 service_token: None,
332 attributes: std::collections::BTreeMap::new(),
333 }
334 }
335
336 #[test]
337 fn create_stack_response_contains_stack_id() {
338 let xml = create_stack_response("stack-123", "req-1");
339 assert!(xml.contains("<StackId>stack-123</StackId>"));
340 assert!(xml.contains("<RequestId>req-1</RequestId>"));
341 assert!(xml.contains("CreateStackResponse"));
342 }
343
344 #[test]
345 fn update_stack_response_contains_stack_id() {
346 let xml = update_stack_response("stack-456", "req-2");
347 assert!(xml.contains("<StackId>stack-456</StackId>"));
348 assert!(xml.contains("UpdateStackResponse"));
349 }
350
351 #[test]
352 fn delete_stack_response_format() {
353 let xml = delete_stack_response("req-3");
354 assert!(xml.contains("<RequestId>req-3</RequestId>"));
355 assert!(xml.contains("DeleteStackResponse"));
356 }
357
358 #[test]
359 fn describe_stacks_response_lists_stacks() {
360 let s1 = make_stack("my-stack");
361 let xml = describe_stacks_response(&[s1], "req-4");
362 assert!(xml.contains("<StackName>my-stack</StackName>"));
363 assert!(xml.contains("CREATE_COMPLETE"));
364 assert!(xml.contains("DescribeStacksResponse"));
365 }
366
367 #[test]
368 fn describe_stacks_with_tags_and_params() {
369 let mut stack = make_stack("tagged-stack");
370 stack.tags.insert("env".to_string(), "prod".to_string());
371 stack
372 .parameters
373 .insert("Param1".to_string(), "Value1".to_string());
374 stack.description = Some("My stack desc".to_string());
375
376 let xml = describe_stacks_response(&[stack], "req-5");
377 assert!(xml.contains("<Key>env</Key>"));
378 assert!(xml.contains("<Value>prod</Value>"));
379 assert!(xml.contains("<ParameterKey>Param1</ParameterKey>"));
380 assert!(xml.contains("<Description>My stack desc</Description>"));
381 }
382
383 #[test]
384 fn list_stacks_response_lists_summaries() {
385 let stacks = vec![make_stack("s1"), make_stack("s2")];
386 let xml = list_stacks_response(&stacks, "req-6");
387 assert!(xml.contains("<StackName>s1</StackName>"));
388 assert!(xml.contains("<StackName>s2</StackName>"));
389 assert!(xml.contains("ListStacksResponse"));
390 }
391
392 #[test]
393 fn list_stack_resources_response_format() {
394 let resources = vec![
395 make_resource("MyBucket", "AWS::S3::Bucket"),
396 make_resource("MyTable", "AWS::DynamoDB::Table"),
397 ];
398 let xml = list_stack_resources_response(&resources, "req-7");
399 assert!(xml.contains("<LogicalResourceId>MyBucket</LogicalResourceId>"));
400 assert!(xml.contains("<ResourceType>AWS::S3::Bucket</ResourceType>"));
401 assert!(xml.contains("<LogicalResourceId>MyTable</LogicalResourceId>"));
402 assert!(xml.contains("ListStackResourcesResponse"));
403 }
404
405 #[test]
406 fn describe_stack_resources_response_includes_stack_name() {
407 let resources = vec![make_resource("Fn", "AWS::Lambda::Function")];
408 let xml = describe_stack_resources_response(&resources, "my-stack", "req-8");
409 assert!(xml.contains("<StackName>my-stack</StackName>"));
410 assert!(xml.contains("<LogicalResourceId>Fn</LogicalResourceId>"));
411 assert!(xml.contains("DescribeStackResourcesResponse"));
412 }
413
414 #[test]
415 fn get_template_response_contains_body() {
416 let xml = get_template_response(r#"{"AWSTemplateFormatVersion":"2010-09-09"}"#, "req-9");
417 assert!(xml.contains("AWSTemplateFormatVersion"));
418 assert!(xml.contains("GetTemplateResponse"));
419 }
420
421 #[test]
422 fn xml_escaping_works() {
423 let xml = create_stack_response("stack<>\"&'123", "req");
424 assert!(xml.contains("<"));
425 assert!(xml.contains(">"));
426 assert!(xml.contains("&"));
427 }
428
429 #[test]
430 fn describe_stacks_with_notification_arns() {
431 let mut stack = make_stack("notif-stack");
432 stack
433 .notification_arns
434 .push("arn:aws:sns:us-east-1:123456789012:my-topic".to_string());
435 let xml = describe_stacks_response(&[stack], "req-10");
436 assert!(xml.contains("<NotificationARNs>"));
437 assert!(xml.contains("my-topic"));
438 }
439
440 #[test]
441 fn empty_stacks_produces_valid_xml() {
442 let xml = describe_stacks_response(&[], "req-11");
443 assert!(xml.contains("<Stacks>"));
444 assert!(xml.contains("</Stacks>"));
445 }
446}