@@ -429,6 +429,174 @@ describe('AmplifyGen2MigrationValidations', () => {
429429 name : 'StackStateError' ,
430430 message : 'Root stack status is ROLLBACK_COMPLETE, expected UPDATE_COMPLETE or CREATE_COMPLETE' ,
431431 resolution : 'Complete the deployment before proceeding.' ,
432+ describe ( 'validateStatefulResources - nested stacks' , ( ) => {
433+ let mockSend : jest . Mock ;
434+
435+ beforeEach ( ( ) => {
436+ mockSend = jest . fn ( ) ;
437+ ( CloudFormationClient as jest . Mock ) . mockImplementation ( ( ) => ( {
438+ send : mockSend ,
439+ } ) ) ;
440+ } ) ;
441+
442+ afterEach ( ( ) => {
443+ jest . clearAllMocks ( ) ;
444+ } ) ;
445+
446+ it ( 'should throw when nested stack contains stateful resources' , async ( ) => {
447+ mockSend . mockResolvedValueOnce ( {
448+ StackResources : [
449+ {
450+ ResourceType : 'AWS::DynamoDB::Table' ,
451+ PhysicalResourceId : 'MyTable' ,
452+ LogicalResourceId : 'Table' ,
453+ } ,
454+ ] ,
455+ } ) ;
456+
457+ const changeSet : DescribeChangeSetOutput = {
458+ Changes : [
459+ {
460+ Type : 'Resource' ,
461+ ResourceChange : {
462+ Action : 'Remove' ,
463+ ResourceType : 'AWS::CloudFormation::Stack' ,
464+ LogicalResourceId : 'AuthStack' ,
465+ PhysicalResourceId : 'auth-stack' ,
466+ } ,
467+ } ,
468+ ] ,
469+ } ;
470+
471+ await expect ( validations . validateStatefulResources ( changeSet ) ) . rejects . toMatchObject ( {
472+ name : 'DestructiveMigrationError' ,
473+ message :
474+ 'Stateful resources scheduled for deletion: AuthStack (AWS::CloudFormation::Stack) containing: Table (AWS::DynamoDB::Table).' ,
475+ } ) ;
476+ } ) ;
477+
478+ it ( 'should pass when nested stack contains only stateless resources' , async ( ) => {
479+ mockSend . mockResolvedValueOnce ( {
480+ StackResources : [
481+ {
482+ ResourceType : 'AWS::Lambda::Function' ,
483+ PhysicalResourceId : 'MyFunction' ,
484+ LogicalResourceId : 'Function' ,
485+ } ,
486+ ] ,
487+ } ) ;
488+
489+ const changeSet : DescribeChangeSetOutput = {
490+ Changes : [
491+ {
492+ Type : 'Resource' ,
493+ ResourceChange : {
494+ Action : 'Remove' ,
495+ ResourceType : 'AWS::CloudFormation::Stack' ,
496+ LogicalResourceId : 'LambdaStack' ,
497+ PhysicalResourceId : 'lambda-stack' ,
498+ } ,
499+ } ,
500+ ] ,
501+ } ;
502+
503+ await expect ( validations . validateStatefulResources ( changeSet ) ) . resolves . not . toThrow ( ) ;
504+ } ) ;
505+
506+ it ( 'should handle multiple levels of nested stacks' , async ( ) => {
507+ mockSend . mockResolvedValueOnce ( {
508+ StackResources : [
509+ {
510+ ResourceType : 'AWS::CloudFormation::Stack' ,
511+ PhysicalResourceId : 'storage-nested-stack' ,
512+ LogicalResourceId : 'StorageNestedStack' ,
513+ } ,
514+ ] ,
515+ } ) ;
516+
517+ mockSend . mockResolvedValueOnce ( {
518+ StackResources : [
519+ {
520+ ResourceType : 'AWS::S3::Bucket' ,
521+ PhysicalResourceId : 'my-bucket' ,
522+ LogicalResourceId : 'Bucket' ,
523+ } ,
524+ ] ,
525+ } ) ;
526+
527+ const changeSet : DescribeChangeSetOutput = {
528+ Changes : [
529+ {
530+ Type : 'Resource' ,
531+ ResourceChange : {
532+ Action : 'Remove' ,
533+ ResourceType : 'AWS::CloudFormation::Stack' ,
534+ LogicalResourceId : 'StorageStack' ,
535+ PhysicalResourceId : 'storage-stack' ,
536+ } ,
537+ } ,
538+ ] ,
539+ } ;
540+
541+ await expect ( validations . validateStatefulResources ( changeSet ) ) . rejects . toMatchObject ( {
542+ name : 'DestructiveMigrationError' ,
543+ } ) ;
544+ } ) ;
545+
546+ it ( 'should pass when nested stack is missing PhysicalResourceId' , async ( ) => {
547+ const changeSet : DescribeChangeSetOutput = {
548+ Changes : [
549+ {
550+ Type : 'Resource' ,
551+ ResourceChange : {
552+ Action : 'Remove' ,
553+ ResourceType : 'AWS::CloudFormation::Stack' ,
554+ LogicalResourceId : 'IncompleteStack' ,
555+ PhysicalResourceId : undefined ,
556+ } ,
557+ } ,
558+ ] ,
559+ } ;
560+
561+ await expect ( validations . validateStatefulResources ( changeSet ) ) . resolves . not . toThrow ( ) ;
562+ } ) ;
563+
564+ it ( 'should handle mixed direct and nested stateful resources' , async ( ) => {
565+ mockSend . mockResolvedValueOnce ( {
566+ StackResources : [
567+ {
568+ ResourceType : 'AWS::Cognito::UserPool' ,
569+ PhysicalResourceId : 'user-pool' ,
570+ LogicalResourceId : 'UserPool' ,
571+ } ,
572+ ] ,
573+ } ) ;
574+
575+ const changeSet : DescribeChangeSetOutput = {
576+ Changes : [
577+ {
578+ Type : 'Resource' ,
579+ ResourceChange : {
580+ Action : 'Remove' ,
581+ ResourceType : 'AWS::DynamoDB::Table' ,
582+ LogicalResourceId : 'DirectTable' ,
583+ } ,
584+ } ,
585+ {
586+ Type : 'Resource' ,
587+ ResourceChange : {
588+ Action : 'Remove' ,
589+ ResourceType : 'AWS::CloudFormation::Stack' ,
590+ LogicalResourceId : 'AuthStack' ,
591+ PhysicalResourceId : 'auth-stack' ,
592+ } ,
593+ } ,
594+ ] ,
595+ } ;
596+
597+ await expect ( validations . validateStatefulResources ( changeSet ) ) . rejects . toMatchObject ( {
598+ name : 'DestructiveMigrationError' ,
599+ message : expect . stringContaining ( 'DirectTable' ) ,
432600 } ) ;
433601 } ) ;
434602 } ) ;
0 commit comments