1212class TestHandler (unittest .TestCase ):
1313 def setUp (self ):
1414 logger = logging .getLogger ()
15- logger .addHandler (logging .NullHandler ())
1615
1716 # clean up old aws.out file (from previous runs)
1817 try : os .remove ("aws.out" )
1918 except OSError : pass
2019
2120 def test_invalid_request (self ):
2221 resp = invoke_handler ("Create" , {}, expected_status = "FAILED" )
23- self .assertEqual (resp ["Reason" ], "missing request resource property 'SourceBucketName'" )
22+ self .assertEqual (resp ["Reason" ], "missing request resource property 'SourceBucketName'. props: {} " )
2423
2524 def test_create_update (self ):
2625 invoke_handler ("Create" , {
@@ -34,6 +33,20 @@ def test_create_update(self):
3433 "s3 sync --delete contents.zip s3://<dest-bucket-name>/"
3534 )
3635
36+ def test_create_with_backslash_prefix_same_as_no_prefix (self ):
37+ invoke_handler ("Create" , {
38+ "SourceBucketName" : "<source-bucket>" ,
39+ "SourceObjectKey" : "<source-object-key>" ,
40+ "DestinationBucketName" : "<dest-bucket-name>" ,
41+ "DestinationBucketKeyPrefix" : "/"
42+ })
43+
44+ self .assertAwsCommands (
45+ "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
46+ "s3 sync --delete contents.zip s3://<dest-bucket-name>/"
47+ )
48+
49+
3750 def test_create_update_with_dest_key (self ):
3851 invoke_handler ("Create" , {
3952 "SourceBucketName" : "<source-bucket>" ,
@@ -47,12 +60,13 @@ def test_create_update_with_dest_key(self):
4760 "s3 sync --delete contents.zip s3://<dest-bucket-name>/<dest-key-prefix>"
4861 )
4962
50- def test_delete (self ):
63+ def test_delete_no_retain (self ):
5164 invoke_handler ("Delete" , {
5265 "SourceBucketName" : "<source-bucket>" ,
5366 "SourceObjectKey" : "<source-object-key>" ,
54- "DestinationBucketName" : "<dest-bucket-name>"
55- })
67+ "DestinationBucketName" : "<dest-bucket-name>" ,
68+ "RetainOnDelete" : "false"
69+ }, physical_id = "<physicalid>" )
5670
5771 self .assertAwsCommands ("s3 rm s3://<dest-bucket-name>/ --recursive" )
5872
@@ -61,18 +75,30 @@ def test_delete_with_dest_key(self):
6175 "SourceBucketName" : "<source-bucket>" ,
6276 "SourceObjectKey" : "<source-object-key>" ,
6377 "DestinationBucketName" : "<dest-bucket-name>" ,
64- "DestinationBucketKeyPrefix" : "<dest-key-prefix>"
65- })
78+ "DestinationBucketKeyPrefix" : "<dest-key-prefix>" ,
79+ "RetainOnDelete" : "false"
80+ }, physical_id = "<physicalid>" )
6681
6782 self .assertAwsCommands ("s3 rm s3://<dest-bucket-name>/<dest-key-prefix> --recursive" )
6883
69- def test_delete_with_retain (self ):
84+ def test_delete_with_retain_explicit (self ):
7085 invoke_handler ("Delete" , {
7186 "SourceBucketName" : "<source-bucket>" ,
7287 "SourceObjectKey" : "<source-object-key>" ,
7388 "DestinationBucketName" : "<dest-bucket-name>" ,
7489 "RetainOnDelete" : "true"
75- })
90+ }, physical_id = "<physicalid>" )
91+
92+ # no aws commands (retain)
93+ self .assertAwsCommands ()
94+
95+ # RetainOnDelete=true is the default
96+ def test_delete_with_retain_implicit_default (self ):
97+ invoke_handler ("Delete" , {
98+ "SourceBucketName" : "<source-bucket>" ,
99+ "SourceObjectKey" : "<source-object-key>" ,
100+ "DestinationBucketName" : "<dest-bucket-name>"
101+ }, physical_id = "<physicalid>" )
76102
77103 # no aws commands (retain)
78104 self .assertAwsCommands ()
@@ -83,7 +109,7 @@ def test_delete_with_retain_explicitly_false(self):
83109 "SourceObjectKey" : "<source-object-key>" ,
84110 "DestinationBucketName" : "<dest-bucket-name>" ,
85111 "RetainOnDelete" : "false"
86- })
112+ }, physical_id = "<physicalid>" )
87113
88114 self .assertAwsCommands (
89115 "s3 rm s3://<dest-bucket-name>/ --recursive"
@@ -100,7 +126,7 @@ def test_update_same_dest(self):
100126 "DestinationBucketName" : "<dest-bucket-name>" ,
101127 }, old_resource_props = {
102128 "DestinationBucketName" : "<dest-bucket-name>" ,
103- })
129+ }, physical_id = "<physical-id>" )
104130
105131 self .assertAwsCommands (
106132 "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
@@ -115,62 +141,136 @@ def test_update_new_dest_retain(self):
115141 }, old_resource_props = {
116142 "DestinationBucketName" : "<dest-bucket-name>" ,
117143 "RetainOnDelete" : "true"
118- })
144+ }, physical_id = "<physical-id>" )
119145
120146 self .assertAwsCommands (
121147 "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
122148 "s3 sync --delete contents.zip s3://<dest-bucket-name>/"
123149 )
124150
125- def test_update_new_dest_no_retain_explicit (self ):
151+ def test_update_new_dest_no_retain (self ):
126152 invoke_handler ("Update" , {
127153 "SourceBucketName" : "<source-bucket>" ,
128154 "SourceObjectKey" : "<source-object-key>" ,
129155 "DestinationBucketName" : "<new-dest-bucket-name>" ,
156+ "RetainOnDelete" : "false"
130157 }, old_resource_props = {
131158 "DestinationBucketName" : "<old-dest-bucket-name>" ,
132159 "DestinationBucketKeyPrefix" : "<old-dest-prefix>" ,
133160 "RetainOnDelete" : "false"
134- })
161+ }, physical_id = "<physical-id>" )
135162
136163 self .assertAwsCommands (
137164 "s3 rm s3://<old-dest-bucket-name>/<old-dest-prefix> --recursive" ,
138165 "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
139166 "s3 sync --delete contents.zip s3://<new-dest-bucket-name>/"
140167 )
141168
142- def test_update_new_dest_no_retain_implicit (self ):
169+ def test_update_new_dest_retain_implicit (self ):
143170 invoke_handler ("Update" , {
144171 "SourceBucketName" : "<source-bucket>" ,
145172 "SourceObjectKey" : "<source-object-key>" ,
146173 "DestinationBucketName" : "<new-dest-bucket-name>" ,
147174 }, old_resource_props = {
148175 "DestinationBucketName" : "<old-dest-bucket-name>" ,
149176 "DestinationBucketKeyPrefix" : "<old-dest-prefix>"
150- })
177+ }, physical_id = "<physical-id>" )
151178
152179 self .assertAwsCommands (
153- "s3 rm s3://<old-dest-bucket-name>/<old-dest-prefix> --recursive" ,
154180 "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
155181 "s3 sync --delete contents.zip s3://<new-dest-bucket-name>/"
156182 )
157183
158- def test_update_new_dest_prefix_no_retain_implicit (self ):
184+ def test_update_new_dest_prefix_no_retain (self ):
159185 invoke_handler ("Update" , {
160186 "SourceBucketName" : "<source-bucket>" ,
161187 "SourceObjectKey" : "<source-object-key>" ,
162188 "DestinationBucketName" : "<dest-bucket-name>" ,
163- "DestinationBucketKeyPrefix" : "<new-dest-prefix>"
189+ "DestinationBucketKeyPrefix" : "<new-dest-prefix>" ,
190+ "RetainOnDelete" : "false"
164191 }, old_resource_props = {
165192 "DestinationBucketName" : "<dest-bucket-name>" ,
166- })
193+ "RetainOnDelete" : "false"
194+ }, physical_id = "<physical id>" )
167195
168196 self .assertAwsCommands (
169197 "s3 rm s3://<dest-bucket-name>/ --recursive" ,
170198 "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
171199 "s3 sync --delete contents.zip s3://<dest-bucket-name>/<new-dest-prefix>"
172200 )
173201
202+ def test_update_new_dest_prefix_retain_implicit (self ):
203+ invoke_handler ("Update" , {
204+ "SourceBucketName" : "<source-bucket>" ,
205+ "SourceObjectKey" : "<source-object-key>" ,
206+ "DestinationBucketName" : "<dest-bucket-name>" ,
207+ "DestinationBucketKeyPrefix" : "<new-dest-prefix>"
208+ }, old_resource_props = {
209+ "DestinationBucketName" : "<dest-bucket-name>" ,
210+ }, physical_id = "<physical id>" )
211+
212+ self .assertAwsCommands (
213+ "s3 cp s3://<source-bucket>/<source-object-key> archive.zip" ,
214+ "s3 sync --delete contents.zip s3://<dest-bucket-name>/<new-dest-prefix>"
215+ )
216+
217+ #
218+ # physical id
219+ #
220+
221+ def test_physical_id_allocated_on_create_and_reused_afterwards (self ):
222+ create_resp = invoke_handler ("Create" , {
223+ "SourceBucketName" : "<source-bucket>" ,
224+ "SourceObjectKey" : "<source-object-key>" ,
225+ "DestinationBucketName" : "<dest-bucket-name>" ,
226+ })
227+
228+ phid = create_resp ['PhysicalResourceId' ]
229+ self .assertTrue (phid .startswith ('aws.cdk.s3deployment' ))
230+
231+ # now issue an update and pass in the physical id. expect the same
232+ # one to be returned back
233+ update_resp = invoke_handler ("Update" , {
234+ "SourceBucketName" : "<source-bucket>" ,
235+ "SourceObjectKey" : "<source-object-key>" ,
236+ "DestinationBucketName" : "<new-dest-bucket-name>" ,
237+ }, old_resource_props = {
238+ "DestinationBucketName" : "<dest-bucket-name>" ,
239+ }, physical_id = phid )
240+ self .assertEqual (update_resp ['PhysicalResourceId' ], phid )
241+
242+ # now issue a delete, and make sure this also applies
243+ delete_resp = invoke_handler ("Delete" , {
244+ "SourceBucketName" : "<source-bucket>" ,
245+ "SourceObjectKey" : "<source-object-key>" ,
246+ "DestinationBucketName" : "<dest-bucket-name>" ,
247+ "RetainOnDelete" : "false"
248+ }, physical_id = phid )
249+ self .assertEqual (delete_resp ['PhysicalResourceId' ], phid )
250+
251+ def test_fails_when_physical_id_not_present_in_update (self ):
252+ update_resp = invoke_handler ("Update" , {
253+ "SourceBucketName" : "<source-bucket>" ,
254+ "SourceObjectKey" : "<source-object-key>" ,
255+ "DestinationBucketName" : "<new-dest-bucket-name>" ,
256+ }, old_resource_props = {
257+ "DestinationBucketName" : "<dest-bucket-name>" ,
258+ }, expected_status = "FAILED" )
259+
260+ self .assertEqual (update_resp ['Reason' ], "invalid request: request type is 'Update' but 'PhysicalResourceId' is not defined" )
261+
262+ def test_fails_when_physical_id_not_present_in_delete (self ):
263+ update_resp = invoke_handler ("Delete" , {
264+ "SourceBucketName" : "<source-bucket>" ,
265+ "SourceObjectKey" : "<source-object-key>" ,
266+ "DestinationBucketName" : "<new-dest-bucket-name>" ,
267+ }, old_resource_props = {
268+ "DestinationBucketName" : "<dest-bucket-name>" ,
269+ }, expected_status = "FAILED" )
270+
271+ self .assertEqual (update_resp ['Reason' ], "invalid request: request type is 'Delete' but 'PhysicalResourceId' is not defined" )
272+
273+
174274 # asserts that a given list of "aws xxx" commands have been invoked (in order)
175275 def assertAwsCommands (self , * expected ):
176276 actual = read_aws_out ()
@@ -193,7 +293,7 @@ def read_aws_out():
193293# requestType: CloudFormation request type ("Create", "Update", "Delete")
194294# resourceProps: map to pass to "ResourceProperties"
195295# expected_status: "SUCCESS" or "FAILED"
196- def invoke_handler (requestType , resourceProps , old_resource_props = None , expected_status = 'SUCCESS' ):
296+ def invoke_handler (requestType , resourceProps , old_resource_props = None , physical_id = None , expected_status = 'SUCCESS' ):
197297 response_url = '<response-url>'
198298
199299 event = {
@@ -208,6 +308,9 @@ def invoke_handler(requestType, resourceProps, old_resource_props=None, expected
208308 if old_resource_props :
209309 event ['OldResourceProperties' ] = old_resource_props
210310
311+ if physical_id :
312+ event ['PhysicalResourceId' ] = physical_id
313+
211314 class ContextMock : log_stream_name = 'log_stream'
212315 class ResponseMock : reason = 'OK'
213316
0 commit comments