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