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 =
169 StepFunctionsSnapshot::of(STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION, state.read().clone());
170 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
171 let bytes = serde_json::to_vec(&snapshot)
172 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
173 store.save(&bytes)
174 })
175 .await;
176 match join {
177 Ok(Ok(())) => {}
178 Ok(Err(err)) => tracing::error!(%err, "failed to write stepfunctions snapshot"),
179 Err(err) => tracing::error!(%err, "stepfunctions snapshot task panicked"),
180 }
181}
182
183pub fn reconcile_interrupted_executions(state: &SharedStepFunctionsState) -> usize {
193 let now = Utc::now();
194 let mut count = 0;
195 let mut accounts = state.write();
196 for (_, _, s) in accounts.iter_regional_mut() {
197 for exec in s.executions.values_mut() {
198 if matches!(
199 exec.status,
200 ExecutionStatus::Running | ExecutionStatus::PendingRedrive
201 ) {
202 exec.status = ExecutionStatus::Aborted;
203 exec.stop_date = Some(now);
204 exec.error = Some("Fakecloud.Restart".to_string());
205 exec.cause = Some(
206 "Execution was interrupted by a fakecloud restart and cannot be resumed"
207 .to_string(),
208 );
209 count += 1;
210 }
211 }
212 }
213 count
214}
215
216fn is_mutating_action(action: &str) -> bool {
217 matches!(
218 action,
219 "CreateStateMachine"
220 | "DeleteStateMachine"
221 | "UpdateStateMachine"
222 | "TagResource"
223 | "UntagResource"
224 | "StartExecution"
225 | "StopExecution"
226 | "CreateActivity"
227 | "DeleteActivity"
228 | "GetActivityTask"
229 | "SendTaskFailure"
230 | "SendTaskHeartbeat"
231 | "SendTaskSuccess"
232 | "PublishStateMachineVersion"
233 | "DeleteStateMachineVersion"
234 | "CreateStateMachineAlias"
235 | "DeleteStateMachineAlias"
236 | "UpdateStateMachineAlias"
237 | "UpdateMapRun"
238 | "RedriveExecution"
239 | "StartSyncExecution"
240 )
241}
242
243#[async_trait]
244impl AwsService for StepFunctionsService {
245 fn service_name(&self) -> &str {
246 "states"
247 }
248
249 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
250 let mutates = is_mutating_action(req.action.as_str());
251 let result = match req.action.as_str() {
252 "CreateStateMachine" => self.create_state_machine(&req),
253 "DescribeStateMachine" => self.describe_state_machine(&req),
254 "ListStateMachines" => self.list_state_machines(&req),
255 "DeleteStateMachine" => self.delete_state_machine(&req),
256 "UpdateStateMachine" => self.update_state_machine(&req),
257 "TagResource" => self.tag_resource(&req),
258 "UntagResource" => self.untag_resource(&req),
259 "ListTagsForResource" => self.list_tags_for_resource(&req),
260 "StartExecution" => self.start_execution(&req),
261 "StopExecution" => self.stop_execution(&req),
262 "DescribeExecution" => self.describe_execution(&req),
263 "ListExecutions" => self.list_executions(&req),
264 "GetExecutionHistory" => self.get_execution_history(&req),
265 "DescribeStateMachineForExecution" => self.describe_state_machine_for_execution(&req),
266 "CreateActivity" => self.create_activity(&req),
267 "DeleteActivity" => self.delete_activity(&req),
268 "DescribeActivity" => self.describe_activity(&req),
269 "ListActivities" => self.list_activities(&req),
270 "GetActivityTask" => self.get_activity_task(&req).await,
271 "SendTaskFailure" => self.send_task_failure(&req),
272 "SendTaskHeartbeat" => self.send_task_heartbeat(&req),
273 "SendTaskSuccess" => self.send_task_success(&req),
274 "PublishStateMachineVersion" => self.publish_state_machine_version(&req),
275 "DeleteStateMachineVersion" => self.delete_state_machine_version(&req),
276 "ListStateMachineVersions" => self.list_state_machine_versions(&req),
277 "CreateStateMachineAlias" => self.create_state_machine_alias(&req),
278 "DeleteStateMachineAlias" => self.delete_state_machine_alias(&req),
279 "DescribeStateMachineAlias" => self.describe_state_machine_alias(&req),
280 "ListStateMachineAliases" => self.list_state_machine_aliases(&req),
281 "UpdateStateMachineAlias" => self.update_state_machine_alias(&req),
282 "DescribeMapRun" => self.describe_map_run(&req),
283 "ListMapRuns" => self.list_map_runs(&req),
284 "UpdateMapRun" => self.update_map_run(&req),
285 "RedriveExecution" => self.redrive_execution(&req),
286 "StartSyncExecution" => self.start_sync_execution(&req).await,
287 "TestState" => self.test_state(&req),
288 "ValidateStateMachineDefinition" => self.validate_state_machine_definition(&req),
289 _ => Err(AwsServiceError::action_not_implemented(
290 "states",
291 &req.action,
292 )),
293 };
294 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
295 self.save_snapshot().await;
296 }
297 result
298 }
299
300 fn supported_actions(&self) -> &[&str] {
301 SUPPORTED
302 }
303}
304
305impl StepFunctionsService {
306 fn list_activities(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
307 let body = req.json_body();
308 let raw_max_results = body["maxResults"].as_i64();
309 if let Some(mr) = raw_max_results {
310 validate_max_results(mr)?;
311 }
312 let next_token = body["nextToken"].as_str();
313 if let Some(t) = next_token {
314 validate_page_token(t)?;
315 }
316 let max_results = match raw_max_results.unwrap_or(0) {
320 0 => 100,
321 n => n as usize,
322 };
323 let accounts = self.state.read();
324 let empty = crate::state::StepFunctionsState::new(&req.account_id, &req.region);
325 let state = accounts
326 .regional(&req.account_id, &req.region)
327 .unwrap_or(&empty);
328 let mut activities: Vec<&crate::state::Activity> = state.activities.values().collect();
329 activities.sort_by(|a, b| a.name.cmp(&b.name));
330 let items: Vec<Value> = activities
331 .iter()
332 .map(|a| {
333 json!({
334 "activityArn": a.arn,
335 "name": a.name,
336 "creationDate": a.creation_date.timestamp(),
337 })
338 })
339 .collect();
340 let (page, token) =
341 paginate_checked(&items, next_token, max_results).map_err(|_| invalid_token())?;
342 let mut resp = json!({ "activities": page });
343 if let Some(t) = token {
344 resp["nextToken"] = json!(t);
345 }
346 Ok(AwsResponse::ok_json(resp))
347 }
348}
349
350fn state_machine_alias_to_json(alias: &crate::state::StateMachineAlias) -> Value {
351 json!({
352 "stateMachineAliasArn": alias.arn,
353 "name": alias.name,
354 "description": alias.description,
355 "routingConfiguration": alias.routing_configuration.iter().map(|r| json!({
356 "stateMachineVersionArn": r.state_machine_version_arn,
357 "weight": r.weight,
358 })).collect::<Vec<_>>(),
359 "creationDate": alias.creation_date.timestamp(),
360 "updateDate": alias.update_date.timestamp(),
361 })
362}
363
364fn map_run_to_json(mr: &crate::state::MapRun) -> Value {
365 let accounted = mr.succeeded_count + mr.failed_count;
368 let running = if mr.status == "RUNNING" {
369 (mr.total_count - accounted).max(0)
370 } else {
371 0
372 };
373 let counts = json!({
376 "pending": 0,
377 "running": running,
378 "succeeded": mr.succeeded_count,
379 "failed": mr.failed_count,
380 "timedOut": 0,
381 "aborted": 0,
382 "results": mr.succeeded_count,
383 "total": mr.total_count,
384 "failuresNotRedrivable": 0,
385 "pendingRedrive": 0,
386 });
387 json!({
388 "mapRunArn": mr.map_run_arn,
389 "executionArn": mr.execution_arn,
390 "maxConcurrency": mr.max_concurrency,
391 "toleratedFailurePercentage": mr.tolerated_failure_percentage,
392 "toleratedFailureCount": mr.tolerated_failure_count,
393 "status": mr.status,
394 "startDate": mr.start_date.timestamp(),
395 "stopDate": mr.stop_date.map(|d| d.timestamp()),
396 "itemCounts": counts,
397 "executionCounts": counts,
398 })
399}
400
401fn state_machine_to_json(sm: &StateMachine) -> Value {
404 let mut resp = json!({
405 "name": sm.name,
406 "stateMachineArn": sm.arn,
407 "definition": sm.definition,
408 "roleArn": sm.role_arn,
409 "type": sm.machine_type.as_str(),
410 "status": sm.status.as_str(),
411 "creationDate": sm.creation_date.timestamp() as f64,
412 "updateDate": sm.update_date.timestamp() as f64,
413 "revisionId": sm.revision_id,
414 "label": sm.name,
415 });
416
417 if !sm.description.is_empty() {
418 resp["description"] = json!(sm.description);
419 }
420
421 if let Some(ref logging) = sm.logging_configuration {
422 resp["loggingConfiguration"] = logging.clone();
423 } else {
424 resp["loggingConfiguration"] = json!({
425 "level": "OFF",
426 "includeExecutionData": false,
427 "destinations": [],
428 });
429 }
430
431 if let Some(ref tracing) = sm.tracing_configuration {
432 resp["tracingConfiguration"] = tracing.clone();
433 } else {
434 resp["tracingConfiguration"] = json!({
435 "enabled": false,
436 });
437 }
438
439 if let Some(ref enc) = sm.encryption_configuration {
440 resp["encryptionConfiguration"] = enc.clone();
441 }
442
443 resp
444}
445
446fn missing(name: &str) -> AwsServiceError {
447 AwsServiceError::aws_error(
448 StatusCode::BAD_REQUEST,
449 "ValidationException",
450 format!("The request must contain the parameter {name}."),
451 )
452}
453
454fn state_machine_not_found(arn: &str) -> AwsServiceError {
455 AwsServiceError::aws_error(
456 StatusCode::BAD_REQUEST,
457 "StateMachineDoesNotExist",
458 format!("State Machine Does Not Exist: '{arn}'"),
459 )
460}
461
462fn activity_not_found(arn: &str) -> AwsServiceError {
463 AwsServiceError::aws_error(
464 StatusCode::BAD_REQUEST,
465 "ActivityDoesNotExist",
466 format!("Activity does not exist: {arn}"),
467 )
468}
469
470fn task_does_not_exist(token: &str) -> AwsServiceError {
471 AwsServiceError::aws_error(
472 StatusCode::BAD_REQUEST,
473 "TaskDoesNotExist",
474 format!("Task does not exist: {token}"),
475 )
476}
477
478fn resource_not_found(arn: &str) -> AwsServiceError {
479 AwsServiceError::aws_error(
480 StatusCode::BAD_REQUEST,
481 "ResourceNotFound",
482 format!("Resource not found: '{arn}'"),
483 )
484}
485
486fn parse_routing_configuration(
491 routes: &[serde_json::Value],
492) -> Result<Vec<crate::state::AliasRoute>, AwsServiceError> {
493 if routes.is_empty() || routes.len() > 2 {
494 return Err(AwsServiceError::aws_error(
495 StatusCode::BAD_REQUEST,
496 "ValidationException",
497 "routingConfiguration must contain 1 or 2 routes.",
498 ));
499 }
500 let parsed: Vec<crate::state::AliasRoute> = routes
501 .iter()
502 .map(|r| {
503 let arn = r["stateMachineVersionArn"].as_str().ok_or_else(|| {
504 AwsServiceError::aws_error(
505 StatusCode::BAD_REQUEST,
506 "ValidationException",
507 "routingConfiguration entries must contain stateMachineVersionArn.",
508 )
509 })?;
510 let weight = r["weight"].as_i64().ok_or_else(|| {
511 AwsServiceError::aws_error(
512 StatusCode::BAD_REQUEST,
513 "ValidationException",
514 "routingConfiguration entries must contain a numeric weight.",
515 )
516 })?;
517 if !(0..=100).contains(&weight) {
518 return Err(AwsServiceError::aws_error(
519 StatusCode::BAD_REQUEST,
520 "ValidationException",
521 format!("Invalid routing weight {weight}; must be 0-100."),
522 ));
523 }
524 Ok(crate::state::AliasRoute {
525 state_machine_version_arn: arn.to_string(),
526 weight: weight as i32,
527 })
528 })
529 .collect::<Result<_, _>>()?;
530 let total: i32 = parsed.iter().map(|r| r.weight).sum();
531 if total != 100 {
532 return Err(AwsServiceError::aws_error(
533 StatusCode::BAD_REQUEST,
534 "ValidationException",
535 format!("routingConfiguration weights must sum to 100, got {total}."),
536 ));
537 }
538 Ok(parsed)
539}
540
541fn validate_name(name: &str) -> Result<(), AwsServiceError> {
542 if name.is_empty() || name.len() > 80 {
543 return Err(AwsServiceError::aws_error(
544 StatusCode::BAD_REQUEST,
545 "InvalidName",
546 format!("Invalid Name: '{name}' (length must be between 1 and 80 characters)"),
547 ));
548 }
549 if !name
551 .chars()
552 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
553 {
554 return Err(AwsServiceError::aws_error(
555 StatusCode::BAD_REQUEST,
556 "InvalidName",
557 format!(
558 "Invalid Name: '{name}' (must only contain alphanumeric characters, hyphens, and underscores)"
559 ),
560 ));
561 }
562 Ok(())
563}
564
565fn validate_definition(definition: &str) -> Result<(), AwsServiceError> {
566 let parsed: Value = serde_json::from_str(definition).map_err(|e| {
567 AwsServiceError::aws_error(
568 StatusCode::BAD_REQUEST,
569 "InvalidDefinition",
570 format!("Invalid State Machine Definition: '{e}'"),
571 )
572 })?;
573
574 if parsed.get("StartAt").and_then(|v| v.as_str()).is_none() {
575 return Err(AwsServiceError::aws_error(
576 StatusCode::BAD_REQUEST,
577 "InvalidDefinition",
578 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
579 .to_string(),
580 ));
581 }
582
583 let states_obj = parsed
584 .get("States")
585 .and_then(|v| v.as_object())
586 .ok_or_else(|| {
587 AwsServiceError::aws_error(
588 StatusCode::BAD_REQUEST,
589 "InvalidDefinition",
590 "Invalid State Machine Definition: 'MISSING_STATES' (States field is required)"
591 .to_string(),
592 )
593 })?;
594
595 let start_at = parsed["StartAt"].as_str().ok_or_else(|| {
596 AwsServiceError::aws_error(
597 StatusCode::BAD_REQUEST,
598 "InvalidDefinition",
599 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
600 .to_string(),
601 )
602 })?;
603 if !states_obj.contains_key(start_at) {
604 return Err(AwsServiceError::aws_error(
605 StatusCode::BAD_REQUEST,
606 "InvalidDefinition",
607 format!(
608 "Invalid State Machine Definition: 'MISSING_TRANSITION_TARGET' \
609 (StartAt '{start_at}' does not reference a valid state)"
610 ),
611 ));
612 }
613
614 validate_timeout_field("<root>", &parsed, "TimeoutSeconds")?;
615
616 for (state_name, state_val) in states_obj {
620 validate_state_paths(state_name, state_val)?;
621 }
622
623 Ok(())
624}
625
626fn validate_state_paths(state_name: &str, state: &Value) -> Result<(), AwsServiceError> {
631 validate_timeout_field(state_name, state, "TimeoutSeconds")?;
632 validate_timeout_field(state_name, state, "HeartbeatSeconds")?;
633 for field in ["InputPath", "OutputPath", "ResultPath"] {
634 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
635 let valid = if field == "ResultPath" {
639 is_valid_reference_path(p)
640 } else {
641 p == "null" || is_valid_path(p)
642 };
643 if valid {
644 continue;
645 }
646 return Err(invalid_reference_path(state_name, field, p));
647 }
648 }
649
650 for field in [
654 "SecondsPath",
655 "TimestampPath",
656 "ItemsPath",
657 "MaxConcurrencyPath",
658 "ToleratedFailureCountPath",
659 "ToleratedFailurePercentagePath",
660 ] {
661 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
662 if is_reference_or_context_path(p) {
663 continue;
664 }
665 return Err(invalid_reference_path(state_name, field, p));
666 }
667 }
668
669 for field in ["Parameters", "ResultSelector", "ItemSelector"] {
673 if let Some(template) = state.get(field) {
674 validate_payload_template(state_name, field, template)?;
675 }
676 }
677
678 match state.get("Type").and_then(|v| v.as_str()) {
679 Some("Choice") => {
680 if let Some(choices) = state.get("Choices").and_then(|v| v.as_array()) {
681 for rule in choices {
682 validate_choice_variables(state_name, rule)?;
683 }
684 }
685 }
686 Some("Parallel") => {
687 if let Some(branches) = state.get("Branches").and_then(|v| v.as_array()) {
688 for branch in branches {
689 validate_nested_definition(state_name, branch)?;
690 }
691 }
692 }
693 Some("Map") => {
694 for key in ["ItemProcessor", "Iterator"] {
697 if let Some(processor) = state.get(key) {
698 validate_nested_definition(state_name, processor)?;
699 }
700 }
701 }
702 _ => {}
703 }
704
705 Ok(())
706}
707
708const MAX_TIMEOUT_SECONDS: u64 = 99_999_999;
710
711fn validate_timeout_field(
715 state_name: &str,
716 state: &Value,
717 field: &str,
718) -> Result<(), AwsServiceError> {
719 let Some(v) = state.get(field) else {
720 return Ok(());
721 };
722 match v.as_u64() {
723 Some(n) if (1..=MAX_TIMEOUT_SECONDS).contains(&n) => Ok(()),
724 _ => Err(AwsServiceError::aws_error(
725 StatusCode::BAD_REQUEST,
726 "InvalidDefinition",
727 format!(
728 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
729 (The value of field '{field}' of state '{state_name}' must be a positive \
730 integer between 1 and {MAX_TIMEOUT_SECONDS}, got {v})"
731 ),
732 )),
733 }
734}
735
736fn validate_nested_definition(parent: &str, def: &Value) -> Result<(), AwsServiceError> {
740 if let Some(states) = def.get("States").and_then(|v| v.as_object()) {
741 for (name, state) in states {
742 validate_state_paths(&format!("{parent}/{name}"), state)?;
743 }
744 }
745 Ok(())
746}
747
748fn validate_payload_template(
752 state_name: &str,
753 field: &str,
754 template: &Value,
755) -> Result<(), AwsServiceError> {
756 match template {
757 Value::Object(map) => {
758 for (key, value) in map {
759 if let Some(stripped) = key.strip_suffix(".$") {
760 let expr = value.as_str().ok_or_else(|| {
761 invalid_payload_template(
762 state_name,
763 field,
764 &format!(
765 "the '{key}' field must be a JSONPath or intrinsic string, \
766 not {value}"
767 ),
768 )
769 })?;
770 let is_ref = expr.starts_with('$');
771 if !is_ref && !crate::intrinsics::is_intrinsic_call(expr) {
772 return Err(invalid_payload_template(
773 state_name,
774 field,
775 &format!(
776 "the '{stripped}.$' field value '{expr}' is neither a JSONPath \
777 reference nor an intrinsic function"
778 ),
779 ));
780 }
781 } else {
782 validate_payload_template(state_name, field, value)?;
783 }
784 }
785 }
786 Value::Array(arr) => {
787 for v in arr {
788 validate_payload_template(state_name, field, v)?;
789 }
790 }
791 _ => {}
792 }
793 Ok(())
794}
795
796fn is_reference_or_context_path(p: &str) -> bool {
799 if let Some(rest) = p.strip_prefix("$$") {
800 return is_valid_path(&format!("${rest}"));
801 }
802 is_valid_path(p)
803}
804
805fn invalid_payload_template(state_name: &str, field: &str, detail: &str) -> AwsServiceError {
806 AwsServiceError::aws_error(
807 StatusCode::BAD_REQUEST,
808 "InvalidDefinition",
809 format!(
810 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
811 (The {field} of state '{state_name}' is invalid: {detail})"
812 ),
813 )
814}
815
816fn validate_choice_variables(state_name: &str, rule: &Value) -> Result<(), AwsServiceError> {
819 if let Some(v) = rule.get("Variable").and_then(|v| v.as_str()) {
820 if !is_reference_or_context_path(v) {
822 return Err(invalid_reference_path(state_name, "Variable", v));
823 }
824 }
825 for combinator in ["And", "Or"] {
826 if let Some(nested) = rule.get(combinator).and_then(|v| v.as_array()) {
827 for n in nested {
828 validate_choice_variables(state_name, n)?;
829 }
830 }
831 }
832 if let Some(n) = rule.get("Not") {
833 validate_choice_variables(state_name, n)?;
834 }
835 Ok(())
836}
837
838fn is_valid_reference_path(path: &str) -> bool {
844 crate::jsonpath::JsonPath::parse(path).is_ok_and(|p| p.is_definite())
845}
846
847fn is_valid_path(path: &str) -> bool {
851 crate::jsonpath::JsonPath::parse(path).is_ok()
852}
853
854fn invalid_reference_path(state_name: &str, field: &str, path: &str) -> AwsServiceError {
855 AwsServiceError::aws_error(
856 StatusCode::BAD_REQUEST,
857 "InvalidDefinition",
858 format!(
859 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
860 (The {field} field of state '{state_name}' is not a valid reference path: '{path}')"
861 ),
862 )
863}
864
865fn execution_not_found(arn: &str) -> AwsServiceError {
866 AwsServiceError::aws_error(
867 StatusCode::BAD_REQUEST,
868 "ExecutionDoesNotExist",
869 format!("Execution Does Not Exist: '{arn}'"),
870 )
871}
872
873fn execution_to_json(exec: &Execution) -> Value {
874 let mut resp = json!({
875 "executionArn": exec.execution_arn,
876 "stateMachineArn": exec.state_machine_arn,
877 "name": exec.name,
878 "status": exec.status.as_str(),
879 "startDate": exec.start_date.timestamp() as f64,
880 "redriveCount": exec.redrive_count,
881 });
882
883 if let Some(ref input) = exec.input {
884 resp["input"] = json!(input);
885 }
886 if let Some(ref output) = exec.output {
887 resp["output"] = json!(output);
888 }
889 if let Some(stop) = exec.stop_date {
890 resp["stopDate"] = json!(stop.timestamp() as f64);
891 }
892 if let Some(ref error) = exec.error {
893 resp["error"] = json!(error);
894 }
895 if let Some(ref cause) = exec.cause {
896 resp["cause"] = json!(cause);
897 }
898
899 resp
900}
901
902fn camel_to_details_key(event_type: &str) -> String {
912 if event_type.ends_with("StateEntered") {
913 return "stateEntered".to_string();
914 }
915 if event_type.ends_with("StateExited") {
916 return "stateExited".to_string();
917 }
918 let mut chars = event_type.chars();
919 match chars.next() {
920 None => String::new(),
921 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
922 }
923}
924
925fn execution_input_matches(stored: Option<&str>, incoming: Option<&str>) -> bool {
930 let stored = stored.unwrap_or("{}");
931 let incoming = incoming.unwrap_or("{}");
932 match (
933 serde_json::from_str::<Value>(stored),
934 serde_json::from_str::<Value>(incoming),
935 ) {
936 (Ok(a), Ok(b)) => a == b,
937 _ => stored == incoming,
938 }
939}
940
941fn validate_arn(arn: &str) -> Result<(), AwsServiceError> {
942 if !arn.starts_with("arn:") {
943 return Err(AwsServiceError::aws_error(
944 StatusCode::BAD_REQUEST,
945 "InvalidArn",
946 format!("Invalid Arn: '{arn}'"),
947 ));
948 }
949 Ok(())
950}
951
952fn validate_arn_length(field: &str, value: &str, max: usize) -> Result<(), AwsServiceError> {
956 if value.is_empty() || value.len() > max {
957 return Err(AwsServiceError::aws_error(
958 StatusCode::BAD_REQUEST,
959 "InvalidArn",
960 format!("Invalid Arn at '{field}': must be 1..={max} characters"),
961 ));
962 }
963 Ok(())
964}
965
966pub(super) fn invalid_token() -> AwsServiceError {
970 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidToken", "Invalid nextToken")
971}
972
973fn validate_page_token(value: &str) -> Result<(), AwsServiceError> {
977 if value.is_empty() || value.len() > 1024 {
978 return Err(AwsServiceError::aws_error(
979 StatusCode::BAD_REQUEST,
980 "InvalidToken",
981 "nextToken must be 1..=1024 characters",
982 ));
983 }
984 Ok(())
985}
986
987fn validate_max_results(value: i64) -> Result<(), AwsServiceError> {
993 if !(0..=1000).contains(&value) {
994 return Err(AwsServiceError::aws_error(
995 StatusCode::BAD_REQUEST,
996 "InvalidToken",
997 format!("maxResults '{value}' is outside 0..=1000"),
998 ));
999 }
1000 Ok(())
1001}
1002
1003pub fn start_execution_from_delivery(
1012 state: &SharedStepFunctionsState,
1013 delivery: &Option<Arc<DeliveryBus>>,
1014 dynamodb_state: &Option<SharedDynamoDbState>,
1015 registry: &Option<SharedServiceRegistry>,
1016 state_machine_arn: &str,
1017 input: &str,
1018) {
1019 if serde_json::from_str::<serde_json::Value>(input).is_err() {
1021 tracing::warn!(
1022 state_machine_arn,
1023 "Step Functions delivery: invalid JSON input, skipping execution"
1024 );
1025 return;
1026 }
1027
1028 let execution_name = uuid::Uuid::new_v4().to_string();
1029
1030 let mut accounts = state.write();
1031 let Some(st) = accounts.by_arn_mut(state_machine_arn) else {
1033 tracing::warn!(
1034 state_machine_arn,
1035 "Step Functions delivery: state machine not found"
1036 );
1037 return;
1038 };
1039 let sm = match st.state_machines.get(state_machine_arn) {
1040 Some(sm) => sm,
1041 None => {
1042 tracing::warn!(
1043 state_machine_arn,
1044 "Step Functions delivery: state machine not found"
1045 );
1046 return;
1047 }
1048 };
1049
1050 let sm_name = sm.name.clone();
1051 let sm_role_arn = sm.role_arn.clone();
1052 let definition = sm.definition.clone();
1053 let region = state_machine_arn
1056 .split(':')
1057 .nth(3)
1058 .unwrap_or("us-east-1")
1059 .to_string();
1060 let exec_arn = st.execution_arn(®ion, &sm_name, &execution_name);
1061
1062 let now = Utc::now();
1063 let execution = Execution {
1064 execution_arn: exec_arn.clone(),
1065 state_machine_arn: state_machine_arn.to_string(),
1066 state_machine_name: sm_name,
1067 name: execution_name,
1068 status: ExecutionStatus::Running,
1069 input: Some(input.to_string()),
1070 output: None,
1071 start_date: now,
1072 stop_date: None,
1073 error: None,
1074 cause: None,
1075 history_events: vec![],
1076 parent_execution_arn: None,
1077 is_sync: false,
1078 billed_duration_ms: None,
1079 billed_memory_mb: None,
1080 role_arn: sm_role_arn,
1081 redrive_count: 0,
1082 };
1083
1084 st.executions.insert(exec_arn.clone(), execution);
1085 let logging_config = sm.logging_configuration.clone();
1086 drop(accounts);
1087
1088 let shared_state = state.clone();
1089 let delivery = delivery.clone();
1090 let dynamodb_state = dynamodb_state.clone();
1091 let registry = registry.clone();
1092 let input = Some(input.to_string());
1093 tokio::spawn(async move {
1094 interpreter::execute_state_machine(
1095 shared_state,
1096 exec_arn,
1097 definition,
1098 input,
1099 delivery,
1100 dynamodb_state,
1101 registry,
1102 logging_config,
1103 )
1104 .await;
1105 });
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110 use super::*;
1111 use http::{HeaderMap, Method};
1112 use parking_lot::RwLock;
1113 use serde_json::Value;
1114 use std::collections::HashMap;
1115 use std::sync::Arc;
1116
1117 fn make_state() -> SharedStepFunctionsState {
1118 Arc::new(RwLock::new(
1119 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
1120 ))
1121 }
1122
1123 fn make_request(action: &str, body: &str) -> AwsRequest {
1124 AwsRequest {
1125 service: "states".to_string(),
1126 action: action.to_string(),
1127 region: "us-east-1".to_string(),
1128 account_id: "123456789012".to_string(),
1129 request_id: "test-id".to_string(),
1130 headers: HeaderMap::new(),
1131 query_params: HashMap::new(),
1132 body: body.as_bytes().to_vec().into(),
1133 body_stream: parking_lot::Mutex::new(None),
1134 path_segments: vec![],
1135 raw_path: "/".to_string(),
1136 raw_query: String::new(),
1137 method: Method::POST,
1138 is_query_protocol: false,
1139 access_key_id: None,
1140 principal: None,
1141 }
1142 }
1143
1144 fn body_json(resp: &AwsResponse) -> Value {
1145 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
1146 }
1147
1148 fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
1149 match result {
1150 Err(e) => e,
1151 Ok(_) => panic!("expected error, got Ok"),
1152 }
1153 }
1154
1155 const VALID_DEF: &str = r#"{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}"#;
1156
1157 fn create_sm(svc: &StepFunctionsService, name: &str) -> String {
1158 let body = json!({
1159 "name": name,
1160 "definition": VALID_DEF,
1161 "roleArn": "arn:aws:iam::123456789012:role/test",
1162 });
1163 let req = make_request("CreateStateMachine", &body.to_string());
1164 let resp = svc.create_state_machine(&req).unwrap();
1165 let b = body_json(&resp);
1166 b["stateMachineArn"].as_str().unwrap().to_string()
1167 }
1168
1169 #[test]
1170 fn same_state_machine_name_coexists_in_two_regions() {
1171 let svc = StepFunctionsService::new(make_state());
1172 let in_region = |action: &str, body: Value, region: &str| {
1173 let mut req = make_request(action, &body.to_string());
1174 req.region = region.to_string();
1175 req
1176 };
1177 let create = |region: &str| {
1178 let body = json!({
1179 "name": "orders",
1180 "definition": VALID_DEF,
1181 "roleArn": "arn:aws:iam::123456789012:role/test",
1182 });
1183 let resp = svc
1184 .create_state_machine(&in_region("CreateStateMachine", body, region))
1185 .unwrap();
1186 body_json(&resp)["stateMachineArn"]
1187 .as_str()
1188 .unwrap()
1189 .to_string()
1190 };
1191 let east = create("us-east-1");
1192 let west = create("eu-west-1");
1193 assert_eq!(
1194 east,
1195 "arn:aws:states:us-east-1:123456789012:stateMachine:orders"
1196 );
1197 assert_eq!(
1198 west,
1199 "arn:aws:states:eu-west-1:123456789012:stateMachine:orders"
1200 );
1201
1202 for (region, arn) in [("us-east-1", &east), ("eu-west-1", &west)] {
1204 let resp = svc
1205 .list_state_machines(&in_region("ListStateMachines", json!({}), region))
1206 .unwrap();
1207 let listed: Vec<String> = body_json(&resp)["stateMachines"]
1208 .as_array()
1209 .unwrap()
1210 .iter()
1211 .map(|m| m["stateMachineArn"].as_str().unwrap().to_string())
1212 .collect();
1213 assert_eq!(listed, vec![arn.clone()]);
1214 }
1215
1216 let err = svc
1218 .describe_state_machine(&in_region(
1219 "DescribeStateMachine",
1220 json!({ "stateMachineArn": west }),
1221 "us-east-1",
1222 ))
1223 .err()
1224 .unwrap();
1225 assert_eq!(err.code(), "StateMachineDoesNotExist");
1226
1227 svc.delete_state_machine(&in_region(
1229 "DeleteStateMachine",
1230 json!({ "stateMachineArn": west }),
1231 "eu-west-1",
1232 ))
1233 .unwrap();
1234 assert!(svc
1235 .describe_state_machine(&in_region(
1236 "DescribeStateMachine",
1237 json!({ "stateMachineArn": east }),
1238 "us-east-1",
1239 ))
1240 .is_ok());
1241 }
1242
1243 #[test]
1244 fn v2_snapshot_migrates_resources_into_their_arn_region() {
1245 let mut legacy: fakecloud_core::multi_account::MultiAccountState<StepFunctionsState> =
1246 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
1247 {
1248 let s = legacy.default_mut();
1249 s.activities.insert(
1250 "arn:aws:states:eu-west-1:123456789012:activity:work".into(),
1251 crate::state::Activity {
1252 name: "work".into(),
1253 arn: "arn:aws:states:eu-west-1:123456789012:activity:work".into(),
1254 creation_date: Utc::now(),
1255 tags: Default::default(),
1256 },
1257 );
1258 }
1259 let bytes = serde_json::to_vec(&json!({"schema_version": 2, "accounts": legacy})).unwrap();
1260 let snap = crate::state::parse_stepfunctions_snapshot(&bytes).unwrap();
1261 assert_eq!(snap.schema_version, STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION);
1262 let accounts = snap.accounts.unwrap();
1263 let west = accounts.regional("123456789012", "eu-west-1").unwrap();
1264 assert_eq!(west.region, "eu-west-1");
1265 assert_eq!(west.activities.len(), 1);
1266 assert!(accounts.regional("123456789012", "us-east-1").is_none());
1267 }
1268
1269 #[test]
1270 fn china_region_arns_use_the_aws_cn_partition() {
1271 let svc = StepFunctionsService::new(make_state());
1272 let in_china = |action: &str, body: Value| {
1273 let mut req = make_request(action, &body.to_string());
1274 req.region = "cn-north-1".to_string();
1275 req
1276 };
1277 let resp = svc
1278 .create_state_machine(&in_china(
1279 "CreateStateMachine",
1280 json!({
1281 "name": "cn-sm",
1282 "definition": VALID_DEF,
1283 "roleArn": "arn:aws-cn:iam::123456789012:role/test",
1284 }),
1285 ))
1286 .unwrap();
1287 let sm_arn = body_json(&resp)["stateMachineArn"]
1288 .as_str()
1289 .unwrap()
1290 .to_string();
1291 assert_eq!(
1292 sm_arn,
1293 "arn:aws-cn:states:cn-north-1:123456789012:stateMachine:cn-sm"
1294 );
1295 let resp = svc
1296 .describe_state_machine(&in_china(
1297 "DescribeStateMachine",
1298 json!({"stateMachineArn": sm_arn}),
1299 ))
1300 .unwrap();
1301 assert_eq!(body_json(&resp)["name"], "cn-sm");
1302
1303 let resp = svc
1304 .create_activity(&in_china("CreateActivity", json!({"name": "cn-act"})))
1305 .unwrap();
1306 let activity_arn = body_json(&resp)["activityArn"]
1307 .as_str()
1308 .unwrap()
1309 .to_string();
1310 assert_eq!(
1311 activity_arn,
1312 "arn:aws-cn:states:cn-north-1:123456789012:activity:cn-act"
1313 );
1314 let resp = svc
1315 .describe_activity(&in_china(
1316 "DescribeActivity",
1317 json!({"activityArn": activity_arn}),
1318 ))
1319 .unwrap();
1320 assert_eq!(body_json(&resp)["name"], "cn-act");
1321 }
1322
1323 #[test]
1326 fn create_state_machine_basic() {
1327 let svc = StepFunctionsService::new(make_state());
1328 let arn = create_sm(&svc, "test-sm");
1329 assert!(arn.contains("test-sm"));
1330 }
1331
1332 #[test]
1336 fn create_state_machine_publish_creates_listable_version() {
1337 let svc = StepFunctionsService::new(make_state());
1338 let body = json!({
1339 "name": "pub-sm",
1340 "definition": VALID_DEF,
1341 "roleArn": "arn:aws:iam::123456789012:role/r",
1342 "publish": true,
1343 "versionDescription": "first",
1344 });
1345 let req = make_request("CreateStateMachine", &body.to_string());
1346 let resp = svc.create_state_machine(&req).unwrap();
1347 let b = body_json(&resp);
1348 let arn = b["stateMachineArn"].as_str().unwrap().to_string();
1349 let version_arn = b["stateMachineVersionArn"].as_str().unwrap().to_string();
1350 assert_eq!(version_arn, format!("{arn}:1"));
1351
1352 let list_req = make_request(
1353 "ListStateMachineVersions",
1354 &json!({ "stateMachineArn": arn }).to_string(),
1355 );
1356 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
1357 let versions = list["stateMachineVersions"].as_array().unwrap();
1358 assert_eq!(versions.len(), 1);
1359 assert_eq!(versions[0]["stateMachineVersionArn"], version_arn);
1360 }
1361
1362 #[test]
1363 fn create_state_machine_no_publish_has_no_version() {
1364 let svc = StepFunctionsService::new(make_state());
1365 let arn = create_sm(&svc, "nopub-sm");
1366 let body = json!({
1368 "name": "nopub-sm2",
1369 "definition": VALID_DEF,
1370 "roleArn": "arn:aws:iam::123456789012:role/r",
1371 });
1372 let req = make_request("CreateStateMachine", &body.to_string());
1373 let b = body_json(&svc.create_state_machine(&req).unwrap());
1374 assert!(b.get("stateMachineVersionArn").is_none());
1375
1376 let list_req = make_request(
1377 "ListStateMachineVersions",
1378 &json!({ "stateMachineArn": arn }).to_string(),
1379 );
1380 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
1381 assert!(list["stateMachineVersions"].as_array().unwrap().is_empty());
1382 }
1383
1384 #[test]
1388 fn update_state_machine_publish_and_encryption_round_trip() {
1389 let svc = StepFunctionsService::new(make_state());
1390 let create = json!({
1391 "name": "enc-sm",
1392 "definition": VALID_DEF,
1393 "roleArn": "arn:aws:iam::123456789012:role/r",
1394 "encryptionConfiguration": { "type": "CUSTOMER_MANAGED_KMS_KEY", "kmsKeyId": "key-123" },
1395 });
1396 let req = make_request("CreateStateMachine", &create.to_string());
1397 let arn = body_json(&svc.create_state_machine(&req).unwrap())["stateMachineArn"]
1398 .as_str()
1399 .unwrap()
1400 .to_string();
1401
1402 let desc_req = make_request(
1404 "DescribeStateMachine",
1405 &json!({ "stateMachineArn": arn }).to_string(),
1406 );
1407 let desc = body_json(&svc.describe_state_machine(&desc_req).unwrap());
1408 assert_eq!(
1409 desc["encryptionConfiguration"]["type"],
1410 "CUSTOMER_MANAGED_KMS_KEY"
1411 );
1412 assert_eq!(desc["encryptionConfiguration"]["kmsKeyId"], "key-123");
1413
1414 let upd = json!({
1416 "stateMachineArn": arn,
1417 "definition": VALID_DEF,
1418 "publish": true,
1419 "versionDescription": "v via update",
1420 });
1421 let upd_req = make_request("UpdateStateMachine", &upd.to_string());
1422 let upd_body = body_json(&svc.update_state_machine(&upd_req).unwrap());
1423 assert_eq!(
1424 upd_body["stateMachineVersionArn"].as_str().unwrap(),
1425 format!("{arn}:1")
1426 );
1427
1428 let list_req = make_request(
1429 "ListStateMachineVersions",
1430 &json!({ "stateMachineArn": arn }).to_string(),
1431 );
1432 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
1433 assert_eq!(list["stateMachineVersions"].as_array().unwrap().len(), 1);
1434 }
1435
1436 #[test]
1437 fn create_state_machine_with_express_type() {
1438 let svc = StepFunctionsService::new(make_state());
1439 let body = json!({
1440 "name": "express-sm",
1441 "definition": VALID_DEF,
1442 "roleArn": "arn:aws:iam::123456789012:role/r",
1443 "type": "EXPRESS",
1444 });
1445 let req = make_request("CreateStateMachine", &body.to_string());
1446 let resp = svc.create_state_machine(&req).unwrap();
1447 let b = body_json(&resp);
1448 assert!(b["stateMachineArn"].as_str().is_some());
1449 }
1450
1451 #[test]
1452 fn create_state_machine_duplicate_fails() {
1453 let svc = StepFunctionsService::new(make_state());
1454 create_sm(&svc, "dup-sm");
1455 let body = json!({
1456 "name": "dup-sm",
1457 "definition": VALID_DEF,
1458 "roleArn": "arn:aws:iam::123456789012:role/r",
1459 });
1460 let req = make_request("CreateStateMachine", &body.to_string());
1461 let err = expect_err(svc.create_state_machine(&req));
1462 assert!(err.to_string().contains("StateMachineAlreadyExists"));
1463 }
1464
1465 #[test]
1466 fn create_state_machine_missing_name() {
1467 let svc = StepFunctionsService::new(make_state());
1468 let body = json!({
1469 "definition": VALID_DEF,
1470 "roleArn": "arn:aws:iam::123456789012:role/r",
1471 });
1472 let req = make_request("CreateStateMachine", &body.to_string());
1473 assert!(svc.create_state_machine(&req).is_err());
1474 }
1475
1476 #[test]
1477 fn create_state_machine_invalid_definition() {
1478 let svc = StepFunctionsService::new(make_state());
1479 let body = json!({
1480 "name": "bad-def",
1481 "definition": "not json",
1482 "roleArn": "arn:aws:iam::123456789012:role/r",
1483 });
1484 let req = make_request("CreateStateMachine", &body.to_string());
1485 let err = expect_err(svc.create_state_machine(&req));
1486 assert!(err.to_string().contains("InvalidDefinition"));
1487 }
1488
1489 #[test]
1490 fn create_state_machine_definition_missing_start_at() {
1491 let svc = StepFunctionsService::new(make_state());
1492 let body = json!({
1493 "name": "no-start",
1494 "definition": r#"{"States":{"S":{"Type":"Pass","End":true}}}"#,
1495 "roleArn": "arn:aws:iam::123456789012:role/r",
1496 });
1497 let req = make_request("CreateStateMachine", &body.to_string());
1498 let err = expect_err(svc.create_state_machine(&req));
1499 assert!(err.to_string().contains("InvalidDefinition"));
1500 }
1501
1502 #[test]
1503 fn create_state_machine_definition_missing_states() {
1504 let svc = StepFunctionsService::new(make_state());
1505 let body = json!({
1506 "name": "no-states",
1507 "definition": r#"{"StartAt":"S"}"#,
1508 "roleArn": "arn:aws:iam::123456789012:role/r",
1509 });
1510 let req = make_request("CreateStateMachine", &body.to_string());
1511 let err = expect_err(svc.create_state_machine(&req));
1512 assert!(err.to_string().contains("InvalidDefinition"));
1513 }
1514
1515 #[test]
1516 fn create_state_machine_definition_start_at_not_in_states() {
1517 let svc = StepFunctionsService::new(make_state());
1518 let body = json!({
1519 "name": "bad-start",
1520 "definition": r#"{"StartAt":"Missing","States":{"S":{"Type":"Pass","End":true}}}"#,
1521 "roleArn": "arn:aws:iam::123456789012:role/r",
1522 });
1523 let req = make_request("CreateStateMachine", &body.to_string());
1524 let err = expect_err(svc.create_state_machine(&req));
1525 assert!(err.to_string().contains("MISSING_TRANSITION_TARGET"));
1526 }
1527
1528 #[test]
1529 fn create_state_machine_invalid_type() {
1530 let svc = StepFunctionsService::new(make_state());
1531 let body = json!({
1532 "name": "bad-type",
1533 "definition": VALID_DEF,
1534 "roleArn": "arn:aws:iam::123456789012:role/r",
1535 "type": "INVALID",
1536 });
1537 let req = make_request("CreateStateMachine", &body.to_string());
1538 assert!(svc.create_state_machine(&req).is_err());
1539 }
1540
1541 #[test]
1542 fn create_state_machine_invalid_arn() {
1543 let svc = StepFunctionsService::new(make_state());
1544 let body = json!({
1545 "name": "bad-arn",
1546 "definition": VALID_DEF,
1547 "roleArn": "not-an-arn",
1548 });
1549 let req = make_request("CreateStateMachine", &body.to_string());
1550 let err = expect_err(svc.create_state_machine(&req));
1551 assert!(err.to_string().contains("InvalidArn"));
1552 }
1553
1554 #[test]
1555 fn create_state_machine_invalid_name() {
1556 let svc = StepFunctionsService::new(make_state());
1557 let body = json!({
1558 "name": "has spaces!",
1559 "definition": VALID_DEF,
1560 "roleArn": "arn:aws:iam::123456789012:role/r",
1561 });
1562 let req = make_request("CreateStateMachine", &body.to_string());
1563 let err = expect_err(svc.create_state_machine(&req));
1564 assert!(err.to_string().contains("InvalidName"));
1565 }
1566
1567 #[test]
1568 fn create_state_machine_name_too_long() {
1569 let svc = StepFunctionsService::new(make_state());
1570 let long_name = "a".repeat(81);
1571 let body = json!({
1572 "name": long_name,
1573 "definition": VALID_DEF,
1574 "roleArn": "arn:aws:iam::123456789012:role/r",
1575 });
1576 let req = make_request("CreateStateMachine", &body.to_string());
1577 let err = expect_err(svc.create_state_machine(&req));
1578 assert!(err.to_string().contains("InvalidName"));
1579 }
1580
1581 #[test]
1584 fn describe_state_machine_found() {
1585 let svc = StepFunctionsService::new(make_state());
1586 let arn = create_sm(&svc, "desc-sm");
1587
1588 let req = make_request(
1589 "DescribeStateMachine",
1590 &json!({"stateMachineArn": arn}).to_string(),
1591 );
1592 let resp = svc.describe_state_machine(&req).unwrap();
1593 let b = body_json(&resp);
1594 assert_eq!(b["name"], "desc-sm");
1595 assert_eq!(b["status"], "ACTIVE");
1596 assert!(b["definition"].as_str().is_some());
1597 }
1598
1599 #[test]
1600 fn describe_state_machine_not_found() {
1601 let svc = StepFunctionsService::new(make_state());
1602 let req = make_request(
1603 "DescribeStateMachine",
1604 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1605 .to_string(),
1606 );
1607 let err = expect_err(svc.describe_state_machine(&req));
1608 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1609 }
1610
1611 #[test]
1614 fn list_state_machines_empty() {
1615 let svc = StepFunctionsService::new(make_state());
1616 let req = make_request("ListStateMachines", "{}");
1617 let resp = svc.list_state_machines(&req).unwrap();
1618 let b = body_json(&resp);
1619 assert!(b["stateMachines"].as_array().unwrap().is_empty());
1620 }
1621
1622 #[test]
1623 fn list_state_machines_returns_created() {
1624 let svc = StepFunctionsService::new(make_state());
1625 create_sm(&svc, "sm-1");
1626 create_sm(&svc, "sm-2");
1627
1628 let req = make_request("ListStateMachines", "{}");
1629 let resp = svc.list_state_machines(&req).unwrap();
1630 let b = body_json(&resp);
1631 assert_eq!(b["stateMachines"].as_array().unwrap().len(), 2);
1632 }
1633
1634 #[test]
1637 fn delete_state_machine() {
1638 let svc = StepFunctionsService::new(make_state());
1639 let arn = create_sm(&svc, "del-sm");
1640
1641 let req = make_request(
1642 "DeleteStateMachine",
1643 &json!({"stateMachineArn": arn}).to_string(),
1644 );
1645 svc.delete_state_machine(&req).unwrap();
1646
1647 let req = make_request(
1649 "DescribeStateMachine",
1650 &json!({"stateMachineArn": arn}).to_string(),
1651 );
1652 assert!(svc.describe_state_machine(&req).is_err());
1653 }
1654
1655 #[test]
1656 fn delete_state_machine_nonexistent_succeeds() {
1657 let svc = StepFunctionsService::new(make_state());
1658 let req = make_request(
1659 "DeleteStateMachine",
1660 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
1661 .to_string(),
1662 );
1663 svc.delete_state_machine(&req).unwrap();
1665 }
1666
1667 #[test]
1670 fn update_state_machine() {
1671 let svc = StepFunctionsService::new(make_state());
1672 let arn = create_sm(&svc, "upd-sm");
1673
1674 let new_def = r#"{"StartAt":"NewPass","States":{"NewPass":{"Type":"Pass","End":true}}}"#;
1675 let body = json!({
1676 "stateMachineArn": arn,
1677 "definition": new_def,
1678 "description": "updated",
1679 });
1680 let req = make_request("UpdateStateMachine", &body.to_string());
1681 let resp = svc.update_state_machine(&req).unwrap();
1682 let b = body_json(&resp);
1683 assert!(b["updateDate"].as_f64().is_some());
1684
1685 let req = make_request(
1687 "DescribeStateMachine",
1688 &json!({"stateMachineArn": arn}).to_string(),
1689 );
1690 let resp = svc.describe_state_machine(&req).unwrap();
1691 let b = body_json(&resp);
1692 assert!(b["definition"].as_str().unwrap().contains("NewPass"));
1693 assert_eq!(b["description"], "updated");
1694 }
1695
1696 #[test]
1697 fn update_state_machine_not_found() {
1698 let svc = StepFunctionsService::new(make_state());
1699 let body = json!({
1700 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1701 "definition": VALID_DEF,
1702 });
1703 let req = make_request("UpdateStateMachine", &body.to_string());
1704 let err = expect_err(svc.update_state_machine(&req));
1705 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1706 }
1707
1708 #[tokio::test]
1711 async fn start_execution_basic() {
1712 let svc = StepFunctionsService::new(make_state());
1713 let arn = create_sm(&svc, "exec-sm");
1714
1715 let body = json!({
1716 "stateMachineArn": arn,
1717 "input": r#"{"key":"value"}"#,
1718 });
1719 let req = make_request("StartExecution", &body.to_string());
1720 let resp = svc.start_execution(&req).unwrap();
1721 let b = body_json(&resp);
1722 assert!(b["executionArn"].as_str().is_some());
1723 assert!(b["startDate"].as_f64().is_some());
1724 }
1725
1726 #[tokio::test]
1727 async fn start_execution_with_name() {
1728 let svc = StepFunctionsService::new(make_state());
1729 let arn = create_sm(&svc, "named-exec");
1730
1731 let body = json!({
1732 "stateMachineArn": arn,
1733 "name": "my-execution",
1734 });
1735 let req = make_request("StartExecution", &body.to_string());
1736 let resp = svc.start_execution(&req).unwrap();
1737 let b = body_json(&resp);
1738 assert!(b["executionArn"].as_str().unwrap().contains("my-execution"));
1739 }
1740
1741 #[tokio::test]
1742 async fn start_execution_sm_not_found() {
1743 let svc = StepFunctionsService::new(make_state());
1744 let body = json!({
1745 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
1746 });
1747 let req = make_request("StartExecution", &body.to_string());
1748 let err = expect_err(svc.start_execution(&req));
1749 assert!(err.to_string().contains("StateMachineDoesNotExist"));
1750 }
1751
1752 #[tokio::test]
1753 async fn start_execution_invalid_input() {
1754 let svc = StepFunctionsService::new(make_state());
1755 let arn = create_sm(&svc, "bad-input");
1756
1757 let body = json!({
1758 "stateMachineArn": arn,
1759 "input": "not json",
1760 });
1761 let req = make_request("StartExecution", &body.to_string());
1762 let err = expect_err(svc.start_execution(&req));
1763 assert!(err.to_string().contains("InvalidExecutionInput"));
1764 }
1765
1766 #[tokio::test]
1767 async fn start_execution_same_name_same_input_is_idempotent() {
1768 let svc = StepFunctionsService::new(make_state());
1769 let arn = create_sm(&svc, "dup-exec");
1770
1771 let body = json!({
1772 "stateMachineArn": arn,
1773 "name": "same-name",
1774 "input": "{\"a\":1}",
1775 });
1776 let req = make_request("StartExecution", &body.to_string());
1777 let first = body_json(&svc.start_execution(&req).unwrap());
1778
1779 let req = make_request("StartExecution", &body.to_string());
1781 let second = body_json(&svc.start_execution(&req).unwrap());
1782 assert_eq!(first["executionArn"], second["executionArn"]);
1783 assert_eq!(first["startDate"], second["startDate"]);
1784 }
1785
1786 #[tokio::test]
1787 async fn start_execution_same_name_different_input_conflicts() {
1788 let svc = StepFunctionsService::new(make_state());
1789 let arn = create_sm(&svc, "dup-exec-diff");
1790
1791 let req = make_request(
1792 "StartExecution",
1793 &json!({
1794 "stateMachineArn": arn,
1795 "name": "same-name",
1796 "input": "{\"a\":1}",
1797 })
1798 .to_string(),
1799 );
1800 svc.start_execution(&req).unwrap();
1801
1802 let req = make_request(
1804 "StartExecution",
1805 &json!({
1806 "stateMachineArn": arn,
1807 "name": "same-name",
1808 "input": "{\"a\":2}",
1809 })
1810 .to_string(),
1811 );
1812 let err = expect_err(svc.start_execution(&req));
1813 assert!(err.to_string().contains("ExecutionAlreadyExists"));
1814 }
1815
1816 #[tokio::test]
1817 async fn start_execution_express_name_collision_never_idempotent() {
1818 let svc = StepFunctionsService::new(make_state());
1819 let arn = create_express_sm(&svc, "dup-exec-express");
1820
1821 let body = json!({
1822 "stateMachineArn": arn,
1823 "name": "same-name",
1824 "input": "{\"a\":1}",
1825 });
1826 let req = make_request("StartExecution", &body.to_string());
1827 svc.start_execution(&req).unwrap();
1828
1829 let req = make_request("StartExecution", &body.to_string());
1831 let err = expect_err(svc.start_execution(&req));
1832 assert!(err.to_string().contains("ExecutionAlreadyExists"));
1833 }
1834
1835 #[tokio::test]
1838 async fn describe_execution_found() {
1839 let svc = StepFunctionsService::new(make_state());
1840 let sm_arn = create_sm(&svc, "desc-exec");
1841
1842 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
1843 let req = make_request("StartExecution", &body.to_string());
1844 let resp = svc.start_execution(&req).unwrap();
1845 let exec_arn = body_json(&resp)["executionArn"]
1846 .as_str()
1847 .unwrap()
1848 .to_string();
1849
1850 let req = make_request(
1851 "DescribeExecution",
1852 &json!({"executionArn": exec_arn}).to_string(),
1853 );
1854 let resp = svc.describe_execution(&req).unwrap();
1855 let b = body_json(&resp);
1856 assert_eq!(b["name"], "e1");
1857 assert_eq!(b["status"], "RUNNING");
1858 }
1859
1860 #[tokio::test]
1861 async fn describe_execution_not_found() {
1862 let svc = StepFunctionsService::new(make_state());
1863 let req = make_request(
1864 "DescribeExecution",
1865 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1866 .to_string(),
1867 );
1868 let err = expect_err(svc.describe_execution(&req));
1869 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1870 }
1871
1872 #[tokio::test]
1875 async fn stop_execution() {
1876 let svc = StepFunctionsService::new(make_state());
1877 let sm_arn = create_sm(&svc, "stop-sm");
1878
1879 let body = json!({"stateMachineArn": sm_arn, "name": "stop-e"});
1880 let req = make_request("StartExecution", &body.to_string());
1881 let resp = svc.start_execution(&req).unwrap();
1882 let exec_arn = body_json(&resp)["executionArn"]
1883 .as_str()
1884 .unwrap()
1885 .to_string();
1886
1887 let body = json!({
1888 "executionArn": exec_arn,
1889 "error": "UserAborted",
1890 "cause": "test stop",
1891 });
1892 let req = make_request("StopExecution", &body.to_string());
1893 let resp = svc.stop_execution(&req).unwrap();
1894 let b = body_json(&resp);
1895 assert!(b["stopDate"].as_f64().is_some());
1896
1897 let req = make_request(
1899 "DescribeExecution",
1900 &json!({"executionArn": exec_arn}).to_string(),
1901 );
1902 let resp = svc.describe_execution(&req).unwrap();
1903 let b = body_json(&resp);
1904 assert_eq!(b["status"], "ABORTED");
1905 assert_eq!(b["error"], "UserAborted");
1906 }
1907
1908 #[tokio::test]
1909 async fn redrive_execution_reruns_to_terminal() {
1910 let svc = StepFunctionsService::new(make_state());
1911 let sm_arn = create_sm(&svc, "redrive-sm");
1912
1913 let body = json!({"stateMachineArn": sm_arn, "name": "redrive-e"});
1914 let req = make_request("StartExecution", &body.to_string());
1915 let resp = svc.start_execution(&req).unwrap();
1916 let exec_arn = body_json(&resp)["executionArn"]
1917 .as_str()
1918 .unwrap()
1919 .to_string();
1920
1921 let req = make_request(
1923 "StopExecution",
1924 &json!({"executionArn": exec_arn, "error": "UserAborted", "cause": "stop"}).to_string(),
1925 );
1926 svc.stop_execution(&req).unwrap();
1927
1928 let req = make_request(
1930 "RedriveExecution",
1931 &json!({"executionArn": exec_arn}).to_string(),
1932 );
1933 let resp = svc.redrive_execution(&req).unwrap();
1934 assert!(body_json(&resp)["redriveDate"].as_i64().is_some());
1935
1936 let mut status = String::new();
1939 for _ in 0..100 {
1940 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1941 let req = make_request(
1942 "DescribeExecution",
1943 &json!({"executionArn": exec_arn}).to_string(),
1944 );
1945 let b = body_json(&svc.describe_execution(&req).unwrap());
1946 status = b["status"].as_str().unwrap().to_string();
1947 if status != "RUNNING" {
1948 break;
1949 }
1950 }
1951 assert_eq!(
1952 status, "SUCCEEDED",
1953 "redriven execution must run to completion"
1954 );
1955 let req = make_request(
1956 "DescribeExecution",
1957 &json!({"executionArn": exec_arn}).to_string(),
1958 );
1959 let b = body_json(&svc.describe_execution(&req).unwrap());
1960 assert_eq!(b["redriveCount"], 1);
1961 }
1962
1963 #[tokio::test]
1964 async fn redrive_execution_running_rejected() {
1965 let svc = StepFunctionsService::new(make_state());
1966 let sm_arn = create_sm(&svc, "redrive-running-sm");
1967 let body = json!({"stateMachineArn": sm_arn, "name": "still-running"});
1968 let req = make_request("StartExecution", &body.to_string());
1969 let exec_arn = body_json(&svc.start_execution(&req).unwrap())["executionArn"]
1970 .as_str()
1971 .unwrap()
1972 .to_string();
1973
1974 let req = make_request(
1976 "RedriveExecution",
1977 &json!({"executionArn": exec_arn}).to_string(),
1978 );
1979 let err = expect_err(svc.redrive_execution(&req));
1980 assert!(err.to_string().contains("ExecutionNotRedrivable"));
1981 }
1982
1983 #[tokio::test]
1984 async fn stop_execution_not_found() {
1985 let svc = StepFunctionsService::new(make_state());
1986 let req = make_request(
1987 "StopExecution",
1988 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
1989 .to_string(),
1990 );
1991 let err = expect_err(svc.stop_execution(&req));
1992 assert!(err.to_string().contains("ExecutionDoesNotExist"));
1993 }
1994
1995 #[tokio::test]
1998 async fn list_executions() {
1999 let svc = StepFunctionsService::new(make_state());
2000 let sm_arn = create_sm(&svc, "list-exec");
2001
2002 for i in 0..3 {
2003 let body = json!({"stateMachineArn": sm_arn, "name": format!("e{i}")});
2004 let req = make_request("StartExecution", &body.to_string());
2005 svc.start_execution(&req).unwrap();
2006 }
2007
2008 let req = make_request(
2009 "ListExecutions",
2010 &json!({"stateMachineArn": sm_arn}).to_string(),
2011 );
2012 let resp = svc.list_executions(&req).unwrap();
2013 let b = body_json(&resp);
2014 assert_eq!(b["executions"].as_array().unwrap().len(), 3);
2015 }
2016
2017 #[tokio::test]
2018 async fn list_executions_sm_not_found() {
2019 let svc = StepFunctionsService::new(make_state());
2020 let req = make_request(
2021 "ListExecutions",
2022 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
2023 .to_string(),
2024 );
2025 let err = expect_err(svc.list_executions(&req));
2026 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2027 }
2028
2029 #[tokio::test]
2032 async fn get_execution_history_not_found() {
2033 let svc = StepFunctionsService::new(make_state());
2034 let req = make_request(
2035 "GetExecutionHistory",
2036 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
2037 .to_string(),
2038 );
2039 let err = expect_err(svc.get_execution_history(&req));
2040 assert!(err.to_string().contains("ExecutionDoesNotExist"));
2041 }
2042
2043 #[tokio::test]
2046 async fn describe_sm_for_execution() {
2047 let svc = StepFunctionsService::new(make_state());
2048 let sm_arn = create_sm(&svc, "sm-for-exec");
2049
2050 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
2051 let req = make_request("StartExecution", &body.to_string());
2052 let resp = svc.start_execution(&req).unwrap();
2053 let exec_arn = body_json(&resp)["executionArn"]
2054 .as_str()
2055 .unwrap()
2056 .to_string();
2057
2058 let req = make_request(
2059 "DescribeStateMachineForExecution",
2060 &json!({"executionArn": exec_arn}).to_string(),
2061 );
2062 let resp = svc.describe_state_machine_for_execution(&req).unwrap();
2063 let b = body_json(&resp);
2064 assert_eq!(b["name"], "sm-for-exec");
2065 }
2066
2067 #[test]
2070 fn tag_untag_list_tags() {
2071 let svc = StepFunctionsService::new(make_state());
2072 let arn = create_sm(&svc, "tagged-sm");
2073
2074 let body = json!({
2076 "resourceArn": arn,
2077 "tags": [{"key": "env", "value": "prod"}],
2078 });
2079 let req = make_request("TagResource", &body.to_string());
2080 svc.tag_resource(&req).unwrap();
2081
2082 let req = make_request(
2084 "ListTagsForResource",
2085 &json!({"resourceArn": arn}).to_string(),
2086 );
2087 let resp = svc.list_tags_for_resource(&req).unwrap();
2088 let b = body_json(&resp);
2089 let tags = b["tags"].as_array().unwrap();
2090 assert_eq!(tags.len(), 1);
2091 assert_eq!(tags[0]["key"], "env");
2092
2093 let body = json!({
2095 "resourceArn": arn,
2096 "tagKeys": ["env"],
2097 });
2098 let req = make_request("UntagResource", &body.to_string());
2099 svc.untag_resource(&req).unwrap();
2100
2101 let req = make_request(
2103 "ListTagsForResource",
2104 &json!({"resourceArn": arn}).to_string(),
2105 );
2106 let resp = svc.list_tags_for_resource(&req).unwrap();
2107 let b = body_json(&resp);
2108 assert!(b["tags"].as_array().unwrap().is_empty());
2109 }
2110
2111 #[test]
2112 fn tag_resource_not_found() {
2113 let svc = StepFunctionsService::new(make_state());
2114 let body = json!({
2115 "resourceArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
2116 "tags": [{"key": "k", "value": "v"}],
2117 });
2118 let req = make_request("TagResource", &body.to_string());
2119 let err = expect_err(svc.tag_resource(&req));
2120 assert!(err.to_string().contains("ResourceNotFound"));
2121 }
2122
2123 #[test]
2126 fn test_validate_name() {
2127 assert!(validate_name("valid-name").is_ok());
2128 assert!(validate_name("under_score").is_ok());
2129 assert!(validate_name("").is_err());
2130 assert!(validate_name("has spaces").is_err());
2131 assert!(validate_name(&"a".repeat(81)).is_err());
2132 }
2133
2134 #[test]
2135 fn test_validate_definition() {
2136 assert!(validate_definition(VALID_DEF).is_ok());
2137 assert!(validate_definition("not json").is_err());
2138 assert!(validate_definition(r#"{"States":{}}"#).is_err()); assert!(validate_definition(r#"{"StartAt":"S"}"#).is_err()); }
2141
2142 #[test]
2143 fn test_validate_definition_timeout_range() {
2144 let task = |field: &str, v: &str| {
2145 format!(
2146 r#"{{"StartAt":"T","States":{{"T":{{"Type":"Task","Resource":"arn:aws:states:::lambda:invoke","{field}":{v},"End":true}}}}}}"#
2147 )
2148 };
2149 for field in ["TimeoutSeconds", "HeartbeatSeconds"] {
2150 assert!(validate_definition(&task(field, "1")).is_ok());
2151 assert!(validate_definition(&task(field, "99999999")).is_ok());
2152 for bad in [
2153 "0",
2154 "-1",
2155 "100000000",
2156 "18446744073709551615",
2157 "1.5",
2158 "\"10\"",
2159 ] {
2160 let err = validate_definition(&task(field, bad)).unwrap_err();
2161 assert!(
2162 err.to_string().contains("InvalidDefinition"),
2163 "{field}={bad}: {err}"
2164 );
2165 }
2166 }
2167 let def = r#"{"StartAt":"P","TimeoutSeconds":18446744073709551615,"States":{"P":{"Type":"Pass","End":true}}}"#;
2169 assert!(validate_definition(def).is_err());
2170 let def = r#"{"StartAt":"Par","States":{"Par":{"Type":"Parallel","End":true,"Branches":[{"StartAt":"T","States":{"T":{"Type":"Task","Resource":"arn:aws:states:::lambda:invoke","TimeoutSeconds":100000000,"End":true}}}]}}}"#;
2172 assert!(validate_definition(def).is_err());
2173 }
2174
2175 #[test]
2176 fn test_validate_definition_rejects_malformed_paths() {
2177 let def =
2179 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"$.arr[","End":true}}}"#;
2180 assert!(validate_definition(def).is_err());
2181
2182 let def =
2184 "{\"StartAt\":\"P\",\"States\":{\"P\":{\"Type\":\"Pass\",\"OutputPath\":\"$.x[\u{00e9}\",\"End\":true}}}";
2185 assert!(validate_definition(def).is_err());
2186
2187 let def = r#"{"StartAt":"C","States":{"C":{"Type":"Choice","Choices":[{"Variable":"$.n[","NumericEquals":1,"Next":"P"}]},"P":{"Type":"Pass","End":true}}}"#;
2189 assert!(validate_definition(def).is_err());
2190
2191 let def =
2193 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"foo.bar","End":true}}}"#;
2194 assert!(validate_definition(def).is_err());
2195
2196 let def =
2198 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}"#;
2199 assert!(validate_definition(def).is_err());
2200 }
2201
2202 #[test]
2203 fn test_validate_definition_accepts_well_formed_paths() {
2204 let def = r#"{"StartAt":"P","States":{
2207 "P":{"Type":"Pass","InputPath":"$.a.b[0].c","OutputPath":"$","ResultPath":"$.out","Next":"C"},
2208 "C":{"Type":"Choice","Choices":[
2209 {"And":[{"Variable":"$.items[2]","NumericEquals":1},{"Variable":"$.flag","BooleanEquals":true}],"Next":"S"}
2210 ],"Default":"S"},
2211 "S":{"Type":"Succeed","InputPath":"null"}
2212 }}"#;
2213 assert!(validate_definition(def).is_ok());
2214 }
2215
2216 #[test]
2219 fn test_validate_definition_recurses_into_parallel_and_map() {
2220 let par = r#"{"StartAt":"Par","States":{"Par":{"Type":"Parallel","End":true,
2221 "Branches":[{"StartAt":"I","States":{"I":{"Type":"Pass","InputPath":"nope","End":true}}}]}}}"#;
2222 assert!(validate_definition(par).is_err());
2223
2224 let map = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
2225 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","OutputPath":"nope","End":true}}}}}}"#;
2226 assert!(validate_definition(map).is_err());
2227
2228 let iter = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
2230 "Iterator":{"StartAt":"E","States":{"E":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}}}}"#;
2231 assert!(validate_definition(iter).is_err());
2232
2233 let ok = r#"{"StartAt":"M","States":{"M":{"Type":"Map","ItemsPath":"$.items","End":true,
2235 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","InputPath":"$.a","End":true}}}}}}"#;
2236 assert!(validate_definition(ok).is_ok());
2237 }
2238
2239 #[test]
2241 fn test_validate_definition_payload_template_dollar_keys() {
2242 let lit = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
2244 "Parameters":{"v.$":"just text"},"End":true}}}"#;
2245 assert!(validate_definition(lit).is_err());
2246
2247 let num = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
2249 "Parameters":{"v.$":42},"End":true}}}"#;
2250 assert!(validate_definition(num).is_err());
2251
2252 let ok = r#"{"StartAt":"P","States":{"P":{"Type":"Pass","Parameters":{
2254 "a.$":"$.input","b.$":"$$.Execution.Id","c.$":"States.Format('{}',$.n)",
2255 "nested":{"d.$":"$.x"},"plain":"literal-ok"},"End":true}}}"#;
2256 assert!(validate_definition(ok).is_ok());
2257
2258 let arr = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
2260 "ResultSelector":{"items":[{"bad.$":"literal"}]},"End":true}}}"#;
2261 assert!(validate_definition(arr).is_err());
2262 }
2263
2264 #[test]
2265 fn test_paths_accept_full_jsonpath_but_result_path_must_be_definite() {
2266 let def = |field: &str, path: &str| {
2267 json!({
2268 "StartAt": "P",
2269 "States": {"P": {"Type": "Pass", field: path, "End": true}}
2270 })
2271 .to_string()
2272 };
2273 for p in [
2274 "$.items[*].id",
2275 "$.a[?(@.x == 1)]",
2276 "$['a']",
2277 "$.a[-1]",
2278 "$..id",
2279 "$.a[1:2]",
2280 ] {
2281 assert!(validate_definition(&def("InputPath", p)).is_ok(), "{p}");
2282 assert!(validate_definition(&def("OutputPath", p)).is_ok(), "{p}");
2283 }
2284 assert!(validate_definition(&def("ResultPath", "$['a'][0]")).is_ok());
2285 let choice = |var: &str| {
2286 json!({
2287 "StartAt": "C",
2288 "States": {
2289 "C": {"Type": "Choice", "Choices": [{"Variable": var, "IsPresent": true, "Next": "D"}], "Default": "D"},
2290 "D": {"Type": "Succeed"}
2291 }
2292 })
2293 .to_string()
2294 };
2295 assert!(validate_definition(&choice("$$.Execution.Name")).is_ok());
2296 assert!(validate_definition(&choice("$$.x[")).is_err());
2297 for p in ["$.items[*]", "$..a", "$.a[?(@.x)]"] {
2298 assert!(validate_definition(&def("ResultPath", p)).is_err(), "{p}");
2299 }
2300 }
2301
2302 #[test]
2303 fn test_is_valid_reference_path() {
2304 assert!(is_valid_reference_path("$"));
2305 assert!(is_valid_reference_path("$.foo"));
2306 assert!(is_valid_reference_path("$.foo.bar[3].baz"));
2307 assert!(is_valid_reference_path("$[0]"));
2308 assert!(!is_valid_reference_path("$.arr["));
2309 assert!(!is_valid_reference_path("$.x[\u{00e9}"));
2310 assert!(!is_valid_reference_path("$.x[]"));
2311 assert!(!is_valid_reference_path("$.x[abc]"));
2312 assert!(!is_valid_reference_path("foo.bar"));
2313 assert!(!is_valid_reference_path(""));
2314 }
2315
2316 #[test]
2317 fn test_validate_arn() {
2318 assert!(validate_arn("arn:aws:states:us-east-1:123:sm:test").is_ok());
2319 assert!(validate_arn("not-an-arn").is_err());
2320 }
2321
2322 #[test]
2323 fn test_camel_to_details_key() {
2324 assert_eq!(camel_to_details_key("PassStateEntered"), "stateEntered");
2326 assert_eq!(camel_to_details_key("TaskStateEntered"), "stateEntered");
2327 assert_eq!(camel_to_details_key("ChoiceStateExited"), "stateExited");
2328 assert_eq!(camel_to_details_key("MapStateExited"), "stateExited");
2329 assert_eq!(camel_to_details_key("TaskScheduled"), "taskScheduled");
2331 assert_eq!(camel_to_details_key("ExecutionStarted"), "executionStarted");
2332 assert_eq!(camel_to_details_key(""), "");
2333 }
2334
2335 #[test]
2336 fn test_is_mutating_action() {
2337 assert!(is_mutating_action("CreateStateMachine"));
2338 assert!(is_mutating_action("StartExecution"));
2339 assert!(!is_mutating_action("DescribeStateMachine"));
2340 assert!(!is_mutating_action("ListStateMachines"));
2341 }
2342
2343 fn create_express_sm(svc: &StepFunctionsService, name: &str) -> String {
2346 let body = json!({
2347 "name": name,
2348 "definition": VALID_DEF,
2349 "roleArn": "arn:aws:iam::123456789012:role/test",
2350 "type": "EXPRESS",
2351 });
2352 let req = make_request("CreateStateMachine", &body.to_string());
2353 let resp = svc.create_state_machine(&req).unwrap();
2354 let b = body_json(&resp);
2355 b["stateMachineArn"].as_str().unwrap().to_string()
2356 }
2357
2358 #[tokio::test]
2359 async fn start_sync_execution_basic() {
2360 let svc = StepFunctionsService::new(make_state());
2361 let arn = create_express_sm(&svc, "sync-sm");
2362
2363 let body = json!({
2364 "stateMachineArn": arn,
2365 "input": r#"{"key":"value"}"#,
2366 });
2367 let req = make_request("StartSyncExecution", &body.to_string());
2368 let resp = svc.start_sync_execution(&req).await.unwrap();
2369 let b = body_json(&resp);
2370 assert!(b["executionArn"]
2371 .as_str()
2372 .unwrap()
2373 .contains("express:sync-sm"));
2374 assert_eq!(b["stateMachineArn"], arn);
2375 assert_eq!(b["status"], "SUCCEEDED");
2376 assert!(b["startDate"].as_i64().is_some());
2377 assert!(b["stopDate"].as_i64().is_some());
2378 assert!(b["output"].as_str().is_some());
2379 assert!(b["billingDetails"]["billedDurationInMilliseconds"]
2380 .as_i64()
2381 .is_some());
2382 }
2383
2384 #[tokio::test]
2385 async fn start_sync_execution_not_express() {
2386 let svc = StepFunctionsService::new(make_state());
2387 let arn = create_sm(&svc, "std-sm");
2388
2389 let body = json!({"stateMachineArn": arn});
2390 let req = make_request("StartSyncExecution", &body.to_string());
2391 let err = expect_err(svc.start_sync_execution(&req).await);
2392 assert!(err.to_string().contains("StateMachineTypeNotSupported"));
2393 }
2394
2395 #[tokio::test]
2396 async fn start_sync_execution_sm_not_found() {
2397 let svc = StepFunctionsService::new(make_state());
2398 let body = json!({
2399 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
2400 });
2401 let req = make_request("StartSyncExecution", &body.to_string());
2402 let err = expect_err(svc.start_sync_execution(&req).await);
2403 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2404 }
2405
2406 #[tokio::test]
2407 async fn start_sync_execution_records_introspection_fields() {
2408 let svc = StepFunctionsService::new(make_state());
2409 let arn = create_express_sm(&svc, "sync-introspect");
2410
2411 let body = json!({"stateMachineArn": arn, "input": "{}"});
2412 let req = make_request("StartSyncExecution", &body.to_string());
2413 let resp = svc.start_sync_execution(&req).await.unwrap();
2414 let b = body_json(&resp);
2415 let exec_arn = b["executionArn"].as_str().unwrap().to_string();
2416
2417 let accounts = svc.state.read();
2418 let state = accounts.regional("123456789012", "us-east-1").unwrap();
2419 let stored = state
2420 .executions
2421 .get(&exec_arn)
2422 .expect("sync execution should be persisted for introspection");
2423 assert!(stored.is_sync, "sync executions must be marked is_sync");
2424 assert_eq!(stored.billed_memory_mb, Some(64));
2425 assert!(
2426 stored.billed_duration_ms.is_some(),
2427 "billed_duration_ms must be populated after sync run"
2428 );
2429 assert!(
2430 stored.parent_execution_arn.is_none(),
2431 "top-level sync execution has no parent"
2432 );
2433 }
2434
2435 #[tokio::test]
2436 async fn start_sync_execution_invalid_input() {
2437 let svc = StepFunctionsService::new(make_state());
2438 let arn = create_express_sm(&svc, "bad-input-sync");
2439
2440 let body = json!({
2441 "stateMachineArn": arn,
2442 "input": "not json",
2443 });
2444 let req = make_request("StartSyncExecution", &body.to_string());
2445 let err = expect_err(svc.start_sync_execution(&req).await);
2446 assert!(err.to_string().contains("InvalidExecutionInput"));
2447 }
2448
2449 #[test]
2451 fn snapshot_hook_is_none_without_store() {
2452 let svc = StepFunctionsService::new(make_state());
2453 assert!(svc.snapshot_hook().is_none());
2454 }
2455
2456 #[tokio::test]
2460 async fn snapshot_hook_fires_with_store() {
2461 let store: Arc<dyn fakecloud_persistence::SnapshotStore> =
2462 Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
2463 let svc = StepFunctionsService::new(make_state()).with_snapshot_store(store);
2464 let hook = svc
2465 .snapshot_hook()
2466 .expect("hook present when a store is set");
2467 hook().await;
2468 }
2469
2470 fn make_execution(arn: &str, status: ExecutionStatus) -> Execution {
2471 Execution {
2472 execution_arn: arn.to_string(),
2473 state_machine_arn: "arn:aws:states:us-east-1:123456789012:stateMachine:sm".to_string(),
2474 state_machine_name: "sm".to_string(),
2475 name: arn.to_string(),
2476 status,
2477 input: None,
2478 output: None,
2479 start_date: Utc::now(),
2480 stop_date: None,
2481 error: None,
2482 cause: None,
2483 history_events: vec![],
2484 parent_execution_arn: None,
2485 is_sync: false,
2486 billed_duration_ms: None,
2487 billed_memory_mb: None,
2488 role_arn: String::new(),
2489 redrive_count: 0,
2490 }
2491 }
2492
2493 #[test]
2494 fn reconcile_aborts_running_executions_on_restart() {
2495 let state = make_state();
2499 {
2500 let mut accounts = state.write();
2501 let s = accounts.regional_mut("123456789012", "us-east-1");
2502 s.executions.insert(
2503 "running".into(),
2504 make_execution("running", ExecutionStatus::Running),
2505 );
2506 s.executions.insert(
2507 "done".into(),
2508 make_execution("done", ExecutionStatus::Succeeded),
2509 );
2510 }
2511
2512 let n = reconcile_interrupted_executions(&state);
2513 assert_eq!(n, 1, "only the RUNNING execution is reconciled");
2514
2515 let accounts = state.read();
2516 let s = accounts.regional("123456789012", "us-east-1").unwrap();
2517 let running = &s.executions["running"];
2518 assert_eq!(running.status, ExecutionStatus::Aborted);
2519 assert!(running.stop_date.is_some());
2520 assert_eq!(running.error.as_deref(), Some("Fakecloud.Restart"));
2521 assert_eq!(s.executions["done"].status, ExecutionStatus::Succeeded);
2522 }
2523
2524 #[tokio::test]
2527 async fn distributed_map_populates_map_run() {
2528 let state = make_state();
2529 let svc = StepFunctionsService::new(state.clone());
2530 let execution_arn =
2531 "arn:aws:states:us-east-1:123456789012:execution:dist-sm:exec-1".to_string();
2532
2533 let def = json!({
2534 "StartAt": "M",
2535 "States": {
2536 "M": {
2537 "Type": "Map",
2538 "ItemsPath": "$.items",
2539 "ItemProcessor": {
2540 "ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "STANDARD" },
2541 "StartAt": "Item",
2542 "States": { "Item": { "Type": "Pass", "End": true } }
2543 },
2544 "End": true
2545 }
2546 }
2547 });
2548
2549 interpreter::execute_state_machine(
2550 state.clone(),
2551 execution_arn.clone(),
2552 def.to_string(),
2553 Some(r#"{"items":[1,2,3]}"#.to_string()),
2554 None,
2555 None,
2556 None,
2557 None,
2558 )
2559 .await;
2560
2561 let req = make_request(
2563 "ListMapRuns",
2564 &json!({ "executionArn": execution_arn }).to_string(),
2565 );
2566 let listed = body_json(&svc.list_map_runs(&req).unwrap());
2567 let runs = listed["mapRuns"].as_array().unwrap();
2568 assert_eq!(
2569 runs.len(),
2570 1,
2571 "distributed map must create exactly one MapRun"
2572 );
2573 let map_run_arn = runs[0]["mapRunArn"].as_str().unwrap().to_string();
2574 assert!(map_run_arn.contains(":mapRun:"));
2575 assert_eq!(runs[0]["executionArn"], execution_arn);
2576
2577 let req = make_request(
2579 "DescribeMapRun",
2580 &json!({ "mapRunArn": map_run_arn }).to_string(),
2581 );
2582 let mr = body_json(&svc.describe_map_run(&req).unwrap());
2583 assert_eq!(mr["status"], "SUCCEEDED");
2584 assert_eq!(mr["itemCounts"]["total"], 3);
2585 assert_eq!(mr["itemCounts"]["succeeded"], 3);
2586 assert_eq!(mr["itemCounts"]["failed"], 0);
2587 assert_eq!(mr["executionCounts"]["succeeded"], 3);
2588 }
2589
2590 #[tokio::test]
2591 async fn inline_map_does_not_create_map_run() {
2592 let state = make_state();
2593 let svc = StepFunctionsService::new(state.clone());
2594 let execution_arn =
2595 "arn:aws:states:us-east-1:123456789012:execution:inline-sm:exec-1".to_string();
2596
2597 let def = json!({
2599 "StartAt": "M",
2600 "States": {
2601 "M": {
2602 "Type": "Map",
2603 "ItemsPath": "$.items",
2604 "ItemProcessor": {
2605 "StartAt": "Item",
2606 "States": { "Item": { "Type": "Pass", "End": true } }
2607 },
2608 "End": true
2609 }
2610 }
2611 });
2612
2613 interpreter::execute_state_machine(
2614 state.clone(),
2615 execution_arn.clone(),
2616 def.to_string(),
2617 Some(r#"{"items":[1,2]}"#.to_string()),
2618 None,
2619 None,
2620 None,
2621 None,
2622 )
2623 .await;
2624
2625 let req = make_request(
2626 "ListMapRuns",
2627 &json!({ "executionArn": execution_arn }).to_string(),
2628 );
2629 let listed = body_json(&svc.list_map_runs(&req).unwrap());
2630 assert!(listed["mapRuns"].as_array().unwrap().is_empty());
2631 }
2632}
2633
2634#[cfg(test)]
2635mod pagination_reject_test {
2636 #[test]
2637 fn paginate_checked_rejects_invalid_token() {
2638 use fakecloud_core::pagination::paginate_checked;
2639 let items: Vec<i32> = (0..5).collect();
2640 assert!(paginate_checked(&items, Some("bad"), 3).is_err());
2641 assert!(paginate_checked(&items, Some("2"), 3).is_ok());
2642 }
2643}