1+ import fs from 'fs' ;
2+ import os from 'os' ;
3+ import path from 'path' ;
4+ import { DescribeOrganizationCommand , ListRootsCommand , OrganizationsClient } from '@aws-sdk/client-organizations' ;
5+ import { GetParameterCommand , SSMClient } from '@aws-sdk/client-ssm' ;
6+ import { ListInstancesCommand , SSOAdminClient } from '@aws-sdk/client-sso-admin' ;
7+ import { GetCallerIdentityCommand , STSClient } from '@aws-sdk/client-sts' ;
8+ import { mockClient } from 'aws-sdk-client-mock' ;
9+ import { Cli } from 'clipanion' ;
10+ import { Init } from '../../src/commands/init' ;
11+ import { LzaConfigValidate } from '../../src/commands/lza-config-validate' ;
12+ import { AWS_ACCELERATOR_INSTALLER_STACK_VERSION_SSM_PARAMETER_NAME , LZA_SOURCE_PATH , loadConfigSync } from '../../src/config' ;
13+ import { getCheckoutPath } from '../../src/core/accelerator/repository/checkout' ;
14+ import * as execModule from '../../src/core/util/exec' ;
15+
16+ describe ( 'LZA Config Validate command' , ( ) => {
17+ // Create mocks for AWS services (used during init rendering)
18+ const ssmMock = mockClient ( SSMClient ) ;
19+ const stsMock = mockClient ( STSClient ) ;
20+ const organizationsMock = mockClient ( OrganizationsClient ) ;
21+ const ssoAdminMock = mockClient ( SSOAdminClient ) ;
22+
23+ let testProjectDirectory = '' ;
24+ let execSpy : jest . SpyInstance ;
25+ const realExecute = execModule . executeCommand ;
26+
27+ beforeAll ( ( ) => {
28+ // Create a temporary directory for the test project
29+ testProjectDirectory = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'aws-luminarlz-cli-test-' ) ) ;
30+
31+ // Change working directory for test
32+ process . chdir ( testProjectDirectory ) ;
33+ } ) ;
34+
35+ beforeEach ( ( ) => {
36+ // Clear and reset mocks before each test
37+ ssmMock . reset ( ) ;
38+ stsMock . reset ( ) ;
39+ organizationsMock . reset ( ) ;
40+ ssoAdminMock . reset ( ) ;
41+ jest . clearAllMocks ( ) ;
42+
43+ // Set up executeCommand spy with passthrough. Intercept only cloning and building of the LZA repo
44+ // Do NOT intercept npx cdk synth
45+ execSpy = jest . spyOn ( execModule , 'executeCommand' ) . mockImplementation ( ( ( command : any , opts : any ) => {
46+ if ( typeof command === 'string' ) {
47+ if ( command . startsWith ( 'git clone ' ) ) {
48+ return Promise . resolve ( { stdout : '' , stderr : '' } as any ) as any ;
49+ }
50+ if ( command . includes ( 'yarn' ) ) {
51+ return Promise . resolve ( { stdout : '' , stderr : '' } as any ) as any ;
52+ }
53+ }
54+ return ( realExecute as any ) ( command , opts ) ;
55+ } ) as any ) ;
56+
57+ // Mock SSM parameter for AWS Accelerator version
58+ ssmMock . on ( GetParameterCommand ) . resolves ( {
59+ Parameter : {
60+ Name : AWS_ACCELERATOR_INSTALLER_STACK_VERSION_SSM_PARAMETER_NAME ,
61+ Value : '1.12.2' ,
62+ Type : 'String' ,
63+ } ,
64+ } ) ;
65+
66+ // Mock STS GetCallerIdentity
67+ stsMock . on ( GetCallerIdentityCommand ) . resolves ( {
68+ Account : '123456789012' ,
69+ Arn : 'arn:aws:iam::123456789012:role/Admin' ,
70+ UserId : 'AROAEXAMPLE123' ,
71+ } ) ;
72+
73+ // Mock Organizations DescribeOrganization
74+ organizationsMock . on ( DescribeOrganizationCommand ) . resolves ( {
75+ Organization : {
76+ Id : 'o-exampleorg' ,
77+ Arn : 'arn:aws:organizations::123456789012:organization/o-exampleorg' ,
78+ MasterAccountId : '123456789012' ,
79+ } ,
80+ } ) ;
81+
82+ // Mock Organizations ListRoots
83+ organizationsMock . on ( ListRootsCommand ) . resolves ( {
84+ Roots : [
85+ {
86+ Id : 'r-exampleroot' ,
87+ Arn : 'arn:aws:organizations::123456789012:root/o-exampleorg/r-exampleroot' ,
88+ Name : 'Root' ,
89+ } ,
90+ ] ,
91+ } ) ;
92+
93+ // Mock SSO Admin ListInstances
94+ ssoAdminMock . on ( ListInstancesCommand ) . resolves ( {
95+ Instances : [
96+ {
97+ InstanceArn : 'arn:aws:sso:::instance/ssoins-example' ,
98+ IdentityStoreId : 'd-example123' ,
99+ } ,
100+ ] ,
101+ } ) ;
102+ } ) ;
103+
104+ it ( 'should synthesize and validate after initializing a project with the specified blueprint' , async ( ) => {
105+ // Run the init command to set up the project
106+ const initCli = new Cli ( ) ;
107+ initCli . register ( Init ) ;
108+ const initExitCode = await initCli . run ( [
109+ 'init' ,
110+ '--blueprint' , 'foundational' ,
111+ '--accounts-root-email' , 'test@example.com' ,
112+ '--region' , 'us-east-1' ,
113+ '--force' ,
114+ ] ) ;
115+
116+ // Install dependencies after initialization
117+ await execModule . executeCommand ( 'npm install' , { cwd : testProjectDirectory } ) ;
118+
119+ // Verify init was successful
120+ expect ( initExitCode ) . toBe ( 0 ) ;
121+
122+ // Now create CLI instance with LzaConfigValidate command
123+ const validateCli = new Cli ( ) ;
124+ validateCli . register ( LzaConfigValidate ) ;
125+
126+ // Run the lza config validate command
127+ const validateExitCode = await validateCli . run ( [ 'lza' , 'config' , 'validate' ] ) ;
128+
129+ // Verify command was successful
130+ expect ( validateExitCode ) . toBe ( 0 ) ;
131+
132+ // Verify that the accelerator config output directory was created and contains files
133+ const config = loadConfigSync ( ) ;
134+ const outPath = path . join ( testProjectDirectory , config . awsAcceleratorConfigOutPath ) ;
135+ expect ( fs . existsSync ( outPath ) ) . toBe ( true ) ;
136+ const outFiles = fs . readdirSync ( outPath , { recursive : false } ) ;
137+ expect ( outFiles . length ) . toBeGreaterThan ( 0 ) ;
138+
139+ // Verify that cdk.out templates were copied into the output directory
140+ const cdkOutPath = path . join ( outPath , config . cdkOutPath ) ;
141+ expect ( fs . existsSync ( cdkOutPath ) ) . toBe ( true ) ;
142+ const cdkFiles = fs
143+ . readdirSync ( cdkOutPath , { recursive : true } )
144+ . filter ( ( f ) => f . toString ( ) . endsWith ( '.template.json' ) ) ;
145+ expect ( cdkFiles . length ) . toBeGreaterThan ( 0 ) ;
146+
147+ // Ensure executeCommand was called to run validate-config with correct parameters
148+ const expectedConfigDir = path . join ( testProjectDirectory , config . awsAcceleratorConfigOutPath ) ;
149+ const expectedCwd = path . join ( getCheckoutPath ( ) , LZA_SOURCE_PATH ) ;
150+ const validateCalls = execSpy . mock . calls . filter ( ( [ cmd ] ) => typeof cmd === 'string' && cmd . startsWith ( 'yarn validate-config' ) ) ;
151+ expect ( validateCalls . length ) . toBe ( 1 ) ;
152+ expect ( validateCalls [ 0 ] [ 0 ] ) . toBe ( `yarn validate-config ${ expectedConfigDir } ` ) ;
153+ expect ( validateCalls [ 0 ] [ 1 ] ?. cwd ) . toBe ( expectedCwd ) ;
154+
155+ // Ensure executeCommand was called to clone the repository and then build it
156+ const cloneCalls = execSpy . mock . calls . filter ( ( [ cmd ] ) => typeof cmd === 'string' && cmd . startsWith ( 'git clone ' ) ) ;
157+ expect ( cloneCalls . length ) . toBe ( 1 ) ;
158+
159+ const buildCalls = execSpy . mock . calls . filter ( ( [ cmd , opts ] ) => typeof cmd === 'string' && cmd . includes ( 'yarn' ) && cmd . includes ( 'build' ) && opts ?. cwd === expectedCwd ) ;
160+ expect ( buildCalls . length ) . toBe ( 1 ) ;
161+
162+ // Verify that yarn && yarn build was called after git clone
163+ const cloneIndex = execSpy . mock . calls . findIndex ( ( [ cmd ] ) => typeof cmd === 'string' && cmd . startsWith ( 'git clone ' ) ) ;
164+ const buildIndex = execSpy . mock . calls . findIndex ( ( [ cmd , opts ] ) => typeof cmd === 'string' && cmd . includes ( 'yarn' ) && cmd . includes ( 'build' ) && opts ?. cwd === expectedCwd ) ;
165+ expect ( cloneIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
166+ expect ( buildIndex ) . toBeGreaterThan ( cloneIndex ) ;
167+ } , 120 * 1000 ) ;
168+ } ) ;
0 commit comments