Skip to main content

fakecloud_cloudformation/
service.rs

1use async_trait::async_trait;
2use chrono::Utc;
3use http::StatusCode;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7use fakecloud_core::delivery::DeliveryBus;
8use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
9use fakecloud_dynamodb::state::SharedDynamoDbState;
10use fakecloud_eventbridge::state::SharedEventBridgeState;
11use fakecloud_iam::state::SharedIamState;
12use fakecloud_logs::state::SharedLogsState;
13use fakecloud_s3::state::SharedS3State;
14use fakecloud_sns::state::SharedSnsState;
15use fakecloud_sqs::state::SharedSqsState;
16use fakecloud_ssm::state::SharedSsmState;
17
18use crate::resource_provisioner::ResourceProvisioner;
19use crate::state::{SharedCloudFormationState, Stack};
20use crate::template;
21use crate::xml_responses;
22
23pub struct CloudFormationService {
24    state: SharedCloudFormationState,
25    sqs_state: SharedSqsState,
26    sns_state: SharedSnsState,
27    ssm_state: SharedSsmState,
28    iam_state: SharedIamState,
29    s3_state: SharedS3State,
30    eventbridge_state: SharedEventBridgeState,
31    dynamodb_state: SharedDynamoDbState,
32    logs_state: SharedLogsState,
33    delivery: Arc<DeliveryBus>,
34}
35
36impl CloudFormationService {
37    #[allow(clippy::too_many_arguments)]
38    pub fn new(
39        state: SharedCloudFormationState,
40        sqs_state: SharedSqsState,
41        sns_state: SharedSnsState,
42        ssm_state: SharedSsmState,
43        iam_state: SharedIamState,
44        s3_state: SharedS3State,
45        eventbridge_state: SharedEventBridgeState,
46        dynamodb_state: SharedDynamoDbState,
47        logs_state: SharedLogsState,
48        delivery: Arc<DeliveryBus>,
49    ) -> Self {
50        Self {
51            state,
52            sqs_state,
53            sns_state,
54            ssm_state,
55            iam_state,
56            s3_state,
57            eventbridge_state,
58            dynamodb_state,
59            logs_state,
60            delivery,
61        }
62    }
63
64    fn provisioner(&self, stack_id: &str) -> ResourceProvisioner {
65        let cf_state = self.state.read();
66        ResourceProvisioner {
67            sqs_state: self.sqs_state.clone(),
68            sns_state: self.sns_state.clone(),
69            ssm_state: self.ssm_state.clone(),
70            iam_state: self.iam_state.clone(),
71            s3_state: self.s3_state.clone(),
72            eventbridge_state: self.eventbridge_state.clone(),
73            dynamodb_state: self.dynamodb_state.clone(),
74            logs_state: self.logs_state.clone(),
75            delivery: self.delivery.clone(),
76            account_id: cf_state.account_id.clone(),
77            region: cf_state.region.clone(),
78            stack_id: stack_id.to_string(),
79        }
80    }
81
82    fn get_param(req: &AwsRequest, key: &str) -> Option<String> {
83        // Check query params first (for Query protocol)
84        if let Some(v) = req.query_params.get(key) {
85            return Some(v.clone());
86        }
87        // Then check form-encoded body
88        let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
89        body_params.get(key).cloned()
90    }
91
92    fn get_all_params(req: &AwsRequest) -> HashMap<String, String> {
93        let mut params = req.query_params.clone();
94        let body_params = fakecloud_core::protocol::parse_query_body(&req.body);
95        for (k, v) in body_params {
96            params.entry(k).or_insert(v);
97        }
98        params
99    }
100
101    fn extract_tags(params: &HashMap<String, String>) -> HashMap<String, String> {
102        let mut tags = HashMap::new();
103        for i in 1.. {
104            let key_param = format!("Tags.member.{i}.Key");
105            let value_param = format!("Tags.member.{i}.Value");
106            match (params.get(&key_param), params.get(&value_param)) {
107                (Some(k), Some(v)) => {
108                    tags.insert(k.clone(), v.clone());
109                }
110                _ => break,
111            }
112        }
113        tags
114    }
115
116    fn extract_parameters(params: &HashMap<String, String>) -> HashMap<String, String> {
117        let mut result = HashMap::new();
118        for i in 1.. {
119            let key_param = format!("Parameters.member.{i}.ParameterKey");
120            let value_param = format!("Parameters.member.{i}.ParameterValue");
121            match (params.get(&key_param), params.get(&value_param)) {
122                (Some(k), Some(v)) => {
123                    result.insert(k.clone(), v.clone());
124                }
125                _ => break,
126            }
127        }
128        result
129    }
130
131    fn extract_notification_arns(params: &HashMap<String, String>) -> Vec<String> {
132        let mut arns = Vec::new();
133        for i in 1.. {
134            let key = format!("NotificationARNs.member.{i}");
135            match params.get(&key) {
136                Some(arn) => arns.push(arn.clone()),
137                None => break,
138            }
139        }
140        arns
141    }
142
143    fn send_stack_notification(
144        delivery: &DeliveryBus,
145        notification_arns: &[String],
146        stack_name: &str,
147        stack_id: &str,
148        status: &str,
149    ) {
150        if notification_arns.is_empty() {
151            return;
152        }
153        let message = format!(
154            "StackId='{}'\nTimestamp='{}'\nEventId='{}'\nLogicalResourceId='{}'\nResourceStatus='{}'\nResourceType='AWS::CloudFormation::Stack'\nStackName='{}'",
155            stack_id,
156            chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
157            uuid::Uuid::new_v4(),
158            stack_name,
159            status,
160            stack_name,
161        );
162        for arn in notification_arns {
163            delivery.publish_to_sns(arn, &message, Some("AWS CloudFormation Notification"));
164        }
165    }
166
167    fn create_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
168        let params = Self::get_all_params(req);
169
170        let stack_name = params.get("StackName").ok_or_else(|| {
171            AwsServiceError::aws_error(
172                StatusCode::BAD_REQUEST,
173                "ValidationError",
174                "StackName is required",
175            )
176        })?;
177
178        let template_body = params.get("TemplateBody").ok_or_else(|| {
179            AwsServiceError::aws_error(
180                StatusCode::BAD_REQUEST,
181                "ValidationError",
182                "TemplateBody is required",
183            )
184        })?;
185
186        // Check if stack already exists and is not deleted
187        {
188            let state = self.state.read();
189            if let Some(existing) = state.stacks.get(stack_name.as_str()) {
190                if existing.status != "DELETE_COMPLETE" {
191                    return Err(AwsServiceError::aws_error(
192                        StatusCode::BAD_REQUEST,
193                        "AlreadyExistsException",
194                        format!("Stack [{stack_name}] already exists"),
195                    ));
196                }
197            }
198        }
199
200        let tags = Self::extract_tags(&params);
201        let parameters = Self::extract_parameters(&params);
202        let notification_arns = Self::extract_notification_arns(&params);
203
204        // First pass: parse to get resource definitions (without physical ID resolution)
205        let parsed = template::parse_template(template_body, &parameters).map_err(|e| {
206            AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
207        })?;
208
209        let stack_id = {
210            let state = self.state.read();
211            format!(
212                "arn:aws:cloudformation:{}:{}:stack/{}/{}",
213                state.region,
214                state.account_id,
215                stack_name,
216                uuid::Uuid::new_v4()
217            )
218        };
219
220        let provisioner = self.provisioner(&stack_id);
221        let mut resources = Vec::new();
222        let mut physical_ids: HashMap<String, String> = HashMap::new();
223
224        // Create resources incrementally, re-resolving Refs with known physical IDs.
225        // Use multi-pass to handle dependency ordering (resources may reference each
226        // other via Ref, and JSON object key order is not guaranteed).
227        let mut pending: Vec<&template::ResourceDefinition> = parsed.resources.iter().collect();
228        let max_passes = pending.len() + 1;
229        for _ in 0..max_passes {
230            if pending.is_empty() {
231                break;
232            }
233            let mut still_pending = Vec::new();
234            let mut made_progress = false;
235
236            for resource_def in pending {
237                let resolved_def = template::resolve_resource_properties(
238                    resource_def,
239                    template_body,
240                    &parameters,
241                    &physical_ids,
242                )
243                .map_err(|e| {
244                    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
245                })?;
246
247                match provisioner.create_resource(&resolved_def) {
248                    Ok(stack_resource) => {
249                        physical_ids.insert(
250                            stack_resource.logical_id.clone(),
251                            stack_resource.physical_id.clone(),
252                        );
253                        resources.push(stack_resource);
254                        made_progress = true;
255                    }
256                    Err(_) => {
257                        still_pending.push(resource_def);
258                    }
259                }
260            }
261
262            pending = still_pending;
263            if !made_progress && !pending.is_empty() {
264                // No progress made — report the first failure
265                let resource_def = pending[0];
266                let resolved_def = template::resolve_resource_properties(
267                    resource_def,
268                    template_body,
269                    &parameters,
270                    &physical_ids,
271                )
272                .unwrap_or_else(|_| resource_def.clone());
273                let err = provisioner.create_resource(&resolved_def).unwrap_err();
274                // Rollback
275                for r in &resources {
276                    let _ = provisioner.delete_resource(r);
277                }
278                return Err(AwsServiceError::aws_error(
279                    StatusCode::BAD_REQUEST,
280                    "ValidationError",
281                    format!(
282                        "Failed to create resource {}: {err}",
283                        resource_def.logical_id
284                    ),
285                ));
286            }
287        }
288
289        let stack = Stack {
290            name: stack_name.clone(),
291            stack_id: stack_id.clone(),
292            template: template_body.clone(),
293            status: "CREATE_COMPLETE".to_string(),
294            resources,
295            parameters,
296            tags,
297            created_at: Utc::now(),
298            updated_at: None,
299            description: parsed.description,
300            notification_arns: notification_arns.clone(),
301        };
302
303        {
304            let mut state = self.state.write();
305            state.stacks.insert(stack_name.clone(), stack);
306        }
307
308        Self::send_stack_notification(
309            &self.delivery,
310            &notification_arns,
311            stack_name,
312            &stack_id,
313            "CREATE_COMPLETE",
314        );
315
316        Ok(AwsResponse::xml(
317            StatusCode::OK,
318            xml_responses::create_stack_response(&stack_id, &req.request_id),
319        ))
320    }
321
322    fn delete_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
323        let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
324            AwsServiceError::aws_error(
325                StatusCode::BAD_REQUEST,
326                "ValidationError",
327                "StackName is required",
328            )
329        })?;
330
331        let mut state = self.state.write();
332
333        // Find stack by name or stack ID
334        let stack = state.stacks.values_mut().find(|s| {
335            (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
336        });
337
338        if let Some(stack) = stack {
339            let stack_id = stack.stack_id.clone();
340            let stack_name_for_notif = stack.name.clone();
341            let notification_arns = stack.notification_arns.clone();
342            let resources: Vec<_> = stack.resources.clone();
343
344            // Build the provisioner while we still have the stack_id
345            // Drop the write lock temporarily so the provisioner can read state
346            drop(state);
347            let provisioner = self.provisioner(&stack_id);
348
349            // Delete resources in reverse order
350            for resource in resources.iter().rev() {
351                let _ = provisioner.delete_resource(resource);
352            }
353
354            // Re-acquire the write lock to update stack status
355            let mut state = self.state.write();
356            if let Some(stack) = state.stacks.values_mut().find(|s| s.stack_id == stack_id) {
357                stack.status = "DELETE_COMPLETE".to_string();
358                stack.resources.clear();
359            }
360            drop(state);
361
362            Self::send_stack_notification(
363                &self.delivery,
364                &notification_arns,
365                &stack_name_for_notif,
366                &stack_id,
367                "DELETE_COMPLETE",
368            );
369        }
370
371        Ok(AwsResponse::xml(
372            StatusCode::OK,
373            xml_responses::delete_stack_response(&req.request_id),
374        ))
375    }
376
377    fn describe_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
378        let stack_name = Self::get_param(req, "StackName");
379
380        let state = self.state.read();
381        let stacks: Vec<Stack> = if let Some(ref name) = stack_name {
382            state
383                .stacks
384                .values()
385                .filter(|s| {
386                    (s.name == *name || s.stack_id == *name) && s.status != "DELETE_COMPLETE"
387                })
388                .cloned()
389                .collect()
390        } else {
391            state
392                .stacks
393                .values()
394                .filter(|s| s.status != "DELETE_COMPLETE")
395                .cloned()
396                .collect()
397        };
398
399        if let Some(ref name) = stack_name {
400            if stacks.is_empty() {
401                return Err(AwsServiceError::aws_error(
402                    StatusCode::BAD_REQUEST,
403                    "ValidationError",
404                    format!("Stack with id {name} does not exist"),
405                ));
406            }
407        }
408
409        Ok(AwsResponse::xml(
410            StatusCode::OK,
411            xml_responses::describe_stacks_response(&stacks, &req.request_id),
412        ))
413    }
414
415    fn list_stacks(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
416        let state = self.state.read();
417        let stacks: Vec<Stack> = state.stacks.values().cloned().collect();
418
419        Ok(AwsResponse::xml(
420            StatusCode::OK,
421            xml_responses::list_stacks_response(&stacks, &req.request_id),
422        ))
423    }
424
425    fn list_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
426        let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
427            AwsServiceError::aws_error(
428                StatusCode::BAD_REQUEST,
429                "ValidationError",
430                "StackName is required",
431            )
432        })?;
433
434        let state = self.state.read();
435        let stack = state
436            .stacks
437            .values()
438            .find(|s| {
439                (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
440            })
441            .ok_or_else(|| {
442                AwsServiceError::aws_error(
443                    StatusCode::BAD_REQUEST,
444                    "ValidationError",
445                    format!("Stack [{stack_name}] does not exist"),
446                )
447            })?;
448
449        Ok(AwsResponse::xml(
450            StatusCode::OK,
451            xml_responses::list_stack_resources_response(&stack.resources, &req.request_id),
452        ))
453    }
454
455    fn describe_stack_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
456        let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
457            AwsServiceError::aws_error(
458                StatusCode::BAD_REQUEST,
459                "ValidationError",
460                "StackName is required",
461            )
462        })?;
463
464        let state = self.state.read();
465        let stack = state
466            .stacks
467            .values()
468            .find(|s| {
469                (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
470            })
471            .ok_or_else(|| {
472                AwsServiceError::aws_error(
473                    StatusCode::BAD_REQUEST,
474                    "ValidationError",
475                    format!("Stack [{stack_name}] does not exist"),
476                )
477            })?;
478
479        Ok(AwsResponse::xml(
480            StatusCode::OK,
481            xml_responses::describe_stack_resources_response(
482                &stack.resources,
483                &stack.name,
484                &req.request_id,
485            ),
486        ))
487    }
488
489    fn update_stack(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
490        let params = Self::get_all_params(req);
491
492        let stack_name = params.get("StackName").ok_or_else(|| {
493            AwsServiceError::aws_error(
494                StatusCode::BAD_REQUEST,
495                "ValidationError",
496                "StackName is required",
497            )
498        })?;
499
500        let template_body = params.get("TemplateBody").ok_or_else(|| {
501            AwsServiceError::aws_error(
502                StatusCode::BAD_REQUEST,
503                "ValidationError",
504                "TemplateBody is required",
505            )
506        })?;
507
508        let new_parameters = Self::extract_parameters(&params);
509        let new_tags = Self::extract_tags(&params);
510        let new_notification_arns = Self::extract_notification_arns(&params);
511
512        let parsed = template::parse_template(template_body, &new_parameters).map_err(|e| {
513            AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
514        })?;
515
516        // Get stack_id before write lock for the provisioner
517        let found_stack_id = {
518            let state = self.state.read();
519            state
520                .stacks
521                .values()
522                .find(|s| {
523                    (s.name == *stack_name || s.stack_id == *stack_name)
524                        && s.status != "DELETE_COMPLETE"
525                })
526                .map(|s| s.stack_id.clone())
527                .unwrap_or_default()
528        };
529
530        let provisioner = self.provisioner(&found_stack_id);
531
532        let mut state = self.state.write();
533        let stack = state
534            .stacks
535            .values_mut()
536            .find(|s| {
537                (s.name == *stack_name || s.stack_id == *stack_name)
538                    && s.status != "DELETE_COMPLETE"
539            })
540            .ok_or_else(|| {
541                AwsServiceError::aws_error(
542                    StatusCode::BAD_REQUEST,
543                    "ValidationError",
544                    format!("Stack [{stack_name}] does not exist"),
545                )
546            })?;
547
548        // Determine which resources to add and remove
549        let old_logical_ids: std::collections::HashSet<String> = stack
550            .resources
551            .iter()
552            .map(|r| r.logical_id.clone())
553            .collect();
554        let new_logical_ids: std::collections::HashSet<String> = parsed
555            .resources
556            .iter()
557            .map(|r| r.logical_id.clone())
558            .collect();
559
560        // Delete resources that are no longer in the template
561        let to_remove: Vec<_> = stack
562            .resources
563            .iter()
564            .filter(|r| !new_logical_ids.contains(&r.logical_id))
565            .cloned()
566            .collect();
567        for resource in &to_remove {
568            let _ = provisioner.delete_resource(resource);
569        }
570        stack
571            .resources
572            .retain(|r| new_logical_ids.contains(&r.logical_id));
573
574        // Build physical ID map from existing resources
575        let mut physical_ids: HashMap<String, String> = stack
576            .resources
577            .iter()
578            .map(|r| (r.logical_id.clone(), r.physical_id.clone()))
579            .collect();
580
581        // Create new resources
582        let mut update_failed = false;
583        let mut update_error_msg = String::new();
584        for resource_def in &parsed.resources {
585            if !old_logical_ids.contains(&resource_def.logical_id) {
586                let resolved_def = match template::resolve_resource_properties(
587                    resource_def,
588                    template_body,
589                    &new_parameters,
590                    &physical_ids,
591                ) {
592                    Ok(d) => d,
593                    Err(e) => {
594                        update_failed = true;
595                        update_error_msg = format!(
596                            "Failed to resolve resource {}: {e}",
597                            resource_def.logical_id
598                        );
599                        continue;
600                    }
601                };
602                match provisioner.create_resource(&resolved_def) {
603                    Ok(stack_resource) => {
604                        physical_ids.insert(
605                            stack_resource.logical_id.clone(),
606                            stack_resource.physical_id.clone(),
607                        );
608                        stack.resources.push(stack_resource);
609                    }
610                    Err(e) => {
611                        tracing::warn!(
612                            "Failed to create resource {} during update: {e}",
613                            resource_def.logical_id
614                        );
615                        update_failed = true;
616                        update_error_msg =
617                            format!("Failed to create resource {}: {e}", resource_def.logical_id);
618                    }
619                }
620            }
621        }
622
623        let stack_id = stack.stack_id.clone();
624        stack.template = template_body.clone();
625        stack.status = if update_failed {
626            "UPDATE_FAILED".to_string()
627        } else {
628            "UPDATE_COMPLETE".to_string()
629        };
630        stack.parameters = new_parameters;
631        if !new_tags.is_empty() {
632            stack.tags = new_tags;
633        }
634        stack.updated_at = Some(Utc::now());
635        stack.description = parsed.description;
636        if !new_notification_arns.is_empty() {
637            stack.notification_arns = new_notification_arns;
638        }
639        let notification_arns = stack.notification_arns.clone();
640        let stack_name_for_notif = stack.name.clone();
641
642        if update_failed {
643            drop(state);
644            Self::send_stack_notification(
645                &self.delivery,
646                &notification_arns,
647                &stack_name_for_notif,
648                &stack_id,
649                "UPDATE_FAILED",
650            );
651            return Err(AwsServiceError::aws_error(
652                StatusCode::BAD_REQUEST,
653                "ValidationError",
654                update_error_msg,
655            ));
656        }
657
658        drop(state);
659        Self::send_stack_notification(
660            &self.delivery,
661            &notification_arns,
662            &stack_name_for_notif,
663            &stack_id,
664            "UPDATE_COMPLETE",
665        );
666
667        Ok(AwsResponse::xml(
668            StatusCode::OK,
669            xml_responses::update_stack_response(&stack_id, &req.request_id),
670        ))
671    }
672
673    fn get_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
674        let stack_name = Self::get_param(req, "StackName").ok_or_else(|| {
675            AwsServiceError::aws_error(
676                StatusCode::BAD_REQUEST,
677                "ValidationError",
678                "StackName is required",
679            )
680        })?;
681
682        let state = self.state.read();
683        let stack = state
684            .stacks
685            .values()
686            .find(|s| {
687                (s.name == stack_name || s.stack_id == stack_name) && s.status != "DELETE_COMPLETE"
688            })
689            .ok_or_else(|| {
690                AwsServiceError::aws_error(
691                    StatusCode::BAD_REQUEST,
692                    "ValidationError",
693                    format!("Stack [{stack_name}] does not exist"),
694                )
695            })?;
696
697        Ok(AwsResponse::xml(
698            StatusCode::OK,
699            xml_responses::get_template_response(&stack.template, &req.request_id),
700        ))
701    }
702}
703
704#[async_trait]
705impl AwsService for CloudFormationService {
706    fn service_name(&self) -> &str {
707        "cloudformation"
708    }
709
710    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
711        match req.action.as_str() {
712            "CreateStack" => self.create_stack(&req),
713            "DeleteStack" => self.delete_stack(&req),
714            "DescribeStacks" => self.describe_stacks(&req),
715            "ListStacks" => self.list_stacks(&req),
716            "ListStackResources" => self.list_stack_resources(&req),
717            "DescribeStackResources" => self.describe_stack_resources(&req),
718            "UpdateStack" => self.update_stack(&req),
719            "GetTemplate" => self.get_template(&req),
720            _ => Err(AwsServiceError::action_not_implemented(
721                "cloudformation",
722                &req.action,
723            )),
724        }
725    }
726
727    fn supported_actions(&self) -> &[&str] {
728        &[
729            "CreateStack",
730            "DeleteStack",
731            "DescribeStacks",
732            "ListStacks",
733            "ListStackResources",
734            "DescribeStackResources",
735            "UpdateStack",
736            "GetTemplate",
737        ]
738    }
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744    use crate::state::CloudFormationState;
745    use http::HeaderMap;
746    use parking_lot::RwLock;
747    use std::sync::Arc;
748
749    fn make_service() -> CloudFormationService {
750        let cf_state = Arc::new(RwLock::new(CloudFormationState::new(
751            "123456789012",
752            "us-east-1",
753        )));
754        CloudFormationService::new(
755            cf_state,
756            Arc::new(RwLock::new(fakecloud_sqs::state::SqsState::new(
757                "123456789012",
758                "us-east-1",
759                "http://localhost:4566",
760            ))),
761            Arc::new(RwLock::new(fakecloud_sns::state::SnsState::new(
762                "123456789012",
763                "us-east-1",
764                "http://localhost:4566",
765            ))),
766            Arc::new(RwLock::new(fakecloud_ssm::state::SsmState::new(
767                "123456789012",
768                "us-east-1",
769            ))),
770            Arc::new(RwLock::new(fakecloud_iam::state::IamState::new(
771                "123456789012",
772            ))),
773            Arc::new(RwLock::new(fakecloud_s3::state::S3State::new(
774                "123456789012",
775                "us-east-1",
776            ))),
777            Arc::new(RwLock::new(
778                fakecloud_eventbridge::state::EventBridgeState::new("123456789012", "us-east-1"),
779            )),
780            Arc::new(RwLock::new(fakecloud_dynamodb::state::DynamoDbState::new(
781                "123456789012",
782                "us-east-1",
783            ))),
784            Arc::new(RwLock::new(fakecloud_logs::state::LogsState::new(
785                "123456789012",
786                "us-east-1",
787            ))),
788            Arc::new(DeliveryBus::new()),
789        )
790    }
791
792    fn make_request(action: &str, params: HashMap<String, String>) -> AwsRequest {
793        AwsRequest {
794            service: "cloudformation".to_string(),
795            action: action.to_string(),
796            region: "us-east-1".to_string(),
797            account_id: "123456789012".to_string(),
798            request_id: "test-request-id".to_string(),
799            headers: HeaderMap::new(),
800            query_params: params,
801            body: bytes::Bytes::new(),
802            path_segments: vec![],
803            raw_path: "/".to_string(),
804            raw_query: String::new(),
805            method: http::Method::POST,
806            is_query_protocol: true,
807            access_key_id: None,
808        }
809    }
810
811    #[test]
812    fn update_stack_sets_failed_status_on_resource_error() {
813        let svc = make_service();
814
815        // Create a stack with just a queue
816        let mut create_params = HashMap::new();
817        create_params.insert("StackName".to_string(), "test-stack".to_string());
818        create_params.insert(
819            "TemplateBody".to_string(),
820            r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}}}}"#.to_string(),
821        );
822        let req = make_request("CreateStack", create_params);
823        let result = svc.create_stack(&req);
824        assert!(result.is_ok());
825
826        // Update stack adding an SNS subscription with a non-existent topic
827        let mut update_params = HashMap::new();
828        update_params.insert("StackName".to_string(), "test-stack".to_string());
829        update_params.insert(
830            "TemplateBody".to_string(),
831            r#"{"Resources":{"MyQueue":{"Type":"AWS::SQS::Queue","Properties":{"QueueName":"q1"}},"BadSub":{"Type":"AWS::SNS::Subscription","Properties":{"TopicArn":"arn:aws:sns:us-east-1:123456789012:nope","Protocol":"sqs","Endpoint":"arn:aws:sqs:us-east-1:123456789012:q1"}}}}"#.to_string(),
832        );
833        let req = make_request("UpdateStack", update_params);
834        let result = svc.update_stack(&req);
835
836        // Should return an error
837        assert!(result.is_err());
838
839        // Stack status should be UPDATE_FAILED
840        let state = svc.state.read();
841        let stack = state.stacks.get("test-stack").unwrap();
842        assert_eq!(stack.status, "UPDATE_FAILED");
843    }
844
845    #[test]
846    fn create_stack_resolves_ref_to_physical_id() {
847        let svc = make_service();
848
849        // Template where subscription Refs the topic
850        let template = r#"{
851            "Resources": {
852                "MyTopic": {
853                    "Type": "AWS::SNS::Topic",
854                    "Properties": { "TopicName": "ref-test-topic" }
855                },
856                "MySub": {
857                    "Type": "AWS::SNS::Subscription",
858                    "Properties": {
859                        "TopicArn": { "Ref": "MyTopic" },
860                        "Protocol": "sqs",
861                        "Endpoint": "arn:aws:sqs:us-east-1:123456789012:some-queue"
862                    }
863                }
864            }
865        }"#;
866
867        let mut params = HashMap::new();
868        params.insert("StackName".to_string(), "ref-stack".to_string());
869        params.insert("TemplateBody".to_string(), template.to_string());
870        let req = make_request("CreateStack", params);
871        let result = svc.create_stack(&req);
872        assert!(result.is_ok(), "CreateStack failed: {:?}", result.err());
873
874        // Verify both resources were created
875        let state = svc.state.read();
876        let stack = state.stacks.get("ref-stack").unwrap();
877        assert_eq!(stack.resources.len(), 2);
878        assert_eq!(stack.status, "CREATE_COMPLETE");
879
880        // The subscription's physical ID should be an ARN (not just "MyTopic")
881        let sub = stack
882            .resources
883            .iter()
884            .find(|r| r.logical_id == "MySub")
885            .unwrap();
886        assert!(
887            sub.physical_id.contains("ref-test-topic"),
888            "Subscription physical ID should reference the topic ARN, got: {}",
889            sub.physical_id
890        );
891    }
892}