1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use chrono::Utc;
6use http::StatusCode;
7use serde_json::{json, Value};
8use tokio::sync::Mutex as AsyncMutex;
9
10use fakecloud_core::delivery::DeliveryBus;
11use fakecloud_core::pagination::paginate_checked;
12use fakecloud_core::registry::ServiceRegistry;
13use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
14use fakecloud_core::validation::*;
15use fakecloud_dynamodb::SharedDynamoDbState;
16use fakecloud_persistence::SnapshotStore;
17
18use crate::interpreter;
19use crate::state::{
20 Execution, ExecutionStatus, SharedStepFunctionsState, StateMachine, StateMachineStatus,
21 StateMachineType, StepFunctionsSnapshot, StepFunctionsState,
22 STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION,
23};
24
25const SUPPORTED: &[&str] = &[
26 "CreateStateMachine",
27 "DescribeStateMachine",
28 "ListStateMachines",
29 "DeleteStateMachine",
30 "UpdateStateMachine",
31 "TagResource",
32 "UntagResource",
33 "ListTagsForResource",
34 "StartExecution",
35 "StopExecution",
36 "DescribeExecution",
37 "ListExecutions",
38 "GetExecutionHistory",
39 "DescribeStateMachineForExecution",
40 "CreateActivity",
41 "DeleteActivity",
42 "DescribeActivity",
43 "ListActivities",
44 "GetActivityTask",
45 "SendTaskFailure",
46 "SendTaskHeartbeat",
47 "SendTaskSuccess",
48 "PublishStateMachineVersion",
49 "DeleteStateMachineVersion",
50 "ListStateMachineVersions",
51 "CreateStateMachineAlias",
52 "DeleteStateMachineAlias",
53 "DescribeStateMachineAlias",
54 "ListStateMachineAliases",
55 "UpdateStateMachineAlias",
56 "DescribeMapRun",
57 "ListMapRuns",
58 "UpdateMapRun",
59 "RedriveExecution",
60 "StartSyncExecution",
61 "TestState",
62 "ValidateStateMachineDefinition",
63];
64
65pub type SharedServiceRegistry = Arc<std::sync::OnceLock<Arc<ServiceRegistry>>>;
71
72pub struct StepFunctionsService {
73 state: SharedStepFunctionsState,
74 delivery: Option<Arc<DeliveryBus>>,
75 dynamodb_state: Option<SharedDynamoDbState>,
76 registry: Option<SharedServiceRegistry>,
77 snapshot_store: Option<Arc<dyn SnapshotStore>>,
78 snapshot_lock: Arc<AsyncMutex<()>>,
79}
80
81mod activities;
82mod executions;
83mod map_runs;
84mod state_machines;
85mod tags;
86mod tasks;
87mod validation;
88
89impl StepFunctionsService {
90 pub fn new(state: SharedStepFunctionsState) -> Self {
91 Self {
92 state,
93 delivery: None,
94 dynamodb_state: None,
95 registry: None,
96 snapshot_store: None,
97 snapshot_lock: Arc::new(AsyncMutex::new(())),
98 }
99 }
100
101 pub fn with_delivery(mut self, delivery: Arc<DeliveryBus>) -> Self {
102 self.delivery = Some(delivery);
103 self
104 }
105
106 pub fn with_dynamodb(mut self, dynamodb_state: SharedDynamoDbState) -> Self {
107 self.dynamodb_state = Some(dynamodb_state);
108 self
109 }
110
111 pub fn with_registry(mut self, registry: SharedServiceRegistry) -> Self {
116 self.registry = Some(registry);
117 self
118 }
119
120 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
121 self.snapshot_store = Some(store);
122 self
123 }
124
125 async fn save_snapshot(&self) {
126 save_stepfunctions_snapshot(
127 &self.state,
128 self.snapshot_store.clone(),
129 &self.snapshot_lock,
130 )
131 .await;
132 }
133
134 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
140 let store = self.snapshot_store.clone()?;
141 let state = self.state.clone();
142 let lock = self.snapshot_lock.clone();
143 Some(Arc::new(move || {
144 let state = state.clone();
145 let store = store.clone();
146 let lock = lock.clone();
147 Box::pin(async move {
148 save_stepfunctions_snapshot(&state, Some(store), &lock).await;
149 })
150 }))
151 }
152}
153
154pub async fn save_stepfunctions_snapshot(
160 state: &SharedStepFunctionsState,
161 store: Option<Arc<dyn SnapshotStore>>,
162 lock: &AsyncMutex<()>,
163) {
164 let Some(store) = store else {
165 return;
166 };
167 let _guard = lock.lock().await;
168 let snapshot = StepFunctionsSnapshot {
169 schema_version: STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION,
170 state: None,
171 accounts: Some(state.read().clone()),
172 };
173 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
174 let bytes = serde_json::to_vec(&snapshot)
175 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
176 store.save(&bytes)
177 })
178 .await;
179 match join {
180 Ok(Ok(())) => {}
181 Ok(Err(err)) => tracing::error!(%err, "failed to write stepfunctions snapshot"),
182 Err(err) => tracing::error!(%err, "stepfunctions snapshot task panicked"),
183 }
184}
185
186pub fn reconcile_interrupted_executions(state: &SharedStepFunctionsState) -> usize {
196 let now = Utc::now();
197 let mut count = 0;
198 let mut accounts = state.write();
199 let account_ids: Vec<String> = accounts.iter().map(|(id, _)| id.to_string()).collect();
200 for account_id in account_ids {
201 let Some(s) = accounts.get_mut(&account_id) else {
202 continue;
203 };
204 for exec in s.executions.values_mut() {
205 if matches!(
206 exec.status,
207 ExecutionStatus::Running | ExecutionStatus::PendingRedrive
208 ) {
209 exec.status = ExecutionStatus::Aborted;
210 exec.stop_date = Some(now);
211 exec.error = Some("Fakecloud.Restart".to_string());
212 exec.cause = Some(
213 "Execution was interrupted by a fakecloud restart and cannot be resumed"
214 .to_string(),
215 );
216 count += 1;
217 }
218 }
219 }
220 count
221}
222
223fn is_mutating_action(action: &str) -> bool {
224 matches!(
225 action,
226 "CreateStateMachine"
227 | "DeleteStateMachine"
228 | "UpdateStateMachine"
229 | "TagResource"
230 | "UntagResource"
231 | "StartExecution"
232 | "StopExecution"
233 | "CreateActivity"
234 | "DeleteActivity"
235 | "GetActivityTask"
236 | "SendTaskFailure"
237 | "SendTaskHeartbeat"
238 | "SendTaskSuccess"
239 | "PublishStateMachineVersion"
240 | "DeleteStateMachineVersion"
241 | "CreateStateMachineAlias"
242 | "DeleteStateMachineAlias"
243 | "UpdateStateMachineAlias"
244 | "UpdateMapRun"
245 | "RedriveExecution"
246 | "StartSyncExecution"
247 )
248}
249
250#[async_trait]
251impl AwsService for StepFunctionsService {
252 fn service_name(&self) -> &str {
253 "states"
254 }
255
256 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
257 let mutates = is_mutating_action(req.action.as_str());
258 let result = match req.action.as_str() {
259 "CreateStateMachine" => self.create_state_machine(&req),
260 "DescribeStateMachine" => self.describe_state_machine(&req),
261 "ListStateMachines" => self.list_state_machines(&req),
262 "DeleteStateMachine" => self.delete_state_machine(&req),
263 "UpdateStateMachine" => self.update_state_machine(&req),
264 "TagResource" => self.tag_resource(&req),
265 "UntagResource" => self.untag_resource(&req),
266 "ListTagsForResource" => self.list_tags_for_resource(&req),
267 "StartExecution" => self.start_execution(&req),
268 "StopExecution" => self.stop_execution(&req),
269 "DescribeExecution" => self.describe_execution(&req),
270 "ListExecutions" => self.list_executions(&req),
271 "GetExecutionHistory" => self.get_execution_history(&req),
272 "DescribeStateMachineForExecution" => self.describe_state_machine_for_execution(&req),
273 "CreateActivity" => self.create_activity(&req),
274 "DeleteActivity" => self.delete_activity(&req),
275 "DescribeActivity" => self.describe_activity(&req),
276 "ListActivities" => self.list_activities(&req),
277 "GetActivityTask" => self.get_activity_task(&req).await,
278 "SendTaskFailure" => self.send_task_failure(&req),
279 "SendTaskHeartbeat" => self.send_task_heartbeat(&req),
280 "SendTaskSuccess" => self.send_task_success(&req),
281 "PublishStateMachineVersion" => self.publish_state_machine_version(&req),
282 "DeleteStateMachineVersion" => self.delete_state_machine_version(&req),
283 "ListStateMachineVersions" => self.list_state_machine_versions(&req),
284 "CreateStateMachineAlias" => self.create_state_machine_alias(&req),
285 "DeleteStateMachineAlias" => self.delete_state_machine_alias(&req),
286 "DescribeStateMachineAlias" => self.describe_state_machine_alias(&req),
287 "ListStateMachineAliases" => self.list_state_machine_aliases(&req),
288 "UpdateStateMachineAlias" => self.update_state_machine_alias(&req),
289 "DescribeMapRun" => self.describe_map_run(&req),
290 "ListMapRuns" => self.list_map_runs(&req),
291 "UpdateMapRun" => self.update_map_run(&req),
292 "RedriveExecution" => self.redrive_execution(&req),
293 "StartSyncExecution" => self.start_sync_execution(&req).await,
294 "TestState" => self.test_state(&req),
295 "ValidateStateMachineDefinition" => self.validate_state_machine_definition(&req),
296 _ => Err(AwsServiceError::action_not_implemented(
297 "states",
298 &req.action,
299 )),
300 };
301 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
302 self.save_snapshot().await;
303 }
304 result
305 }
306
307 fn supported_actions(&self) -> &[&str] {
308 SUPPORTED
309 }
310}
311
312impl StepFunctionsService {
313 fn list_activities(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
314 let body = req.json_body();
315 let raw_max_results = body["maxResults"].as_i64();
316 if let Some(mr) = raw_max_results {
317 validate_max_results(mr)?;
318 }
319 let next_token = body["nextToken"].as_str();
320 if let Some(t) = next_token {
321 validate_page_token(t)?;
322 }
323 let max_results = match raw_max_results.unwrap_or(0) {
327 0 => 100,
328 n => n as usize,
329 };
330 let accounts = self.state.read();
331 let empty = crate::state::StepFunctionsState::new(&req.account_id, &req.region);
332 let state = accounts.get(&req.account_id).unwrap_or(&empty);
333 let mut activities: Vec<&crate::state::Activity> = state.activities.values().collect();
334 activities.sort_by(|a, b| a.name.cmp(&b.name));
335 let items: Vec<Value> = activities
336 .iter()
337 .map(|a| {
338 json!({
339 "activityArn": a.arn,
340 "name": a.name,
341 "creationDate": a.creation_date.timestamp(),
342 })
343 })
344 .collect();
345 let (page, token) =
346 paginate_checked(&items, next_token, max_results).map_err(|_| invalid_token())?;
347 let mut resp = json!({ "activities": page });
348 if let Some(t) = token {
349 resp["nextToken"] = json!(t);
350 }
351 Ok(AwsResponse::ok_json(resp))
352 }
353}
354
355fn state_machine_alias_to_json(alias: &crate::state::StateMachineAlias) -> Value {
356 json!({
357 "stateMachineAliasArn": alias.arn,
358 "name": alias.name,
359 "description": alias.description,
360 "routingConfiguration": alias.routing_configuration.iter().map(|r| json!({
361 "stateMachineVersionArn": r.state_machine_version_arn,
362 "weight": r.weight,
363 })).collect::<Vec<_>>(),
364 "creationDate": alias.creation_date.timestamp(),
365 "updateDate": alias.update_date.timestamp(),
366 })
367}
368
369fn map_run_to_json(mr: &crate::state::MapRun) -> Value {
370 let accounted = mr.succeeded_count + mr.failed_count;
373 let running = if mr.status == "RUNNING" {
374 (mr.total_count - accounted).max(0)
375 } else {
376 0
377 };
378 let counts = json!({
381 "pending": 0,
382 "running": running,
383 "succeeded": mr.succeeded_count,
384 "failed": mr.failed_count,
385 "timedOut": 0,
386 "aborted": 0,
387 "results": mr.succeeded_count,
388 "total": mr.total_count,
389 "failuresNotRedrivable": 0,
390 "pendingRedrive": 0,
391 });
392 json!({
393 "mapRunArn": mr.map_run_arn,
394 "executionArn": mr.execution_arn,
395 "maxConcurrency": mr.max_concurrency,
396 "toleratedFailurePercentage": mr.tolerated_failure_percentage,
397 "toleratedFailureCount": mr.tolerated_failure_count,
398 "status": mr.status,
399 "startDate": mr.start_date.timestamp(),
400 "stopDate": mr.stop_date.map(|d| d.timestamp()),
401 "itemCounts": counts,
402 "executionCounts": counts,
403 })
404}
405
406fn state_machine_to_json(sm: &StateMachine) -> Value {
409 let mut resp = json!({
410 "name": sm.name,
411 "stateMachineArn": sm.arn,
412 "definition": sm.definition,
413 "roleArn": sm.role_arn,
414 "type": sm.machine_type.as_str(),
415 "status": sm.status.as_str(),
416 "creationDate": sm.creation_date.timestamp() as f64,
417 "updateDate": sm.update_date.timestamp() as f64,
418 "revisionId": sm.revision_id,
419 "label": sm.name,
420 });
421
422 if !sm.description.is_empty() {
423 resp["description"] = json!(sm.description);
424 }
425
426 if let Some(ref logging) = sm.logging_configuration {
427 resp["loggingConfiguration"] = logging.clone();
428 } else {
429 resp["loggingConfiguration"] = json!({
430 "level": "OFF",
431 "includeExecutionData": false,
432 "destinations": [],
433 });
434 }
435
436 if let Some(ref tracing) = sm.tracing_configuration {
437 resp["tracingConfiguration"] = tracing.clone();
438 } else {
439 resp["tracingConfiguration"] = json!({
440 "enabled": false,
441 });
442 }
443
444 resp
445}
446
447fn missing(name: &str) -> AwsServiceError {
448 AwsServiceError::aws_error(
449 StatusCode::BAD_REQUEST,
450 "ValidationException",
451 format!("The request must contain the parameter {name}."),
452 )
453}
454
455fn state_machine_not_found(arn: &str) -> AwsServiceError {
456 AwsServiceError::aws_error(
457 StatusCode::BAD_REQUEST,
458 "StateMachineDoesNotExist",
459 format!("State Machine Does Not Exist: '{arn}'"),
460 )
461}
462
463fn activity_not_found(arn: &str) -> AwsServiceError {
464 AwsServiceError::aws_error(
465 StatusCode::BAD_REQUEST,
466 "ActivityDoesNotExist",
467 format!("Activity does not exist: {arn}"),
468 )
469}
470
471fn task_does_not_exist(token: &str) -> AwsServiceError {
472 AwsServiceError::aws_error(
473 StatusCode::BAD_REQUEST,
474 "TaskDoesNotExist",
475 format!("Task does not exist: {token}"),
476 )
477}
478
479fn resource_not_found(arn: &str) -> AwsServiceError {
480 AwsServiceError::aws_error(
481 StatusCode::BAD_REQUEST,
482 "ResourceNotFound",
483 format!("Resource not found: '{arn}'"),
484 )
485}
486
487fn parse_routing_configuration(
492 routes: &[serde_json::Value],
493) -> Result<Vec<crate::state::AliasRoute>, AwsServiceError> {
494 if routes.is_empty() || routes.len() > 2 {
495 return Err(AwsServiceError::aws_error(
496 StatusCode::BAD_REQUEST,
497 "ValidationException",
498 "routingConfiguration must contain 1 or 2 routes.",
499 ));
500 }
501 let parsed: Vec<crate::state::AliasRoute> = routes
502 .iter()
503 .map(|r| {
504 let arn = r["stateMachineVersionArn"].as_str().ok_or_else(|| {
505 AwsServiceError::aws_error(
506 StatusCode::BAD_REQUEST,
507 "ValidationException",
508 "routingConfiguration entries must contain stateMachineVersionArn.",
509 )
510 })?;
511 let weight = r["weight"].as_i64().ok_or_else(|| {
512 AwsServiceError::aws_error(
513 StatusCode::BAD_REQUEST,
514 "ValidationException",
515 "routingConfiguration entries must contain a numeric weight.",
516 )
517 })?;
518 if !(0..=100).contains(&weight) {
519 return Err(AwsServiceError::aws_error(
520 StatusCode::BAD_REQUEST,
521 "ValidationException",
522 format!("Invalid routing weight {weight}; must be 0-100."),
523 ));
524 }
525 Ok(crate::state::AliasRoute {
526 state_machine_version_arn: arn.to_string(),
527 weight: weight as i32,
528 })
529 })
530 .collect::<Result<_, _>>()?;
531 let total: i32 = parsed.iter().map(|r| r.weight).sum();
532 if total != 100 {
533 return Err(AwsServiceError::aws_error(
534 StatusCode::BAD_REQUEST,
535 "ValidationException",
536 format!("routingConfiguration weights must sum to 100, got {total}."),
537 ));
538 }
539 Ok(parsed)
540}
541
542fn validate_name(name: &str) -> Result<(), AwsServiceError> {
543 if name.is_empty() || name.len() > 80 {
544 return Err(AwsServiceError::aws_error(
545 StatusCode::BAD_REQUEST,
546 "InvalidName",
547 format!("Invalid Name: '{name}' (length must be between 1 and 80 characters)"),
548 ));
549 }
550 if !name
552 .chars()
553 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
554 {
555 return Err(AwsServiceError::aws_error(
556 StatusCode::BAD_REQUEST,
557 "InvalidName",
558 format!(
559 "Invalid Name: '{name}' (must only contain alphanumeric characters, hyphens, and underscores)"
560 ),
561 ));
562 }
563 Ok(())
564}
565
566fn validate_definition(definition: &str) -> Result<(), AwsServiceError> {
567 let parsed: Value = serde_json::from_str(definition).map_err(|e| {
568 AwsServiceError::aws_error(
569 StatusCode::BAD_REQUEST,
570 "InvalidDefinition",
571 format!("Invalid State Machine Definition: '{e}'"),
572 )
573 })?;
574
575 if parsed.get("StartAt").and_then(|v| v.as_str()).is_none() {
576 return Err(AwsServiceError::aws_error(
577 StatusCode::BAD_REQUEST,
578 "InvalidDefinition",
579 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
580 .to_string(),
581 ));
582 }
583
584 let states_obj = parsed
585 .get("States")
586 .and_then(|v| v.as_object())
587 .ok_or_else(|| {
588 AwsServiceError::aws_error(
589 StatusCode::BAD_REQUEST,
590 "InvalidDefinition",
591 "Invalid State Machine Definition: 'MISSING_STATES' (States field is required)"
592 .to_string(),
593 )
594 })?;
595
596 let start_at = parsed["StartAt"].as_str().ok_or_else(|| {
597 AwsServiceError::aws_error(
598 StatusCode::BAD_REQUEST,
599 "InvalidDefinition",
600 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
601 .to_string(),
602 )
603 })?;
604 if !states_obj.contains_key(start_at) {
605 return Err(AwsServiceError::aws_error(
606 StatusCode::BAD_REQUEST,
607 "InvalidDefinition",
608 format!(
609 "Invalid State Machine Definition: 'MISSING_TRANSITION_TARGET' \
610 (StartAt '{start_at}' does not reference a valid state)"
611 ),
612 ));
613 }
614
615 for (state_name, state_val) in states_obj {
619 validate_state_paths(state_name, state_val)?;
620 }
621
622 Ok(())
623}
624
625fn validate_state_paths(state_name: &str, state: &Value) -> Result<(), AwsServiceError> {
630 for field in ["InputPath", "OutputPath", "ResultPath"] {
631 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
632 if (field != "ResultPath" && p == "null") || is_valid_reference_path(p) {
636 continue;
637 }
638 return Err(invalid_reference_path(state_name, field, p));
639 }
640 }
641
642 for field in [
646 "SecondsPath",
647 "TimestampPath",
648 "ItemsPath",
649 "MaxConcurrencyPath",
650 "ToleratedFailureCountPath",
651 "ToleratedFailurePercentagePath",
652 ] {
653 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
654 if is_reference_or_context_path(p) {
655 continue;
656 }
657 return Err(invalid_reference_path(state_name, field, p));
658 }
659 }
660
661 for field in ["Parameters", "ResultSelector", "ItemSelector"] {
665 if let Some(template) = state.get(field) {
666 validate_payload_template(state_name, field, template)?;
667 }
668 }
669
670 match state.get("Type").and_then(|v| v.as_str()) {
671 Some("Choice") => {
672 if let Some(choices) = state.get("Choices").and_then(|v| v.as_array()) {
673 for rule in choices {
674 validate_choice_variables(state_name, rule)?;
675 }
676 }
677 }
678 Some("Parallel") => {
679 if let Some(branches) = state.get("Branches").and_then(|v| v.as_array()) {
680 for branch in branches {
681 validate_nested_definition(state_name, branch)?;
682 }
683 }
684 }
685 Some("Map") => {
686 for key in ["ItemProcessor", "Iterator"] {
689 if let Some(processor) = state.get(key) {
690 validate_nested_definition(state_name, processor)?;
691 }
692 }
693 }
694 _ => {}
695 }
696
697 Ok(())
698}
699
700fn validate_nested_definition(parent: &str, def: &Value) -> Result<(), AwsServiceError> {
704 if let Some(states) = def.get("States").and_then(|v| v.as_object()) {
705 for (name, state) in states {
706 validate_state_paths(&format!("{parent}/{name}"), state)?;
707 }
708 }
709 Ok(())
710}
711
712fn validate_payload_template(
716 state_name: &str,
717 field: &str,
718 template: &Value,
719) -> Result<(), AwsServiceError> {
720 match template {
721 Value::Object(map) => {
722 for (key, value) in map {
723 if let Some(stripped) = key.strip_suffix(".$") {
724 let expr = value.as_str().ok_or_else(|| {
725 invalid_payload_template(
726 state_name,
727 field,
728 &format!(
729 "the '{key}' field must be a JSONPath or intrinsic string, \
730 not {value}"
731 ),
732 )
733 })?;
734 let is_ref = expr.starts_with('$');
735 if !is_ref && !crate::intrinsics::is_intrinsic_call(expr) {
736 return Err(invalid_payload_template(
737 state_name,
738 field,
739 &format!(
740 "the '{stripped}.$' field value '{expr}' is neither a JSONPath \
741 reference nor an intrinsic function"
742 ),
743 ));
744 }
745 } else {
746 validate_payload_template(state_name, field, value)?;
747 }
748 }
749 }
750 Value::Array(arr) => {
751 for v in arr {
752 validate_payload_template(state_name, field, v)?;
753 }
754 }
755 _ => {}
756 }
757 Ok(())
758}
759
760fn is_reference_or_context_path(p: &str) -> bool {
763 if let Some(rest) = p.strip_prefix("$$") {
764 return rest.is_empty() || rest.starts_with('.') || rest.starts_with('[');
766 }
767 is_valid_reference_path(p)
768}
769
770fn invalid_payload_template(state_name: &str, field: &str, detail: &str) -> AwsServiceError {
771 AwsServiceError::aws_error(
772 StatusCode::BAD_REQUEST,
773 "InvalidDefinition",
774 format!(
775 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
776 (The {field} of state '{state_name}' is invalid: {detail})"
777 ),
778 )
779}
780
781fn validate_choice_variables(state_name: &str, rule: &Value) -> Result<(), AwsServiceError> {
784 if let Some(v) = rule.get("Variable").and_then(|v| v.as_str()) {
785 if !is_valid_reference_path(v) {
786 return Err(invalid_reference_path(state_name, "Variable", v));
787 }
788 }
789 for combinator in ["And", "Or"] {
790 if let Some(nested) = rule.get(combinator).and_then(|v| v.as_array()) {
791 for n in nested {
792 validate_choice_variables(state_name, n)?;
793 }
794 }
795 }
796 if let Some(n) = rule.get("Not") {
797 validate_choice_variables(state_name, n)?;
798 }
799 Ok(())
800}
801
802fn is_valid_reference_path(path: &str) -> bool {
806 if path != "$" && !path.starts_with("$.") && !path.starts_with("$[") {
807 return false;
808 }
809 let body = path
810 .strip_prefix("$.")
811 .or_else(|| path.strip_prefix('$'))
812 .unwrap_or(path);
813 for part in body.split('.') {
814 if !segment_is_valid(part) {
815 return false;
816 }
817 }
818 true
819}
820
821fn segment_is_valid(part: &str) -> bool {
822 match part.find('[') {
823 None => !part.contains(']'),
824 Some(open) => {
825 if !part.ends_with(']') {
826 return false;
827 }
828 let inner = &part[open + 1..part.len() - 1];
829 !inner.is_empty() && inner.parse::<usize>().is_ok()
831 }
832 }
833}
834
835fn invalid_reference_path(state_name: &str, field: &str, path: &str) -> AwsServiceError {
836 AwsServiceError::aws_error(
837 StatusCode::BAD_REQUEST,
838 "InvalidDefinition",
839 format!(
840 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
841 (The {field} field of state '{state_name}' is not a valid reference path: '{path}')"
842 ),
843 )
844}
845
846fn execution_not_found(arn: &str) -> AwsServiceError {
847 AwsServiceError::aws_error(
848 StatusCode::BAD_REQUEST,
849 "ExecutionDoesNotExist",
850 format!("Execution Does Not Exist: '{arn}'"),
851 )
852}
853
854fn execution_to_json(exec: &Execution) -> Value {
855 let mut resp = json!({
856 "executionArn": exec.execution_arn,
857 "stateMachineArn": exec.state_machine_arn,
858 "name": exec.name,
859 "status": exec.status.as_str(),
860 "startDate": exec.start_date.timestamp() as f64,
861 });
862
863 if let Some(ref input) = exec.input {
864 resp["input"] = json!(input);
865 }
866 if let Some(ref output) = exec.output {
867 resp["output"] = json!(output);
868 }
869 if let Some(stop) = exec.stop_date {
870 resp["stopDate"] = json!(stop.timestamp() as f64);
871 }
872 if let Some(ref error) = exec.error {
873 resp["error"] = json!(error);
874 }
875 if let Some(ref cause) = exec.cause {
876 resp["cause"] = json!(cause);
877 }
878
879 resp
880}
881
882fn camel_to_details_key(event_type: &str) -> String {
892 if event_type.ends_with("StateEntered") {
893 return "stateEntered".to_string();
894 }
895 if event_type.ends_with("StateExited") {
896 return "stateExited".to_string();
897 }
898 let mut chars = event_type.chars();
899 match chars.next() {
900 None => String::new(),
901 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
902 }
903}
904
905fn execution_input_matches(stored: Option<&str>, incoming: Option<&str>) -> bool {
910 let stored = stored.unwrap_or("{}");
911 let incoming = incoming.unwrap_or("{}");
912 match (
913 serde_json::from_str::<Value>(stored),
914 serde_json::from_str::<Value>(incoming),
915 ) {
916 (Ok(a), Ok(b)) => a == b,
917 _ => stored == incoming,
918 }
919}
920
921fn validate_arn(arn: &str) -> Result<(), AwsServiceError> {
922 if !arn.starts_with("arn:") {
923 return Err(AwsServiceError::aws_error(
924 StatusCode::BAD_REQUEST,
925 "InvalidArn",
926 format!("Invalid Arn: '{arn}'"),
927 ));
928 }
929 Ok(())
930}
931
932fn validate_arn_length(field: &str, value: &str, max: usize) -> Result<(), AwsServiceError> {
936 if value.is_empty() || value.len() > max {
937 return Err(AwsServiceError::aws_error(
938 StatusCode::BAD_REQUEST,
939 "InvalidArn",
940 format!("Invalid Arn at '{field}': must be 1..={max} characters"),
941 ));
942 }
943 Ok(())
944}
945
946pub(super) fn invalid_token() -> AwsServiceError {
950 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidToken", "Invalid nextToken")
951}
952
953fn validate_page_token(value: &str) -> Result<(), AwsServiceError> {
957 if value.is_empty() || value.len() > 1024 {
958 return Err(AwsServiceError::aws_error(
959 StatusCode::BAD_REQUEST,
960 "InvalidToken",
961 "nextToken must be 1..=1024 characters",
962 ));
963 }
964 Ok(())
965}
966
967fn validate_max_results(value: i64) -> Result<(), AwsServiceError> {
973 if !(0..=1000).contains(&value) {
974 return Err(AwsServiceError::aws_error(
975 StatusCode::BAD_REQUEST,
976 "InvalidToken",
977 format!("maxResults '{value}' is outside 0..=1000"),
978 ));
979 }
980 Ok(())
981}
982
983pub fn start_execution_from_delivery(
992 state: &SharedStepFunctionsState,
993 delivery: &Option<Arc<DeliveryBus>>,
994 dynamodb_state: &Option<SharedDynamoDbState>,
995 registry: &Option<SharedServiceRegistry>,
996 state_machine_arn: &str,
997 input: &str,
998) {
999 if serde_json::from_str::<serde_json::Value>(input).is_err() {
1001 tracing::warn!(
1002 state_machine_arn,
1003 "Step Functions delivery: invalid JSON input, skipping execution"
1004 );
1005 return;
1006 }
1007
1008 let execution_name = uuid::Uuid::new_v4().to_string();
1009
1010 let account_id = state_machine_arn
1012 .split(':')
1013 .nth(4)
1014 .unwrap_or("000000000000")
1015 .to_string();
1016
1017 let mut accounts = state.write();
1018 let st = accounts.get_or_create(&account_id);
1019 let sm = match st.state_machines.get(state_machine_arn) {
1020 Some(sm) => sm,
1021 None => {
1022 tracing::warn!(
1023 state_machine_arn,
1024 "Step Functions delivery: state machine not found"
1025 );
1026 return;
1027 }
1028 };
1029
1030 let sm_name = sm.name.clone();
1031 let definition = sm.definition.clone();
1032 let exec_arn = st.execution_arn(&sm_name, &execution_name);
1033
1034 let now = Utc::now();
1035 let execution = Execution {
1036 execution_arn: exec_arn.clone(),
1037 state_machine_arn: state_machine_arn.to_string(),
1038 state_machine_name: sm_name,
1039 name: execution_name,
1040 status: ExecutionStatus::Running,
1041 input: Some(input.to_string()),
1042 output: None,
1043 start_date: now,
1044 stop_date: None,
1045 error: None,
1046 cause: None,
1047 history_events: vec![],
1048 parent_execution_arn: None,
1049 is_sync: false,
1050 billed_duration_ms: None,
1051 billed_memory_mb: None,
1052 };
1053
1054 st.executions.insert(exec_arn.clone(), execution);
1055 let logging_config = sm.logging_configuration.clone();
1056 drop(accounts);
1057
1058 let shared_state = state.clone();
1059 let delivery = delivery.clone();
1060 let dynamodb_state = dynamodb_state.clone();
1061 let registry = registry.clone();
1062 let input = Some(input.to_string());
1063 tokio::spawn(async move {
1064 interpreter::execute_state_machine(
1065 shared_state,
1066 exec_arn,
1067 definition,
1068 input,
1069 delivery,
1070 dynamodb_state,
1071 registry,
1072 logging_config,
1073 )
1074 .await;
1075 });
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use http::{HeaderMap, Method};
1082 use parking_lot::RwLock;
1083 use serde_json::Value;
1084 use std::collections::HashMap;
1085 use std::sync::Arc;
1086
1087 fn make_state() -> SharedStepFunctionsState {
1088 Arc::new(RwLock::new(
1089 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
1090 ))
1091 }
1092
1093 fn make_request(action: &str, body: &str) -> AwsRequest {
1094 AwsRequest {
1095 service: "states".to_string(),
1096 action: action.to_string(),
1097 region: "us-east-1".to_string(),
1098 account_id: "123456789012".to_string(),
1099 request_id: "test-id".to_string(),
1100 headers: HeaderMap::new(),
1101 query_params: HashMap::new(),
1102 body: body.as_bytes().to_vec().into(),
1103 body_stream: parking_lot::Mutex::new(None),
1104 path_segments: vec![],
1105 raw_path: "/".to_string(),
1106 raw_query: String::new(),
1107 method: Method::POST,
1108 is_query_protocol: false,
1109 access_key_id: None,
1110 principal: None,
1111 }
1112 }
1113
1114 fn body_json(resp: &AwsResponse) -> Value {
1115 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
1116 }
1117
1118 fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
1119 match result {
1120 Err(e) => e,
1121 Ok(_) => panic!("expected error, got Ok"),
1122 }
1123 }
1124
1125 const VALID_DEF: &str = r#"{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}"#;
1126
1127 fn create_sm(svc: &StepFunctionsService, name: &str) -> String {
1128 let body = json!({
1129 "name": name,
1130 "definition": VALID_DEF,
1131 "roleArn": "arn:aws:iam::123456789012:role/test",
1132 });
1133 let req = make_request("CreateStateMachine", &body.to_string());
1134 let resp = svc.create_state_machine(&req).unwrap();
1135 let b = body_json(&resp);
1136 b["stateMachineArn"].as_str().unwrap().to_string()
1137 }
1138
1139 #[test]
1142 fn create_state_machine_basic() {
1143 let svc = StepFunctionsService::new(make_state());
1144 let arn = create_sm(&svc, "test-sm");
1145 assert!(arn.contains("test-sm"));
1146 }
1147
1148 #[test]
1149 fn create_state_machine_with_express_type() {
1150 let svc = StepFunctionsService::new(make_state());
1151 let body = json!({
1152 "name": "express-sm",
1153 "definition": VALID_DEF,
1154 "roleArn": "arn:aws:iam::123456789012:role/r",
1155 "type": "EXPRESS",
1156 });
1157 let req = make_request("CreateStateMachine", &body.to_string());
1158 let resp = svc.create_state_machine(&req).unwrap();
1159 let b = body_json(&resp);
1160 assert!(b["stateMachineArn"].as_str().is_some());
1161 }
1162
1163 #[test]
1164 fn create_state_machine_duplicate_fails() {
1165 let svc = StepFunctionsService::new(make_state());
1166 create_sm(&svc, "dup-sm");
1167 let body = json!({
1168 "name": "dup-sm",
1169 "definition": VALID_DEF,
1170 "roleArn": "arn:aws:iam::123456789012:role/r",
1171 });
1172 let req = make_request("CreateStateMachine", &body.to_string());
1173 let err = expect_err(svc.create_state_machine(&req));
1174 assert!(err.to_string().contains("StateMachineAlreadyExists"));
1175 }
1176
1177 #[test]
1178 fn create_state_machine_missing_name() {
1179 let svc = StepFunctionsService::new(make_state());
1180 let body = json!({
1181 "definition": VALID_DEF,
1182 "roleArn": "arn:aws:iam::123456789012:role/r",
1183 });
1184 let req = make_request("CreateStateMachine", &body.to_string());
1185 assert!(svc.create_state_machine(&req).is_err());
1186 }
1187
1188 #[test]
1189 fn create_state_machine_invalid_definition() {
1190 let svc = StepFunctionsService::new(make_state());
1191 let body = json!({
1192 "name": "bad-def",
1193 "definition": "not json",
1194 "roleArn": "arn:aws:iam::123456789012:role/r",
1195 });
1196 let req = make_request("CreateStateMachine", &body.to_string());
1197 let err = expect_err(svc.create_state_machine(&req));
1198 assert!(err.to_string().contains("InvalidDefinition"));
1199 }
1200
1201 #[test]
1202 fn create_state_machine_definition_missing_start_at() {
1203 let svc = StepFunctionsService::new(make_state());
1204 let body = json!({
1205 "name": "no-start",
1206 "definition": r#"{"States":{"S":{"Type":"Pass","End":true}}}"#,
1207 "roleArn": "arn:aws:iam::123456789012:role/r",
1208 });
1209 let req = make_request("CreateStateMachine", &body.to_string());
1210 let err = expect_err(svc.create_state_machine(&req));
1211 assert!(err.to_string().contains("InvalidDefinition"));
1212 }
1213
1214 #[test]
1215 fn create_state_machine_definition_missing_states() {
1216 let svc = StepFunctionsService::new(make_state());
1217 let body = json!({
1218 "name": "no-states",
1219 "definition": r#"{"StartAt":"S"}"#,
1220 "roleArn": "arn:aws:iam::123456789012:role/r",
1221 });
1222 let req = make_request("CreateStateMachine", &body.to_string());
1223 let err = expect_err(svc.create_state_machine(&req));
1224 assert!(err.to_string().contains("InvalidDefinition"));
1225 }
1226
1227 #[test]
1228 fn create_state_machine_definition_start_at_not_in_states() {
1229 let svc = StepFunctionsService::new(make_state());
1230 let body = json!({
1231 "name": "bad-start",
1232 "definition": r#"{"StartAt":"Missing","States":{"S":{"Type":"Pass","End":true}}}"#,
1233 "roleArn": "arn:aws:iam::123456789012:role/r",
1234 });
1235 let req = make_request("CreateStateMachine", &body.to_string());
1236 let err = expect_err(svc.create_state_machine(&req));
1237 assert!(err.to_string().contains("MISSING_TRANSITION_TARGET"));
1238 }
1239
1240 #[test]
1241 fn create_state_machine_invalid_type() {
1242 let svc = StepFunctionsService::new(make_state());
1243 let body = json!({
1244 "name": "bad-type",
1245 "definition": VALID_DEF,
1246 "roleArn": "arn:aws:iam::123456789012:role/r",
1247 "type": "INVALID",
1248 });
1249 let req = make_request("CreateStateMachine", &body.to_string());
1250 assert!(svc.create_state_machine(&req).is_err());
1251 }
1252
1253 #[test]
1254 fn create_state_machine_invalid_arn() {
1255 let svc = StepFunctionsService::new(make_state());
1256 let body = json!({
1257 "name": "bad-arn",
1258 "definition": VALID_DEF,
1259 "roleArn": "not-an-arn",
1260 });
1261 let req = make_request("CreateStateMachine", &body.to_string());
1262 let err = expect_err(svc.create_state_machine(&req));
1263 assert!(err.to_string().contains("InvalidArn"));
1264 }
1265
1266 #[test]
1267 fn create_state_machine_invalid_name() {
1268 let svc = StepFunctionsService::new(make_state());
1269 let body = json!({
1270 "name": "has spaces!",
1271 "definition": VALID_DEF,
1272 "roleArn": "arn:aws:iam::123456789012:role/r",
1273 });
1274 let req = make_request("CreateStateMachine", &body.to_string());
1275 let err = expect_err(svc.create_state_machine(&req));
1276 assert!(err.to_string().contains("InvalidName"));
1277 }
1278
1279 #[test]
1280 fn create_state_machine_name_too_long() {
1281 let svc = StepFunctionsService::new(make_state());
1282 let long_name = "a".repeat(81);
1283 let body = json!({
1284 "name": long_name,
1285 "definition": VALID_DEF,
1286 "roleArn": "arn:aws:iam::123456789012:role/r",
1287 });
1288 let req = make_request("CreateStateMachine", &body.to_string());
1289 let err = expect_err(svc.create_state_machine(&req));
1290 assert!(err.to_string().contains("InvalidName"));
1291 }
1292
1293 #[test]
1296 fn describe_state_machine_found() {
1297 let svc = StepFunctionsService::new(make_state());
1298 let arn = create_sm(&svc, "desc-sm");
1299
1300 let req = make_request(
1301 "DescribeStateMachine",
1302 &json!({"stateMachineArn": arn}).to_string(),
1303 );
1304 let resp = svc.describe_state_machine(&req).unwrap();
1305 let b = body_json(&resp);
1306 assert_eq!(b["name"], "desc-sm");
1307 assert_eq!(b["status"], "ACTIVE");
1308 assert!(b["definition"].as_str().is_some());
1309 }
1310
1311 #[test]
1312 fn describe_state_machine_not_found() {
1313 let svc = StepFunctionsService::new(make_state());
1314 let req = make_request(
1315 "DescribeStateMachine",
1316 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1317 .to_string(),
1318 );
1319 let err = expect_err(svc.describe_state_machine(&req));
1320 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1321 }
1322
1323 #[test]
1326 fn list_state_machines_empty() {
1327 let svc = StepFunctionsService::new(make_state());
1328 let req = make_request("ListStateMachines", "{}");
1329 let resp = svc.list_state_machines(&req).unwrap();
1330 let b = body_json(&resp);
1331 assert!(b["stateMachines"].as_array().unwrap().is_empty());
1332 }
1333
1334 #[test]
1335 fn list_state_machines_returns_created() {
1336 let svc = StepFunctionsService::new(make_state());
1337 create_sm(&svc, "sm-1");
1338 create_sm(&svc, "sm-2");
1339
1340 let req = make_request("ListStateMachines", "{}");
1341 let resp = svc.list_state_machines(&req).unwrap();
1342 let b = body_json(&resp);
1343 assert_eq!(b["stateMachines"].as_array().unwrap().len(), 2);
1344 }
1345
1346 #[test]
1349 fn delete_state_machine() {
1350 let svc = StepFunctionsService::new(make_state());
1351 let arn = create_sm(&svc, "del-sm");
1352
1353 let req = make_request(
1354 "DeleteStateMachine",
1355 &json!({"stateMachineArn": arn}).to_string(),
1356 );
1357 svc.delete_state_machine(&req).unwrap();
1358
1359 let req = make_request(
1361 "DescribeStateMachine",
1362 &json!({"stateMachineArn": arn}).to_string(),
1363 );
1364 assert!(svc.describe_state_machine(&req).is_err());
1365 }
1366
1367 #[test]
1368 fn delete_state_machine_nonexistent_succeeds() {
1369 let svc = StepFunctionsService::new(make_state());
1370 let req = make_request(
1371 "DeleteStateMachine",
1372 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1373 .to_string(),
1374 );
1375 svc.delete_state_machine(&req).unwrap();
1377 }
1378
1379 #[test]
1382 fn update_state_machine() {
1383 let svc = StepFunctionsService::new(make_state());
1384 let arn = create_sm(&svc, "upd-sm");
1385
1386 let new_def = r#"{"StartAt":"NewPass","States":{"NewPass":{"Type":"Pass","End":true}}}"#;
1387 let body = json!({
1388 "stateMachineArn": arn,
1389 "definition": new_def,
1390 "description": "updated",
1391 });
1392 let req = make_request("UpdateStateMachine", &body.to_string());
1393 let resp = svc.update_state_machine(&req).unwrap();
1394 let b = body_json(&resp);
1395 assert!(b["updateDate"].as_f64().is_some());
1396
1397 let req = make_request(
1399 "DescribeStateMachine",
1400 &json!({"stateMachineArn": arn}).to_string(),
1401 );
1402 let resp = svc.describe_state_machine(&req).unwrap();
1403 let b = body_json(&resp);
1404 assert!(b["definition"].as_str().unwrap().contains("NewPass"));
1405 assert_eq!(b["description"], "updated");
1406 }
1407
1408 #[test]
1409 fn update_state_machine_not_found() {
1410 let svc = StepFunctionsService::new(make_state());
1411 let body = json!({
1412 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1413 "definition": VALID_DEF,
1414 });
1415 let req = make_request("UpdateStateMachine", &body.to_string());
1416 let err = expect_err(svc.update_state_machine(&req));
1417 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1418 }
1419
1420 #[tokio::test]
1423 async fn start_execution_basic() {
1424 let svc = StepFunctionsService::new(make_state());
1425 let arn = create_sm(&svc, "exec-sm");
1426
1427 let body = json!({
1428 "stateMachineArn": arn,
1429 "input": r#"{"key":"value"}"#,
1430 });
1431 let req = make_request("StartExecution", &body.to_string());
1432 let resp = svc.start_execution(&req).unwrap();
1433 let b = body_json(&resp);
1434 assert!(b["executionArn"].as_str().is_some());
1435 assert!(b["startDate"].as_f64().is_some());
1436 }
1437
1438 #[tokio::test]
1439 async fn start_execution_with_name() {
1440 let svc = StepFunctionsService::new(make_state());
1441 let arn = create_sm(&svc, "named-exec");
1442
1443 let body = json!({
1444 "stateMachineArn": arn,
1445 "name": "my-execution",
1446 });
1447 let req = make_request("StartExecution", &body.to_string());
1448 let resp = svc.start_execution(&req).unwrap();
1449 let b = body_json(&resp);
1450 assert!(b["executionArn"].as_str().unwrap().contains("my-execution"));
1451 }
1452
1453 #[tokio::test]
1454 async fn start_execution_sm_not_found() {
1455 let svc = StepFunctionsService::new(make_state());
1456 let body = json!({
1457 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1458 });
1459 let req = make_request("StartExecution", &body.to_string());
1460 let err = expect_err(svc.start_execution(&req));
1461 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1462 }
1463
1464 #[tokio::test]
1465 async fn start_execution_invalid_input() {
1466 let svc = StepFunctionsService::new(make_state());
1467 let arn = create_sm(&svc, "bad-input");
1468
1469 let body = json!({
1470 "stateMachineArn": arn,
1471 "input": "not json",
1472 });
1473 let req = make_request("StartExecution", &body.to_string());
1474 let err = expect_err(svc.start_execution(&req));
1475 assert!(err.to_string().contains("InvalidExecutionInput"));
1476 }
1477
1478 #[tokio::test]
1479 async fn start_execution_same_name_same_input_is_idempotent() {
1480 let svc = StepFunctionsService::new(make_state());
1481 let arn = create_sm(&svc, "dup-exec");
1482
1483 let body = json!({
1484 "stateMachineArn": arn,
1485 "name": "same-name",
1486 "input": "{\"a\":1}",
1487 });
1488 let req = make_request("StartExecution", &body.to_string());
1489 let first = body_json(&svc.start_execution(&req).unwrap());
1490
1491 let req = make_request("StartExecution", &body.to_string());
1493 let second = body_json(&svc.start_execution(&req).unwrap());
1494 assert_eq!(first["executionArn"], second["executionArn"]);
1495 assert_eq!(first["startDate"], second["startDate"]);
1496 }
1497
1498 #[tokio::test]
1499 async fn start_execution_same_name_different_input_conflicts() {
1500 let svc = StepFunctionsService::new(make_state());
1501 let arn = create_sm(&svc, "dup-exec-diff");
1502
1503 let req = make_request(
1504 "StartExecution",
1505 &json!({
1506 "stateMachineArn": arn,
1507 "name": "same-name",
1508 "input": "{\"a\":1}",
1509 })
1510 .to_string(),
1511 );
1512 svc.start_execution(&req).unwrap();
1513
1514 let req = make_request(
1516 "StartExecution",
1517 &json!({
1518 "stateMachineArn": arn,
1519 "name": "same-name",
1520 "input": "{\"a\":2}",
1521 })
1522 .to_string(),
1523 );
1524 let err = expect_err(svc.start_execution(&req));
1525 assert!(err.to_string().contains("ExecutionAlreadyExists"));
1526 }
1527
1528 #[tokio::test]
1529 async fn start_execution_express_name_collision_never_idempotent() {
1530 let svc = StepFunctionsService::new(make_state());
1531 let arn = create_express_sm(&svc, "dup-exec-express");
1532
1533 let body = json!({
1534 "stateMachineArn": arn,
1535 "name": "same-name",
1536 "input": "{\"a\":1}",
1537 });
1538 let req = make_request("StartExecution", &body.to_string());
1539 svc.start_execution(&req).unwrap();
1540
1541 let req = make_request("StartExecution", &body.to_string());
1543 let err = expect_err(svc.start_execution(&req));
1544 assert!(err.to_string().contains("ExecutionAlreadyExists"));
1545 }
1546
1547 #[tokio::test]
1550 async fn describe_execution_found() {
1551 let svc = StepFunctionsService::new(make_state());
1552 let sm_arn = create_sm(&svc, "desc-exec");
1553
1554 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
1555 let req = make_request("StartExecution", &body.to_string());
1556 let resp = svc.start_execution(&req).unwrap();
1557 let exec_arn = body_json(&resp)["executionArn"]
1558 .as_str()
1559 .unwrap()
1560 .to_string();
1561
1562 let req = make_request(
1563 "DescribeExecution",
1564 &json!({"executionArn": exec_arn}).to_string(),
1565 );
1566 let resp = svc.describe_execution(&req).unwrap();
1567 let b = body_json(&resp);
1568 assert_eq!(b["name"], "e1");
1569 assert_eq!(b["status"], "RUNNING");
1570 }
1571
1572 #[tokio::test]
1573 async fn describe_execution_not_found() {
1574 let svc = StepFunctionsService::new(make_state());
1575 let req = make_request(
1576 "DescribeExecution",
1577 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1578 .to_string(),
1579 );
1580 let err = expect_err(svc.describe_execution(&req));
1581 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1582 }
1583
1584 #[tokio::test]
1587 async fn stop_execution() {
1588 let svc = StepFunctionsService::new(make_state());
1589 let sm_arn = create_sm(&svc, "stop-sm");
1590
1591 let body = json!({"stateMachineArn": sm_arn, "name": "stop-e"});
1592 let req = make_request("StartExecution", &body.to_string());
1593 let resp = svc.start_execution(&req).unwrap();
1594 let exec_arn = body_json(&resp)["executionArn"]
1595 .as_str()
1596 .unwrap()
1597 .to_string();
1598
1599 let body = json!({
1600 "executionArn": exec_arn,
1601 "error": "UserAborted",
1602 "cause": "test stop",
1603 });
1604 let req = make_request("StopExecution", &body.to_string());
1605 let resp = svc.stop_execution(&req).unwrap();
1606 let b = body_json(&resp);
1607 assert!(b["stopDate"].as_f64().is_some());
1608
1609 let req = make_request(
1611 "DescribeExecution",
1612 &json!({"executionArn": exec_arn}).to_string(),
1613 );
1614 let resp = svc.describe_execution(&req).unwrap();
1615 let b = body_json(&resp);
1616 assert_eq!(b["status"], "ABORTED");
1617 assert_eq!(b["error"], "UserAborted");
1618 }
1619
1620 #[tokio::test]
1621 async fn redrive_execution_reruns_to_terminal() {
1622 let svc = StepFunctionsService::new(make_state());
1623 let sm_arn = create_sm(&svc, "redrive-sm");
1624
1625 let body = json!({"stateMachineArn": sm_arn, "name": "redrive-e"});
1626 let req = make_request("StartExecution", &body.to_string());
1627 let resp = svc.start_execution(&req).unwrap();
1628 let exec_arn = body_json(&resp)["executionArn"]
1629 .as_str()
1630 .unwrap()
1631 .to_string();
1632
1633 let req = make_request(
1635 "StopExecution",
1636 &json!({"executionArn": exec_arn, "error": "UserAborted", "cause": "stop"}).to_string(),
1637 );
1638 svc.stop_execution(&req).unwrap();
1639
1640 let req = make_request(
1642 "RedriveExecution",
1643 &json!({"executionArn": exec_arn}).to_string(),
1644 );
1645 let resp = svc.redrive_execution(&req).unwrap();
1646 assert!(body_json(&resp)["redriveDate"].as_i64().is_some());
1647
1648 let mut status = String::new();
1651 for _ in 0..100 {
1652 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1653 let req = make_request(
1654 "DescribeExecution",
1655 &json!({"executionArn": exec_arn}).to_string(),
1656 );
1657 let b = body_json(&svc.describe_execution(&req).unwrap());
1658 status = b["status"].as_str().unwrap().to_string();
1659 if status != "RUNNING" {
1660 break;
1661 }
1662 }
1663 assert_eq!(
1664 status, "SUCCEEDED",
1665 "redriven execution must run to completion"
1666 );
1667 }
1668
1669 #[tokio::test]
1670 async fn redrive_execution_running_rejected() {
1671 let svc = StepFunctionsService::new(make_state());
1672 let sm_arn = create_sm(&svc, "redrive-running-sm");
1673 let body = json!({"stateMachineArn": sm_arn, "name": "still-running"});
1674 let req = make_request("StartExecution", &body.to_string());
1675 let exec_arn = body_json(&svc.start_execution(&req).unwrap())["executionArn"]
1676 .as_str()
1677 .unwrap()
1678 .to_string();
1679
1680 let req = make_request(
1682 "RedriveExecution",
1683 &json!({"executionArn": exec_arn}).to_string(),
1684 );
1685 let err = expect_err(svc.redrive_execution(&req));
1686 assert!(err.to_string().contains("ExecutionNotRedrivable"));
1687 }
1688
1689 #[tokio::test]
1690 async fn stop_execution_not_found() {
1691 let svc = StepFunctionsService::new(make_state());
1692 let req = make_request(
1693 "StopExecution",
1694 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1695 .to_string(),
1696 );
1697 let err = expect_err(svc.stop_execution(&req));
1698 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1699 }
1700
1701 #[tokio::test]
1704 async fn list_executions() {
1705 let svc = StepFunctionsService::new(make_state());
1706 let sm_arn = create_sm(&svc, "list-exec");
1707
1708 for i in 0..3 {
1709 let body = json!({"stateMachineArn": sm_arn, "name": format!("e{i}")});
1710 let req = make_request("StartExecution", &body.to_string());
1711 svc.start_execution(&req).unwrap();
1712 }
1713
1714 let req = make_request(
1715 "ListExecutions",
1716 &json!({"stateMachineArn": sm_arn}).to_string(),
1717 );
1718 let resp = svc.list_executions(&req).unwrap();
1719 let b = body_json(&resp);
1720 assert_eq!(b["executions"].as_array().unwrap().len(), 3);
1721 }
1722
1723 #[tokio::test]
1724 async fn list_executions_sm_not_found() {
1725 let svc = StepFunctionsService::new(make_state());
1726 let req = make_request(
1727 "ListExecutions",
1728 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1729 .to_string(),
1730 );
1731 let err = expect_err(svc.list_executions(&req));
1732 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1733 }
1734
1735 #[tokio::test]
1738 async fn get_execution_history_not_found() {
1739 let svc = StepFunctionsService::new(make_state());
1740 let req = make_request(
1741 "GetExecutionHistory",
1742 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1743 .to_string(),
1744 );
1745 let err = expect_err(svc.get_execution_history(&req));
1746 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1747 }
1748
1749 #[tokio::test]
1752 async fn describe_sm_for_execution() {
1753 let svc = StepFunctionsService::new(make_state());
1754 let sm_arn = create_sm(&svc, "sm-for-exec");
1755
1756 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
1757 let req = make_request("StartExecution", &body.to_string());
1758 let resp = svc.start_execution(&req).unwrap();
1759 let exec_arn = body_json(&resp)["executionArn"]
1760 .as_str()
1761 .unwrap()
1762 .to_string();
1763
1764 let req = make_request(
1765 "DescribeStateMachineForExecution",
1766 &json!({"executionArn": exec_arn}).to_string(),
1767 );
1768 let resp = svc.describe_state_machine_for_execution(&req).unwrap();
1769 let b = body_json(&resp);
1770 assert_eq!(b["name"], "sm-for-exec");
1771 }
1772
1773 #[test]
1776 fn tag_untag_list_tags() {
1777 let svc = StepFunctionsService::new(make_state());
1778 let arn = create_sm(&svc, "tagged-sm");
1779
1780 let body = json!({
1782 "resourceArn": arn,
1783 "tags": [{"key": "env", "value": "prod"}],
1784 });
1785 let req = make_request("TagResource", &body.to_string());
1786 svc.tag_resource(&req).unwrap();
1787
1788 let req = make_request(
1790 "ListTagsForResource",
1791 &json!({"resourceArn": arn}).to_string(),
1792 );
1793 let resp = svc.list_tags_for_resource(&req).unwrap();
1794 let b = body_json(&resp);
1795 let tags = b["tags"].as_array().unwrap();
1796 assert_eq!(tags.len(), 1);
1797 assert_eq!(tags[0]["key"], "env");
1798
1799 let body = json!({
1801 "resourceArn": arn,
1802 "tagKeys": ["env"],
1803 });
1804 let req = make_request("UntagResource", &body.to_string());
1805 svc.untag_resource(&req).unwrap();
1806
1807 let req = make_request(
1809 "ListTagsForResource",
1810 &json!({"resourceArn": arn}).to_string(),
1811 );
1812 let resp = svc.list_tags_for_resource(&req).unwrap();
1813 let b = body_json(&resp);
1814 assert!(b["tags"].as_array().unwrap().is_empty());
1815 }
1816
1817 #[test]
1818 fn tag_resource_not_found() {
1819 let svc = StepFunctionsService::new(make_state());
1820 let body = json!({
1821 "resourceArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1822 "tags": [{"key": "k", "value": "v"}],
1823 });
1824 let req = make_request("TagResource", &body.to_string());
1825 let err = expect_err(svc.tag_resource(&req));
1826 assert!(err.to_string().contains("ResourceNotFound"));
1827 }
1828
1829 #[test]
1832 fn test_validate_name() {
1833 assert!(validate_name("valid-name").is_ok());
1834 assert!(validate_name("under_score").is_ok());
1835 assert!(validate_name("").is_err());
1836 assert!(validate_name("has spaces").is_err());
1837 assert!(validate_name(&"a".repeat(81)).is_err());
1838 }
1839
1840 #[test]
1841 fn test_validate_definition() {
1842 assert!(validate_definition(VALID_DEF).is_ok());
1843 assert!(validate_definition("not json").is_err());
1844 assert!(validate_definition(r#"{"States":{}}"#).is_err()); assert!(validate_definition(r#"{"StartAt":"S"}"#).is_err()); }
1847
1848 #[test]
1849 fn test_validate_definition_rejects_malformed_paths() {
1850 let def =
1852 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"$.arr[","End":true}}}"#;
1853 assert!(validate_definition(def).is_err());
1854
1855 let def =
1857 "{\"StartAt\":\"P\",\"States\":{\"P\":{\"Type\":\"Pass\",\"OutputPath\":\"$.x[\u{00e9}\",\"End\":true}}}";
1858 assert!(validate_definition(def).is_err());
1859
1860 let def = r#"{"StartAt":"C","States":{"C":{"Type":"Choice","Choices":[{"Variable":"$.n[","NumericEquals":1,"Next":"P"}]},"P":{"Type":"Pass","End":true}}}"#;
1862 assert!(validate_definition(def).is_err());
1863
1864 let def =
1866 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"foo.bar","End":true}}}"#;
1867 assert!(validate_definition(def).is_err());
1868
1869 let def =
1871 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}"#;
1872 assert!(validate_definition(def).is_err());
1873 }
1874
1875 #[test]
1876 fn test_validate_definition_accepts_well_formed_paths() {
1877 let def = r#"{"StartAt":"P","States":{
1880 "P":{"Type":"Pass","InputPath":"$.a.b[0].c","OutputPath":"$","ResultPath":"$.out","Next":"C"},
1881 "C":{"Type":"Choice","Choices":[
1882 {"And":[{"Variable":"$.items[2]","NumericEquals":1},{"Variable":"$.flag","BooleanEquals":true}],"Next":"S"}
1883 ],"Default":"S"},
1884 "S":{"Type":"Succeed","InputPath":"null"}
1885 }}"#;
1886 assert!(validate_definition(def).is_ok());
1887 }
1888
1889 #[test]
1892 fn test_validate_definition_recurses_into_parallel_and_map() {
1893 let par = r#"{"StartAt":"Par","States":{"Par":{"Type":"Parallel","End":true,
1894 "Branches":[{"StartAt":"I","States":{"I":{"Type":"Pass","InputPath":"nope","End":true}}}]}}}"#;
1895 assert!(validate_definition(par).is_err());
1896
1897 let map = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
1898 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","OutputPath":"nope","End":true}}}}}}"#;
1899 assert!(validate_definition(map).is_err());
1900
1901 let iter = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
1903 "Iterator":{"StartAt":"E","States":{"E":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}}}}"#;
1904 assert!(validate_definition(iter).is_err());
1905
1906 let ok = r#"{"StartAt":"M","States":{"M":{"Type":"Map","ItemsPath":"$.items","End":true,
1908 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","InputPath":"$.a","End":true}}}}}}"#;
1909 assert!(validate_definition(ok).is_ok());
1910 }
1911
1912 #[test]
1914 fn test_validate_definition_payload_template_dollar_keys() {
1915 let lit = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
1917 "Parameters":{"v.$":"just text"},"End":true}}}"#;
1918 assert!(validate_definition(lit).is_err());
1919
1920 let num = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
1922 "Parameters":{"v.$":42},"End":true}}}"#;
1923 assert!(validate_definition(num).is_err());
1924
1925 let ok = r#"{"StartAt":"P","States":{"P":{"Type":"Pass","Parameters":{
1927 "a.$":"$.input","b.$":"$$.Execution.Id","c.$":"States.Format('{}',$.n)",
1928 "nested":{"d.$":"$.x"},"plain":"literal-ok"},"End":true}}}"#;
1929 assert!(validate_definition(ok).is_ok());
1930
1931 let arr = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
1933 "ResultSelector":{"items":[{"bad.$":"literal"}]},"End":true}}}"#;
1934 assert!(validate_definition(arr).is_err());
1935 }
1936
1937 #[test]
1938 fn test_is_valid_reference_path() {
1939 assert!(is_valid_reference_path("$"));
1940 assert!(is_valid_reference_path("$.foo"));
1941 assert!(is_valid_reference_path("$.foo.bar[3].baz"));
1942 assert!(is_valid_reference_path("$[0]"));
1943 assert!(!is_valid_reference_path("$.arr["));
1944 assert!(!is_valid_reference_path("$.x[\u{00e9}"));
1945 assert!(!is_valid_reference_path("$.x[]"));
1946 assert!(!is_valid_reference_path("$.x[abc]"));
1947 assert!(!is_valid_reference_path("foo.bar"));
1948 assert!(!is_valid_reference_path(""));
1949 }
1950
1951 #[test]
1952 fn test_validate_arn() {
1953 assert!(validate_arn("arn:aws:states:us-east-1:123:sm:test").is_ok());
1954 assert!(validate_arn("not-an-arn").is_err());
1955 }
1956
1957 #[test]
1958 fn test_camel_to_details_key() {
1959 assert_eq!(camel_to_details_key("PassStateEntered"), "stateEntered");
1961 assert_eq!(camel_to_details_key("TaskStateEntered"), "stateEntered");
1962 assert_eq!(camel_to_details_key("ChoiceStateExited"), "stateExited");
1963 assert_eq!(camel_to_details_key("MapStateExited"), "stateExited");
1964 assert_eq!(camel_to_details_key("TaskScheduled"), "taskScheduled");
1966 assert_eq!(camel_to_details_key("ExecutionStarted"), "executionStarted");
1967 assert_eq!(camel_to_details_key(""), "");
1968 }
1969
1970 #[test]
1971 fn test_is_mutating_action() {
1972 assert!(is_mutating_action("CreateStateMachine"));
1973 assert!(is_mutating_action("StartExecution"));
1974 assert!(!is_mutating_action("DescribeStateMachine"));
1975 assert!(!is_mutating_action("ListStateMachines"));
1976 }
1977
1978 fn create_express_sm(svc: &StepFunctionsService, name: &str) -> String {
1981 let body = json!({
1982 "name": name,
1983 "definition": VALID_DEF,
1984 "roleArn": "arn:aws:iam::123456789012:role/test",
1985 "type": "EXPRESS",
1986 });
1987 let req = make_request("CreateStateMachine", &body.to_string());
1988 let resp = svc.create_state_machine(&req).unwrap();
1989 let b = body_json(&resp);
1990 b["stateMachineArn"].as_str().unwrap().to_string()
1991 }
1992
1993 #[tokio::test]
1994 async fn start_sync_execution_basic() {
1995 let svc = StepFunctionsService::new(make_state());
1996 let arn = create_express_sm(&svc, "sync-sm");
1997
1998 let body = json!({
1999 "stateMachineArn": arn,
2000 "input": r#"{"key":"value"}"#,
2001 });
2002 let req = make_request("StartSyncExecution", &body.to_string());
2003 let resp = svc.start_sync_execution(&req).await.unwrap();
2004 let b = body_json(&resp);
2005 assert!(b["executionArn"]
2006 .as_str()
2007 .unwrap()
2008 .contains("express:sync-sm"));
2009 assert_eq!(b["stateMachineArn"], arn);
2010 assert_eq!(b["status"], "SUCCEEDED");
2011 assert!(b["startDate"].as_i64().is_some());
2012 assert!(b["stopDate"].as_i64().is_some());
2013 assert!(b["output"].as_str().is_some());
2014 assert!(b["billingDetails"]["billedDurationInMilliseconds"]
2015 .as_i64()
2016 .is_some());
2017 }
2018
2019 #[tokio::test]
2020 async fn start_sync_execution_not_express() {
2021 let svc = StepFunctionsService::new(make_state());
2022 let arn = create_sm(&svc, "std-sm");
2023
2024 let body = json!({"stateMachineArn": arn});
2025 let req = make_request("StartSyncExecution", &body.to_string());
2026 let err = expect_err(svc.start_sync_execution(&req).await);
2027 assert!(err.to_string().contains("StateMachineTypeNotSupported"));
2028 }
2029
2030 #[tokio::test]
2031 async fn start_sync_execution_sm_not_found() {
2032 let svc = StepFunctionsService::new(make_state());
2033 let body = json!({
2034 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
2035 });
2036 let req = make_request("StartSyncExecution", &body.to_string());
2037 let err = expect_err(svc.start_sync_execution(&req).await);
2038 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2039 }
2040
2041 #[tokio::test]
2042 async fn start_sync_execution_records_introspection_fields() {
2043 let svc = StepFunctionsService::new(make_state());
2044 let arn = create_express_sm(&svc, "sync-introspect");
2045
2046 let body = json!({"stateMachineArn": arn, "input": "{}"});
2047 let req = make_request("StartSyncExecution", &body.to_string());
2048 let resp = svc.start_sync_execution(&req).await.unwrap();
2049 let b = body_json(&resp);
2050 let exec_arn = b["executionArn"].as_str().unwrap().to_string();
2051
2052 let accounts = svc.state.read();
2053 let state = accounts.get("123456789012").unwrap();
2054 let stored = state
2055 .executions
2056 .get(&exec_arn)
2057 .expect("sync execution should be persisted for introspection");
2058 assert!(stored.is_sync, "sync executions must be marked is_sync");
2059 assert_eq!(stored.billed_memory_mb, Some(64));
2060 assert!(
2061 stored.billed_duration_ms.is_some(),
2062 "billed_duration_ms must be populated after sync run"
2063 );
2064 assert!(
2065 stored.parent_execution_arn.is_none(),
2066 "top-level sync execution has no parent"
2067 );
2068 }
2069
2070 #[tokio::test]
2071 async fn start_sync_execution_invalid_input() {
2072 let svc = StepFunctionsService::new(make_state());
2073 let arn = create_express_sm(&svc, "bad-input-sync");
2074
2075 let body = json!({
2076 "stateMachineArn": arn,
2077 "input": "not json",
2078 });
2079 let req = make_request("StartSyncExecution", &body.to_string());
2080 let err = expect_err(svc.start_sync_execution(&req).await);
2081 assert!(err.to_string().contains("InvalidExecutionInput"));
2082 }
2083
2084 #[test]
2086 fn snapshot_hook_is_none_without_store() {
2087 let svc = StepFunctionsService::new(make_state());
2088 assert!(svc.snapshot_hook().is_none());
2089 }
2090
2091 #[tokio::test]
2095 async fn snapshot_hook_fires_with_store() {
2096 let store: Arc<dyn fakecloud_persistence::SnapshotStore> =
2097 Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
2098 let svc = StepFunctionsService::new(make_state()).with_snapshot_store(store);
2099 let hook = svc
2100 .snapshot_hook()
2101 .expect("hook present when a store is set");
2102 hook().await;
2103 }
2104
2105 fn make_execution(arn: &str, status: ExecutionStatus) -> Execution {
2106 Execution {
2107 execution_arn: arn.to_string(),
2108 state_machine_arn: "arn:aws:states:us-east-1:123456789012:stateMachine:sm".to_string(),
2109 state_machine_name: "sm".to_string(),
2110 name: arn.to_string(),
2111 status,
2112 input: None,
2113 output: None,
2114 start_date: Utc::now(),
2115 stop_date: None,
2116 error: None,
2117 cause: None,
2118 history_events: vec![],
2119 parent_execution_arn: None,
2120 is_sync: false,
2121 billed_duration_ms: None,
2122 billed_memory_mb: None,
2123 }
2124 }
2125
2126 #[test]
2127 fn reconcile_aborts_running_executions_on_restart() {
2128 let state = make_state();
2132 {
2133 let mut accounts = state.write();
2134 let s = accounts.get_or_create("123456789012");
2135 s.executions.insert(
2136 "running".into(),
2137 make_execution("running", ExecutionStatus::Running),
2138 );
2139 s.executions.insert(
2140 "done".into(),
2141 make_execution("done", ExecutionStatus::Succeeded),
2142 );
2143 }
2144
2145 let n = reconcile_interrupted_executions(&state);
2146 assert_eq!(n, 1, "only the RUNNING execution is reconciled");
2147
2148 let accounts = state.read();
2149 let s = accounts.get("123456789012").unwrap();
2150 let running = &s.executions["running"];
2151 assert_eq!(running.status, ExecutionStatus::Aborted);
2152 assert!(running.stop_date.is_some());
2153 assert_eq!(running.error.as_deref(), Some("Fakecloud.Restart"));
2154 assert_eq!(s.executions["done"].status, ExecutionStatus::Succeeded);
2155 }
2156
2157 #[tokio::test]
2160 async fn distributed_map_populates_map_run() {
2161 let state = make_state();
2162 let svc = StepFunctionsService::new(state.clone());
2163 let execution_arn =
2164 "arn:aws:states:us-east-1:123456789012:execution:dist-sm:exec-1".to_string();
2165
2166 let def = json!({
2167 "StartAt": "M",
2168 "States": {
2169 "M": {
2170 "Type": "Map",
2171 "ItemsPath": "$.items",
2172 "ItemProcessor": {
2173 "ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "STANDARD" },
2174 "StartAt": "Item",
2175 "States": { "Item": { "Type": "Pass", "End": true } }
2176 },
2177 "End": true
2178 }
2179 }
2180 });
2181
2182 interpreter::execute_state_machine(
2183 state.clone(),
2184 execution_arn.clone(),
2185 def.to_string(),
2186 Some(r#"{"items":[1,2,3]}"#.to_string()),
2187 None,
2188 None,
2189 None,
2190 None,
2191 )
2192 .await;
2193
2194 let req = make_request(
2196 "ListMapRuns",
2197 &json!({ "executionArn": execution_arn }).to_string(),
2198 );
2199 let listed = body_json(&svc.list_map_runs(&req).unwrap());
2200 let runs = listed["mapRuns"].as_array().unwrap();
2201 assert_eq!(
2202 runs.len(),
2203 1,
2204 "distributed map must create exactly one MapRun"
2205 );
2206 let map_run_arn = runs[0]["mapRunArn"].as_str().unwrap().to_string();
2207 assert!(map_run_arn.contains(":mapRun:"));
2208 assert_eq!(runs[0]["executionArn"], execution_arn);
2209
2210 let req = make_request(
2212 "DescribeMapRun",
2213 &json!({ "mapRunArn": map_run_arn }).to_string(),
2214 );
2215 let mr = body_json(&svc.describe_map_run(&req).unwrap());
2216 assert_eq!(mr["status"], "SUCCEEDED");
2217 assert_eq!(mr["itemCounts"]["total"], 3);
2218 assert_eq!(mr["itemCounts"]["succeeded"], 3);
2219 assert_eq!(mr["itemCounts"]["failed"], 0);
2220 assert_eq!(mr["executionCounts"]["succeeded"], 3);
2221 }
2222
2223 #[tokio::test]
2224 async fn inline_map_does_not_create_map_run() {
2225 let state = make_state();
2226 let svc = StepFunctionsService::new(state.clone());
2227 let execution_arn =
2228 "arn:aws:states:us-east-1:123456789012:execution:inline-sm:exec-1".to_string();
2229
2230 let def = json!({
2232 "StartAt": "M",
2233 "States": {
2234 "M": {
2235 "Type": "Map",
2236 "ItemsPath": "$.items",
2237 "ItemProcessor": {
2238 "StartAt": "Item",
2239 "States": { "Item": { "Type": "Pass", "End": true } }
2240 },
2241 "End": true
2242 }
2243 }
2244 });
2245
2246 interpreter::execute_state_machine(
2247 state.clone(),
2248 execution_arn.clone(),
2249 def.to_string(),
2250 Some(r#"{"items":[1,2]}"#.to_string()),
2251 None,
2252 None,
2253 None,
2254 None,
2255 )
2256 .await;
2257
2258 let req = make_request(
2259 "ListMapRuns",
2260 &json!({ "executionArn": execution_arn }).to_string(),
2261 );
2262 let listed = body_json(&svc.list_map_runs(&req).unwrap());
2263 assert!(listed["mapRuns"].as_array().unwrap().is_empty());
2264 }
2265}
2266
2267#[cfg(test)]
2268mod pagination_reject_test {
2269 #[test]
2270 fn paginate_checked_rejects_invalid_token() {
2271 use fakecloud_core::pagination::paginate_checked;
2272 let items: Vec<i32> = (0..5).collect();
2273 assert!(paginate_checked(&items, Some("bad"), 3).is_err());
2274 assert!(paginate_checked(&items, Some("2"), 3).is_ok());
2275 }
2276}