@@ -65,7 +65,9 @@ describe("checkDestructiveChanges", () => {
6565 logicalId : "AlarmsAccountLambdaConcurrencyAlarm8AF49AD8" ,
6666 physicalId : "monitoring-Account_Lambda_Concurrency" ,
6767 resourceType : "AWS::CloudWatch::Alarm" ,
68- reason : "Replacement: True"
68+ policyAction : "ReplaceAndDelete" ,
69+ action : "Modify" ,
70+ replacement : "True"
6971 } )
7072 } )
7173
@@ -80,6 +82,7 @@ describe("checkDestructiveChanges", () => {
8082 Changes : [
8183 {
8284 ResourceChange : {
85+ PolicyAction : "Delete" ,
8386 LogicalResourceId : "ResourceToRemove" ,
8487 PhysicalResourceId : "physical-id" ,
8588 ResourceType : "AWS::S3::Bucket" ,
@@ -96,19 +99,82 @@ describe("checkDestructiveChanges", () => {
9699 logicalId : "ResourceToRemove" ,
97100 physicalId : "physical-id" ,
98101 resourceType : "AWS::S3::Bucket" ,
99- reason : "Action: Remove"
102+ policyAction : "Delete" ,
103+ action : "Remove" ,
104+ replacement : "False"
100105 }
101106 ] )
102107 } )
103108
104- test ( "ignores conditional CDK metadata replacements " , ( ) => {
109+ test ( "marks changes with Delete policy action as destructive even without removal " , ( ) => {
105110 const changeSet = {
106111 Changes : [
107112 {
108113 ResourceChange : {
109- LogicalResourceId : "CDKMetadata" ,
110- PhysicalResourceId : "metadata-id" ,
111- ResourceType : "AWS::CDK::Metadata" ,
114+ PolicyAction : "Delete" ,
115+ LogicalResourceId : "PolicyOnly" ,
116+ PhysicalResourceId : "policy-only" ,
117+ ResourceType : "Custom::Thing" ,
118+ Action : "Modify" ,
119+ Replacement : "False"
120+ }
121+ }
122+ ]
123+ }
124+
125+ const replacements = checkDestructiveChanges ( changeSet )
126+
127+ expect ( replacements ) . toEqual ( [
128+ {
129+ logicalId : "PolicyOnly" ,
130+ physicalId : "policy-only" ,
131+ resourceType : "Custom::Thing" ,
132+ policyAction : "Delete" ,
133+ action : "Modify" ,
134+ replacement : "False"
135+ }
136+ ] )
137+ } )
138+
139+ test ( "marks changes with ReplaceAndDelete policy action as destructive even when replacement is false" , ( ) => {
140+ const changeSet = {
141+ Changes : [
142+ {
143+ ResourceChange : {
144+ PolicyAction : "ReplaceAndDelete" ,
145+ LogicalResourceId : "PolicyReplace" ,
146+ PhysicalResourceId : "policy-replace" ,
147+ ResourceType : "Custom::Thing" ,
148+ Action : "Modify" ,
149+ Replacement : "False"
150+ }
151+ }
152+ ]
153+ }
154+
155+ const replacements = checkDestructiveChanges ( changeSet )
156+
157+ expect ( replacements ) . toEqual ( [
158+ {
159+ logicalId : "PolicyReplace" ,
160+ physicalId : "policy-replace" ,
161+ resourceType : "Custom::Thing" ,
162+ policyAction : "ReplaceAndDelete" ,
163+ action : "Modify" ,
164+ replacement : "False"
165+ }
166+ ] )
167+ } )
168+
169+ test ( "does not mark conditional replacements as destructive when no other indicator is present" , ( ) => {
170+ const changeSet = {
171+ Changes : [
172+ {
173+ ResourceChange : {
174+ LogicalResourceId : "Conditional" ,
175+ PhysicalResourceId : "conditional" ,
176+ ResourceType : "Custom::Thing" ,
177+ Action : "Modify" ,
112178 Replacement : "Conditional"
113179 }
114180 }
@@ -162,6 +228,7 @@ describe("checkDestructiveChangeSet", () => {
162228 LogicalResourceId : "ResourceToRemove" ,
163229 PhysicalResourceId : "physical-id" ,
164230 ResourceType : "AWS::S3::Bucket" ,
231+ PolicyAction : "Delete" ,
165232 Action : "Remove"
166233 }
167234 }
@@ -174,6 +241,9 @@ describe("checkDestructiveChangeSet", () => {
174241 LogicalResourceId : "ResourceToRemove" ,
175242 PhysicalResourceId : "physical-id" ,
176243 ResourceType : "AWS::S3::Bucket" ,
244+ PolicyAction : "Delete" ,
245+ Action : "Remove" ,
246+ Replacement : null ,
177247 ExpiryDate : "2026-03-01T00:00:00Z" ,
178248 StackName : "stack" ,
179249 AllowedReason : "Pending migration"
@@ -199,6 +269,7 @@ describe("checkDestructiveChangeSet", () => {
199269 LogicalResourceId : "ResourceToRemove" ,
200270 PhysicalResourceId : "physical-id" ,
201271 ResourceType : "AWS::S3::Bucket" ,
272+ PolicyAction : "Delete" ,
202273 Action : "Remove"
203274 }
204275 }
@@ -211,6 +282,9 @@ describe("checkDestructiveChangeSet", () => {
211282 LogicalResourceId : "ResourceToRemove" ,
212283 PhysicalResourceId : "physical-id" ,
213284 ResourceType : "AWS::S3::Bucket" ,
285+ PolicyAction : "Delete" ,
286+ Action : "Remove" ,
287+ Replacement : null ,
214288 ExpiryDate : "2026-02-01T00:00:00Z" ,
215289 StackName : "stack" ,
216290 AllowedReason : "Expired waiver"
@@ -224,4 +298,42 @@ describe("checkDestructiveChangeSet", () => {
224298 expect ( errorSpy ) . toHaveBeenCalledWith ( "Resources that require attention:" )
225299 expect ( logSpy ) . not . toHaveBeenCalledWith ( "Change set cs for stack stack has no destructive changes." )
226300 } )
301+
302+ test ( "does not allow waivers that mismatch policy or action" , async ( ) => {
303+ const changeSet = {
304+ CreationTime : "2026-02-20T11:54:17.083Z" ,
305+ StackName : "stack" ,
306+ Changes : [
307+ {
308+ ResourceChange : {
309+ LogicalResourceId : "ResourceToRemove" ,
310+ PhysicalResourceId : "physical-id" ,
311+ ResourceType : "AWS::S3::Bucket" ,
312+ PolicyAction : "Delete" ,
313+ Action : "Remove"
314+ }
315+ }
316+ ]
317+ }
318+ mockCloudFormationSend . mockResolvedValueOnce ( changeSet )
319+
320+ const allowedChanges : Array < AllowedDestructiveChange > = [
321+ {
322+ LogicalResourceId : "ResourceToRemove" ,
323+ PhysicalResourceId : "physical-id" ,
324+ ResourceType : "AWS::S3::Bucket" ,
325+ PolicyAction : "ReplaceAndDelete" ,
326+ Action : "Remove" ,
327+ Replacement : null ,
328+ ExpiryDate : "2026-03-01T00:00:00Z" ,
329+ StackName : "stack" ,
330+ AllowedReason : "Incorrect policy"
331+ }
332+ ]
333+
334+ await expect ( checkDestructiveChangeSet ( "cs" , "stack" , "eu-west-2" , allowedChanges ) )
335+ . rejects . toThrow ( "Change set cs contains destructive changes" )
336+
337+ expect ( errorSpy ) . toHaveBeenCalledWith ( "Resources that require attention:" )
338+ } )
227339} )
0 commit comments