1+ const { SFNClient, TestStateCommand, DescribeStateMachineCommand, ListExecutionsCommand, GetExecutionHistoryCommand } = require ( '@aws-sdk/client-sfn' ) ;
2+ const { CloudFormationClient, ListStackResourcesCommand } = require ( '@aws-sdk/client-cloudformation' ) ;
3+ const { STSClient, GetCallerIdentityCommand } = require ( '@aws-sdk/client-sts' ) ;
4+ const { fromSSO } = require ( "@aws-sdk/credential-provider-sso" ) ;
5+ const samConfigParser = require ( '../../shared/samConfigParser' ) ;
6+ const parser = require ( '../../shared/parser' ) ;
7+ const fs = require ( 'fs' ) ;
8+ const inputUtil = require ( '../../shared/inputUtil' ) ;
9+ const clc = require ( "cli-color" ) ;
10+ const path = require ( 'path' ) ;
11+ const { Spinner } = require ( 'cli-spinner' ) ;
12+
13+ const os = require ( 'os' ) ;
14+ let clientParams ;
15+ async function run ( cmd ) {
16+ const config = await samConfigParser . parse ( ) ;
17+ const credentials = await fromSSO ( { profile : cmd . profile || config . profile || 'default' } ) ;
18+ clientParams = { credentials, region : cmd . region || config . region }
19+ const sfnClient = new SFNClient ( clientParams ) ;
20+ const cloudFormation = new CloudFormationClient ( clientParams ) ;
21+ const sts = new STSClient ( clientParams ) ;
22+ const template = await parser . findSAMTemplateFile ( process . cwd ( ) ) ;
23+ const templateContent = fs . readFileSync ( template , 'utf8' ) ;
24+ const templateObj = parser . parse ( "template" , templateContent ) ;
25+ const stateMachines = findAllStateMachines ( templateObj ) ;
26+ const stateMachine = stateMachines . length === 1 ? stateMachines [ 0 ] : await inputUtil . list ( "Select state machine" , stateMachines ) ;
27+
28+ const spinner = new Spinner ( `Fetching state machine ${ stateMachine } ... %s` ) ;
29+ spinner . setSpinnerString ( 30 ) ;
30+ spinner . start ( ) ;
31+
32+ const stackResources = await listAllStackResourcesWithPagination ( cloudFormation , cmd . stackName || config . stack_name ) ;
33+
34+ const stateMachineArn = stackResources . find ( r => r . LogicalResourceId === stateMachine ) . PhysicalResourceId ;
35+ const stateMachineRoleName = stackResources . find ( r => r . LogicalResourceId === `${ stateMachine } Role` ) . PhysicalResourceId ;
36+
37+ const describedStateMachine = await sfnClient . send ( new DescribeStateMachineCommand ( { stateMachineArn } ) ) ;
38+ const definition = JSON . parse ( describedStateMachine . definition ) ;
39+
40+ spinner . stop ( true ) ;
41+ const states = findStates ( definition ) ;
42+ const state = await inputUtil . autocomplete ( "Select state" , states . map ( s => { return { name : s . key , value : { name : s . key , state : s . state } } } ) ) ;
43+
44+ const input = await getInput ( stateMachineArn , state . name , describedStateMachine . type ) ;
45+
46+ const accountId = ( await sts . send ( new GetCallerIdentityCommand ( { } ) ) ) . Account ;
47+ console . log ( `Invoking state ${ clc . green ( state . name ) } with input:\n${ clc . green ( input ) } \n` ) ;
48+ const testResult = await sfnClient . send ( new TestStateCommand (
49+ {
50+ definition : JSON . stringify ( state . state ) ,
51+ roleArn : `arn:aws:iam::${ accountId } :role/${ stateMachineRoleName } ` ,
52+ input : input
53+ }
54+ ) ) ;
55+ delete testResult . $metadata ;
56+ let color = "green" ;
57+ if ( testResult . error ) {
58+ color = "red" ;
59+ }
60+ for ( const key in testResult ) {
61+ console . log ( `${ clc [ color ] ( key . charAt ( 0 ) . toUpperCase ( ) + key . slice ( 1 ) ) } : ${ testResult [ key ] } ` ) ;
62+ }
63+ }
64+
65+ async function getInput ( stateMachineArn , state , stateMachineType ) {
66+ let types = [
67+ "Empty JSON" ,
68+ "Manual input" ,
69+ "From file" ] ;
70+
71+ if ( stateMachineType === "STANDARD" ) {
72+ types . push ( "From recent execution" ) ;
73+ }
74+
75+ const configDirExists = fs . existsSync ( path . join ( os . homedir ( ) , '.samp-cli' , 'state-tests' ) ) ;
76+ if ( ! configDirExists ) {
77+ fs . mkdirSync ( path . join ( os . homedir ( ) , '.samp-cli' , 'state-tests' ) , { recursive : true } ) ;
78+ }
79+
80+ const stateMachineStateFileExists = fs . existsSync ( path . join ( os . homedir ( ) , '.samp-cli' , 'state-tests' , stateMachineArn ) ) ;
81+
82+ if ( ! stateMachineStateFileExists ) {
83+ fs . writeFileSync ( path . join ( os . homedir ( ) , '.samp-cli' , 'state-tests' , stateMachineArn ) , "{}" ) ;
84+ }
85+
86+ const storedState = JSON . parse ( fs . readFileSync ( path . join ( os . homedir ( ) , '.samp-cli' , 'state-tests' , stateMachineArn ) , "utf8" ) ) ;
87+ if ( Object . keys ( storedState ) . length > 0 ) {
88+ types = [ "Latest input" , ...types ] ;
89+ }
90+
91+ const type = await inputUtil . list ( "Select input type" , types ) ;
92+
93+ if ( type === "Empty JSON" ) {
94+ return "{}" ;
95+ }
96+
97+ if ( type === "Manual input" ) {
98+ return inputUtil . text ( "Enter input JSON" , "{}" ) ;
99+ }
100+
101+ if ( type === "From file" ) {
102+ const file = await inputUtil . file ( "Select input file" , "json" ) ;
103+ return fs . readFileSync ( file , "utf8" ) ;
104+ }
105+
106+ if ( type === "Latest input" ) {
107+ return JSON . stringify ( storedState [ state ] ) ;
108+ }
109+
110+ if ( type === "From recent execution" ) {
111+ const sfnClient = new SFNClient ( clientParams ) ;
112+
113+ const executions = await sfnClient . send ( new ListExecutionsCommand ( { stateMachineArn } ) ) ;
114+ const execution = await inputUtil . autocomplete ( "Select execution" , executions . executions . map ( e => { return { name : `[${ e . startDate . toLocaleTimeString ( ) } ] ${ e . name } ` , value : e . executionArn } } ) ) ;
115+ const executionHistory = await sfnClient . send ( new GetExecutionHistoryCommand ( { executionArn : execution } ) ) ;
116+ const input = findFirstTaskEnteredEvent ( executionHistory , state ) ;
117+ if ( ! input ) {
118+ console . log ( "No input found for state. Did it execute in the chosen execution?" ) ;
119+ process . exit ( 1 ) ;
120+ }
121+ return input . stateEnteredEventDetails . input ;
122+ }
123+ }
124+
125+ function findFirstTaskEnteredEvent ( jsonData , state ) {
126+ console . log ( "state" , state ) ;
127+ for ( const event of jsonData . events ) {
128+ if ( event . type . endsWith ( "StateEntered" ) && event . stateEnteredEventDetails . name === state ) {
129+ return event ;
130+ }
131+ }
132+ return null ; // or any appropriate default value
133+ }
134+
135+
136+ function findStates ( aslDefinition ) {
137+ const result = [ ] ;
138+
139+ function traverseStates ( states ) {
140+ Object . keys ( states ) . forEach ( key => {
141+ const state = states [ key ] ;
142+ if ( state . Type === 'Task' || state . Type === 'Pass' || state . Type === 'Choice' ) {
143+ result . push ( { key, state } ) ;
144+ }
145+ // Recursively search in Parallel and Map structures
146+ if ( state . Type === 'Parallel' && state . Branches ) {
147+ state . Branches . forEach ( branch => {
148+ traverseStates ( branch . States ) ;
149+ } ) ;
150+ }
151+ if ( state . Type === 'Map' && state . ItemProcessor && state . ItemProcessor . States ) {
152+ traverseStates ( state . ItemProcessor . States ) ;
153+ }
154+ } ) ;
155+ }
156+
157+ traverseStates ( aslDefinition . States ) ;
158+ return result ;
159+ }
160+
161+ function listAllStackResourcesWithPagination ( cloudFormation , stackName ) {
162+ const params = {
163+ StackName : stackName
164+ } ;
165+ const resources = [ ] ;
166+ const listStackResources = async ( params ) => {
167+ const response = await cloudFormation . send ( new ListStackResourcesCommand ( params ) ) ;
168+ resources . push ( ...response . StackResourceSummaries ) ;
169+ if ( response . NextToken ) {
170+ params . NextToken = response . NextToken ;
171+ await listStackResources ( params ) ;
172+ }
173+ } ;
174+
175+ return listStackResources ( params ) . then ( ( ) => resources ) ;
176+ }
177+
178+ function findAllStateMachines ( templateObj ) {
179+ const stateMachines = Object . keys ( templateObj . Resources ) . filter ( r => templateObj . Resources [ r ] . Type === "AWS::Serverless::StateMachine" ) ;
180+ if ( stateMachines . length === 0 ) {
181+ console . log ( "No state machines found in template" ) ;
182+ process . exit ( 0 ) ;
183+ }
184+
185+ return stateMachines ;
186+ }
187+
188+ module . exports = {
189+ run
190+ }
0 commit comments