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 mut 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 crate::jsonata::propagate_query_language(&mut parsed, None);
576
577 if parsed.get("StartAt").and_then(|v| v.as_str()).is_none() {
578 return Err(AwsServiceError::aws_error(
579 StatusCode::BAD_REQUEST,
580 "InvalidDefinition",
581 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
582 .to_string(),
583 ));
584 }
585
586 let states_obj = parsed
587 .get("States")
588 .and_then(|v| v.as_object())
589 .ok_or_else(|| {
590 AwsServiceError::aws_error(
591 StatusCode::BAD_REQUEST,
592 "InvalidDefinition",
593 "Invalid State Machine Definition: 'MISSING_STATES' (States field is required)"
594 .to_string(),
595 )
596 })?;
597
598 let start_at = parsed["StartAt"].as_str().ok_or_else(|| {
599 AwsServiceError::aws_error(
600 StatusCode::BAD_REQUEST,
601 "InvalidDefinition",
602 "Invalid State Machine Definition: 'MISSING_START_AT' (StartAt field is required)"
603 .to_string(),
604 )
605 })?;
606 if !states_obj.contains_key(start_at) {
607 return Err(AwsServiceError::aws_error(
608 StatusCode::BAD_REQUEST,
609 "InvalidDefinition",
610 format!(
611 "Invalid State Machine Definition: 'MISSING_TRANSITION_TARGET' \
612 (StartAt '{start_at}' does not reference a valid state)"
613 ),
614 ));
615 }
616
617 validate_states_are_objects("", &parsed)?;
618 validate_state_schemas(&parsed)?;
619 validate_timeout_field("<root>", &parsed, "TimeoutSeconds")?;
620
621 for (state_name, state_val) in states_obj {
625 validate_state_paths(state_name, state_val)?;
626 }
627 validate_integration_schemas("", states_obj)?;
628
629 Ok(())
630}
631
632fn assigned_variables(state: &Value) -> Vec<String> {
636 let mut holders = vec![state];
637 for list in ["Choices", "Catch"] {
638 holders.extend(state[list].as_array().into_iter().flatten());
639 }
640 holders
641 .into_iter()
642 .filter_map(|holder| holder["Assign"].as_object())
643 .flat_map(|assign| assign.keys())
644 .map(|key| {
645 let name = if crate::jsonata::is_jsonata(state) {
646 key.as_str()
647 } else {
648 key.strip_suffix(".$").unwrap_or(key)
649 };
650 name.to_string()
651 })
652 .collect()
653}
654
655fn variable_name_problem(name: &str) -> Option<&'static str> {
658 if name.chars().count() > 80 {
659 return Some("is too long");
660 }
661 let mut chars = name.chars();
662 let valid = chars.next().is_some_and(|c| c.is_alphabetic() || c == '_')
663 && chars.all(|c| c.is_alphanumeric() || c == '_');
664 (!valid).then_some("contains invalid characters")
665}
666
667const JSONPATH_ONLY_FIELDS: &[&str] = &[
669 "InputPath",
670 "Parameters",
671 "ResultSelector",
672 "ResultPath",
673 "OutputPath",
674 "ItemsPath",
675 "MaxConcurrencyPath",
676 "ToleratedFailureCountPath",
677 "ToleratedFailurePercentagePath",
678 "SecondsPath",
679 "TimestampPath",
680 "ErrorPath",
681 "CausePath",
682 "TimeoutSecondsPath",
683 "HeartbeatSecondsPath",
684];
685const JSONATA_ONLY_FIELDS: &[&str] = &["Arguments", "Output", "Items"];
686
687const JSONATA_TEMPLATE_FIELDS: &[&str] = &[
690 "Arguments",
691 "Output",
692 "Assign",
693 "Condition",
694 "Items",
695 "ItemSelector",
696 "ItemBatcher",
697 "ItemReader",
698 "ResultWriter",
699 "Seconds",
700 "Timestamp",
701 "Error",
702 "Cause",
703 "TimeoutSeconds",
704 "HeartbeatSeconds",
705 "MaxConcurrency",
706 "ToleratedFailureCount",
707 "ToleratedFailurePercentage",
708];
709
710const WAIT_DURATION_FIELDS: &[&str] = &["SecondsPath", "Seconds", "TimestampPath", "Timestamp"];
711
712fn schema_detail(detail: String) -> String {
713 format!("SCHEMA_VALIDATION_FAILED: {detail}")
714}
715
716fn check_exactly_one(state: &Value, fields: &[&str], path: &str, errors: &mut Vec<String>) {
719 let list = fields.join(", ");
720 match fields.iter().filter(|f| state.get(**f).is_some()).count() {
721 1 => {}
722 0 => errors.push(schema_detail(format!(
723 "This object should have only one of the following fields: [{list}] at {path}"
724 ))),
725 _ => errors.push(schema_detail(format!(
726 "There should only be one of the following fields: [{list}] at {path}"
727 ))),
728 }
729}
730
731fn check_jsonata_delimiters(value: &Value, path: &str, errors: &mut Vec<String>) {
734 match value {
735 Value::String(text) if crate::jsonata::is_unclosed_expression(text) => {
736 errors.push(schema_detail(format!(
737 "JSONata string must start with {{% and end with %}} at {path}"
738 )));
739 }
740 Value::Object(members) => {
741 for (key, member) in members {
742 check_jsonata_delimiters(member, &format!("{path}/{key}"), errors);
743 }
744 }
745 Value::Array(items) => {
746 for (index, item) in items.iter().enumerate() {
747 check_jsonata_delimiters(item, &format!("{path}/{index}"), errors);
748 }
749 }
750 _ => {}
751 }
752}
753
754fn check_jsonata_holder(holder: &Value, path: &str, errors: &mut Vec<String>) {
755 for field in JSONATA_TEMPLATE_FIELDS {
756 if let Some(value) = holder.get(*field) {
757 check_jsonata_delimiters(value, &format!("{path}/{field}"), errors);
758 }
759 }
760}
761
762fn collect_state_schema_errors(
770 at: &str,
771 states: &serde_json::Map<String, Value>,
772 machine_jsonata: bool,
773 errors: &mut Vec<String>,
774) {
775 for (name, state) in states {
776 let path = format!("{at}/States/{name}");
777 let declared = state["QueryLanguage"].as_str();
778 if machine_jsonata && declared == Some("JSONPath") {
779 errors.push(schema_detail(format!(
780 "'QueryLanguage' can not be 'JSONPath' if set to 'JSONata' for whole state \
781 machine at {path}"
782 )));
783 }
784 let jsonata = machine_jsonata || declared == Some("JSONata");
785 let (language, other, wrong_fields) = if jsonata {
786 ("JSONata", "JSONPath", JSONPATH_ONLY_FIELDS)
787 } else {
788 ("JSONPath", "JSONata", JSONATA_ONLY_FIELDS)
789 };
790 for field in wrong_fields {
791 if state.get(*field).is_some() {
792 errors.push(schema_detail(format!(
793 "The QueryLanguage is set to '{language}', but field '{field}' is only \
794 supported for the '{other}' QueryLanguage at {path}"
795 )));
796 }
797 }
798
799 let kind = state["Type"].as_str().unwrap_or_default();
800 if !jsonata && kind == "Task" && state.get("Parameters").is_none() {
801 if let Some(resource) = state["Resource"]
802 .as_str()
803 .filter(|resource| resource.contains(":::aws-sdk:"))
804 {
805 errors.push(schema_detail(format!(
806 "Parameters field is required for resource ARN: {resource} at \
807 {path}/Resource"
808 )));
809 }
810 }
811 if !matches!(kind, "Task" | "Parallel" | "Map") {
812 for field in ["Retry", "Catch"] {
813 if state.get(field).is_some() {
814 errors.push(schema_detail(format!(
815 "Field '{field}' is not supported at {path}"
816 )));
817 }
818 }
819 }
820 if matches!(kind, "Task" | "Pass" | "Wait" | "Parallel" | "Map") {
821 check_exactly_one(state, &["Next", "End"], &path, errors);
822 }
823 if kind == "Wait" {
824 check_exactly_one(state, WAIT_DURATION_FIELDS, &path, errors);
825 }
826 let integer_fields: &[(&str, Option<i64>)] = match kind {
829 "Wait" => &[("Seconds", Some(MAX_TIMEOUT_SECONDS as i64))],
830 "Map" => &[("ToleratedFailureCount", None)],
831 _ => &[],
832 };
833 for (field, max) in integer_fields {
834 let Some(value) = state.get(*field) else {
835 continue;
836 };
837 if jsonata && value.is_string() {
838 continue;
839 }
840 let problem = match value.as_i64() {
841 None if value.is_u64() => max.map(|max| format!("Maximum value is {max}")),
842 None => Some("Expected value of type Integer".to_string()),
843 Some(n) if n < 0 => Some("Minimum value is 0".to_string()),
844 Some(n) => max
845 .filter(|max| n > *max)
846 .map(|max| format!("Maximum value is {max}")),
847 };
848 if let Some(problem) = problem {
849 errors.push(schema_detail(format!("{problem} at {path}/{field}")));
850 }
851 }
852 if kind == "Fail" && !jsonata {
853 for field in ["ErrorPath", "CausePath"] {
854 let Some(value) = state.get(field) else {
855 continue;
856 };
857 let valid = value.as_str().is_some_and(|text| {
858 (text.starts_with('$') && is_reference_or_context_path(text))
859 || crate::intrinsics::is_intrinsic_call(text)
860 });
861 if !valid {
862 errors.push(schema_detail(format!(
863 "The value must be a valid Reference Path or a valid intrinsic function \
864 call at {path}/{field}"
865 )));
866 }
867 }
868 }
869
870 if jsonata {
871 check_jsonata_holder(state, &path, errors);
872 for list in ["Catch", "Choices"] {
873 for (index, holder) in state[list].as_array().into_iter().flatten().enumerate() {
874 check_jsonata_holder(holder, &format!("{path}/{list}/{index}"), errors);
875 }
876 }
877 }
878
879 for key in ["ItemProcessor", "Iterator"] {
880 if let Some(nested) = state[key]["States"].as_object() {
881 collect_state_schema_errors(
882 &format!("{path}/{key}"),
883 nested,
884 machine_jsonata,
885 errors,
886 );
887 }
888 }
889 for (index, branch) in state["Branches"]
890 .as_array()
891 .into_iter()
892 .flatten()
893 .enumerate()
894 {
895 if let Some(nested) = branch["States"].as_object() {
896 collect_state_schema_errors(
897 &format!("{path}/Branches[{index}]"),
898 nested,
899 machine_jsonata,
900 errors,
901 );
902 }
903 }
904 }
905}
906
907fn collect_variable_errors(
911 at: &str,
912 states: &serde_json::Map<String, Value>,
913 outer: &[String],
914 errors: &mut Vec<String>,
915) {
916 let assigned_by: Vec<(&String, Vec<String>)> = states
917 .iter()
918 .map(|(name, state)| (name, assigned_variables(state)))
919 .collect();
920 for (name, state) in states {
921 let state_path = format!("{at}/States/{name}");
922 let mut holders = vec![(state_path.clone(), state)];
923 for list in ["Choices", "Catch"] {
924 for (index, holder) in state[list].as_array().into_iter().flatten().enumerate() {
925 holders.push((format!("{state_path}/{list}/{index}"), holder));
926 }
927 }
928 for (path, holder) in holders {
929 if holder
930 .get("Assign")
931 .is_some_and(|assign| !assign.is_object())
932 {
933 errors.push(schema_detail(format!(
934 "Expected value of type [OBJECT] at {path}/Assign"
935 )));
936 }
937 }
938 for variable in assigned_variables(state) {
939 if variable == "states" {
940 errors.push(format!(
941 "RESERVED_VARIABLE_NAME: Cannot assign to reserved variable '$states'. at \
942 {state_path}"
943 ));
944 } else if let Some(problem) = variable_name_problem(&variable) {
945 errors.push(format!(
946 "INVALID_VARIABLE_NAME: Invalid variable name '{variable}': the variable \
947 name {problem}. at {state_path}/Assign/{variable}"
948 ));
949 }
950 if outer.contains(&variable) {
951 errors.push(schema_detail(format!(
952 "The variable '{variable}' is already assigned in an outer scope at \
953 {state_path}/Assign"
954 )));
955 }
956 }
957
958 let visible: Vec<String> = outer
959 .iter()
960 .cloned()
961 .chain(
962 assigned_by
963 .iter()
964 .filter(|(other, _)| *other != name)
965 .flat_map(|(_, variables)| variables.iter().cloned()),
966 )
967 .collect();
968 for key in ["ItemProcessor", "Iterator"] {
969 if let Some(nested) = state[key]["States"].as_object() {
970 collect_variable_errors(&format!("{state_path}/{key}"), nested, &visible, errors);
971 }
972 }
973 for (index, branch) in state["Branches"]
974 .as_array()
975 .into_iter()
976 .flatten()
977 .enumerate()
978 {
979 if let Some(nested) = branch["States"].as_object() {
980 collect_variable_errors(
981 &format!("{state_path}/Branches[{index}]"),
982 nested,
983 &visible,
984 errors,
985 );
986 }
987 }
988 }
989}
990
991fn validate_state_schemas(def: &Value) -> Result<(), AwsServiceError> {
994 let Some(states) = def["States"].as_object() else {
995 return Ok(());
996 };
997 let mut errors = Vec::new();
998 collect_state_schema_errors("", states, def["QueryLanguage"] == "JSONata", &mut errors);
999 collect_variable_errors("", states, &[], &mut errors);
1000 if errors.is_empty() {
1001 return Ok(());
1002 }
1003 Err(AwsServiceError::aws_error(
1004 StatusCode::BAD_REQUEST,
1005 "InvalidDefinition",
1006 format!("Invalid State Machine Definition: '{}'", errors.join(", ")),
1007 ))
1008}
1009
1010const READER_CONFIG_FIELDS: &[&str] = &[
1012 "InputType",
1013 "Transformation",
1014 "ManifestType",
1015 "CSVDelimiter",
1016 "CSVHeaderLocation",
1017 "CSVHeaders",
1018 "ItemsPointer",
1019 "MaxItems",
1020 "MaxItemsPath",
1021];
1022
1023fn validate_states_are_objects(at: &str, def: &Value) -> Result<(), AwsServiceError> {
1026 let Some(states) = def["States"].as_object() else {
1027 return Ok(());
1028 };
1029 for (name, state) in states {
1030 let path = format!("{at}/States/{name}");
1031 if !state.is_object() {
1032 let mut detail = format!("Expected value of type [OBJECT] at {path}");
1033 let targeted = def["StartAt"] == name.as_str()
1034 || states.values().any(|other| {
1035 other["Next"] == name.as_str()
1036 || other["Default"] == name.as_str()
1037 || ["Choices", "Catch"].iter().any(|list| {
1038 other[*list].as_array().is_some_and(|items| {
1039 items.iter().any(|i| i["Next"] == name.as_str())
1040 })
1041 })
1042 });
1043 if !targeted {
1044 detail.push_str(&format!(
1045 ", MISSING_TRANSITION_TARGET: State \"{name}\" is not reachable. at {path}"
1046 ));
1047 }
1048 return Err(schema_error(detail));
1049 }
1050 for key in ["ItemProcessor", "Iterator"] {
1051 validate_states_are_objects(&format!("{path}/{key}"), &state[key])?;
1052 }
1053 for (index, branch) in state["Branches"]
1054 .as_array()
1055 .into_iter()
1056 .flatten()
1057 .enumerate()
1058 {
1059 validate_states_are_objects(&format!("{path}/Branches[{index}]"), branch)?;
1060 }
1061 }
1062 Ok(())
1063}
1064
1065fn schema_error(detail: String) -> AwsServiceError {
1066 AwsServiceError::aws_error(
1067 StatusCode::BAD_REQUEST,
1068 "InvalidDefinition",
1069 format!("Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: {detail}'"),
1070 )
1071}
1072
1073fn integration_of(resource: &str) -> Option<&str> {
1076 fakecloud_aws::arn::arn_resource(resource, "states").and_then(|r| r.strip_prefix("::"))
1077}
1078
1079fn validate_integration_schemas(
1083 at: &str,
1084 states: &serde_json::Map<String, Value>,
1085) -> Result<(), AwsServiceError> {
1086 for (name, state) in states {
1087 let path = format!("{at}/States/{name}");
1088 match state["Type"].as_str() {
1089 Some("Task") => {
1090 let is_nested_start = state["Resource"]
1091 .as_str()
1092 .and_then(integration_of)
1093 .is_some_and(|tail| tail.starts_with("states:startExecution"));
1094 let field = if crate::jsonata::is_jsonata(state) {
1095 "Arguments"
1096 } else {
1097 "Parameters"
1098 };
1099 let is_expression = state[field].is_string();
1101 let has_arn = ["StateMachineArn", "StateMachineArn.$"]
1102 .iter()
1103 .any(|key| state[field].get(*key).is_some());
1104 if is_nested_start && !is_expression && !has_arn {
1105 return Err(schema_error(format!(
1106 "The field 'StateMachineArn' is required but was missing at \
1107 {path}/{field}"
1108 )));
1109 }
1110 }
1111 Some("Map") => {
1112 let reader = &state["ItemReader"];
1113 if reader["Resource"].as_str().and_then(integration_of) == Some("s3:getObject")
1114 && reader["ReaderConfig"].as_object().is_none()
1115 {
1116 return Err(schema_error(format!(
1117 "These fields are required: [ReaderConfig] at {path}/ItemReader"
1118 )));
1119 }
1120 if let Some(config) = reader["ReaderConfig"].as_object() {
1121 let at = format!("{path}/ItemReader/ReaderConfig");
1122 if config.contains_key("ManifestType")
1124 && config.get("InputType") == Some(&Value::from("MANIFEST"))
1125 {
1126 return Err(schema_error(format!(
1127 "Field 'ManifestType' is not supported at {at}"
1128 )));
1129 }
1130 if let Some(unknown) = config
1131 .keys()
1132 .find(|field| !READER_CONFIG_FIELDS.contains(&field.as_str()))
1133 {
1134 return Err(schema_error(format!(
1135 "Field '{unknown}' is not supported at {at}"
1136 )));
1137 }
1138 }
1139 for key in ["ItemProcessor", "Iterator"] {
1140 if let Some(nested) = state[key]["States"].as_object() {
1141 validate_integration_schemas(&format!("{path}/{key}"), nested)?;
1142 }
1143 }
1144 }
1145 Some("Parallel") => {
1146 for (index, branch) in state["Branches"]
1147 .as_array()
1148 .into_iter()
1149 .flatten()
1150 .enumerate()
1151 {
1152 if let Some(nested) = branch["States"].as_object() {
1153 validate_integration_schemas(&format!("{path}/Branches[{index}]"), nested)?;
1154 }
1155 }
1156 }
1157 _ => {}
1158 }
1159 }
1160 Ok(())
1161}
1162
1163fn validate_state_paths(state_name: &str, state: &Value) -> Result<(), AwsServiceError> {
1168 validate_timeout_field(state_name, state, "TimeoutSeconds")?;
1169 validate_timeout_field(state_name, state, "HeartbeatSeconds")?;
1170 for field in ["InputPath", "OutputPath", "ResultPath"] {
1171 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
1172 let valid = if field == "ResultPath" {
1176 is_valid_reference_path(p)
1177 } else {
1178 p == "null" || is_valid_path(p)
1179 };
1180 if valid {
1181 continue;
1182 }
1183 return Err(invalid_reference_path(state_name, field, p));
1184 }
1185 }
1186
1187 for field in [
1191 "SecondsPath",
1192 "TimestampPath",
1193 "ItemsPath",
1194 "MaxConcurrencyPath",
1195 "ToleratedFailureCountPath",
1196 "ToleratedFailurePercentagePath",
1197 ] {
1198 if let Some(p) = state.get(field).and_then(|v| v.as_str()) {
1199 if is_reference_or_context_path(p) {
1200 continue;
1201 }
1202 return Err(invalid_reference_path(state_name, field, p));
1203 }
1204 }
1205
1206 if !crate::jsonata::is_jsonata(state) {
1211 for field in ["Parameters", "ResultSelector", "ItemSelector", "Assign"] {
1212 if let Some(template) = state.get(field) {
1213 validate_payload_template(state_name, field, template)?;
1214 }
1215 }
1216 }
1217 match state.get("Type").and_then(|v| v.as_str()) {
1218 Some("Choice") => {
1219 if let Some(choices) = state.get("Choices").and_then(|v| v.as_array()) {
1220 for rule in choices {
1221 validate_choice_variables(state_name, rule)?;
1222 }
1223 }
1224 }
1225 Some("Parallel") => {
1226 if let Some(branches) = state.get("Branches").and_then(|v| v.as_array()) {
1227 for branch in branches {
1228 validate_nested_definition(state_name, branch)?;
1229 }
1230 }
1231 }
1232 Some("Map") => {
1233 for key in ["ItemProcessor", "Iterator"] {
1236 if let Some(processor) = state.get(key) {
1237 validate_nested_definition(state_name, processor)?;
1238 }
1239 }
1240 }
1241 _ => {}
1242 }
1243
1244 Ok(())
1245}
1246
1247const MAX_TIMEOUT_SECONDS: u64 = 99_999_999;
1249
1250fn validate_timeout_field(
1254 state_name: &str,
1255 state: &Value,
1256 field: &str,
1257) -> Result<(), AwsServiceError> {
1258 let Some(v) = state.get(field) else {
1259 return Ok(());
1260 };
1261 if crate::jsonata::is_jsonata(state) && v.as_str().is_some_and(|text| text.starts_with("{%")) {
1263 return Ok(());
1264 }
1265 match v.as_u64() {
1266 Some(n) if (1..=MAX_TIMEOUT_SECONDS).contains(&n) => Ok(()),
1267 _ => Err(AwsServiceError::aws_error(
1268 StatusCode::BAD_REQUEST,
1269 "InvalidDefinition",
1270 format!(
1271 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
1272 (The value of field '{field}' of state '{state_name}' must be a positive \
1273 integer between 1 and {MAX_TIMEOUT_SECONDS}, got {v})"
1274 ),
1275 )),
1276 }
1277}
1278
1279fn validate_nested_definition(parent: &str, def: &Value) -> Result<(), AwsServiceError> {
1283 if let Some(states) = def.get("States").and_then(|v| v.as_object()) {
1284 for (name, state) in states {
1285 validate_state_paths(&format!("{parent}/{name}"), state)?;
1286 }
1287 }
1288 Ok(())
1289}
1290
1291fn validate_payload_template(
1295 state_name: &str,
1296 field: &str,
1297 template: &Value,
1298) -> Result<(), AwsServiceError> {
1299 match template {
1300 Value::Object(map) => {
1301 for (key, value) in map {
1302 if let Some(stripped) = key.strip_suffix(".$") {
1303 let expr = value.as_str().ok_or_else(|| {
1304 invalid_payload_template(
1305 state_name,
1306 field,
1307 &format!(
1308 "the '{key}' field must be a JSONPath or intrinsic string, \
1309 not {value}"
1310 ),
1311 )
1312 })?;
1313 let is_ref = expr.starts_with('$');
1314 if !is_ref && !crate::intrinsics::is_intrinsic_call(expr) {
1315 return Err(invalid_payload_template(
1316 state_name,
1317 field,
1318 &format!(
1319 "the '{stripped}.$' field value '{expr}' is neither a JSONPath \
1320 reference nor an intrinsic function"
1321 ),
1322 ));
1323 }
1324 } else {
1325 validate_payload_template(state_name, field, value)?;
1326 }
1327 }
1328 }
1329 Value::Array(arr) => {
1330 for v in arr {
1331 validate_payload_template(state_name, field, v)?;
1332 }
1333 }
1334 _ => {}
1335 }
1336 Ok(())
1337}
1338
1339fn is_reference_or_context_path(p: &str) -> bool {
1342 if let Some(rest) = p.strip_prefix("$$") {
1343 return is_valid_path(&format!("${rest}"));
1344 }
1345 is_valid_path(p)
1346}
1347
1348fn invalid_payload_template(state_name: &str, field: &str, detail: &str) -> AwsServiceError {
1349 AwsServiceError::aws_error(
1350 StatusCode::BAD_REQUEST,
1351 "InvalidDefinition",
1352 format!(
1353 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
1354 (The {field} of state '{state_name}' is invalid: {detail})"
1355 ),
1356 )
1357}
1358
1359fn validate_choice_variables(state_name: &str, rule: &Value) -> Result<(), AwsServiceError> {
1362 if let Some(v) = rule.get("Variable").and_then(|v| v.as_str()) {
1363 if !is_reference_or_context_path(v) {
1365 return Err(invalid_reference_path(state_name, "Variable", v));
1366 }
1367 }
1368 for combinator in ["And", "Or"] {
1369 if let Some(nested) = rule.get(combinator).and_then(|v| v.as_array()) {
1370 for n in nested {
1371 validate_choice_variables(state_name, n)?;
1372 }
1373 }
1374 }
1375 if let Some(n) = rule.get("Not") {
1376 validate_choice_variables(state_name, n)?;
1377 }
1378 Ok(())
1379}
1380
1381fn is_valid_reference_path(path: &str) -> bool {
1387 crate::jsonpath::JsonPath::parse(path).is_ok_and(|p| p.is_definite())
1388}
1389
1390fn is_valid_path(path: &str) -> bool {
1394 let within = crate::variables::split_reference(path).map(|(_, within)| within);
1396 crate::jsonpath::JsonPath::parse(within.as_deref().unwrap_or(path)).is_ok()
1397}
1398
1399fn invalid_reference_path(state_name: &str, field: &str, path: &str) -> AwsServiceError {
1400 AwsServiceError::aws_error(
1401 StatusCode::BAD_REQUEST,
1402 "InvalidDefinition",
1403 format!(
1404 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED' \
1405 (The {field} field of state '{state_name}' is not a valid reference path: '{path}')"
1406 ),
1407 )
1408}
1409
1410fn execution_not_found(arn: &str) -> AwsServiceError {
1411 AwsServiceError::aws_error(
1412 StatusCode::BAD_REQUEST,
1413 "ExecutionDoesNotExist",
1414 format!("Execution Does Not Exist: '{arn}'"),
1415 )
1416}
1417
1418fn execution_to_json(exec: &Execution) -> Value {
1419 let mut resp = json!({
1420 "executionArn": exec.execution_arn,
1421 "stateMachineArn": exec.state_machine_arn,
1422 "name": exec.name,
1423 "status": exec.status.as_str(),
1424 "startDate": exec.start_date.timestamp_millis() as f64 / 1000.0,
1425 "redriveCount": exec.redrive_count,
1426 });
1427
1428 if let Some(ref input) = exec.input {
1429 resp["input"] = json!(input);
1430 }
1431 if let Some(ref output) = exec.output {
1432 resp["output"] = json!(output);
1433 }
1434 if let Some(stop) = exec.stop_date {
1435 resp["stopDate"] = json!(stop.timestamp_millis() as f64 / 1000.0);
1436 }
1437 if let Some(ref error) = exec.error {
1438 resp["error"] = json!(error);
1439 }
1440 if let Some(ref cause) = exec.cause {
1441 resp["cause"] = json!(cause);
1442 }
1443
1444 resp
1445}
1446
1447fn camel_to_details_key(event_type: &str) -> String {
1457 if event_type.ends_with("StateEntered") {
1458 return "stateEntered".to_string();
1459 }
1460 if event_type.ends_with("StateExited") {
1461 return "stateExited".to_string();
1462 }
1463 let mut chars = event_type.chars();
1464 match chars.next() {
1465 None => String::new(),
1466 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
1467 }
1468}
1469
1470fn execution_input_matches(stored: Option<&str>, incoming: Option<&str>) -> bool {
1475 let stored = stored.unwrap_or("{}");
1476 let incoming = incoming.unwrap_or("{}");
1477 match (
1478 serde_json::from_str::<Value>(stored),
1479 serde_json::from_str::<Value>(incoming),
1480 ) {
1481 (Ok(a), Ok(b)) => a == b,
1482 _ => stored == incoming,
1483 }
1484}
1485
1486fn validate_arn(arn: &str) -> Result<(), AwsServiceError> {
1487 if !arn.starts_with("arn:") {
1488 return Err(AwsServiceError::aws_error(
1489 StatusCode::BAD_REQUEST,
1490 "InvalidArn",
1491 format!("Invalid Arn: '{arn}'"),
1492 ));
1493 }
1494 Ok(())
1495}
1496
1497fn validate_arn_length(field: &str, value: &str, max: usize) -> Result<(), AwsServiceError> {
1501 if value.is_empty() || value.len() > max {
1502 return Err(AwsServiceError::aws_error(
1503 StatusCode::BAD_REQUEST,
1504 "InvalidArn",
1505 format!("Invalid Arn at '{field}': must be 1..={max} characters"),
1506 ));
1507 }
1508 Ok(())
1509}
1510
1511pub(super) fn invalid_token() -> AwsServiceError {
1515 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidToken", "Invalid nextToken")
1516}
1517
1518fn validate_page_token(value: &str) -> Result<(), AwsServiceError> {
1522 if value.is_empty() || value.len() > 1024 {
1523 return Err(AwsServiceError::aws_error(
1524 StatusCode::BAD_REQUEST,
1525 "InvalidToken",
1526 "nextToken must be 1..=1024 characters",
1527 ));
1528 }
1529 Ok(())
1530}
1531
1532fn validate_max_results(value: i64) -> Result<(), AwsServiceError> {
1538 if !(0..=1000).contains(&value) {
1539 return Err(AwsServiceError::aws_error(
1540 StatusCode::BAD_REQUEST,
1541 "InvalidToken",
1542 format!("maxResults '{value}' is outside 0..=1000"),
1543 ));
1544 }
1545 Ok(())
1546}
1547
1548pub fn start_execution_from_delivery(
1557 state: &SharedStepFunctionsState,
1558 delivery: &Option<Arc<DeliveryBus>>,
1559 dynamodb_state: &Option<SharedDynamoDbState>,
1560 registry: &Option<SharedServiceRegistry>,
1561 state_machine_arn: &str,
1562 input: &str,
1563) {
1564 if serde_json::from_str::<serde_json::Value>(input).is_err() {
1566 tracing::warn!(
1567 state_machine_arn,
1568 "Step Functions delivery: invalid JSON input, skipping execution"
1569 );
1570 return;
1571 }
1572
1573 let execution_name = uuid::Uuid::new_v4().to_string();
1574
1575 let mut accounts = state.write();
1576 let Some(st) = accounts.by_arn_mut(state_machine_arn) else {
1578 tracing::warn!(
1579 state_machine_arn,
1580 "Step Functions delivery: state machine not found"
1581 );
1582 return;
1583 };
1584 let sm = match st.state_machines.get(state_machine_arn) {
1585 Some(sm) => sm,
1586 None => {
1587 tracing::warn!(
1588 state_machine_arn,
1589 "Step Functions delivery: state machine not found"
1590 );
1591 return;
1592 }
1593 };
1594
1595 let sm_name = sm.name.clone();
1596 let sm_role_arn = sm.role_arn.clone();
1597 let sm_type = sm.machine_type;
1598 let definition = sm.definition.clone();
1599 let region = state_machine_arn
1602 .split(':')
1603 .nth(3)
1604 .unwrap_or("us-east-1")
1605 .to_string();
1606 let exec_arn = st.new_execution_arn(®ion, sm_type, &sm_name, &execution_name);
1607
1608 let now = Utc::now();
1609 let execution = Execution {
1610 execution_arn: exec_arn.clone(),
1611 state_machine_arn: state_machine_arn.to_string(),
1612 state_machine_name: sm_name,
1613 name: execution_name,
1614 status: ExecutionStatus::Running,
1615 input: Some(input.to_string()),
1616 output: None,
1617 start_date: now,
1618 stop_date: None,
1619 error: None,
1620 cause: None,
1621 history_events: vec![],
1622 parent_execution_arn: None,
1623 is_sync: false,
1624 billed_duration_ms: None,
1625 billed_memory_mb: None,
1626 role_arn: sm_role_arn,
1627 redrive_count: 0,
1628 };
1629
1630 st.executions.insert(exec_arn.clone(), execution);
1631 let logging_config = sm.logging_configuration.clone();
1632 drop(accounts);
1633
1634 let shared_state = state.clone();
1635 let delivery = delivery.clone();
1636 let dynamodb_state = dynamodb_state.clone();
1637 let registry = registry.clone();
1638 let input = Some(input.to_string());
1639 tokio::spawn(async move {
1640 interpreter::execute_state_machine(
1641 shared_state,
1642 exec_arn,
1643 definition,
1644 input,
1645 delivery,
1646 dynamodb_state,
1647 registry,
1648 logging_config,
1649 )
1650 .await;
1651 });
1652}
1653
1654#[cfg(test)]
1655mod integration_schema_tests {
1656 use super::*;
1657
1658 fn message(definition: serde_json::Value) -> Option<String> {
1659 validate_definition(&definition.to_string())
1660 .err()
1661 .map(|e| e.message())
1662 }
1663
1664 #[test]
1665 fn nested_start_requires_the_definition_casing() {
1666 assert_eq!(
1667 message(serde_json::json!({"StartAt":"T","States":{"T":{
1668 "Type":"Task",
1669 "Resource":"arn:aws:states:::states:startExecution.sync:2",
1670 "Parameters":{"stateMachineArn":"arn"},
1671 "End":true
1672 }}})),
1673 Some(
1674 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: The field \
1675 'StateMachineArn' is required but was missing at /States/T/Parameters'"
1676 .to_string()
1677 )
1678 );
1679 for key in ["StateMachineArn", "StateMachineArn.$"] {
1680 let definition = serde_json::json!({"StartAt":"T","States":{"T":{
1681 "Type":"Task",
1682 "Resource":"arn:aws:states:::states:startExecution",
1683 "Parameters":{key:"$.arn"},
1684 "End":true
1685 }}});
1686 assert_eq!(message(definition), None);
1687 }
1688 }
1689
1690 #[test]
1691 fn get_object_item_reader_requires_reader_config() {
1692 let map = |reader: serde_json::Value| {
1693 serde_json::json!({"StartAt":"M","States":{"M":{
1694 "Type":"Map",
1695 "ItemProcessor":{"ProcessorConfig":{"Mode":"DISTRIBUTED"},"StartAt":"P",
1696 "States":{"P":{"Type":"Pass","End":true}}},
1697 "ItemReader":reader,
1698 "End":true
1699 }}})
1700 };
1701 assert_eq!(
1702 message(map(serde_json::json!({
1703 "Resource":"arn:aws:states:::s3:getObject",
1704 "Parameters":{"Bucket":"b","Key":"k"}
1705 }))),
1706 Some(
1707 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: These fields are \
1708 required: [ReaderConfig] at /States/M/ItemReader'"
1709 .to_string()
1710 )
1711 );
1712 assert_eq!(
1713 message(map(serde_json::json!({
1714 "Resource":"arn:aws:states:::s3:listObjectsV2",
1715 "Parameters":{"Bucket":"b"}
1716 }))),
1717 None
1718 );
1719 }
1720
1721 #[test]
1722 fn reader_config_fields_are_checked() {
1723 let map = |config: serde_json::Value| {
1724 serde_json::json!({"StartAt":"M","States":{"M":{
1725 "Type":"Map",
1726 "ItemProcessor":{"ProcessorConfig":{"Mode":"DISTRIBUTED"},"StartAt":"P",
1727 "States":{"P":{"Type":"Pass","End":true}}},
1728 "ItemReader":{
1729 "Resource":"arn:aws:states:::s3:getObject",
1730 "ReaderConfig":config,
1731 "Parameters":{"Bucket":"b","Key":"k"}
1732 },
1733 "End":true
1734 }}})
1735 };
1736 assert_eq!(
1737 message(map(serde_json::json!({"InputType":"MANIFEST","ManifestType":"S3_INVENTORY"}))),
1738 Some(
1739 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: Field 'ManifestType' \
1740 is not supported at /States/M/ItemReader/ReaderConfig'"
1741 .to_string()
1742 )
1743 );
1744 assert!(
1745 message(map(serde_json::json!({"InputType":"JSON","Bogus":1})))
1746 .unwrap()
1747 .contains("Field 'Bogus' is not supported")
1748 );
1749 assert_eq!(
1750 message(map(
1751 serde_json::json!({"InputType":"CSV","CSVHeaderLocation":"FIRST_ROW","MaxItems":1})
1752 )),
1753 None
1754 );
1755 }
1756
1757 #[test]
1758 fn variable_names_and_scopes_are_checked() {
1759 let with_assign = |assign: serde_json::Value| serde_json::json!({"StartAt":"P","States":{"P":{"Type":"Pass","Assign":assign,"End":true}}});
1760 for bad in ["x.y", "x[2]", "1x"] {
1761 let error = message(with_assign(serde_json::json!({ bad: 1 }))).unwrap();
1762 assert!(
1763 error.contains(&format!(
1764 "INVALID_VARIABLE_NAME: Invalid variable name '{bad}': the variable name \
1765 contains invalid characters. at /States/P/Assign/{bad}"
1766 )),
1767 "{error}"
1768 );
1769 }
1770 assert_eq!(
1771 message(with_assign(serde_json::json!({"states": 1}))),
1772 Some(
1773 "Invalid State Machine Definition: 'RESERVED_VARIABLE_NAME: Cannot assign to \
1774 reserved variable '$states'. at /States/P'"
1775 .to_string()
1776 )
1777 );
1778 assert_eq!(
1779 message(with_assign(serde_json::json!({"ok_1": 1, "p.$": "$.a"}))),
1780 None
1781 );
1782
1783 let own = serde_json::json!({"StartAt":"M","States":{"M":{
1785 "Type":"Map",
1786 "Assign":{"x":1},
1787 "ItemProcessor":{"StartAt":"P","States":{"P":{"Type":"Pass","Assign":{"x":2},"End":true}}},
1788 "End":true
1789 }}});
1790 assert_eq!(message(own), None);
1791 let sibling = serde_json::json!({"StartAt":"A","States":{
1792 "A":{"Type":"Pass","Assign":{"x":1},"Next":"M"},
1793 "M":{
1794 "Type":"Map",
1795 "ItemProcessor":{"StartAt":"P","States":{"P":{"Type":"Pass","Assign":{"x":2},"End":true}}},
1796 "End":true
1797 }
1798 }});
1799 assert!(message(sibling).unwrap().contains("outer scope"));
1800 }
1801
1802 #[test]
1803 fn several_problems_are_reported_together_and_nested_ones_found() {
1804 let def = serde_json::json!({"StartAt":"P","States":{"P":{
1805 "Type":"Parallel",
1806 "Branches":[{"StartAt":"W","States":{"W":{"Type":"Wait","Next":"X","End":true}}}],
1807 "End":true
1808 }}});
1809 let error = message(def).unwrap();
1810 assert!(
1811 error.contains(
1812 "There should only be one of the following fields: [Next, End] at \
1813 /States/P/Branches[0]/States/W, SCHEMA_VALIDATION_FAILED: This object should have \
1814 only one of the following fields: [SecondsPath, Seconds, TimestampPath, \
1815 Timestamp] at /States/P/Branches[0]/States/W"
1816 ),
1817 "{error}"
1818 );
1819 }
1820
1821 #[test]
1822 fn jsonata_catchers_and_nested_templates_check_their_delimiters() {
1823 let def = serde_json::json!({"QueryLanguage":"JSONata","StartAt":"T","States":{
1824 "T":{"Type":"Task","Resource":"arn:aws:states:::aws-sdk:sfn:listStateMachines",
1825 "Arguments":{"a":["{% 1"]},
1826 "Catch":[{"ErrorEquals":["States.ALL"],"Output":"{% 2","Next":"E"}],
1827 "End":true},
1828 "E":{"Type":"Succeed"}
1829 }});
1830 let error = message(def).unwrap();
1831 assert!(error.contains("at /States/T/Arguments/a/0"), "{error}");
1832 assert!(error.contains("at /States/T/Catch/0/Output"), "{error}");
1833 }
1834
1835 #[test]
1836 fn variable_references_are_valid_paths() {
1837 assert!(is_valid_path("$order.items[0]"));
1838 assert!(is_valid_path("$x"));
1839 assert!(!is_valid_path("$x["));
1840 }
1841
1842 #[test]
1843 fn a_non_object_state_is_refused_as_recorded() {
1844 let unreachable = serde_json::json!({"StartAt": "A", "States": {
1845 "A": {"Type": "Pass", "End": true},
1846 "B": 1
1847 }});
1848 assert_eq!(
1849 message(unreachable),
1850 Some(
1851 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: Expected value of \
1852 type [OBJECT] at /States/B, MISSING_TRANSITION_TARGET: State \"B\" is not \
1853 reachable. at /States/B'"
1854 .to_string()
1855 )
1856 );
1857 let start = serde_json::json!({"StartAt": "S", "States": {"S": 1}});
1858 assert_eq!(
1859 message(start),
1860 Some(
1861 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: Expected value of \
1862 type [OBJECT] at /States/S'"
1863 .to_string()
1864 )
1865 );
1866 let nested = serde_json::json!({"StartAt": "M", "States": {"M": {
1867 "Type": "Map",
1868 "ItemProcessor": {"StartAt": "S", "States": {"S": "nope"}},
1869 "End": true
1870 }}});
1871 assert!(message(nested)
1872 .unwrap()
1873 .contains("at /States/M/ItemProcessor/States/S"));
1874 }
1875
1876 #[test]
1877 fn nested_start_without_a_template_is_refused() {
1878 let definition = |language: &str, field: Option<serde_json::Value>| {
1879 let mut state = serde_json::json!({
1880 "Type": "Task",
1881 "Resource": "arn:aws:states:::states:startExecution",
1882 "End": true
1883 });
1884 if let Some(value) = field {
1885 state["Arguments"] = value;
1886 }
1887 serde_json::json!({"QueryLanguage": language, "StartAt": "T", "States": {"T": state}})
1888 };
1889 assert!(message(definition("JSONPath", None))
1890 .unwrap()
1891 .contains("/States/T/Parameters"));
1892 assert!(message(definition("JSONata", None))
1893 .unwrap()
1894 .contains("/States/T/Arguments"));
1895 assert_eq!(
1896 message(definition(
1897 "JSONata",
1898 Some(serde_json::json!("{% $args %}"))
1899 )),
1900 None
1901 );
1902 }
1903
1904 #[test]
1905 fn timeout_expressions_need_a_jsonata_state() {
1906 let definition = |language: &str| {
1907 serde_json::json!({"QueryLanguage": language, "StartAt": "T", "States": {"T": {
1908 "Type": "Task",
1909 "Resource": "arn:aws:states:::sqs:sendMessage",
1910 "TimeoutSeconds": "{% 5 %}",
1911 "End": true
1912 }}})
1913 };
1914 assert_eq!(message(definition("JSONata")), None);
1915 assert!(message(definition("JSONPath")).is_some());
1916 }
1917
1918 #[test]
1919 fn assign_and_reader_config_must_be_objects() {
1920 let pass = serde_json::json!({"StartAt": "P", "States": {"P": {
1921 "Type": "Pass", "Assign": 5, "End": true
1922 }}});
1923 assert!(message(pass)
1924 .unwrap()
1925 .contains("Expected value of type [OBJECT] at /States/P/Assign"));
1926 let rule = serde_json::json!({"StartAt": "C", "States": {
1927 "C": {"Type": "Choice", "Choices": [
1928 {"Variable": "$.a", "IsPresent": true, "Assign": [], "Next": "E"}
1929 ]},
1930 "E": {"Type": "Succeed"}
1931 }});
1932 assert!(message(rule)
1933 .unwrap()
1934 .contains("/States/C/Choices/0/Assign"));
1935 let map = serde_json::json!({"StartAt": "M", "States": {"M": {
1936 "Type": "Map",
1937 "ItemProcessor": {"ProcessorConfig": {"Mode": "DISTRIBUTED"}, "StartAt": "P",
1938 "States": {"P": {"Type": "Pass", "End": true}}},
1939 "ItemReader": {
1940 "Resource": "arn:aws:states:::s3:getObject",
1941 "ReaderConfig": "JSON",
1942 "Parameters": {"Bucket": "b", "Key": "k"}
1943 },
1944 "End": true
1945 }}});
1946 assert!(message(map).unwrap().contains("[ReaderConfig]"));
1947 }
1948
1949 #[test]
1950 fn fail_state_paths_must_be_references_or_intrinsics() {
1951 let fail = |path: serde_json::Value| serde_json::json!({"StartAt": "F", "States": {"F": {"Type": "Fail", "ErrorPath": path}}});
1952 assert_eq!(message(fail(serde_json::json!("$.error"))), None);
1953 assert_eq!(
1954 message(fail(serde_json::json!("States.Format('{}', $.a)"))),
1955 None
1956 );
1957 assert_eq!(
1959 message(fail(serde_json::json!("notapath"))).unwrap(),
1960 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: The value must be a \
1961 valid Reference Path or a valid intrinsic function call at /States/F/ErrorPath'"
1962 );
1963 assert!(message(fail(serde_json::json!(5))).is_some());
1964 let cause = serde_json::json!({"StartAt": "F", "States": {"F": {"Type": "Fail", "Error": "e", "CausePath": "notapath"}}});
1965 assert!(message(cause)
1966 .unwrap()
1967 .contains("intrinsic function call at /States/F/CausePath"));
1968 }
1969
1970 #[test]
1972 fn literal_counts_must_be_integers() {
1973 let map = |count: serde_json::Value| {
1974 serde_json::json!({"StartAt":"M","States":{"M":{
1975 "Type":"Map",
1976 "ToleratedFailureCount": count,
1977 "ItemProcessor":{"StartAt":"P","States":{"P":{"Type":"Pass","End":true}}},
1978 "End":true
1979 }}})
1980 };
1981 for bad in [serde_json::json!(2.0), serde_json::json!(2.5)] {
1982 assert_eq!(
1983 message(map(bad)).unwrap(),
1984 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: Expected value of \
1985 type Integer at /States/M/ToleratedFailureCount'"
1986 );
1987 }
1988 assert_eq!(message(map(serde_json::json!(2))), None);
1989 assert!(message(map(serde_json::json!(-1)))
1990 .unwrap()
1991 .contains("Minimum value is 0 at /States/M/ToleratedFailureCount"));
1992 let literal = |seconds: i64| serde_json::json!({"StartAt":"W","States":{"W":{"Type":"Wait","Seconds":seconds,"End":true}}});
1993 assert_eq!(message(literal(99_999_999)), None);
1994 assert_eq!(
1995 message(literal(100_000_000)).unwrap(),
1996 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: Maximum value is \
1997 99999999 at /States/W/Seconds'"
1998 );
1999 assert!(message(literal(-1))
2000 .unwrap()
2001 .contains("Minimum value is 0 at /States/W/Seconds"));
2002 let wait = serde_json::json!({"QueryLanguage":"JSONata","StartAt":"W","States":{
2003 "W":{"Type":"Wait","Seconds":1.5,"End":true}
2004 }});
2005 assert!(message(wait)
2006 .unwrap()
2007 .contains("Expected value of type Integer at /States/W/Seconds"));
2008 let expression = serde_json::json!({"QueryLanguage":"JSONata","StartAt":"W","States":{
2009 "W":{"Type":"Wait","Seconds":"{% 1 %}","End":true}
2010 }});
2011 assert_eq!(message(expression), None);
2012 }
2013
2014 #[test]
2017 fn jsonpath_states_are_refused_only_under_a_jsonata_machine() {
2018 let parallel = |machine: Option<&str>, state: Option<&str>| {
2019 let mut def = serde_json::json!({"StartAt":"A","States":{"A":{
2020 "Type":"Parallel",
2021 "Branches":[{"StartAt":"X","States":{"X":{"Type":"Pass","QueryLanguage":"JSONPath","End":true}}}],
2022 "End":true
2023 }}});
2024 if let Some(language) = machine {
2025 def["QueryLanguage"] = serde_json::json!(language);
2026 }
2027 if let Some(language) = state {
2028 def["States"]["A"]["QueryLanguage"] = serde_json::json!(language);
2029 }
2030 def
2031 };
2032 assert_eq!(message(parallel(None, Some("JSONata"))), None);
2033 assert_eq!(
2034 message(parallel(Some("JSONata"), None)).unwrap(),
2035 "Invalid State Machine Definition: 'SCHEMA_VALIDATION_FAILED: 'QueryLanguage' can \
2036 not be 'JSONPath' if set to 'JSONata' for whole state machine at \
2037 /States/A/Branches[0]/States/X'"
2038 );
2039 let map = serde_json::json!({"StartAt":"A","States":{"A":{
2040 "Type":"Map",
2041 "QueryLanguage":"JSONata",
2042 "Items":"{% [1] %}",
2043 "ItemProcessor":{"StartAt":"X","States":{"X":{"Type":"Pass","QueryLanguage":"JSONPath","End":true}}},
2044 "End":true
2045 }}});
2046 assert_eq!(message(map), None);
2047 }
2048
2049 #[test]
2050 fn nested_states_report_their_full_path() {
2051 let definition = serde_json::json!({"StartAt":"M","States":{"M":{
2052 "Type":"Map",
2053 "ItemProcessor":{"StartAt":"T","States":{"T":{
2054 "Type":"Task",
2055 "Resource":"arn:aws:states:::states:startExecution",
2056 "Parameters":{},
2057 "End":true
2058 }}},
2059 "End":true
2060 }}});
2061 assert!(message(definition)
2062 .unwrap()
2063 .contains("/States/M/ItemProcessor/States/T/Parameters"));
2064 }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069 use super::*;
2070 use http::{HeaderMap, Method};
2071 use parking_lot::RwLock;
2072 use serde_json::Value;
2073 use std::collections::HashMap;
2074 use std::sync::Arc;
2075
2076 fn make_state() -> SharedStepFunctionsState {
2077 Arc::new(RwLock::new(
2078 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
2079 ))
2080 }
2081
2082 fn make_request(action: &str, body: &str) -> AwsRequest {
2083 AwsRequest {
2084 service: "states".to_string(),
2085 action: action.to_string(),
2086 region: "us-east-1".to_string(),
2087 account_id: "123456789012".to_string(),
2088 request_id: "test-id".to_string(),
2089 headers: HeaderMap::new(),
2090 query_params: HashMap::new(),
2091 body: body.as_bytes().to_vec().into(),
2092 body_stream: parking_lot::Mutex::new(None),
2093 path_segments: vec![],
2094 raw_path: "/".to_string(),
2095 raw_query: String::new(),
2096 method: Method::POST,
2097 is_query_protocol: false,
2098 access_key_id: None,
2099 principal: None,
2100 }
2101 }
2102
2103 fn body_json(resp: &AwsResponse) -> Value {
2104 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
2105 }
2106
2107 fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
2108 match result {
2109 Err(e) => e,
2110 Ok(_) => panic!("expected error, got Ok"),
2111 }
2112 }
2113
2114 const VALID_DEF: &str = r#"{"StartAt":"Pass","States":{"Pass":{"Type":"Pass","End":true}}}"#;
2115
2116 fn create_sm(svc: &StepFunctionsService, name: &str) -> String {
2117 let body = json!({
2118 "name": name,
2119 "definition": VALID_DEF,
2120 "roleArn": "arn:aws:iam::123456789012:role/test",
2121 });
2122 let req = make_request("CreateStateMachine", &body.to_string());
2123 let resp = svc.create_state_machine(&req).unwrap();
2124 let b = body_json(&resp);
2125 b["stateMachineArn"].as_str().unwrap().to_string()
2126 }
2127
2128 #[test]
2129 fn same_state_machine_name_coexists_in_two_regions() {
2130 let svc = StepFunctionsService::new(make_state());
2131 let in_region = |action: &str, body: Value, region: &str| {
2132 let mut req = make_request(action, &body.to_string());
2133 req.region = region.to_string();
2134 req
2135 };
2136 let create = |region: &str| {
2137 let body = json!({
2138 "name": "orders",
2139 "definition": VALID_DEF,
2140 "roleArn": "arn:aws:iam::123456789012:role/test",
2141 });
2142 let resp = svc
2143 .create_state_machine(&in_region("CreateStateMachine", body, region))
2144 .unwrap();
2145 body_json(&resp)["stateMachineArn"]
2146 .as_str()
2147 .unwrap()
2148 .to_string()
2149 };
2150 let east = create("us-east-1");
2151 let west = create("eu-west-1");
2152 assert_eq!(
2153 east,
2154 "arn:aws:states:us-east-1:123456789012:stateMachine:orders"
2155 );
2156 assert_eq!(
2157 west,
2158 "arn:aws:states:eu-west-1:123456789012:stateMachine:orders"
2159 );
2160
2161 for (region, arn) in [("us-east-1", &east), ("eu-west-1", &west)] {
2163 let resp = svc
2164 .list_state_machines(&in_region("ListStateMachines", json!({}), region))
2165 .unwrap();
2166 let listed: Vec<String> = body_json(&resp)["stateMachines"]
2167 .as_array()
2168 .unwrap()
2169 .iter()
2170 .map(|m| m["stateMachineArn"].as_str().unwrap().to_string())
2171 .collect();
2172 assert_eq!(listed, vec![arn.clone()]);
2173 }
2174
2175 let err = svc
2177 .describe_state_machine(&in_region(
2178 "DescribeStateMachine",
2179 json!({ "stateMachineArn": west }),
2180 "us-east-1",
2181 ))
2182 .err()
2183 .unwrap();
2184 assert_eq!(err.code(), "StateMachineDoesNotExist");
2185
2186 svc.delete_state_machine(&in_region(
2188 "DeleteStateMachine",
2189 json!({ "stateMachineArn": west }),
2190 "eu-west-1",
2191 ))
2192 .unwrap();
2193 assert!(svc
2194 .describe_state_machine(&in_region(
2195 "DescribeStateMachine",
2196 json!({ "stateMachineArn": east }),
2197 "us-east-1",
2198 ))
2199 .is_ok());
2200 }
2201
2202 #[test]
2203 fn v2_snapshot_migrates_resources_into_their_arn_region() {
2204 let mut legacy: fakecloud_core::multi_account::MultiAccountState<StepFunctionsState> =
2205 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
2206 {
2207 let s = legacy.default_mut();
2208 s.activities.insert(
2209 "arn:aws:states:eu-west-1:123456789012:activity:work".into(),
2210 crate::state::Activity {
2211 name: "work".into(),
2212 arn: "arn:aws:states:eu-west-1:123456789012:activity:work".into(),
2213 creation_date: Utc::now(),
2214 tags: Default::default(),
2215 },
2216 );
2217 }
2218 let bytes = serde_json::to_vec(&json!({"schema_version": 2, "accounts": legacy})).unwrap();
2219 let snap = crate::state::parse_stepfunctions_snapshot(&bytes).unwrap();
2220 assert_eq!(snap.schema_version, STEPFUNCTIONS_SNAPSHOT_SCHEMA_VERSION);
2221 let accounts = snap.accounts.unwrap();
2222 let west = accounts.regional("123456789012", "eu-west-1").unwrap();
2223 assert_eq!(west.region, "eu-west-1");
2224 assert_eq!(west.activities.len(), 1);
2225 assert!(accounts.regional("123456789012", "us-east-1").is_none());
2226 }
2227
2228 #[test]
2229 fn china_region_arns_use_the_aws_cn_partition() {
2230 let svc = StepFunctionsService::new(make_state());
2231 let in_china = |action: &str, body: Value| {
2232 let mut req = make_request(action, &body.to_string());
2233 req.region = "cn-north-1".to_string();
2234 req
2235 };
2236 let resp = svc
2237 .create_state_machine(&in_china(
2238 "CreateStateMachine",
2239 json!({
2240 "name": "cn-sm",
2241 "definition": VALID_DEF,
2242 "roleArn": "arn:aws-cn:iam::123456789012:role/test",
2243 }),
2244 ))
2245 .unwrap();
2246 let sm_arn = body_json(&resp)["stateMachineArn"]
2247 .as_str()
2248 .unwrap()
2249 .to_string();
2250 assert_eq!(
2251 sm_arn,
2252 "arn:aws-cn:states:cn-north-1:123456789012:stateMachine:cn-sm"
2253 );
2254 let resp = svc
2255 .describe_state_machine(&in_china(
2256 "DescribeStateMachine",
2257 json!({"stateMachineArn": sm_arn}),
2258 ))
2259 .unwrap();
2260 assert_eq!(body_json(&resp)["name"], "cn-sm");
2261
2262 let resp = svc
2263 .create_activity(&in_china("CreateActivity", json!({"name": "cn-act"})))
2264 .unwrap();
2265 let activity_arn = body_json(&resp)["activityArn"]
2266 .as_str()
2267 .unwrap()
2268 .to_string();
2269 assert_eq!(
2270 activity_arn,
2271 "arn:aws-cn:states:cn-north-1:123456789012:activity:cn-act"
2272 );
2273 let resp = svc
2274 .describe_activity(&in_china(
2275 "DescribeActivity",
2276 json!({"activityArn": activity_arn}),
2277 ))
2278 .unwrap();
2279 assert_eq!(body_json(&resp)["name"], "cn-act");
2280 }
2281
2282 #[test]
2285 fn create_state_machine_basic() {
2286 let svc = StepFunctionsService::new(make_state());
2287 let arn = create_sm(&svc, "test-sm");
2288 assert!(arn.contains("test-sm"));
2289 }
2290
2291 #[test]
2295 fn create_state_machine_publish_creates_listable_version() {
2296 let svc = StepFunctionsService::new(make_state());
2297 let body = json!({
2298 "name": "pub-sm",
2299 "definition": VALID_DEF,
2300 "roleArn": "arn:aws:iam::123456789012:role/r",
2301 "publish": true,
2302 "versionDescription": "first",
2303 });
2304 let req = make_request("CreateStateMachine", &body.to_string());
2305 let resp = svc.create_state_machine(&req).unwrap();
2306 let b = body_json(&resp);
2307 let arn = b["stateMachineArn"].as_str().unwrap().to_string();
2308 let version_arn = b["stateMachineVersionArn"].as_str().unwrap().to_string();
2309 assert_eq!(version_arn, format!("{arn}:1"));
2310
2311 let list_req = make_request(
2312 "ListStateMachineVersions",
2313 &json!({ "stateMachineArn": arn }).to_string(),
2314 );
2315 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
2316 let versions = list["stateMachineVersions"].as_array().unwrap();
2317 assert_eq!(versions.len(), 1);
2318 assert_eq!(versions[0]["stateMachineVersionArn"], version_arn);
2319 }
2320
2321 #[test]
2322 fn create_state_machine_no_publish_has_no_version() {
2323 let svc = StepFunctionsService::new(make_state());
2324 let arn = create_sm(&svc, "nopub-sm");
2325 let body = json!({
2327 "name": "nopub-sm2",
2328 "definition": VALID_DEF,
2329 "roleArn": "arn:aws:iam::123456789012:role/r",
2330 });
2331 let req = make_request("CreateStateMachine", &body.to_string());
2332 let b = body_json(&svc.create_state_machine(&req).unwrap());
2333 assert!(b.get("stateMachineVersionArn").is_none());
2334
2335 let list_req = make_request(
2336 "ListStateMachineVersions",
2337 &json!({ "stateMachineArn": arn }).to_string(),
2338 );
2339 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
2340 assert!(list["stateMachineVersions"].as_array().unwrap().is_empty());
2341 }
2342
2343 #[test]
2347 fn update_state_machine_publish_and_encryption_round_trip() {
2348 let svc = StepFunctionsService::new(make_state());
2349 let create = json!({
2350 "name": "enc-sm",
2351 "definition": VALID_DEF,
2352 "roleArn": "arn:aws:iam::123456789012:role/r",
2353 "encryptionConfiguration": { "type": "CUSTOMER_MANAGED_KMS_KEY", "kmsKeyId": "key-123" },
2354 });
2355 let req = make_request("CreateStateMachine", &create.to_string());
2356 let arn = body_json(&svc.create_state_machine(&req).unwrap())["stateMachineArn"]
2357 .as_str()
2358 .unwrap()
2359 .to_string();
2360
2361 let desc_req = make_request(
2363 "DescribeStateMachine",
2364 &json!({ "stateMachineArn": arn }).to_string(),
2365 );
2366 let desc = body_json(&svc.describe_state_machine(&desc_req).unwrap());
2367 assert_eq!(
2368 desc["encryptionConfiguration"]["type"],
2369 "CUSTOMER_MANAGED_KMS_KEY"
2370 );
2371 assert_eq!(desc["encryptionConfiguration"]["kmsKeyId"], "key-123");
2372
2373 let upd = json!({
2375 "stateMachineArn": arn,
2376 "definition": VALID_DEF,
2377 "publish": true,
2378 "versionDescription": "v via update",
2379 });
2380 let upd_req = make_request("UpdateStateMachine", &upd.to_string());
2381 let upd_body = body_json(&svc.update_state_machine(&upd_req).unwrap());
2382 assert_eq!(
2383 upd_body["stateMachineVersionArn"].as_str().unwrap(),
2384 format!("{arn}:1")
2385 );
2386
2387 let list_req = make_request(
2388 "ListStateMachineVersions",
2389 &json!({ "stateMachineArn": arn }).to_string(),
2390 );
2391 let list = body_json(&svc.list_state_machine_versions(&list_req).unwrap());
2392 assert_eq!(list["stateMachineVersions"].as_array().unwrap().len(), 1);
2393 }
2394
2395 #[test]
2396 fn create_state_machine_with_express_type() {
2397 let svc = StepFunctionsService::new(make_state());
2398 let body = json!({
2399 "name": "express-sm",
2400 "definition": VALID_DEF,
2401 "roleArn": "arn:aws:iam::123456789012:role/r",
2402 "type": "EXPRESS",
2403 });
2404 let req = make_request("CreateStateMachine", &body.to_string());
2405 let resp = svc.create_state_machine(&req).unwrap();
2406 let b = body_json(&resp);
2407 assert!(b["stateMachineArn"].as_str().is_some());
2408 }
2409
2410 #[test]
2411 fn create_state_machine_duplicate_fails() {
2412 let svc = StepFunctionsService::new(make_state());
2413 create_sm(&svc, "dup-sm");
2414 let body = json!({
2415 "name": "dup-sm",
2416 "definition": VALID_DEF,
2417 "roleArn": "arn:aws:iam::123456789012:role/r",
2418 });
2419 let req = make_request("CreateStateMachine", &body.to_string());
2420 let err = expect_err(svc.create_state_machine(&req));
2421 assert!(err.to_string().contains("StateMachineAlreadyExists"));
2422 }
2423
2424 #[test]
2425 fn create_state_machine_missing_name() {
2426 let svc = StepFunctionsService::new(make_state());
2427 let body = json!({
2428 "definition": VALID_DEF,
2429 "roleArn": "arn:aws:iam::123456789012:role/r",
2430 });
2431 let req = make_request("CreateStateMachine", &body.to_string());
2432 assert!(svc.create_state_machine(&req).is_err());
2433 }
2434
2435 #[test]
2436 fn create_state_machine_invalid_definition() {
2437 let svc = StepFunctionsService::new(make_state());
2438 let body = json!({
2439 "name": "bad-def",
2440 "definition": "not json",
2441 "roleArn": "arn:aws:iam::123456789012:role/r",
2442 });
2443 let req = make_request("CreateStateMachine", &body.to_string());
2444 let err = expect_err(svc.create_state_machine(&req));
2445 assert!(err.to_string().contains("InvalidDefinition"));
2446 }
2447
2448 #[test]
2449 fn create_state_machine_definition_missing_start_at() {
2450 let svc = StepFunctionsService::new(make_state());
2451 let body = json!({
2452 "name": "no-start",
2453 "definition": r#"{"States":{"S":{"Type":"Pass","End":true}}}"#,
2454 "roleArn": "arn:aws:iam::123456789012:role/r",
2455 });
2456 let req = make_request("CreateStateMachine", &body.to_string());
2457 let err = expect_err(svc.create_state_machine(&req));
2458 assert!(err.to_string().contains("InvalidDefinition"));
2459 }
2460
2461 #[test]
2462 fn create_state_machine_definition_missing_states() {
2463 let svc = StepFunctionsService::new(make_state());
2464 let body = json!({
2465 "name": "no-states",
2466 "definition": r#"{"StartAt":"S"}"#,
2467 "roleArn": "arn:aws:iam::123456789012:role/r",
2468 });
2469 let req = make_request("CreateStateMachine", &body.to_string());
2470 let err = expect_err(svc.create_state_machine(&req));
2471 assert!(err.to_string().contains("InvalidDefinition"));
2472 }
2473
2474 #[test]
2475 fn create_state_machine_definition_start_at_not_in_states() {
2476 let svc = StepFunctionsService::new(make_state());
2477 let body = json!({
2478 "name": "bad-start",
2479 "definition": r#"{"StartAt":"Missing","States":{"S":{"Type":"Pass","End":true}}}"#,
2480 "roleArn": "arn:aws:iam::123456789012:role/r",
2481 });
2482 let req = make_request("CreateStateMachine", &body.to_string());
2483 let err = expect_err(svc.create_state_machine(&req));
2484 assert!(err.to_string().contains("MISSING_TRANSITION_TARGET"));
2485 }
2486
2487 #[test]
2488 fn create_state_machine_invalid_type() {
2489 let svc = StepFunctionsService::new(make_state());
2490 let body = json!({
2491 "name": "bad-type",
2492 "definition": VALID_DEF,
2493 "roleArn": "arn:aws:iam::123456789012:role/r",
2494 "type": "INVALID",
2495 });
2496 let req = make_request("CreateStateMachine", &body.to_string());
2497 assert!(svc.create_state_machine(&req).is_err());
2498 }
2499
2500 #[test]
2501 fn create_state_machine_invalid_arn() {
2502 let svc = StepFunctionsService::new(make_state());
2503 let body = json!({
2504 "name": "bad-arn",
2505 "definition": VALID_DEF,
2506 "roleArn": "not-an-arn",
2507 });
2508 let req = make_request("CreateStateMachine", &body.to_string());
2509 let err = expect_err(svc.create_state_machine(&req));
2510 assert!(err.to_string().contains("InvalidArn"));
2511 }
2512
2513 #[test]
2514 fn create_state_machine_invalid_name() {
2515 let svc = StepFunctionsService::new(make_state());
2516 let body = json!({
2517 "name": "has spaces!",
2518 "definition": VALID_DEF,
2519 "roleArn": "arn:aws:iam::123456789012:role/r",
2520 });
2521 let req = make_request("CreateStateMachine", &body.to_string());
2522 let err = expect_err(svc.create_state_machine(&req));
2523 assert!(err.to_string().contains("InvalidName"));
2524 }
2525
2526 #[test]
2527 fn create_state_machine_name_too_long() {
2528 let svc = StepFunctionsService::new(make_state());
2529 let long_name = "a".repeat(81);
2530 let body = json!({
2531 "name": long_name,
2532 "definition": VALID_DEF,
2533 "roleArn": "arn:aws:iam::123456789012:role/r",
2534 });
2535 let req = make_request("CreateStateMachine", &body.to_string());
2536 let err = expect_err(svc.create_state_machine(&req));
2537 assert!(err.to_string().contains("InvalidName"));
2538 }
2539
2540 #[test]
2543 fn describe_state_machine_found() {
2544 let svc = StepFunctionsService::new(make_state());
2545 let arn = create_sm(&svc, "desc-sm");
2546
2547 let req = make_request(
2548 "DescribeStateMachine",
2549 &json!({"stateMachineArn": arn}).to_string(),
2550 );
2551 let resp = svc.describe_state_machine(&req).unwrap();
2552 let b = body_json(&resp);
2553 assert_eq!(b["name"], "desc-sm");
2554 assert_eq!(b["status"], "ACTIVE");
2555 assert!(b["definition"].as_str().is_some());
2556 }
2557
2558 #[test]
2559 fn describe_state_machine_not_found() {
2560 let svc = StepFunctionsService::new(make_state());
2561 let req = make_request(
2562 "DescribeStateMachine",
2563 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
2564 .to_string(),
2565 );
2566 let err = expect_err(svc.describe_state_machine(&req));
2567 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2568 }
2569
2570 #[test]
2573 fn list_state_machines_empty() {
2574 let svc = StepFunctionsService::new(make_state());
2575 let req = make_request("ListStateMachines", "{}");
2576 let resp = svc.list_state_machines(&req).unwrap();
2577 let b = body_json(&resp);
2578 assert!(b["stateMachines"].as_array().unwrap().is_empty());
2579 }
2580
2581 #[test]
2582 fn list_state_machines_returns_created() {
2583 let svc = StepFunctionsService::new(make_state());
2584 create_sm(&svc, "sm-1");
2585 create_sm(&svc, "sm-2");
2586
2587 let req = make_request("ListStateMachines", "{}");
2588 let resp = svc.list_state_machines(&req).unwrap();
2589 let b = body_json(&resp);
2590 assert_eq!(b["stateMachines"].as_array().unwrap().len(), 2);
2591 }
2592
2593 #[test]
2596 fn delete_state_machine() {
2597 let svc = StepFunctionsService::new(make_state());
2598 let arn = create_sm(&svc, "del-sm");
2599
2600 let req = make_request(
2601 "DeleteStateMachine",
2602 &json!({"stateMachineArn": arn}).to_string(),
2603 );
2604 svc.delete_state_machine(&req).unwrap();
2605
2606 let req = make_request(
2608 "DescribeStateMachine",
2609 &json!({"stateMachineArn": arn}).to_string(),
2610 );
2611 assert!(svc.describe_state_machine(&req).is_err());
2612 }
2613
2614 #[test]
2615 fn delete_state_machine_nonexistent_succeeds() {
2616 let svc = StepFunctionsService::new(make_state());
2617 let req = make_request(
2618 "DeleteStateMachine",
2619 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
2620 .to_string(),
2621 );
2622 svc.delete_state_machine(&req).unwrap();
2624 }
2625
2626 #[test]
2629 fn update_state_machine() {
2630 let svc = StepFunctionsService::new(make_state());
2631 let arn = create_sm(&svc, "upd-sm");
2632
2633 let new_def = r#"{"StartAt":"NewPass","States":{"NewPass":{"Type":"Pass","End":true}}}"#;
2634 let body = json!({
2635 "stateMachineArn": arn,
2636 "definition": new_def,
2637 "description": "updated",
2638 });
2639 let req = make_request("UpdateStateMachine", &body.to_string());
2640 let resp = svc.update_state_machine(&req).unwrap();
2641 let b = body_json(&resp);
2642 assert!(b["updateDate"].as_f64().is_some());
2643
2644 let req = make_request(
2646 "DescribeStateMachine",
2647 &json!({"stateMachineArn": arn}).to_string(),
2648 );
2649 let resp = svc.describe_state_machine(&req).unwrap();
2650 let b = body_json(&resp);
2651 assert!(b["definition"].as_str().unwrap().contains("NewPass"));
2652 assert_eq!(b["description"], "updated");
2653 }
2654
2655 #[test]
2656 fn update_state_machine_not_found() {
2657 let svc = StepFunctionsService::new(make_state());
2658 let body = json!({
2659 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
2660 "definition": VALID_DEF,
2661 });
2662 let req = make_request("UpdateStateMachine", &body.to_string());
2663 let err = expect_err(svc.update_state_machine(&req));
2664 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2665 }
2666
2667 #[tokio::test]
2670 async fn start_execution_basic() {
2671 let svc = StepFunctionsService::new(make_state());
2672 let arn = create_sm(&svc, "exec-sm");
2673
2674 let body = json!({
2675 "stateMachineArn": arn,
2676 "input": r#"{"key":"value"}"#,
2677 });
2678 let req = make_request("StartExecution", &body.to_string());
2679 let resp = svc.start_execution(&req).unwrap();
2680 let b = body_json(&resp);
2681 assert!(b["executionArn"].as_str().is_some());
2682 assert!(b["startDate"].as_f64().is_some());
2683 }
2684
2685 #[tokio::test]
2686 async fn start_execution_with_name() {
2687 let svc = StepFunctionsService::new(make_state());
2688 let arn = create_sm(&svc, "named-exec");
2689
2690 let body = json!({
2691 "stateMachineArn": arn,
2692 "name": "my-execution",
2693 });
2694 let req = make_request("StartExecution", &body.to_string());
2695 let resp = svc.start_execution(&req).unwrap();
2696 let b = body_json(&resp);
2697 assert!(b["executionArn"].as_str().unwrap().contains("my-execution"));
2698 }
2699
2700 #[tokio::test]
2701 async fn start_execution_sm_not_found() {
2702 let svc = StepFunctionsService::new(make_state());
2703 let body = json!({
2704 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
2705 });
2706 let req = make_request("StartExecution", &body.to_string());
2707 let err = expect_err(svc.start_execution(&req));
2708 assert!(err.to_string().contains("StateMachineDoesNotExist"));
2709 }
2710
2711 #[tokio::test]
2712 async fn start_execution_invalid_input() {
2713 let svc = StepFunctionsService::new(make_state());
2714 let arn = create_sm(&svc, "bad-input");
2715
2716 let body = json!({
2717 "stateMachineArn": arn,
2718 "input": "not json",
2719 });
2720 let req = make_request("StartExecution", &body.to_string());
2721 let err = expect_err(svc.start_execution(&req));
2722 assert!(err.to_string().contains("InvalidExecutionInput"));
2723 }
2724
2725 #[tokio::test]
2726 async fn start_execution_same_name_same_input_is_idempotent() {
2727 let svc = StepFunctionsService::new(make_state());
2728 let arn = create_sm(&svc, "dup-exec");
2729
2730 let body = json!({
2731 "stateMachineArn": arn,
2732 "name": "same-name",
2733 "input": "{\"a\":1}",
2734 });
2735 let req = make_request("StartExecution", &body.to_string());
2736 let first = body_json(&svc.start_execution(&req).unwrap());
2737
2738 let req = make_request("StartExecution", &body.to_string());
2740 let second = body_json(&svc.start_execution(&req).unwrap());
2741 assert_eq!(first["executionArn"], second["executionArn"]);
2742 assert_eq!(first["startDate"], second["startDate"]);
2743 }
2744
2745 #[tokio::test]
2746 async fn start_execution_same_name_same_input_conflicts_once_closed() {
2747 let svc = StepFunctionsService::new(make_state());
2748 let arn = create_sm(&svc, "dup-exec-closed");
2749 let body = json!({
2750 "stateMachineArn": arn,
2751 "name": "same-name",
2752 "input": "{\"a\":1}",
2753 });
2754 let req = make_request("StartExecution", &body.to_string());
2755 let first = body_json(&svc.start_execution(&req).unwrap());
2756 let exec_arn = first["executionArn"].as_str().unwrap().to_string();
2757 svc.state
2758 .write()
2759 .by_arn_mut(&exec_arn)
2760 .and_then(|s| s.executions.get_mut(&exec_arn))
2761 .unwrap()
2762 .status = ExecutionStatus::Succeeded;
2763
2764 let req = make_request("StartExecution", &body.to_string());
2766 let err = expect_err(svc.start_execution(&req));
2767 assert!(err.to_string().contains("ExecutionAlreadyExists"));
2768 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
2769 }
2770
2771 fn parent_of(svc: &StepFunctionsService, exec_arn: &str) -> Option<String> {
2772 svc.state
2773 .read()
2774 .by_arn(exec_arn)
2775 .and_then(|s| s.executions.get(exec_arn))
2776 .and_then(|e| e.parent_execution_arn.clone())
2777 }
2778
2779 const PARENT_EXEC: &str = "arn:aws:states:us-east-1:123456789012:execution:parent:p";
2780
2781 #[tokio::test]
2782 async fn start_execution_records_the_execution_starting_it() {
2783 let svc = StepFunctionsService::new(make_state());
2784 let arn = create_sm(&svc, "child-sm");
2785 let req = make_request(
2786 "StartExecution",
2787 &json!({"stateMachineArn": arn}).to_string(),
2788 );
2789
2790 let started = crate::state::STARTED_BY_EXECUTION
2791 .scope(PARENT_EXEC.to_string(), async { svc.start_execution(&req) })
2792 .await;
2793 let child = body_json(&started.unwrap())["executionArn"]
2794 .as_str()
2795 .unwrap()
2796 .to_string();
2797 assert_eq!(parent_of(&svc, &child).as_deref(), Some(PARENT_EXEC));
2798
2799 let direct = body_json(&svc.start_execution(&req).unwrap())["executionArn"]
2801 .as_str()
2802 .unwrap()
2803 .to_string();
2804 assert_eq!(parent_of(&svc, &direct), None);
2805 }
2806
2807 #[tokio::test]
2808 async fn idempotent_start_execution_never_reparents_the_running_execution() {
2809 let svc = StepFunctionsService::new(make_state());
2810 let arn = create_sm(&svc, "idem-child-sm");
2811 let req = make_request(
2812 "StartExecution",
2813 &json!({"stateMachineArn": arn, "name": "fixed", "input": "{}"}).to_string(),
2814 );
2815 let first = body_json(&svc.start_execution(&req).unwrap())["executionArn"]
2816 .as_str()
2817 .unwrap()
2818 .to_string();
2819
2820 let again = crate::state::STARTED_BY_EXECUTION
2823 .scope(PARENT_EXEC.to_string(), async { svc.start_execution(&req) })
2824 .await;
2825 assert_eq!(body_json(&again.unwrap())["executionArn"], first.as_str());
2826 assert_eq!(parent_of(&svc, &first), None);
2827 }
2828
2829 #[tokio::test]
2830 async fn start_execution_same_name_different_input_conflicts() {
2831 let svc = StepFunctionsService::new(make_state());
2832 let arn = create_sm(&svc, "dup-exec-diff");
2833
2834 let req = make_request(
2835 "StartExecution",
2836 &json!({
2837 "stateMachineArn": arn,
2838 "name": "same-name",
2839 "input": "{\"a\":1}",
2840 })
2841 .to_string(),
2842 );
2843 svc.start_execution(&req).unwrap();
2844
2845 let req = make_request(
2847 "StartExecution",
2848 &json!({
2849 "stateMachineArn": arn,
2850 "name": "same-name",
2851 "input": "{\"a\":2}",
2852 })
2853 .to_string(),
2854 );
2855 let err = expect_err(svc.start_execution(&req));
2856 assert!(err.to_string().contains("ExecutionAlreadyExists"));
2857 }
2858
2859 #[tokio::test]
2860 async fn start_execution_express_reuses_names_as_new_executions() {
2861 let svc = StepFunctionsService::new(make_state());
2862 let arn = create_express_sm(&svc, "dup-exec-express");
2863
2864 let body = json!({
2865 "stateMachineArn": arn,
2866 "name": "same-name",
2867 "input": "{\"a\":1}",
2868 });
2869 let req = make_request("StartExecution", &body.to_string());
2872 let first = body_json(&svc.start_execution(&req).unwrap());
2873 let req = make_request("StartExecution", &body.to_string());
2874 let second = body_json(&svc.start_execution(&req).unwrap());
2875
2876 let (a, b) = (
2877 first["executionArn"].as_str().unwrap(),
2878 second["executionArn"].as_str().unwrap(),
2879 );
2880 assert_ne!(a, b);
2881 for exec_arn in [a, b] {
2882 assert!(
2883 exec_arn.starts_with(
2884 "arn:aws:states:us-east-1:123456789012:express:dup-exec-express:same-name:"
2885 ),
2886 "{exec_arn}"
2887 );
2888 }
2889 let accounts = svc.state.read();
2890 let state = accounts.by_arn(a).unwrap();
2891 assert_eq!(state.executions[a].name, "same-name");
2892 assert_eq!(state.executions[b].name, "same-name");
2893 }
2894
2895 #[tokio::test]
2896 async fn delivered_express_executions_get_express_arns() {
2897 let svc = StepFunctionsService::new(make_state());
2898 let arn = create_express_sm(&svc, "delivered-express");
2899 start_execution_from_delivery(&svc.state, &None, &None, &None, &arn, "{}");
2900 start_execution_from_delivery(&svc.state, &None, &None, &None, &arn, "{}");
2901
2902 let accounts = svc.state.read();
2903 let state = accounts.by_arn(&arn).unwrap();
2904 let arns: Vec<_> = state
2905 .executions
2906 .keys()
2907 .filter(|k| k.contains("delivered-express"))
2908 .collect();
2909 assert_eq!(arns.len(), 2);
2910 for exec_arn in arns {
2911 assert!(
2912 exec_arn.starts_with(
2913 "arn:aws:states:us-east-1:123456789012:express:delivered-express:"
2914 ),
2915 "{exec_arn}"
2916 );
2917 }
2918 }
2919
2920 #[tokio::test]
2921 async fn start_sync_execution_rejects_a_malformed_arn() {
2922 let svc = StepFunctionsService::new(make_state());
2923 let req = make_request(
2924 "StartSyncExecution",
2925 &json!({"stateMachineArn": "foo"}).to_string(),
2926 );
2927 let err = expect_err(svc.start_sync_execution(&req).await);
2928 assert!(err.to_string().contains("InvalidArn"), "{err}");
2929 }
2930
2931 #[tokio::test]
2932 async fn start_sync_execution_uses_the_given_name() {
2933 let svc = StepFunctionsService::new(make_state());
2934 let arn = create_express_sm(&svc, "sync-named");
2935 let body = json!({"stateMachineArn": arn, "name": "my-run"});
2936
2937 let req = make_request("StartSyncExecution", &body.to_string());
2938 let first = body_json(&svc.start_sync_execution(&req).await.unwrap());
2939 let req = make_request("StartSyncExecution", &body.to_string());
2940 let second = body_json(&svc.start_sync_execution(&req).await.unwrap());
2941
2942 assert_eq!(first["name"], "my-run");
2943 assert_eq!(second["name"], "my-run");
2944 assert_ne!(first["executionArn"], second["executionArn"]);
2945 assert!(first["executionArn"]
2946 .as_str()
2947 .unwrap()
2948 .contains(":express:sync-named:my-run:"));
2949
2950 let bad = json!({"stateMachineArn": arn, "name": "has space"});
2951 let req = make_request("StartSyncExecution", &bad.to_string());
2952 assert!(svc.start_sync_execution(&req).await.is_err());
2953 }
2954
2955 #[tokio::test]
2958 async fn describe_execution_found() {
2959 let svc = StepFunctionsService::new(make_state());
2960 let sm_arn = create_sm(&svc, "desc-exec");
2961
2962 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
2963 let req = make_request("StartExecution", &body.to_string());
2964 let resp = svc.start_execution(&req).unwrap();
2965 let exec_arn = body_json(&resp)["executionArn"]
2966 .as_str()
2967 .unwrap()
2968 .to_string();
2969
2970 let req = make_request(
2971 "DescribeExecution",
2972 &json!({"executionArn": exec_arn}).to_string(),
2973 );
2974 let resp = svc.describe_execution(&req).unwrap();
2975 let b = body_json(&resp);
2976 assert_eq!(b["name"], "e1");
2977 assert_eq!(b["status"], "RUNNING");
2978 }
2979
2980 #[tokio::test]
2981 async fn describe_execution_not_found() {
2982 let svc = StepFunctionsService::new(make_state());
2983 let req = make_request(
2984 "DescribeExecution",
2985 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
2986 .to_string(),
2987 );
2988 let err = expect_err(svc.describe_execution(&req));
2989 assert!(err.to_string().contains("ExecutionDoesNotExist"));
2990 }
2991
2992 #[tokio::test]
2995 async fn stop_execution() {
2996 let svc = StepFunctionsService::new(make_state());
2997 let sm_arn = create_sm(&svc, "stop-sm");
2998
2999 let body = json!({"stateMachineArn": sm_arn, "name": "stop-e"});
3000 let req = make_request("StartExecution", &body.to_string());
3001 let resp = svc.start_execution(&req).unwrap();
3002 let exec_arn = body_json(&resp)["executionArn"]
3003 .as_str()
3004 .unwrap()
3005 .to_string();
3006
3007 let body = json!({
3008 "executionArn": exec_arn,
3009 "error": "UserAborted",
3010 "cause": "test stop",
3011 });
3012 let req = make_request("StopExecution", &body.to_string());
3013 let resp = svc.stop_execution(&req).unwrap();
3014 let b = body_json(&resp);
3015 assert!(b["stopDate"].as_f64().is_some());
3016
3017 let req = make_request(
3019 "DescribeExecution",
3020 &json!({"executionArn": exec_arn}).to_string(),
3021 );
3022 let resp = svc.describe_execution(&req).unwrap();
3023 let b = body_json(&resp);
3024 assert_eq!(b["status"], "ABORTED");
3025 assert_eq!(b["error"], "UserAborted");
3026 }
3027
3028 #[tokio::test]
3029 async fn redrive_execution_reruns_to_terminal() {
3030 let svc = StepFunctionsService::new(make_state());
3031 let sm_arn = create_sm(&svc, "redrive-sm");
3032
3033 let body = json!({"stateMachineArn": sm_arn, "name": "redrive-e"});
3034 let req = make_request("StartExecution", &body.to_string());
3035 let resp = svc.start_execution(&req).unwrap();
3036 let exec_arn = body_json(&resp)["executionArn"]
3037 .as_str()
3038 .unwrap()
3039 .to_string();
3040
3041 let req = make_request(
3043 "StopExecution",
3044 &json!({"executionArn": exec_arn, "error": "UserAborted", "cause": "stop"}).to_string(),
3045 );
3046 svc.stop_execution(&req).unwrap();
3047
3048 let req = make_request(
3050 "RedriveExecution",
3051 &json!({"executionArn": exec_arn}).to_string(),
3052 );
3053 let resp = svc.redrive_execution(&req).unwrap();
3054 assert!(body_json(&resp)["redriveDate"].as_i64().is_some());
3055
3056 let mut status = String::new();
3059 for _ in 0..100 {
3060 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
3061 let req = make_request(
3062 "DescribeExecution",
3063 &json!({"executionArn": exec_arn}).to_string(),
3064 );
3065 let b = body_json(&svc.describe_execution(&req).unwrap());
3066 status = b["status"].as_str().unwrap().to_string();
3067 if status != "RUNNING" {
3068 break;
3069 }
3070 }
3071 assert_eq!(
3072 status, "SUCCEEDED",
3073 "redriven execution must run to completion"
3074 );
3075 let req = make_request(
3076 "DescribeExecution",
3077 &json!({"executionArn": exec_arn}).to_string(),
3078 );
3079 let b = body_json(&svc.describe_execution(&req).unwrap());
3080 assert_eq!(b["redriveCount"], 1);
3081 }
3082
3083 #[tokio::test]
3084 async fn redrive_execution_running_rejected() {
3085 let svc = StepFunctionsService::new(make_state());
3086 let sm_arn = create_sm(&svc, "redrive-running-sm");
3087 let body = json!({"stateMachineArn": sm_arn, "name": "still-running"});
3088 let req = make_request("StartExecution", &body.to_string());
3089 let exec_arn = body_json(&svc.start_execution(&req).unwrap())["executionArn"]
3090 .as_str()
3091 .unwrap()
3092 .to_string();
3093
3094 let req = make_request(
3096 "RedriveExecution",
3097 &json!({"executionArn": exec_arn}).to_string(),
3098 );
3099 let err = expect_err(svc.redrive_execution(&req));
3100 assert!(err.to_string().contains("ExecutionNotRedrivable"));
3101 }
3102
3103 #[tokio::test]
3104 async fn stop_execution_not_found() {
3105 let svc = StepFunctionsService::new(make_state());
3106 let req = make_request(
3107 "StopExecution",
3108 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
3109 .to_string(),
3110 );
3111 let err = expect_err(svc.stop_execution(&req));
3112 assert!(err.to_string().contains("ExecutionDoesNotExist"));
3113 }
3114
3115 #[tokio::test]
3118 async fn list_executions() {
3119 let svc = StepFunctionsService::new(make_state());
3120 let sm_arn = create_sm(&svc, "list-exec");
3121
3122 for i in 0..3 {
3123 let body = json!({"stateMachineArn": sm_arn, "name": format!("e{i}")});
3124 let req = make_request("StartExecution", &body.to_string());
3125 svc.start_execution(&req).unwrap();
3126 }
3127
3128 let req = make_request(
3129 "ListExecutions",
3130 &json!({"stateMachineArn": sm_arn}).to_string(),
3131 );
3132 let resp = svc.list_executions(&req).unwrap();
3133 let b = body_json(&resp);
3134 assert_eq!(b["executions"].as_array().unwrap().len(), 3);
3135 }
3136
3137 #[tokio::test]
3138 async fn list_executions_sm_not_found() {
3139 let svc = StepFunctionsService::new(make_state());
3140 let req = make_request(
3141 "ListExecutions",
3142 &json!({"stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope"})
3143 .to_string(),
3144 );
3145 let err = expect_err(svc.list_executions(&req));
3146 assert!(err.to_string().contains("StateMachineDoesNotExist"));
3147 }
3148
3149 #[tokio::test]
3152 async fn get_execution_history_not_found() {
3153 let svc = StepFunctionsService::new(make_state());
3154 let req = make_request(
3155 "GetExecutionHistory",
3156 &json!({"executionArn": "arn:aws:states:us-east-1:123456789012:execution:sm:nope"})
3157 .to_string(),
3158 );
3159 let err = expect_err(svc.get_execution_history(&req));
3160 assert!(err.to_string().contains("ExecutionDoesNotExist"));
3161 }
3162
3163 #[tokio::test]
3166 async fn describe_sm_for_execution() {
3167 let svc = StepFunctionsService::new(make_state());
3168 let sm_arn = create_sm(&svc, "sm-for-exec");
3169
3170 let body = json!({"stateMachineArn": sm_arn, "name": "e1"});
3171 let req = make_request("StartExecution", &body.to_string());
3172 let resp = svc.start_execution(&req).unwrap();
3173 let exec_arn = body_json(&resp)["executionArn"]
3174 .as_str()
3175 .unwrap()
3176 .to_string();
3177
3178 let req = make_request(
3179 "DescribeStateMachineForExecution",
3180 &json!({"executionArn": exec_arn}).to_string(),
3181 );
3182 let resp = svc.describe_state_machine_for_execution(&req).unwrap();
3183 let b = body_json(&resp);
3184 assert_eq!(b["name"], "sm-for-exec");
3185 }
3186
3187 #[test]
3190 fn tag_untag_list_tags() {
3191 let svc = StepFunctionsService::new(make_state());
3192 let arn = create_sm(&svc, "tagged-sm");
3193
3194 let body = json!({
3196 "resourceArn": arn,
3197 "tags": [{"key": "env", "value": "prod"}],
3198 });
3199 let req = make_request("TagResource", &body.to_string());
3200 svc.tag_resource(&req).unwrap();
3201
3202 let req = make_request(
3204 "ListTagsForResource",
3205 &json!({"resourceArn": arn}).to_string(),
3206 );
3207 let resp = svc.list_tags_for_resource(&req).unwrap();
3208 let b = body_json(&resp);
3209 let tags = b["tags"].as_array().unwrap();
3210 assert_eq!(tags.len(), 1);
3211 assert_eq!(tags[0]["key"], "env");
3212
3213 let body = json!({
3215 "resourceArn": arn,
3216 "tagKeys": ["env"],
3217 });
3218 let req = make_request("UntagResource", &body.to_string());
3219 svc.untag_resource(&req).unwrap();
3220
3221 let req = make_request(
3223 "ListTagsForResource",
3224 &json!({"resourceArn": arn}).to_string(),
3225 );
3226 let resp = svc.list_tags_for_resource(&req).unwrap();
3227 let b = body_json(&resp);
3228 assert!(b["tags"].as_array().unwrap().is_empty());
3229 }
3230
3231 #[test]
3232 fn tag_resource_not_found() {
3233 let svc = StepFunctionsService::new(make_state());
3234 let body = json!({
3235 "resourceArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
3236 "tags": [{"key": "k", "value": "v"}],
3237 });
3238 let req = make_request("TagResource", &body.to_string());
3239 let err = expect_err(svc.tag_resource(&req));
3240 assert!(err.to_string().contains("ResourceNotFound"));
3241 }
3242
3243 #[test]
3246 fn test_validate_name() {
3247 assert!(validate_name("valid-name").is_ok());
3248 assert!(validate_name("under_score").is_ok());
3249 assert!(validate_name("").is_err());
3250 assert!(validate_name("has spaces").is_err());
3251 assert!(validate_name(&"a".repeat(81)).is_err());
3252 }
3253
3254 #[test]
3255 fn test_validate_definition() {
3256 assert!(validate_definition(VALID_DEF).is_ok());
3257 assert!(validate_definition("not json").is_err());
3258 assert!(validate_definition(r#"{"States":{}}"#).is_err()); assert!(validate_definition(r#"{"StartAt":"S"}"#).is_err()); }
3261
3262 #[test]
3263 fn test_validate_definition_timeout_range() {
3264 let task = |field: &str, v: &str| {
3265 format!(
3266 r#"{{"StartAt":"T","States":{{"T":{{"Type":"Task","Resource":"arn:aws:states:::lambda:invoke","{field}":{v},"End":true}}}}}}"#
3267 )
3268 };
3269 for field in ["TimeoutSeconds", "HeartbeatSeconds"] {
3270 assert!(validate_definition(&task(field, "1")).is_ok());
3271 assert!(validate_definition(&task(field, "99999999")).is_ok());
3272 for bad in [
3273 "0",
3274 "-1",
3275 "100000000",
3276 "18446744073709551615",
3277 "1.5",
3278 "\"10\"",
3279 ] {
3280 let err = validate_definition(&task(field, bad)).unwrap_err();
3281 assert!(
3282 err.to_string().contains("InvalidDefinition"),
3283 "{field}={bad}: {err}"
3284 );
3285 }
3286 }
3287 let def = r#"{"StartAt":"P","TimeoutSeconds":18446744073709551615,"States":{"P":{"Type":"Pass","End":true}}}"#;
3289 assert!(validate_definition(def).is_err());
3290 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}}}]}}}"#;
3292 assert!(validate_definition(def).is_err());
3293 }
3294
3295 #[test]
3296 fn test_validate_definition_rejects_malformed_paths() {
3297 let def =
3299 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"$.arr[","End":true}}}"#;
3300 assert!(validate_definition(def).is_err());
3301
3302 let def =
3304 "{\"StartAt\":\"P\",\"States\":{\"P\":{\"Type\":\"Pass\",\"OutputPath\":\"$.x[\u{00e9}\",\"End\":true}}}";
3305 assert!(validate_definition(def).is_err());
3306
3307 let def = r#"{"StartAt":"C","States":{"C":{"Type":"Choice","Choices":[{"Variable":"$.n[","NumericEquals":1,"Next":"P"}]},"P":{"Type":"Pass","End":true}}}"#;
3309 assert!(validate_definition(def).is_err());
3310
3311 let def =
3313 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","InputPath":"foo.bar","End":true}}}"#;
3314 assert!(validate_definition(def).is_err());
3315
3316 let def =
3318 r#"{"StartAt":"P","States":{"P":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}"#;
3319 assert!(validate_definition(def).is_err());
3320 }
3321
3322 #[test]
3323 fn test_validate_definition_accepts_well_formed_paths() {
3324 let def = r#"{"StartAt":"P","States":{
3327 "P":{"Type":"Pass","InputPath":"$.a.b[0].c","OutputPath":"$","ResultPath":"$.out","Next":"C"},
3328 "C":{"Type":"Choice","Choices":[
3329 {"And":[{"Variable":"$.items[2]","NumericEquals":1},{"Variable":"$.flag","BooleanEquals":true}],"Next":"S"}
3330 ],"Default":"S"},
3331 "S":{"Type":"Succeed","InputPath":"null"}
3332 }}"#;
3333 assert!(validate_definition(def).is_ok());
3334 }
3335
3336 #[test]
3339 fn test_validate_definition_recurses_into_parallel_and_map() {
3340 let par = r#"{"StartAt":"Par","States":{"Par":{"Type":"Parallel","End":true,
3341 "Branches":[{"StartAt":"I","States":{"I":{"Type":"Pass","InputPath":"nope","End":true}}}]}}}"#;
3342 assert!(validate_definition(par).is_err());
3343
3344 let map = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
3345 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","OutputPath":"nope","End":true}}}}}}"#;
3346 assert!(validate_definition(map).is_err());
3347
3348 let iter = r#"{"StartAt":"M","States":{"M":{"Type":"Map","End":true,
3350 "Iterator":{"StartAt":"E","States":{"E":{"Type":"Pass","ResultPath":"$.x[]","End":true}}}}}}"#;
3351 assert!(validate_definition(iter).is_err());
3352
3353 let ok = r#"{"StartAt":"M","States":{"M":{"Type":"Map","ItemsPath":"$.items","End":true,
3355 "ItemProcessor":{"StartAt":"E","States":{"E":{"Type":"Pass","InputPath":"$.a","End":true}}}}}}"#;
3356 assert!(validate_definition(ok).is_ok());
3357 }
3358
3359 #[test]
3361 fn test_validate_definition_payload_template_dollar_keys() {
3362 let lit = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
3364 "Parameters":{"v.$":"just text"},"End":true}}}"#;
3365 assert!(validate_definition(lit).is_err());
3366
3367 let num = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
3369 "Parameters":{"v.$":42},"End":true}}}"#;
3370 assert!(validate_definition(num).is_err());
3371
3372 let ok = r#"{"StartAt":"P","States":{"P":{"Type":"Pass","Parameters":{
3374 "a.$":"$.input","b.$":"$$.Execution.Id","c.$":"States.Format('{}',$.n)",
3375 "nested":{"d.$":"$.x"},"plain":"literal-ok"},"End":true}}}"#;
3376 assert!(validate_definition(ok).is_ok());
3377
3378 let arr = r#"{"StartAt":"P","States":{"P":{"Type":"Pass",
3380 "ResultSelector":{"items":[{"bad.$":"literal"}]},"End":true}}}"#;
3381 assert!(validate_definition(arr).is_err());
3382 }
3383
3384 #[test]
3385 fn test_paths_accept_full_jsonpath_but_result_path_must_be_definite() {
3386 let def = |field: &str, path: &str| {
3387 json!({
3388 "StartAt": "P",
3389 "States": {"P": {"Type": "Pass", field: path, "End": true}}
3390 })
3391 .to_string()
3392 };
3393 for p in [
3394 "$.items[*].id",
3395 "$.a[?(@.x == 1)]",
3396 "$['a']",
3397 "$.a[-1]",
3398 "$..id",
3399 "$.a[1:2]",
3400 ] {
3401 assert!(validate_definition(&def("InputPath", p)).is_ok(), "{p}");
3402 assert!(validate_definition(&def("OutputPath", p)).is_ok(), "{p}");
3403 }
3404 assert!(validate_definition(&def("ResultPath", "$['a'][0]")).is_ok());
3405 let choice = |var: &str| {
3406 json!({
3407 "StartAt": "C",
3408 "States": {
3409 "C": {"Type": "Choice", "Choices": [{"Variable": var, "IsPresent": true, "Next": "D"}], "Default": "D"},
3410 "D": {"Type": "Succeed"}
3411 }
3412 })
3413 .to_string()
3414 };
3415 assert!(validate_definition(&choice("$$.Execution.Name")).is_ok());
3416 assert!(validate_definition(&choice("$$.x[")).is_err());
3417 for p in ["$.items[*]", "$..a", "$.a[?(@.x)]"] {
3418 assert!(validate_definition(&def("ResultPath", p)).is_err(), "{p}");
3419 }
3420 }
3421
3422 #[test]
3423 fn test_is_valid_reference_path() {
3424 assert!(is_valid_reference_path("$"));
3425 assert!(is_valid_reference_path("$.foo"));
3426 assert!(is_valid_reference_path("$.foo.bar[3].baz"));
3427 assert!(is_valid_reference_path("$[0]"));
3428 assert!(!is_valid_reference_path("$.arr["));
3429 assert!(!is_valid_reference_path("$.x[\u{00e9}"));
3430 assert!(!is_valid_reference_path("$.x[]"));
3431 assert!(!is_valid_reference_path("$.x[abc]"));
3432 assert!(!is_valid_reference_path("foo.bar"));
3433 assert!(!is_valid_reference_path(""));
3434 }
3435
3436 #[test]
3437 fn test_validate_arn() {
3438 assert!(validate_arn("arn:aws:states:us-east-1:123:sm:test").is_ok());
3439 assert!(validate_arn("not-an-arn").is_err());
3440 }
3441
3442 #[test]
3443 fn test_camel_to_details_key() {
3444 assert_eq!(camel_to_details_key("PassStateEntered"), "stateEntered");
3446 assert_eq!(camel_to_details_key("TaskStateEntered"), "stateEntered");
3447 assert_eq!(camel_to_details_key("ChoiceStateExited"), "stateExited");
3448 assert_eq!(camel_to_details_key("MapStateExited"), "stateExited");
3449 assert_eq!(camel_to_details_key("TaskScheduled"), "taskScheduled");
3451 assert_eq!(camel_to_details_key("ExecutionStarted"), "executionStarted");
3452 assert_eq!(camel_to_details_key(""), "");
3453 }
3454
3455 #[test]
3456 fn test_is_mutating_action() {
3457 assert!(is_mutating_action("CreateStateMachine"));
3458 assert!(is_mutating_action("StartExecution"));
3459 assert!(!is_mutating_action("DescribeStateMachine"));
3460 assert!(!is_mutating_action("ListStateMachines"));
3461 }
3462
3463 fn create_express_sm(svc: &StepFunctionsService, name: &str) -> String {
3466 let body = json!({
3467 "name": name,
3468 "definition": VALID_DEF,
3469 "roleArn": "arn:aws:iam::123456789012:role/test",
3470 "type": "EXPRESS",
3471 });
3472 let req = make_request("CreateStateMachine", &body.to_string());
3473 let resp = svc.create_state_machine(&req).unwrap();
3474 let b = body_json(&resp);
3475 b["stateMachineArn"].as_str().unwrap().to_string()
3476 }
3477
3478 #[tokio::test]
3479 async fn start_sync_execution_basic() {
3480 let svc = StepFunctionsService::new(make_state());
3481 let arn = create_express_sm(&svc, "sync-sm");
3482
3483 let body = json!({
3484 "stateMachineArn": arn,
3485 "input": r#"{"key":"value"}"#,
3486 });
3487 let req = make_request("StartSyncExecution", &body.to_string());
3488 let resp = svc.start_sync_execution(&req).await.unwrap();
3489 let b = body_json(&resp);
3490 assert!(b["executionArn"]
3491 .as_str()
3492 .unwrap()
3493 .contains("express:sync-sm"));
3494 assert_eq!(b["stateMachineArn"], arn);
3495 assert_eq!(b["status"], "SUCCEEDED");
3496 assert!(b["startDate"].as_f64().is_some());
3497 assert!(b["stopDate"].as_f64().is_some());
3498 assert!(b["output"].as_str().is_some());
3499 assert!(b["billingDetails"]["billedDurationInMilliseconds"]
3500 .as_i64()
3501 .is_some());
3502 }
3503
3504 #[tokio::test]
3505 async fn start_sync_execution_not_express() {
3506 let svc = StepFunctionsService::new(make_state());
3507 let arn = create_sm(&svc, "std-sm");
3508
3509 let body = json!({"stateMachineArn": arn});
3510 let req = make_request("StartSyncExecution", &body.to_string());
3511 let err = expect_err(svc.start_sync_execution(&req).await);
3512 assert!(err.to_string().contains("StateMachineTypeNotSupported"));
3513 }
3514
3515 #[tokio::test]
3516 async fn start_sync_execution_sm_not_found() {
3517 let svc = StepFunctionsService::new(make_state());
3518 let body = json!({
3519 "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:nope",
3520 });
3521 let req = make_request("StartSyncExecution", &body.to_string());
3522 let err = expect_err(svc.start_sync_execution(&req).await);
3523 assert!(err.to_string().contains("StateMachineDoesNotExist"));
3524 }
3525
3526 #[tokio::test]
3527 async fn start_sync_execution_records_introspection_fields() {
3528 let svc = StepFunctionsService::new(make_state());
3529 let arn = create_express_sm(&svc, "sync-introspect");
3530
3531 let body = json!({"stateMachineArn": arn, "input": "{}"});
3532 let req = make_request("StartSyncExecution", &body.to_string());
3533 let resp = svc.start_sync_execution(&req).await.unwrap();
3534 let b = body_json(&resp);
3535 let exec_arn = b["executionArn"].as_str().unwrap().to_string();
3536
3537 let accounts = svc.state.read();
3538 let state = accounts.regional("123456789012", "us-east-1").unwrap();
3539 let stored = state
3540 .executions
3541 .get(&exec_arn)
3542 .expect("sync execution should be persisted for introspection");
3543 assert!(stored.is_sync, "sync executions must be marked is_sync");
3544 assert_eq!(stored.billed_memory_mb, Some(64));
3545 assert!(
3546 stored.billed_duration_ms.is_some(),
3547 "billed_duration_ms must be populated after sync run"
3548 );
3549 assert!(
3550 stored.parent_execution_arn.is_none(),
3551 "top-level sync execution has no parent"
3552 );
3553 }
3554
3555 #[tokio::test]
3556 async fn start_sync_execution_invalid_input() {
3557 let svc = StepFunctionsService::new(make_state());
3558 let arn = create_express_sm(&svc, "bad-input-sync");
3559
3560 let body = json!({
3561 "stateMachineArn": arn,
3562 "input": "not json",
3563 });
3564 let req = make_request("StartSyncExecution", &body.to_string());
3565 let err = expect_err(svc.start_sync_execution(&req).await);
3566 assert!(err.to_string().contains("InvalidExecutionInput"));
3567 }
3568
3569 #[test]
3571 fn snapshot_hook_is_none_without_store() {
3572 let svc = StepFunctionsService::new(make_state());
3573 assert!(svc.snapshot_hook().is_none());
3574 }
3575
3576 #[tokio::test]
3580 async fn snapshot_hook_fires_with_store() {
3581 let store: Arc<dyn fakecloud_persistence::SnapshotStore> =
3582 Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
3583 let svc = StepFunctionsService::new(make_state()).with_snapshot_store(store);
3584 let hook = svc
3585 .snapshot_hook()
3586 .expect("hook present when a store is set");
3587 hook().await;
3588 }
3589
3590 fn make_execution(arn: &str, status: ExecutionStatus) -> Execution {
3591 Execution {
3592 execution_arn: arn.to_string(),
3593 state_machine_arn: "arn:aws:states:us-east-1:123456789012:stateMachine:sm".to_string(),
3594 state_machine_name: "sm".to_string(),
3595 name: arn.to_string(),
3596 status,
3597 input: None,
3598 output: None,
3599 start_date: Utc::now(),
3600 stop_date: None,
3601 error: None,
3602 cause: None,
3603 history_events: vec![],
3604 parent_execution_arn: None,
3605 is_sync: false,
3606 billed_duration_ms: None,
3607 billed_memory_mb: None,
3608 role_arn: String::new(),
3609 redrive_count: 0,
3610 }
3611 }
3612
3613 #[test]
3614 fn reconcile_aborts_running_executions_on_restart() {
3615 let state = make_state();
3619 {
3620 let mut accounts = state.write();
3621 let s = accounts.regional_mut("123456789012", "us-east-1");
3622 s.executions.insert(
3623 "running".into(),
3624 make_execution("running", ExecutionStatus::Running),
3625 );
3626 s.executions.insert(
3627 "done".into(),
3628 make_execution("done", ExecutionStatus::Succeeded),
3629 );
3630 }
3631
3632 let n = reconcile_interrupted_executions(&state);
3633 assert_eq!(n, 1, "only the RUNNING execution is reconciled");
3634
3635 let accounts = state.read();
3636 let s = accounts.regional("123456789012", "us-east-1").unwrap();
3637 let running = &s.executions["running"];
3638 assert_eq!(running.status, ExecutionStatus::Aborted);
3639 assert!(running.stop_date.is_some());
3640 assert_eq!(running.error.as_deref(), Some("Fakecloud.Restart"));
3641 assert_eq!(s.executions["done"].status, ExecutionStatus::Succeeded);
3642 }
3643
3644 #[tokio::test]
3647 async fn distributed_map_populates_map_run() {
3648 let state = make_state();
3649 let svc = StepFunctionsService::new(state.clone());
3650 let execution_arn =
3651 "arn:aws:states:us-east-1:123456789012:execution:dist-sm:exec-1".to_string();
3652
3653 let def = json!({
3654 "StartAt": "M",
3655 "States": {
3656 "M": {
3657 "Type": "Map",
3658 "ItemsPath": "$.items",
3659 "ItemProcessor": {
3660 "ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "STANDARD" },
3661 "StartAt": "Item",
3662 "States": { "Item": { "Type": "Pass", "End": true } }
3663 },
3664 "End": true
3665 }
3666 }
3667 });
3668
3669 interpreter::execute_state_machine(
3670 state.clone(),
3671 execution_arn.clone(),
3672 def.to_string(),
3673 Some(r#"{"items":[1,2,3]}"#.to_string()),
3674 None,
3675 None,
3676 None,
3677 None,
3678 )
3679 .await;
3680
3681 let req = make_request(
3683 "ListMapRuns",
3684 &json!({ "executionArn": execution_arn }).to_string(),
3685 );
3686 let listed = body_json(&svc.list_map_runs(&req).unwrap());
3687 let runs = listed["mapRuns"].as_array().unwrap();
3688 assert_eq!(
3689 runs.len(),
3690 1,
3691 "distributed map must create exactly one MapRun"
3692 );
3693 let map_run_arn = runs[0]["mapRunArn"].as_str().unwrap().to_string();
3694 assert!(map_run_arn.contains(":mapRun:"));
3695 assert_eq!(runs[0]["executionArn"], execution_arn);
3696
3697 let req = make_request(
3699 "DescribeMapRun",
3700 &json!({ "mapRunArn": map_run_arn }).to_string(),
3701 );
3702 let mr = body_json(&svc.describe_map_run(&req).unwrap());
3703 assert_eq!(mr["status"], "SUCCEEDED");
3704 assert_eq!(mr["itemCounts"]["total"], 3);
3705 assert_eq!(mr["itemCounts"]["succeeded"], 3);
3706 assert_eq!(mr["itemCounts"]["failed"], 0);
3707 assert_eq!(mr["executionCounts"]["succeeded"], 3);
3708 }
3709
3710 #[tokio::test]
3711 async fn inline_map_does_not_create_map_run() {
3712 let state = make_state();
3713 let svc = StepFunctionsService::new(state.clone());
3714 let execution_arn =
3715 "arn:aws:states:us-east-1:123456789012:execution:inline-sm:exec-1".to_string();
3716
3717 let def = json!({
3719 "StartAt": "M",
3720 "States": {
3721 "M": {
3722 "Type": "Map",
3723 "ItemsPath": "$.items",
3724 "ItemProcessor": {
3725 "StartAt": "Item",
3726 "States": { "Item": { "Type": "Pass", "End": true } }
3727 },
3728 "End": true
3729 }
3730 }
3731 });
3732
3733 interpreter::execute_state_machine(
3734 state.clone(),
3735 execution_arn.clone(),
3736 def.to_string(),
3737 Some(r#"{"items":[1,2]}"#.to_string()),
3738 None,
3739 None,
3740 None,
3741 None,
3742 )
3743 .await;
3744
3745 let req = make_request(
3746 "ListMapRuns",
3747 &json!({ "executionArn": execution_arn }).to_string(),
3748 );
3749 let listed = body_json(&svc.list_map_runs(&req).unwrap());
3750 assert!(listed["mapRuns"].as_array().unwrap().is_empty());
3751 }
3752}
3753
3754#[cfg(test)]
3755mod pagination_reject_test {
3756 #[test]
3757 fn paginate_checked_rejects_invalid_token() {
3758 use fakecloud_core::pagination::paginate_checked;
3759 let items: Vec<i32> = (0..5).collect();
3760 assert!(paginate_checked(&items, Some("bad"), 3).is_err());
3761 assert!(paginate_checked(&items, Some("2"), 3).is_ok());
3762 }
3763}