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 if let Some(ref enc) = sm.encryption_configuration {
445 resp["encryptionConfiguration"] = enc.clone();
446 }
447
448 resp
449}
450
451fn missing(name: &str) -> AwsServiceError {
452 AwsServiceError::aws_error(
453 StatusCode::BAD_REQUEST,
454 "ValidationException",
455 format!("The request must contain the parameter {name}."),
456 )
457}
458
459fn state_machine_not_found(arn: &str) -> AwsServiceError {
460 AwsServiceError::aws_error(
461 StatusCode::BAD_REQUEST,
462 "StateMachineDoesNotExist",
463 format!("State Machine Does Not Exist: '{arn}'"),
464 )
465}
466
467fn activity_not_found(arn: &str) -> AwsServiceError {
468 AwsServiceError::aws_error(
469 StatusCode::BAD_REQUEST,
470 "ActivityDoesNotExist",
471 format!("Activity does not exist: {arn}"),
472 )
473}
474
475fn task_does_not_exist(token: &str) -> AwsServiceError {
476 AwsServiceError::aws_error(
477 StatusCode::BAD_REQUEST,
478 "TaskDoesNotExist",
479 format!("Task does not exist: {token}"),
480 )
481}
482
483fn resource_not_found(arn: &str) -> AwsServiceError {
484 AwsServiceError::aws_error(
485 StatusCode::BAD_REQUEST,
486 "ResourceNotFound",
487 format!("Resource not found: '{arn}'"),
488 )
489}
490
491fn parse_routing_configuration(
496 routes: &[serde_json::Value],
497) -> Result<Vec<crate::state::AliasRoute>, AwsServiceError> {
498 if routes.is_empty() || routes.len() > 2 {
499 return Err(AwsServiceError::aws_error(
500 StatusCode::BAD_REQUEST,
501 "ValidationException",
502 "routingConfiguration must contain 1 or 2 routes.",
503 ));
504 }
505 let parsed: Vec<crate::state::AliasRoute> = routes
506 .iter()
507 .map(|r| {
508 let arn = r["stateMachineVersionArn"].as_str().ok_or_else(|| {
509 AwsServiceError::aws_error(
510 StatusCode::BAD_REQUEST,
511 "ValidationException",
512 "routingConfiguration entries must contain stateMachineVersionArn.",
513 )
514 })?;
515 let weight = r["weight"].as_i64().ok_or_else(|| {
516 AwsServiceError::aws_error(
517 StatusCode::BAD_REQUEST,
518 "ValidationException",
519 "routingConfiguration entries must contain a numeric weight.",
520 )
521 })?;
522 if !(0..=100).contains(&weight) {
523 return Err(AwsServiceError::aws_error(
524 StatusCode::BAD_REQUEST,
525 "ValidationException",
526 format!("Invalid routing weight {weight}; must be 0-100."),
527 ));
528 }
529 Ok(crate::state::AliasRoute {
530 state_machine_version_arn: arn.to_string(),
531 weight: weight as i32,
532 })
533 })
534 .collect::<Result<_, _>>()?;
535 let total: i32 = parsed.iter().map(|r| r.weight).sum();
536 if total != 100 {
537 return Err(AwsServiceError::aws_error(
538 StatusCode::BAD_REQUEST,
539 "ValidationException",
540 format!("routingConfiguration weights must sum to 100, got {total}."),
541 ));
542 }
543 Ok(parsed)
544}
545
546fn validate_name(name: &str) -> Result<(), AwsServiceError> {
547 if name.is_empty() || name.len() > 80 {
548 return Err(AwsServiceError::aws_error(
549 StatusCode::BAD_REQUEST,
550 "InvalidName",
551 format!("Invalid Name: '{name}' (length must be between 1 and 80 characters)"),
552 ));
553 }
554 if !name
556 .chars()
557 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
558 {
559 return Err(AwsServiceError::aws_error(
560 StatusCode::BAD_REQUEST,
561 "InvalidName",
562 format!(
563 "Invalid Name: '{name}' (must only contain alphanumeric characters, hyphens, and underscores)"
564 ),
565 ));
566 }
567 Ok(())
568}
569
570fn validate_definition(definition: &str) -> Result<(), AwsServiceError> {
571 let parsed: Value = serde_json::from_str(definition).map_err(|e| {
572 AwsServiceError::aws_error(
573 StatusCode::BAD_REQUEST,
574 "InvalidDefinition",
575 format!("Invalid State Machine Definition: '{e}'"),
576 )
577 })?;
578
579 if parsed.get("StartAt").and_then(|v| v.as_str()).is_none() {
580 return Err(AwsServiceError::aws_error(
581 StatusCode::BAD_REQUEST,
582 "InvalidDefinition",
583 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
584 .to_string(),
585 ));
586 }
587
588 let states_obj = parsed
589 .get("States")
590 .and_then(|v| v.as_object())
591 .ok_or_else(|| {
592 AwsServiceError::aws_error(
593 StatusCode::BAD_REQUEST,
594 "InvalidDefinition",
595 "Invalid State Machine Definition: 'MISSING_STATES' (States field is required)"
596 .to_string(),
597 )
598 })?;
599
600 let start_at = parsed["StartAt"].as_str().ok_or_else(|| {
601 AwsServiceError::aws_error(
602 StatusCode::BAD_REQUEST,
603 "InvalidDefinition",
604 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
605 .to_string(),
606 )
607 })?;
608 if !states_obj.contains_key(start_at) {
609 return Err(AwsServiceError::aws_error(
610 StatusCode::BAD_REQUEST,
611 "InvalidDefinition",
612 format!(
613 "Invalid State Machine Definition: 'MISSING_TRANSITION_TARGET' \
614 (StartAt '{start_at}' does not reference a valid state)"
615 ),
616 ));
617 }
618
619 for (state_name, state_val) in states_obj {
623 validate_state_paths(state_name, state_val)?;
624 }
625
626 Ok(())
627}
628
629fn validate_state_paths(state_name: &str, state: &Value) -> Result<(), AwsServiceError> {
634 for field in ["InputPath", "OutputPath", "ResultPath"] {
635 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
636 if (field != "ResultPath" && p == "null") || is_valid_reference_path(p) {
640 continue;
641 }
642 return Err(invalid_reference_path(state_name, field, p));
643 }
644 }
645
646 for field in [
650 "SecondsPath",
651 "TimestampPath",
652 "ItemsPath",
653 "MaxConcurrencyPath",
654 "ToleratedFailureCountPath",
655 "ToleratedFailurePercentagePath",
656 ] {
657 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
658 if is_reference_or_context_path(p) {
659 continue;
660 }
661 return Err(invalid_reference_path(state_name, field, p));
662 }
663 }
664
665 for field in ["Parameters", "ResultSelector", "ItemSelector"] {
669 if let Some(template) = state.get(field) {
670 validate_payload_template(state_name, field, template)?;
671 }
672 }
673
674 match state.get("Type").and_then(|v| v.as_str()) {
675 Some("Choice") => {
676 if let Some(choices) = state.get("Choices").and_then(|v| v.as_array()) {
677 for rule in choices {
678 validate_choice_variables(state_name, rule)?;
679 }
680 }
681 }
682 Some("Parallel") => {
683 if let Some(branches) = state.get("Branches").and_then(|v| v.as_array()) {
684 for branch in branches {
685 validate_nested_definition(state_name, branch)?;
686 }
687 }
688 }
689 Some("Map") => {
690 for key in ["ItemProcessor", "Iterator"] {
693 if let Some(processor) = state.get(key) {
694 validate_nested_definition(state_name, processor)?;
695 }
696 }
697 }
698 _ => {}
699 }
700
701 Ok(())
702}
703
704fn validate_nested_definition(parent: &str, def: &Value) -> Result<(), AwsServiceError> {
708 if let Some(states) = def.get("States").and_then(|v| v.as_object()) {
709 for (name, state) in states {
710 validate_state_paths(&format!("{parent}/{name}"), state)?;
711 }
712 }
713 Ok(())
714}
715
716fn validate_payload_template(
720 state_name: &str,
721 field: &str,
722 template: &Value,
723) -> Result<(), AwsServiceError> {
724 match template {
725 Value::Object(map) => {
726 for (key, value) in map {
727 if let Some(stripped) = key.strip_suffix(".$") {
728 let expr = value.as_str().ok_or_else(|| {
729 invalid_payload_template(
730 state_name,
731 field,
732 &format!(
733 "the '{key}' field must be a JSONPath or intrinsic string, \
734 not {value}"
735 ),
736 )
737 })?;
738 let is_ref = expr.starts_with('$');
739 if !is_ref && !crate::intrinsics::is_intrinsic_call(expr) {
740 return Err(invalid_payload_template(
741 state_name,
742 field,
743 &format!(
744 "the '{stripped}.$' field value '{expr}' is neither a JSONPath \
745 reference nor an intrinsic function"
746 ),
747 ));
748 }
749 } else {
750 validate_payload_template(state_name, field, value)?;
751 }
752 }
753 }
754 Value::Array(arr) => {
755 for v in arr {
756 validate_payload_template(state_name, field, v)?;
757 }
758 }
759 _ => {}
760 }
761 Ok(())
762}
763
764fn is_reference_or_context_path(p: &str) -> bool {
767 if let Some(rest) = p.strip_prefix("$$") {
768 return rest.is_empty() || rest.starts_with('.') || rest.starts_with('[');
770 }
771 is_valid_reference_path(p)
772}
773
774fn invalid_payload_template(state_name: &str, field: &str, detail: &str) -> AwsServiceError {
775 AwsServiceError::aws_error(
776 StatusCode::BAD_REQUEST,
777 "InvalidDefinition",
778 format!(
779 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
780 (The {field} of state '{state_name}' is invalid: {detail})"
781 ),
782 )
783}
784
785fn validate_choice_variables(state_name: &str, rule: &Value) -> Result<(), AwsServiceError> {
788 if let Some(v) = rule.get("Variable").and_then(|v| v.as_str()) {
789 if !is_valid_reference_path(v) {
790 return Err(invalid_reference_path(state_name, "Variable", v));
791 }
792 }
793 for combinator in ["And", "Or"] {
794 if let Some(nested) = rule.get(combinator).and_then(|v| v.as_array()) {
795 for n in nested {
796 validate_choice_variables(state_name, n)?;
797 }
798 }
799 }
800 if let Some(n) = rule.get("Not") {
801 validate_choice_variables(state_name, n)?;
802 }
803 Ok(())
804}
805
806fn is_valid_reference_path(path: &str) -> bool {
810 if path != "$" && !path.starts_with("$.") && !path.starts_with("$[") {
811 return false;
812 }
813 let body = path
814 .strip_prefix("$.")
815 .or_else(|| path.strip_prefix('$'))
816 .unwrap_or(path);
817 for part in body.split('.') {
818 if !segment_is_valid(part) {
819 return false;
820 }
821 }
822 true
823}
824
825fn segment_is_valid(part: &str) -> bool {
826 match part.find('[') {
827 None => !part.contains(']'),
828 Some(open) => {
829 if !part.ends_with(']') {
830 return false;
831 }
832 let inner = &part[open + 1..part.len() - 1];
833 !inner.is_empty() && inner.parse::<usize>().is_ok()
835 }
836 }
837}
838
839fn invalid_reference_path(state_name: &str, field: &str, path: &str) -> AwsServiceError {
840 AwsServiceError::aws_error(
841 StatusCode::BAD_REQUEST,
842 "InvalidDefinition",
843 format!(
844 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
845 (The {field} field of state '{state_name}' is not a valid reference path: '{path}')"
846 ),
847 )
848}
849
850fn execution_not_found(arn: &str) -> AwsServiceError {
851 AwsServiceError::aws_error(
852 StatusCode::BAD_REQUEST,
853 "ExecutionDoesNotExist",
854 format!("Execution Does Not Exist: '{arn}'"),
855 )
856}
857
858fn execution_to_json(exec: &Execution) -> Value {
859 let mut resp = json!({
860 "executionArn": exec.execution_arn,
861 "stateMachineArn": exec.state_machine_arn,
862 "name": exec.name,
863 "status": exec.status.as_str(),
864 "startDate": exec.start_date.timestamp() as f64,
865 });
866
867 if let Some(ref input) = exec.input {
868 resp["input"] = json!(input);
869 }
870 if let Some(ref output) = exec.output {
871 resp["output"] = json!(output);
872 }
873 if let Some(stop) = exec.stop_date {
874 resp["stopDate"] = json!(stop.timestamp() as f64);
875 }
876 if let Some(ref error) = exec.error {
877 resp["error"] = json!(error);
878 }
879 if let Some(ref cause) = exec.cause {
880 resp["cause"] = json!(cause);
881 }
882
883 resp
884}
885
886fn camel_to_details_key(event_type: &str) -> String {
896 if event_type.ends_with("StateEntered") {
897 return "stateEntered".to_string();
898 }
899 if event_type.ends_with("StateExited") {
900 return "stateExited".to_string();
901 }
902 let mut chars = event_type.chars();
903 match chars.next() {
904 None => String::new(),
905 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
906 }
907}
908
909fn execution_input_matches(stored: Option<&str>, incoming: Option<&str>) -> bool {
914 let stored = stored.unwrap_or("{}");
915 let incoming = incoming.unwrap_or("{}");
916 match (
917 serde_json::from_str::<Value>(stored),
918 serde_json::from_str::<Value>(incoming),
919 ) {
920 (Ok(a), Ok(b)) => a == b,
921 _ => stored == incoming,
922 }
923}
924
925fn validate_arn(arn: &str) -> Result<(), AwsServiceError> {
926 if !arn.starts_with("arn:") {
927 return Err(AwsServiceError::aws_error(
928 StatusCode::BAD_REQUEST,
929 "InvalidArn",
930 format!("Invalid Arn: '{arn}'"),
931 ));
932 }
933 Ok(())
934}
935
936fn validate_arn_length(field: &str, value: &str, max: usize) -> Result<(), AwsServiceError> {
940 if value.is_empty() || value.len() > max {
941 return Err(AwsServiceError::aws_error(
942 StatusCode::BAD_REQUEST,
943 "InvalidArn",
944 format!("Invalid Arn at '{field}': must be 1..={max} characters"),
945 ));
946 }
947 Ok(())
948}
949
950pub(super) fn invalid_token() -> AwsServiceError {
954 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidToken", "Invalid nextToken")
955}
956
957fn validate_page_token(value: &str) -> Result<(), AwsServiceError> {
961 if value.is_empty() || value.len() > 1024 {
962 return Err(AwsServiceError::aws_error(
963 StatusCode::BAD_REQUEST,
964 "InvalidToken",
965 "nextToken must be 1..=1024 characters",
966 ));
967 }
968 Ok(())
969}
970
971fn validate_max_results(value: i64) -> Result<(), AwsServiceError> {
977 if !(0..=1000).contains(&value) {
978 return Err(AwsServiceError::aws_error(
979 StatusCode::BAD_REQUEST,
980 "InvalidToken",
981 format!("maxResults '{value}' is outside 0..=1000"),
982 ));
983 }
984 Ok(())
985}
986
987pub fn start_execution_from_delivery(
996 state: &SharedStepFunctionsState,
997 delivery: &Option<Arc<DeliveryBus>>,
998 dynamodb_state: &Option<SharedDynamoDbState>,
999 registry: &Option<SharedServiceRegistry>,
1000 state_machine_arn: &str,
1001 input: &str,
1002) {
1003 if serde_json::from_str::<serde_json::Value>(input).is_err() {
1005 tracing::warn!(
1006 state_machine_arn,
1007 "Step Functions delivery: invalid JSON input, skipping execution"
1008 );
1009 return;
1010 }
1011
1012 let execution_name = uuid::Uuid::new_v4().to_string();
1013
1014 let account_id = state_machine_arn
1016 .split(':')
1017 .nth(4)
1018 .unwrap_or("000000000000")
1019 .to_string();
1020
1021 let mut accounts = state.write();
1022 let st = accounts.get_or_create(&account_id);
1023 let sm = match st.state_machines.get(state_machine_arn) {
1024 Some(sm) => sm,
1025 None => {
1026 tracing::warn!(
1027 state_machine_arn,
1028 "Step Functions delivery: state machine not found"
1029 );
1030 return;
1031 }
1032 };
1033
1034 let sm_name = sm.name.clone();
1035 let sm_role_arn = sm.role_arn.clone();
1036 let definition = sm.definition.clone();
1037 let region = state_machine_arn
1040 .split(':')
1041 .nth(3)
1042 .unwrap_or("us-east-1")
1043 .to_string();
1044 let exec_arn = st.execution_arn(®ion, &sm_name, &execution_name);
1045
1046 let now = Utc::now();
1047 let execution = Execution {
1048 execution_arn: exec_arn.clone(),
1049 state_machine_arn: state_machine_arn.to_string(),
1050 state_machine_name: sm_name,
1051 name: execution_name,
1052 status: ExecutionStatus::Running,
1053 input: Some(input.to_string()),
1054 output: None,
1055 start_date: now,
1056 stop_date: None,
1057 error: None,
1058 cause: None,
1059 history_events: vec![],
1060 parent_execution_arn: None,
1061 is_sync: false,
1062 billed_duration_ms: None,
1063 billed_memory_mb: None,
1064 role_arn: sm_role_arn,
1065 };
1066
1067 st.executions.insert(exec_arn.clone(), execution);
1068 let logging_config = sm.logging_configuration.clone();
1069 drop(accounts);
1070
1071 let shared_state = state.clone();
1072 let delivery = delivery.clone();
1073 let dynamodb_state = dynamodb_state.clone();
1074 let registry = registry.clone();
1075 let input = Some(input.to_string());
1076 tokio::spawn(async move {
1077 interpreter::execute_state_machine(
1078 shared_state,
1079 exec_arn,
1080 definition,
1081 input,
1082 delivery,
1083 dynamodb_state,
1084 registry,
1085 logging_config,
1086 )
1087 .await;
1088 });
1089}
1090
1091#[cfg(test)]
1092mod tests {
1093 use super::*;
1094 use http::{HeaderMap, Method};
1095 use parking_lot::RwLock;
1096 use serde_json::Value;
1097 use std::collections::HashMap;
1098 use std::sync::Arc;
1099
1100 fn make_state() -> SharedStepFunctionsState {
1101 Arc::new(RwLock::new(
1102 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
1103 ))
1104 }
1105
1106 fn make_request(action: &str, body: &str) -> AwsRequest {
1107 AwsRequest {
1108 service: "states".to_string(),
1109 action: action.to_string(),
1110 region: "us-east-1".to_string(),
1111 account_id: "123456789012".to_string(),
1112 request_id: "test-id".to_string(),
1113 headers: HeaderMap::new(),
1114 query_params: HashMap::new(),
1115 body: body.as_bytes().to_vec().into(),
1116 body_stream: parking_lot::Mutex::new(None),
1117 path_segments: vec![],
1118 raw_path: "/".to_string(),
1119 raw_query: String::new(),
1120 method: Method::POST,
1121 is_query_protocol: false,
1122 access_key_id: None,
1123 principal: None,
1124 }
1125 }
1126
1127 fn body_json(resp: &AwsResponse) -> Value {
1128 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
1129 }
1130
1131 fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
1132 match result {
1133 Err(e) => e,
1134 Ok(_) => panic!("expected error, got Ok"),
1135 }
1136 }
1137
1138 const VALID_DEF: &str = r#"{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}"#;
1139
1140 fn create_sm(svc: &StepFunctionsService, name: &str) -> String {
1141 let body = json!({
1142 "name": name,
1143 "definition": VALID_DEF,
1144 "roleArn": "arn:aws:iam::123456789012:role/test",
1145 });
1146 let req = make_request("CreateStateMachine", &body.to_string());
1147 let resp = svc.create_state_machine(&req).unwrap();
1148 let b = body_json(&resp);
1149 b["stateMachineArn"].as_str().unwrap().to_string()
1150 }
1151
1152 #[test]
1153 fn china_region_arns_use_the_aws_cn_partition() {
1154 let svc = StepFunctionsService::new(make_state());
1155 let in_china = |action: &str, body: Value| {
1156 let mut req = make_request(action, &body.to_string());
1157 req.region = "cn-north-1".to_string();
1158 req
1159 };
1160 let resp = svc
1161 .create_state_machine(&in_china(
1162 "CreateStateMachine",
1163 json!({
1164 "name": "cn-sm",
1165 "definition": VALID_DEF,
1166 "roleArn": "arn:aws-cn:iam::123456789012:role/test",
1167 }),
1168 ))
1169 .unwrap();
1170 let sm_arn = body_json(&resp)["stateMachineArn"]
1171 .as_str()
1172 .unwrap()
1173 .to_string();
1174 assert_eq!(
1175 sm_arn,
1176 "arn:aws-cn:states:cn-north-1:123456789012:stateMachine:cn-sm"
1177 );
1178 let resp = svc
1179 .describe_state_machine(&in_china(
1180 "DescribeStateMachine",
1181 json!({"stateMachineArn": sm_arn}),
1182 ))
1183 .unwrap();
1184 assert_eq!(body_json(&resp)["name"], "cn-sm");
1185
1186 let resp = svc
1187 .create_activity(&in_china("CreateActivity", json!({"name": "cn-act"})))
1188 .unwrap();
1189 let activity_arn = body_json(&resp)["activityArn"]
1190 .as_str()
1191 .unwrap()
1192 .to_string();
1193 assert_eq!(
1194 activity_arn,
1195 "arn:aws-cn:states:cn-north-1:123456789012:activity:cn-act"
1196 );
1197 let resp = svc
1198 .describe_activity(&in_china(
1199 "DescribeActivity",
1200 json!({"activityArn": activity_arn}),
1201 ))
1202 .unwrap();
1203 assert_eq!(body_json(&resp)["name"], "cn-act");
1204 }
1205
1206 #[test]
1209 fn create_state_machine_basic() {
1210 let svc = StepFunctionsService::new(make_state());
1211 let arn = create_sm(&svc, "test-sm");
1212 assert!(arn.contains("test-sm"));
1213 }
1214
1215 #[test]
1219 fn create_state_machine_publish_creates_listable_version() {
1220 let svc = StepFunctionsService::new(make_state());
1221 let body = json!({
1222 "name": "pub-sm",
1223 "definition": VALID_DEF,
1224 "roleArn": "arn:aws:iam::123456789012:role/r",
1225 "publish": true,
1226 "versionDescription": "first",
1227 });
1228 let req = make_request("CreateStateMachine", &body.to_string());
1229 let resp = svc.create_state_machine(&req).unwrap();
1230 let b = body_json(&resp);
1231 let arn = b["stateMachineArn"].as_str().unwrap().to_string();
1232 let version_arn = b["stateMachineVersionArn"].as_str().unwrap().to_string();
1233 assert_eq!(version_arn, format!("{arn}:1"));
1234
1235 let list_req = make_request(
1236 "ListStateMachineVersions",
1237 &json!({ "stateMachineArn": arn }).to_string(),
1238 );
1239 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
1240 let versions = list["stateMachineVersions"].as_array().unwrap();
1241 assert_eq!(versions.len(), 1);
1242 assert_eq!(versions[0]["stateMachineVersionArn"], version_arn);
1243 }
1244
1245 #[test]
1246 fn create_state_machine_no_publish_has_no_version() {
1247 let svc = StepFunctionsService::new(make_state());
1248 let arn = create_sm(&svc, "nopub-sm");
1249 let body = json!({
1251 "name": "nopub-sm2",
1252 "definition": VALID_DEF,
1253 "roleArn": "arn:aws:iam::123456789012:role/r",
1254 });
1255 let req = make_request("CreateStateMachine", &body.to_string());
1256 let b = body_json(&svc.create_state_machine(&req).unwrap());
1257 assert!(b.get("stateMachineVersionArn").is_none());
1258
1259 let list_req = make_request(
1260 "ListStateMachineVersions",
1261 &json!({ "stateMachineArn": arn }).to_string(),
1262 );
1263 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
1264 assert!(list["stateMachineVersions"].as_array().unwrap().is_empty());
1265 }
1266
1267 #[test]
1271 fn update_state_machine_publish_and_encryption_round_trip() {
1272 let svc = StepFunctionsService::new(make_state());
1273 let create = json!({
1274 "name": "enc-sm",
1275 "definition": VALID_DEF,
1276 "roleArn": "arn:aws:iam::123456789012:role/r",
1277 "encryptionConfiguration": { "type": "CUSTOMER_MANAGED_KMS_KEY", "kmsKeyId": "key-123" },
1278 });
1279 let req = make_request("CreateStateMachine", &create.to_string());
1280 let arn = body_json(&svc.create_state_machine(&req).unwrap())["stateMachineArn"]
1281 .as_str()
1282 .unwrap()
1283 .to_string();
1284
1285 let desc_req = make_request(
1287 "DescribeStateMachine",
1288 &json!({ "stateMachineArn": arn }).to_string(),
1289 );
1290 let desc = body_json(&svc.describe_state_machine(&desc_req).unwrap());
1291 assert_eq!(
1292 desc["encryptionConfiguration"]["type"],
1293 "CUSTOMER_MANAGED_KMS_KEY"
1294 );
1295 assert_eq!(desc["encryptionConfiguration"]["kmsKeyId"], "key-123");
1296
1297 let upd = json!({
1299 "stateMachineArn": arn,
1300 "definition": VALID_DEF,
1301 "publish": true,
1302 "versionDescription": "v via update",
1303 });
1304 let upd_req = make_request("UpdateStateMachine", &upd.to_string());
1305 let upd_body = body_json(&svc.update_state_machine(&upd_req).unwrap());
1306 assert_eq!(
1307 upd_body["stateMachineVersionArn"].as_str().unwrap(),
1308 format!("{arn}:1")
1309 );
1310
1311 let list_req = make_request(
1312 "ListStateMachineVersions",
1313 &json!({ "stateMachineArn": arn }).to_string(),
1314 );
1315 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
1316 assert_eq!(list["stateMachineVersions"].as_array().unwrap().len(), 1);
1317 }
1318
1319 #[test]
1320 fn create_state_machine_with_express_type() {
1321 let svc = StepFunctionsService::new(make_state());
1322 let body = json!({
1323 "name": "express-sm",
1324 "definition": VALID_DEF,
1325 "roleArn": "arn:aws:iam::123456789012:role/r",
1326 "type": "EXPRESS",
1327 });
1328 let req = make_request("CreateStateMachine", &body.to_string());
1329 let resp = svc.create_state_machine(&req).unwrap();
1330 let b = body_json(&resp);
1331 assert!(b["stateMachineArn"].as_str().is_some());
1332 }
1333
1334 #[test]
1335 fn create_state_machine_duplicate_fails() {
1336 let svc = StepFunctionsService::new(make_state());
1337 create_sm(&svc, "dup-sm");
1338 let body = json!({
1339 "name": "dup-sm",
1340 "definition": VALID_DEF,
1341 "roleArn": "arn:aws:iam::123456789012:role/r",
1342 });
1343 let req = make_request("CreateStateMachine", &body.to_string());
1344 let err = expect_err(svc.create_state_machine(&req));
1345 assert!(err.to_string().contains("StateMachineAlreadyExists"));
1346 }
1347
1348 #[test]
1349 fn create_state_machine_missing_name() {
1350 let svc = StepFunctionsService::new(make_state());
1351 let body = json!({
1352 "definition": VALID_DEF,
1353 "roleArn": "arn:aws:iam::123456789012:role/r",
1354 });
1355 let req = make_request("CreateStateMachine", &body.to_string());
1356 assert!(svc.create_state_machine(&req).is_err());
1357 }
1358
1359 #[test]
1360 fn create_state_machine_invalid_definition() {
1361 let svc = StepFunctionsService::new(make_state());
1362 let body = json!({
1363 "name": "bad-def",
1364 "definition": "not json",
1365 "roleArn": "arn:aws:iam::123456789012:role/r",
1366 });
1367 let req = make_request("CreateStateMachine", &body.to_string());
1368 let err = expect_err(svc.create_state_machine(&req));
1369 assert!(err.to_string().contains("InvalidDefinition"));
1370 }
1371
1372 #[test]
1373 fn create_state_machine_definition_missing_start_at() {
1374 let svc = StepFunctionsService::new(make_state());
1375 let body = json!({
1376 "name": "no-start",
1377 "definition": r#"{"States":{"S":{"Type":"Pass","End":true}}}"#,
1378 "roleArn": "arn:aws:iam::123456789012:role/r",
1379 });
1380 let req = make_request("CreateStateMachine", &body.to_string());
1381 let err = expect_err(svc.create_state_machine(&req));
1382 assert!(err.to_string().contains("InvalidDefinition"));
1383 }
1384
1385 #[test]
1386 fn create_state_machine_definition_missing_states() {
1387 let svc = StepFunctionsService::new(make_state());
1388 let body = json!({
1389 "name": "no-states",
1390 "definition": r#"{"StartAt":"S"}"#,
1391 "roleArn": "arn:aws:iam::123456789012:role/r",
1392 });
1393 let req = make_request("CreateStateMachine", &body.to_string());
1394 let err = expect_err(svc.create_state_machine(&req));
1395 assert!(err.to_string().contains("InvalidDefinition"));
1396 }
1397
1398 #[test]
1399 fn create_state_machine_definition_start_at_not_in_states() {
1400 let svc = StepFunctionsService::new(make_state());
1401 let body = json!({
1402 "name": "bad-start",
1403 "definition": r#"{"StartAt":"Missing","States":{"S":{"Type":"Pass","End":true}}}"#,
1404 "roleArn": "arn:aws:iam::123456789012:role/r",
1405 });
1406 let req = make_request("CreateStateMachine", &body.to_string());
1407 let err = expect_err(svc.create_state_machine(&req));
1408 assert!(err.to_string().contains("MISSING_TRANSITION_TARGET"));
1409 }
1410
1411 #[test]
1412 fn create_state_machine_invalid_type() {
1413 let svc = StepFunctionsService::new(make_state());
1414 let body = json!({
1415 "name": "bad-type",
1416 "definition": VALID_DEF,
1417 "roleArn": "arn:aws:iam::123456789012:role/r",
1418 "type": "INVALID",
1419 });
1420 let req = make_request("CreateStateMachine", &body.to_string());
1421 assert!(svc.create_state_machine(&req).is_err());
1422 }
1423
1424 #[test]
1425 fn create_state_machine_invalid_arn() {
1426 let svc = StepFunctionsService::new(make_state());
1427 let body = json!({
1428 "name": "bad-arn",
1429 "definition": VALID_DEF,
1430 "roleArn": "not-an-arn",
1431 });
1432 let req = make_request("CreateStateMachine", &body.to_string());
1433 let err = expect_err(svc.create_state_machine(&req));
1434 assert!(err.to_string().contains("InvalidArn"));
1435 }
1436
1437 #[test]
1438 fn create_state_machine_invalid_name() {
1439 let svc = StepFunctionsService::new(make_state());
1440 let body = json!({
1441 "name": "has spaces!",
1442 "definition": VALID_DEF,
1443 "roleArn": "arn:aws:iam::123456789012:role/r",
1444 });
1445 let req = make_request("CreateStateMachine", &body.to_string());
1446 let err = expect_err(svc.create_state_machine(&req));
1447 assert!(err.to_string().contains("InvalidName"));
1448 }
1449
1450 #[test]
1451 fn create_state_machine_name_too_long() {
1452 let svc = StepFunctionsService::new(make_state());
1453 let long_name = "a".repeat(81);
1454 let body = json!({
1455 "name": long_name,
1456 "definition": VALID_DEF,
1457 "roleArn": "arn:aws:iam::123456789012:role/r",
1458 });
1459 let req = make_request("CreateStateMachine", &body.to_string());
1460 let err = expect_err(svc.create_state_machine(&req));
1461 assert!(err.to_string().contains("InvalidName"));
1462 }
1463
1464 #[test]
1467 fn describe_state_machine_found() {
1468 let svc = StepFunctionsService::new(make_state());
1469 let arn = create_sm(&svc, "desc-sm");
1470
1471 let req = make_request(
1472 "DescribeStateMachine",
1473 &json!({"stateMachineArn": arn}).to_string(),
1474 );
1475 let resp = svc.describe_state_machine(&req).unwrap();
1476 let b = body_json(&resp);
1477 assert_eq!(b["name"], "desc-sm");
1478 assert_eq!(b["status"], "ACTIVE");
1479 assert!(b["definition"].as_str().is_some());
1480 }
1481
1482 #[test]
1483 fn describe_state_machine_not_found() {
1484 let svc = StepFunctionsService::new(make_state());
1485 let req = make_request(
1486 "DescribeStateMachine",
1487 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1488 .to_string(),
1489 );
1490 let err = expect_err(svc.describe_state_machine(&req));
1491 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1492 }
1493
1494 #[test]
1497 fn list_state_machines_empty() {
1498 let svc = StepFunctionsService::new(make_state());
1499 let req = make_request("ListStateMachines", "{}");
1500 let resp = svc.list_state_machines(&req).unwrap();
1501 let b = body_json(&resp);
1502 assert!(b["stateMachines"].as_array().unwrap().is_empty());
1503 }
1504
1505 #[test]
1506 fn list_state_machines_returns_created() {
1507 let svc = StepFunctionsService::new(make_state());
1508 create_sm(&svc, "sm-1");
1509 create_sm(&svc, "sm-2");
1510
1511 let req = make_request("ListStateMachines", "{}");
1512 let resp = svc.list_state_machines(&req).unwrap();
1513 let b = body_json(&resp);
1514 assert_eq!(b["stateMachines"].as_array().unwrap().len(), 2);
1515 }
1516
1517 #[test]
1520 fn delete_state_machine() {
1521 let svc = StepFunctionsService::new(make_state());
1522 let arn = create_sm(&svc, "del-sm");
1523
1524 let req = make_request(
1525 "DeleteStateMachine",
1526 &json!({"stateMachineArn": arn}).to_string(),
1527 );
1528 svc.delete_state_machine(&req).unwrap();
1529
1530 let req = make_request(
1532 "DescribeStateMachine",
1533 &json!({"stateMachineArn": arn}).to_string(),
1534 );
1535 assert!(svc.describe_state_machine(&req).is_err());
1536 }
1537
1538 #[test]
1539 fn delete_state_machine_nonexistent_succeeds() {
1540 let svc = StepFunctionsService::new(make_state());
1541 let req = make_request(
1542 "DeleteStateMachine",
1543 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1544 .to_string(),
1545 );
1546 svc.delete_state_machine(&req).unwrap();
1548 }
1549
1550 #[test]
1553 fn update_state_machine() {
1554 let svc = StepFunctionsService::new(make_state());
1555 let arn = create_sm(&svc, "upd-sm");
1556
1557 let new_def = r#"{"StartAt":"NewPass","States":{"NewPass":{"Type":"Pass","End":true}}}"#;
1558 let body = json!({
1559 "stateMachineArn": arn,
1560 "definition": new_def,
1561 "description": "updated",
1562 });
1563 let req = make_request("UpdateStateMachine", &body.to_string());
1564 let resp = svc.update_state_machine(&req).unwrap();
1565 let b = body_json(&resp);
1566 assert!(b["updateDate"].as_f64().is_some());
1567
1568 let req = make_request(
1570 "DescribeStateMachine",
1571 &json!({"stateMachineArn": arn}).to_string(),
1572 );
1573 let resp = svc.describe_state_machine(&req).unwrap();
1574 let b = body_json(&resp);
1575 assert!(b["definition"].as_str().unwrap().contains("NewPass"));
1576 assert_eq!(b["description"], "updated");
1577 }
1578
1579 #[test]
1580 fn update_state_machine_not_found() {
1581 let svc = StepFunctionsService::new(make_state());
1582 let body = json!({
1583 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1584 "definition": VALID_DEF,
1585 });
1586 let req = make_request("UpdateStateMachine", &body.to_string());
1587 let err = expect_err(svc.update_state_machine(&req));
1588 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1589 }
1590
1591 #[tokio::test]
1594 async fn start_execution_basic() {
1595 let svc = StepFunctionsService::new(make_state());
1596 let arn = create_sm(&svc, "exec-sm");
1597
1598 let body = json!({
1599 "stateMachineArn": arn,
1600 "input": r#"{"key":"value"}"#,
1601 });
1602 let req = make_request("StartExecution", &body.to_string());
1603 let resp = svc.start_execution(&req).unwrap();
1604 let b = body_json(&resp);
1605 assert!(b["executionArn"].as_str().is_some());
1606 assert!(b["startDate"].as_f64().is_some());
1607 }
1608
1609 #[tokio::test]
1610 async fn start_execution_with_name() {
1611 let svc = StepFunctionsService::new(make_state());
1612 let arn = create_sm(&svc, "named-exec");
1613
1614 let body = json!({
1615 "stateMachineArn": arn,
1616 "name": "my-execution",
1617 });
1618 let req = make_request("StartExecution", &body.to_string());
1619 let resp = svc.start_execution(&req).unwrap();
1620 let b = body_json(&resp);
1621 assert!(b["executionArn"].as_str().unwrap().contains("my-execution"));
1622 }
1623
1624 #[tokio::test]
1625 async fn start_execution_sm_not_found() {
1626 let svc = StepFunctionsService::new(make_state());
1627 let body = json!({
1628 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1629 });
1630 let req = make_request("StartExecution", &body.to_string());
1631 let err = expect_err(svc.start_execution(&req));
1632 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1633 }
1634
1635 #[tokio::test]
1636 async fn start_execution_invalid_input() {
1637 let svc = StepFunctionsService::new(make_state());
1638 let arn = create_sm(&svc, "bad-input");
1639
1640 let body = json!({
1641 "stateMachineArn": arn,
1642 "input": "not json",
1643 });
1644 let req = make_request("StartExecution", &body.to_string());
1645 let err = expect_err(svc.start_execution(&req));
1646 assert!(err.to_string().contains("InvalidExecutionInput"));
1647 }
1648
1649 #[tokio::test]
1650 async fn start_execution_same_name_same_input_is_idempotent() {
1651 let svc = StepFunctionsService::new(make_state());
1652 let arn = create_sm(&svc, "dup-exec");
1653
1654 let body = json!({
1655 "stateMachineArn": arn,
1656 "name": "same-name",
1657 "input": "{\"a\":1}",
1658 });
1659 let req = make_request("StartExecution", &body.to_string());
1660 let first = body_json(&svc.start_execution(&req).unwrap());
1661
1662 let req = make_request("StartExecution", &body.to_string());
1664 let second = body_json(&svc.start_execution(&req).unwrap());
1665 assert_eq!(first["executionArn"], second["executionArn"]);
1666 assert_eq!(first["startDate"], second["startDate"]);
1667 }
1668
1669 #[tokio::test]
1670 async fn start_execution_same_name_different_input_conflicts() {
1671 let svc = StepFunctionsService::new(make_state());
1672 let arn = create_sm(&svc, "dup-exec-diff");
1673
1674 let req = make_request(
1675 "StartExecution",
1676 &json!({
1677 "stateMachineArn": arn,
1678 "name": "same-name",
1679 "input": "{\"a\":1}",
1680 })
1681 .to_string(),
1682 );
1683 svc.start_execution(&req).unwrap();
1684
1685 let req = make_request(
1687 "StartExecution",
1688 &json!({
1689 "stateMachineArn": arn,
1690 "name": "same-name",
1691 "input": "{\"a\":2}",
1692 })
1693 .to_string(),
1694 );
1695 let err = expect_err(svc.start_execution(&req));
1696 assert!(err.to_string().contains("ExecutionAlreadyExists"));
1697 }
1698
1699 #[tokio::test]
1700 async fn start_execution_express_name_collision_never_idempotent() {
1701 let svc = StepFunctionsService::new(make_state());
1702 let arn = create_express_sm(&svc, "dup-exec-express");
1703
1704 let body = json!({
1705 "stateMachineArn": arn,
1706 "name": "same-name",
1707 "input": "{\"a\":1}",
1708 });
1709 let req = make_request("StartExecution", &body.to_string());
1710 svc.start_execution(&req).unwrap();
1711
1712 let req = make_request("StartExecution", &body.to_string());
1714 let err = expect_err(svc.start_execution(&req));
1715 assert!(err.to_string().contains("ExecutionAlreadyExists"));
1716 }
1717
1718 #[tokio::test]
1721 async fn describe_execution_found() {
1722 let svc = StepFunctionsService::new(make_state());
1723 let sm_arn = create_sm(&svc, "desc-exec");
1724
1725 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
1726 let req = make_request("StartExecution", &body.to_string());
1727 let resp = svc.start_execution(&req).unwrap();
1728 let exec_arn = body_json(&resp)["executionArn"]
1729 .as_str()
1730 .unwrap()
1731 .to_string();
1732
1733 let req = make_request(
1734 "DescribeExecution",
1735 &json!({"executionArn": exec_arn}).to_string(),
1736 );
1737 let resp = svc.describe_execution(&req).unwrap();
1738 let b = body_json(&resp);
1739 assert_eq!(b["name"], "e1");
1740 assert_eq!(b["status"], "RUNNING");
1741 }
1742
1743 #[tokio::test]
1744 async fn describe_execution_not_found() {
1745 let svc = StepFunctionsService::new(make_state());
1746 let req = make_request(
1747 "DescribeExecution",
1748 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1749 .to_string(),
1750 );
1751 let err = expect_err(svc.describe_execution(&req));
1752 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1753 }
1754
1755 #[tokio::test]
1758 async fn stop_execution() {
1759 let svc = StepFunctionsService::new(make_state());
1760 let sm_arn = create_sm(&svc, "stop-sm");
1761
1762 let body = json!({"stateMachineArn": sm_arn, "name": "stop-e"});
1763 let req = make_request("StartExecution", &body.to_string());
1764 let resp = svc.start_execution(&req).unwrap();
1765 let exec_arn = body_json(&resp)["executionArn"]
1766 .as_str()
1767 .unwrap()
1768 .to_string();
1769
1770 let body = json!({
1771 "executionArn": exec_arn,
1772 "error": "UserAborted",
1773 "cause": "test stop",
1774 });
1775 let req = make_request("StopExecution", &body.to_string());
1776 let resp = svc.stop_execution(&req).unwrap();
1777 let b = body_json(&resp);
1778 assert!(b["stopDate"].as_f64().is_some());
1779
1780 let req = make_request(
1782 "DescribeExecution",
1783 &json!({"executionArn": exec_arn}).to_string(),
1784 );
1785 let resp = svc.describe_execution(&req).unwrap();
1786 let b = body_json(&resp);
1787 assert_eq!(b["status"], "ABORTED");
1788 assert_eq!(b["error"], "UserAborted");
1789 }
1790
1791 #[tokio::test]
1792 async fn redrive_execution_reruns_to_terminal() {
1793 let svc = StepFunctionsService::new(make_state());
1794 let sm_arn = create_sm(&svc, "redrive-sm");
1795
1796 let body = json!({"stateMachineArn": sm_arn, "name": "redrive-e"});
1797 let req = make_request("StartExecution", &body.to_string());
1798 let resp = svc.start_execution(&req).unwrap();
1799 let exec_arn = body_json(&resp)["executionArn"]
1800 .as_str()
1801 .unwrap()
1802 .to_string();
1803
1804 let req = make_request(
1806 "StopExecution",
1807 &json!({"executionArn": exec_arn, "error": "UserAborted", "cause": "stop"}).to_string(),
1808 );
1809 svc.stop_execution(&req).unwrap();
1810
1811 let req = make_request(
1813 "RedriveExecution",
1814 &json!({"executionArn": exec_arn}).to_string(),
1815 );
1816 let resp = svc.redrive_execution(&req).unwrap();
1817 assert!(body_json(&resp)["redriveDate"].as_i64().is_some());
1818
1819 let mut status = String::new();
1822 for _ in 0..100 {
1823 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1824 let req = make_request(
1825 "DescribeExecution",
1826 &json!({"executionArn": exec_arn}).to_string(),
1827 );
1828 let b = body_json(&svc.describe_execution(&req).unwrap());
1829 status = b["status"].as_str().unwrap().to_string();
1830 if status != "RUNNING" {
1831 break;
1832 }
1833 }
1834 assert_eq!(
1835 status, "SUCCEEDED",
1836 "redriven execution must run to completion"
1837 );
1838 }
1839
1840 #[tokio::test]
1841 async fn redrive_execution_running_rejected() {
1842 let svc = StepFunctionsService::new(make_state());
1843 let sm_arn = create_sm(&svc, "redrive-running-sm");
1844 let body = json!({"stateMachineArn": sm_arn, "name": "still-running"});
1845 let req = make_request("StartExecution", &body.to_string());
1846 let exec_arn = body_json(&svc.start_execution(&req).unwrap())["executionArn"]
1847 .as_str()
1848 .unwrap()
1849 .to_string();
1850
1851 let req = make_request(
1853 "RedriveExecution",
1854 &json!({"executionArn": exec_arn}).to_string(),
1855 );
1856 let err = expect_err(svc.redrive_execution(&req));
1857 assert!(err.to_string().contains("ExecutionNotRedrivable"));
1858 }
1859
1860 #[tokio::test]
1861 async fn stop_execution_not_found() {
1862 let svc = StepFunctionsService::new(make_state());
1863 let req = make_request(
1864 "StopExecution",
1865 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1866 .to_string(),
1867 );
1868 let err = expect_err(svc.stop_execution(&req));
1869 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1870 }
1871
1872 #[tokio::test]
1875 async fn list_executions() {
1876 let svc = StepFunctionsService::new(make_state());
1877 let sm_arn = create_sm(&svc, "list-exec");
1878
1879 for i in 0..3 {
1880 let body = json!({"stateMachineArn": sm_arn, "name": format!("e{i}")});
1881 let req = make_request("StartExecution", &body.to_string());
1882 svc.start_execution(&req).unwrap();
1883 }
1884
1885 let req = make_request(
1886 "ListExecutions",
1887 &json!({"stateMachineArn": sm_arn}).to_string(),
1888 );
1889 let resp = svc.list_executions(&req).unwrap();
1890 let b = body_json(&resp);
1891 assert_eq!(b["executions"].as_array().unwrap().len(), 3);
1892 }
1893
1894 #[tokio::test]
1895 async fn list_executions_sm_not_found() {
1896 let svc = StepFunctionsService::new(make_state());
1897 let req = make_request(
1898 "ListExecutions",
1899 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1900 .to_string(),
1901 );
1902 let err = expect_err(svc.list_executions(&req));
1903 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1904 }
1905
1906 #[tokio::test]
1909 async fn get_execution_history_not_found() {
1910 let svc = StepFunctionsService::new(make_state());
1911 let req = make_request(
1912 "GetExecutionHistory",
1913 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1914 .to_string(),
1915 );
1916 let err = expect_err(svc.get_execution_history(&req));
1917 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1918 }
1919
1920 #[tokio::test]
1923 async fn describe_sm_for_execution() {
1924 let svc = StepFunctionsService::new(make_state());
1925 let sm_arn = create_sm(&svc, "sm-for-exec");
1926
1927 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
1928 let req = make_request("StartExecution", &body.to_string());
1929 let resp = svc.start_execution(&req).unwrap();
1930 let exec_arn = body_json(&resp)["executionArn"]
1931 .as_str()
1932 .unwrap()
1933 .to_string();
1934
1935 let req = make_request(
1936 "DescribeStateMachineForExecution",
1937 &json!({"executionArn": exec_arn}).to_string(),
1938 );
1939 let resp = svc.describe_state_machine_for_execution(&req).unwrap();
1940 let b = body_json(&resp);
1941 assert_eq!(b["name"], "sm-for-exec");
1942 }
1943
1944 #[test]
1947 fn tag_untag_list_tags() {
1948 let svc = StepFunctionsService::new(make_state());
1949 let arn = create_sm(&svc, "tagged-sm");
1950
1951 let body = json!({
1953 "resourceArn": arn,
1954 "tags": [{"key": "env", "value": "prod"}],
1955 });
1956 let req = make_request("TagResource", &body.to_string());
1957 svc.tag_resource(&req).unwrap();
1958
1959 let req = make_request(
1961 "ListTagsForResource",
1962 &json!({"resourceArn": arn}).to_string(),
1963 );
1964 let resp = svc.list_tags_for_resource(&req).unwrap();
1965 let b = body_json(&resp);
1966 let tags = b["tags"].as_array().unwrap();
1967 assert_eq!(tags.len(), 1);
1968 assert_eq!(tags[0]["key"], "env");
1969
1970 let body = json!({
1972 "resourceArn": arn,
1973 "tagKeys": ["env"],
1974 });
1975 let req = make_request("UntagResource", &body.to_string());
1976 svc.untag_resource(&req).unwrap();
1977
1978 let req = make_request(
1980 "ListTagsForResource",
1981 &json!({"resourceArn": arn}).to_string(),
1982 );
1983 let resp = svc.list_tags_for_resource(&req).unwrap();
1984 let b = body_json(&resp);
1985 assert!(b["tags"].as_array().unwrap().is_empty());
1986 }
1987
1988 #[test]
1989 fn tag_resource_not_found() {
1990 let svc = StepFunctionsService::new(make_state());
1991 let body = json!({
1992 "resourceArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1993 "tags": [{"key": "k", "value": "v"}],
1994 });
1995 let req = make_request("TagResource", &body.to_string());
1996 let err = expect_err(svc.tag_resource(&req));
1997 assert!(err.to_string().contains("ResourceNotFound"));
1998 }
1999
2000 #[test]
2003 fn test_validate_name() {
2004 assert!(validate_name("valid-name").is_ok());
2005 assert!(validate_name("under_score").is_ok());
2006 assert!(validate_name("").is_err());
2007 assert!(validate_name("has spaces").is_err());
2008 assert!(validate_name(&"a".repeat(81)).is_err());
2009 }
2010
2011 #[test]
2012 fn test_validate_definition() {
2013 assert!(validate_definition(VALID_DEF).is_ok());
2014 assert!(validate_definition("not json").is_err());
2015 assert!(validate_definition(r#"{"States":{}}"#).is_err()); assert!(validate_definition(r#"{"StartAt":"S"}"#).is_err()); }
2018
2019 #[test]
2020 fn test_validate_definition_rejects_malformed_paths() {
2021 let def =
2023 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"$.arr[","End":true}}}"#;
2024 assert!(validate_definition(def).is_err());
2025
2026 let def =
2028 "{\"StartAt\":\"P\",\"States\":{\"P\":{\"Type\":\"Pass\",\"OutputPath\":\"$.x[\u{00e9}\",\"End\":true}}}";
2029 assert!(validate_definition(def).is_err());
2030
2031 let def = r#"{"StartAt":"C","States":{"C":{"Type":"Choice","Choices":[{"Variable":"$.n[","NumericEquals":1,"Next":"P"}]},"P":{"Type":"Pass","End":true}}}"#;
2033 assert!(validate_definition(def).is_err());
2034
2035 let def =
2037 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"foo.bar","End":true}}}"#;
2038 assert!(validate_definition(def).is_err());
2039
2040 let def =
2042 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}"#;
2043 assert!(validate_definition(def).is_err());
2044 }
2045
2046 #[test]
2047 fn test_validate_definition_accepts_well_formed_paths() {
2048 let def = r#"{"StartAt":"P","States":{
2051 "P":{"Type":"Pass","InputPath":"$.a.b[0].c","OutputPath":"$","ResultPath":"$.out","Next":"C"},
2052 "C":{"Type":"Choice","Choices":[
2053 {"And":[{"Variable":"$.items[2]","NumericEquals":1},{"Variable":"$.flag","BooleanEquals":true}],"Next":"S"}
2054 ],"Default":"S"},
2055 "S":{"Type":"Succeed","InputPath":"null"}
2056 }}"#;
2057 assert!(validate_definition(def).is_ok());
2058 }
2059
2060 #[test]
2063 fn test_validate_definition_recurses_into_parallel_and_map() {
2064 let par = r#"{"StartAt":"Par","States":{"Par":{"Type":"Parallel","End":true,
2065 "Branches":[{"StartAt":"I","States":{"I":{"Type":"Pass","InputPath":"nope","End":true}}}]}}}"#;
2066 assert!(validate_definition(par).is_err());
2067
2068 let map = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
2069 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","OutputPath":"nope","End":true}}}}}}"#;
2070 assert!(validate_definition(map).is_err());
2071
2072 let iter = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
2074 "Iterator":{"StartAt":"E","States":{"E":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}}}}"#;
2075 assert!(validate_definition(iter).is_err());
2076
2077 let ok = r#"{"StartAt":"M","States":{"M":{"Type":"Map","ItemsPath":"$.items","End":true,
2079 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","InputPath":"$.a","End":true}}}}}}"#;
2080 assert!(validate_definition(ok).is_ok());
2081 }
2082
2083 #[test]
2085 fn test_validate_definition_payload_template_dollar_keys() {
2086 let lit = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
2088 "Parameters":{"v.$":"just text"},"End":true}}}"#;
2089 assert!(validate_definition(lit).is_err());
2090
2091 let num = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
2093 "Parameters":{"v.$":42},"End":true}}}"#;
2094 assert!(validate_definition(num).is_err());
2095
2096 let ok = r#"{"StartAt":"P","States":{"P":{"Type":"Pass","Parameters":{
2098 "a.$":"$.input","b.$":"$$.Execution.Id","c.$":"States.Format('{}',$.n)",
2099 "nested":{"d.$":"$.x"},"plain":"literal-ok"},"End":true}}}"#;
2100 assert!(validate_definition(ok).is_ok());
2101
2102 let arr = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
2104 "ResultSelector":{"items":[{"bad.$":"literal"}]},"End":true}}}"#;
2105 assert!(validate_definition(arr).is_err());
2106 }
2107
2108 #[test]
2109 fn test_is_valid_reference_path() {
2110 assert!(is_valid_reference_path("$"));
2111 assert!(is_valid_reference_path("$.foo"));
2112 assert!(is_valid_reference_path("$.foo.bar[3].baz"));
2113 assert!(is_valid_reference_path("$[0]"));
2114 assert!(!is_valid_reference_path("$.arr["));
2115 assert!(!is_valid_reference_path("$.x[\u{00e9}"));
2116 assert!(!is_valid_reference_path("$.x[]"));
2117 assert!(!is_valid_reference_path("$.x[abc]"));
2118 assert!(!is_valid_reference_path("foo.bar"));
2119 assert!(!is_valid_reference_path(""));
2120 }
2121
2122 #[test]
2123 fn test_validate_arn() {
2124 assert!(validate_arn("arn:aws:states:us-east-1:123:sm:test").is_ok());
2125 assert!(validate_arn("not-an-arn").is_err());
2126 }
2127
2128 #[test]
2129 fn test_camel_to_details_key() {
2130 assert_eq!(camel_to_details_key("PassStateEntered"), "stateEntered");
2132 assert_eq!(camel_to_details_key("TaskStateEntered"), "stateEntered");
2133 assert_eq!(camel_to_details_key("ChoiceStateExited"), "stateExited");
2134 assert_eq!(camel_to_details_key("MapStateExited"), "stateExited");
2135 assert_eq!(camel_to_details_key("TaskScheduled"), "taskScheduled");
2137 assert_eq!(camel_to_details_key("ExecutionStarted"), "executionStarted");
2138 assert_eq!(camel_to_details_key(""), "");
2139 }
2140
2141 #[test]
2142 fn test_is_mutating_action() {
2143 assert!(is_mutating_action("CreateStateMachine"));
2144 assert!(is_mutating_action("StartExecution"));
2145 assert!(!is_mutating_action("DescribeStateMachine"));
2146 assert!(!is_mutating_action("ListStateMachines"));
2147 }
2148
2149 fn create_express_sm(svc: &StepFunctionsService, name: &str) -> String {
2152 let body = json!({
2153 "name": name,
2154 "definition": VALID_DEF,
2155 "roleArn": "arn:aws:iam::123456789012:role/test",
2156 "type": "EXPRESS",
2157 });
2158 let req = make_request("CreateStateMachine", &body.to_string());
2159 let resp = svc.create_state_machine(&req).unwrap();
2160 let b = body_json(&resp);
2161 b["stateMachineArn"].as_str().unwrap().to_string()
2162 }
2163
2164 #[tokio::test]
2165 async fn start_sync_execution_basic() {
2166 let svc = StepFunctionsService::new(make_state());
2167 let arn = create_express_sm(&svc, "sync-sm");
2168
2169 let body = json!({
2170 "stateMachineArn": arn,
2171 "input": r#"{"key":"value"}"#,
2172 });
2173 let req = make_request("StartSyncExecution", &body.to_string());
2174 let resp = svc.start_sync_execution(&req).await.unwrap();
2175 let b = body_json(&resp);
2176 assert!(b["executionArn"]
2177 .as_str()
2178 .unwrap()
2179 .contains("express:sync-sm"));
2180 assert_eq!(b["stateMachineArn"], arn);
2181 assert_eq!(b["status"], "SUCCEEDED");
2182 assert!(b["startDate"].as_i64().is_some());
2183 assert!(b["stopDate"].as_i64().is_some());
2184 assert!(b["output"].as_str().is_some());
2185 assert!(b["billingDetails"]["billedDurationInMilliseconds"]
2186 .as_i64()
2187 .is_some());
2188 }
2189
2190 #[tokio::test]
2191 async fn start_sync_execution_not_express() {
2192 let svc = StepFunctionsService::new(make_state());
2193 let arn = create_sm(&svc, "std-sm");
2194
2195 let body = json!({"stateMachineArn": arn});
2196 let req = make_request("StartSyncExecution", &body.to_string());
2197 let err = expect_err(svc.start_sync_execution(&req).await);
2198 assert!(err.to_string().contains("StateMachineTypeNotSupported"));
2199 }
2200
2201 #[tokio::test]
2202 async fn start_sync_execution_sm_not_found() {
2203 let svc = StepFunctionsService::new(make_state());
2204 let body = json!({
2205 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
2206 });
2207 let req = make_request("StartSyncExecution", &body.to_string());
2208 let err = expect_err(svc.start_sync_execution(&req).await);
2209 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2210 }
2211
2212 #[tokio::test]
2213 async fn start_sync_execution_records_introspection_fields() {
2214 let svc = StepFunctionsService::new(make_state());
2215 let arn = create_express_sm(&svc, "sync-introspect");
2216
2217 let body = json!({"stateMachineArn": arn, "input": "{}"});
2218 let req = make_request("StartSyncExecution", &body.to_string());
2219 let resp = svc.start_sync_execution(&req).await.unwrap();
2220 let b = body_json(&resp);
2221 let exec_arn = b["executionArn"].as_str().unwrap().to_string();
2222
2223 let accounts = svc.state.read();
2224 let state = accounts.get("123456789012").unwrap();
2225 let stored = state
2226 .executions
2227 .get(&exec_arn)
2228 .expect("sync execution should be persisted for introspection");
2229 assert!(stored.is_sync, "sync executions must be marked is_sync");
2230 assert_eq!(stored.billed_memory_mb, Some(64));
2231 assert!(
2232 stored.billed_duration_ms.is_some(),
2233 "billed_duration_ms must be populated after sync run"
2234 );
2235 assert!(
2236 stored.parent_execution_arn.is_none(),
2237 "top-level sync execution has no parent"
2238 );
2239 }
2240
2241 #[tokio::test]
2242 async fn start_sync_execution_invalid_input() {
2243 let svc = StepFunctionsService::new(make_state());
2244 let arn = create_express_sm(&svc, "bad-input-sync");
2245
2246 let body = json!({
2247 "stateMachineArn": arn,
2248 "input": "not json",
2249 });
2250 let req = make_request("StartSyncExecution", &body.to_string());
2251 let err = expect_err(svc.start_sync_execution(&req).await);
2252 assert!(err.to_string().contains("InvalidExecutionInput"));
2253 }
2254
2255 #[test]
2257 fn snapshot_hook_is_none_without_store() {
2258 let svc = StepFunctionsService::new(make_state());
2259 assert!(svc.snapshot_hook().is_none());
2260 }
2261
2262 #[tokio::test]
2266 async fn snapshot_hook_fires_with_store() {
2267 let store: Arc<dyn fakecloud_persistence::SnapshotStore> =
2268 Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
2269 let svc = StepFunctionsService::new(make_state()).with_snapshot_store(store);
2270 let hook = svc
2271 .snapshot_hook()
2272 .expect("hook present when a store is set");
2273 hook().await;
2274 }
2275
2276 fn make_execution(arn: &str, status: ExecutionStatus) -> Execution {
2277 Execution {
2278 execution_arn: arn.to_string(),
2279 state_machine_arn: "arn:aws:states:us-east-1:123456789012:stateMachine:sm".to_string(),
2280 state_machine_name: "sm".to_string(),
2281 name: arn.to_string(),
2282 status,
2283 input: None,
2284 output: None,
2285 start_date: Utc::now(),
2286 stop_date: None,
2287 error: None,
2288 cause: None,
2289 history_events: vec![],
2290 parent_execution_arn: None,
2291 is_sync: false,
2292 billed_duration_ms: None,
2293 billed_memory_mb: None,
2294 role_arn: String::new(),
2295 }
2296 }
2297
2298 #[test]
2299 fn reconcile_aborts_running_executions_on_restart() {
2300 let state = make_state();
2304 {
2305 let mut accounts = state.write();
2306 let s = accounts.get_or_create("123456789012");
2307 s.executions.insert(
2308 "running".into(),
2309 make_execution("running", ExecutionStatus::Running),
2310 );
2311 s.executions.insert(
2312 "done".into(),
2313 make_execution("done", ExecutionStatus::Succeeded),
2314 );
2315 }
2316
2317 let n = reconcile_interrupted_executions(&state);
2318 assert_eq!(n, 1, "only the RUNNING execution is reconciled");
2319
2320 let accounts = state.read();
2321 let s = accounts.get("123456789012").unwrap();
2322 let running = &s.executions["running"];
2323 assert_eq!(running.status, ExecutionStatus::Aborted);
2324 assert!(running.stop_date.is_some());
2325 assert_eq!(running.error.as_deref(), Some("Fakecloud.Restart"));
2326 assert_eq!(s.executions["done"].status, ExecutionStatus::Succeeded);
2327 }
2328
2329 #[tokio::test]
2332 async fn distributed_map_populates_map_run() {
2333 let state = make_state();
2334 let svc = StepFunctionsService::new(state.clone());
2335 let execution_arn =
2336 "arn:aws:states:us-east-1:123456789012:execution:dist-sm:exec-1".to_string();
2337
2338 let def = json!({
2339 "StartAt": "M",
2340 "States": {
2341 "M": {
2342 "Type": "Map",
2343 "ItemsPath": "$.items",
2344 "ItemProcessor": {
2345 "ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "STANDARD" },
2346 "StartAt": "Item",
2347 "States": { "Item": { "Type": "Pass", "End": true } }
2348 },
2349 "End": true
2350 }
2351 }
2352 });
2353
2354 interpreter::execute_state_machine(
2355 state.clone(),
2356 execution_arn.clone(),
2357 def.to_string(),
2358 Some(r#"{"items":[1,2,3]}"#.to_string()),
2359 None,
2360 None,
2361 None,
2362 None,
2363 )
2364 .await;
2365
2366 let req = make_request(
2368 "ListMapRuns",
2369 &json!({ "executionArn": execution_arn }).to_string(),
2370 );
2371 let listed = body_json(&svc.list_map_runs(&req).unwrap());
2372 let runs = listed["mapRuns"].as_array().unwrap();
2373 assert_eq!(
2374 runs.len(),
2375 1,
2376 "distributed map must create exactly one MapRun"
2377 );
2378 let map_run_arn = runs[0]["mapRunArn"].as_str().unwrap().to_string();
2379 assert!(map_run_arn.contains(":mapRun:"));
2380 assert_eq!(runs[0]["executionArn"], execution_arn);
2381
2382 let req = make_request(
2384 "DescribeMapRun",
2385 &json!({ "mapRunArn": map_run_arn }).to_string(),
2386 );
2387 let mr = body_json(&svc.describe_map_run(&req).unwrap());
2388 assert_eq!(mr["status"], "SUCCEEDED");
2389 assert_eq!(mr["itemCounts"]["total"], 3);
2390 assert_eq!(mr["itemCounts"]["succeeded"], 3);
2391 assert_eq!(mr["itemCounts"]["failed"], 0);
2392 assert_eq!(mr["executionCounts"]["succeeded"], 3);
2393 }
2394
2395 #[tokio::test]
2396 async fn inline_map_does_not_create_map_run() {
2397 let state = make_state();
2398 let svc = StepFunctionsService::new(state.clone());
2399 let execution_arn =
2400 "arn:aws:states:us-east-1:123456789012:execution:inline-sm:exec-1".to_string();
2401
2402 let def = json!({
2404 "StartAt": "M",
2405 "States": {
2406 "M": {
2407 "Type": "Map",
2408 "ItemsPath": "$.items",
2409 "ItemProcessor": {
2410 "StartAt": "Item",
2411 "States": { "Item": { "Type": "Pass", "End": true } }
2412 },
2413 "End": true
2414 }
2415 }
2416 });
2417
2418 interpreter::execute_state_machine(
2419 state.clone(),
2420 execution_arn.clone(),
2421 def.to_string(),
2422 Some(r#"{"items":[1,2]}"#.to_string()),
2423 None,
2424 None,
2425 None,
2426 None,
2427 )
2428 .await;
2429
2430 let req = make_request(
2431 "ListMapRuns",
2432 &json!({ "executionArn": execution_arn }).to_string(),
2433 );
2434 let listed = body_json(&svc.list_map_runs(&req).unwrap());
2435 assert!(listed["mapRuns"].as_array().unwrap().is_empty());
2436 }
2437}
2438
2439#[cfg(test)]
2440mod pagination_reject_test {
2441 #[test]
2442 fn paginate_checked_rejects_invalid_token() {
2443 use fakecloud_core::pagination::paginate_checked;
2444 let items: Vec<i32> = (0..5).collect();
2445 assert!(paginate_checked(&items, Some("bad"), 3).is_err());
2446 assert!(paginate_checked(&items, Some("2"), 3).is_ok());
2447 }
2448}