1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import pytest
5
+ from unittest .mock import Mock , patch , MagicMock
6
+ from botocore .exceptions import ClientError , WaiterError
7
+ import json
8
+
9
+ from s3_batch import CloudFormationHelper , S3BatchScenario , setup_resources
10
+
11
+
12
+ class TestCloudFormationHelper :
13
+ """Test cases for CloudFormationHelper class."""
14
+
15
+ @pytest .fixture
16
+ def cfn_helper (self ):
17
+ """Create CloudFormationHelper instance for testing."""
18
+ return CloudFormationHelper ('us-west-2' )
19
+
20
+ @patch ('boto3.client' )
21
+ def test_init (self , mock_boto3_client ):
22
+ """Test CloudFormationHelper initialization."""
23
+ helper = CloudFormationHelper ('us-east-1' )
24
+ mock_boto3_client .assert_called_with ('cloudformation' , region_name = 'us-east-1' )
25
+
26
+ @patch ('boto3.client' )
27
+ def test_deploy_cloudformation_stack_success (self , mock_boto3_client , cfn_helper ):
28
+ """Test successful CloudFormation stack deployment."""
29
+ mock_client = Mock ()
30
+ mock_boto3_client .return_value = mock_client
31
+ cfn_helper .cfn_client = mock_client
32
+
33
+ with patch .object (cfn_helper , '_wait_for_stack_completion' ):
34
+ cfn_helper .deploy_cloudformation_stack ('test-stack' )
35
+
36
+ mock_client .create_stack .assert_called_once ()
37
+ call_args = mock_client .create_stack .call_args
38
+ assert call_args [1 ]['StackName' ] == 'test-stack'
39
+ assert 'CAPABILITY_IAM' in call_args [1 ]['Capabilities' ]
40
+
41
+ @patch ('boto3.client' )
42
+ def test_deploy_cloudformation_stack_failure (self , mock_boto3_client , cfn_helper ):
43
+ """Test CloudFormation stack deployment failure."""
44
+ mock_client = Mock ()
45
+ mock_client .create_stack .side_effect = ClientError (
46
+ {'Error' : {'Code' : 'ValidationError' , 'Message' : 'Invalid template' }},
47
+ 'CreateStack'
48
+ )
49
+ mock_boto3_client .return_value = mock_client
50
+ cfn_helper .cfn_client = mock_client
51
+
52
+ with pytest .raises (ClientError ):
53
+ cfn_helper .deploy_cloudformation_stack ('test-stack' )
54
+
55
+ @patch ('boto3.client' )
56
+ def test_get_stack_outputs_success (self , mock_boto3_client , cfn_helper ):
57
+ """Test successful retrieval of stack outputs."""
58
+ mock_client = Mock ()
59
+ mock_client .describe_stacks .return_value = {
60
+ 'Stacks' : [{
61
+ 'Outputs' : [
62
+ {'OutputKey' : 'S3BatchRoleArn' , 'OutputValue' : 'arn:aws:iam::123456789012:role/test-role' }
63
+ ]
64
+ }]
65
+ }
66
+ mock_boto3_client .return_value = mock_client
67
+ cfn_helper .cfn_client = mock_client
68
+
69
+ outputs = cfn_helper .get_stack_outputs ('test-stack' )
70
+ assert outputs ['S3BatchRoleArn' ] == 'arn:aws:iam::123456789012:role/test-role'
71
+
72
+ @patch ('boto3.client' )
73
+ def test_destroy_cloudformation_stack_success (self , mock_boto3_client , cfn_helper ):
74
+ """Test successful CloudFormation stack deletion."""
75
+ mock_client = Mock ()
76
+ mock_boto3_client .return_value = mock_client
77
+ cfn_helper .cfn_client = mock_client
78
+
79
+ with patch .object (cfn_helper , '_wait_for_stack_completion' ):
80
+ cfn_helper .destroy_cloudformation_stack ('test-stack' )
81
+
82
+ mock_client .delete_stack .assert_called_once_with (StackName = 'test-stack' )
83
+
84
+
85
+ class TestS3BatchScenario :
86
+ """Test cases for S3BatchScenario class."""
87
+
88
+ @pytest .fixture
89
+ def s3_scenario (self ):
90
+ """Create S3BatchScenario instance for testing."""
91
+ return S3BatchScenario ('us-west-2' )
92
+
93
+ @patch ('boto3.client' )
94
+ def test_init (self , mock_boto3_client ):
95
+ """Test S3BatchScenario initialization."""
96
+ scenario = S3BatchScenario ('us-east-1' )
97
+ assert mock_boto3_client .call_count == 3
98
+ assert scenario .region_name == 'us-east-1'
99
+
100
+ @patch ('boto3.client' )
101
+ def test_get_account_id (self , mock_boto3_client , s3_scenario ):
102
+ """Test getting AWS account ID."""
103
+ mock_sts_client = Mock ()
104
+ mock_sts_client .get_caller_identity .return_value = {'Account' : '123456789012' }
105
+ s3_scenario .sts_client = mock_sts_client
106
+
107
+ account_id = s3_scenario .get_account_id ()
108
+ assert account_id == '123456789012'
109
+
110
+ @patch ('boto3.client' )
111
+ def test_create_bucket_us_west_2 (self , mock_boto3_client , s3_scenario ):
112
+ """Test bucket creation in us-west-2."""
113
+ mock_s3_client = Mock ()
114
+ s3_scenario .s3_client = mock_s3_client
115
+
116
+ s3_scenario .create_bucket ('test-bucket' )
117
+
118
+ mock_s3_client .create_bucket .assert_called_once_with (
119
+ Bucket = 'test-bucket' ,
120
+ CreateBucketConfiguration = {'LocationConstraint' : 'us-west-2' }
121
+ )
122
+
123
+ @patch ('boto3.client' )
124
+ def test_create_bucket_us_east_1 (self , mock_boto3_client ):
125
+ """Test bucket creation in us-east-1."""
126
+ scenario = S3BatchScenario ('us-east-1' )
127
+ mock_s3_client = Mock ()
128
+ scenario .s3_client = mock_s3_client
129
+
130
+ scenario .create_bucket ('test-bucket' )
131
+
132
+ mock_s3_client .create_bucket .assert_called_once_with (Bucket = 'test-bucket' )
133
+
134
+ @patch ('boto3.client' )
135
+ def test_upload_files_to_bucket (self , mock_boto3_client , s3_scenario ):
136
+ """Test uploading files to S3 bucket."""
137
+ mock_s3_client = Mock ()
138
+ mock_s3_client .put_object .return_value = {'ETag' : '"test-etag"' }
139
+ s3_scenario .s3_client = mock_s3_client
140
+
141
+ file_names = ['job-manifest.csv' , 'test-file.txt' ]
142
+ etag = s3_scenario .upload_files_to_bucket ('test-bucket' , file_names )
143
+
144
+ assert etag == 'test-etag'
145
+ assert mock_s3_client .put_object .call_count == 2
146
+
147
+ @patch ('boto3.client' )
148
+ def test_create_s3_batch_job_success (self , mock_boto3_client , s3_scenario ):
149
+ """Test successful S3 batch job creation."""
150
+ mock_s3_client = Mock ()
151
+ mock_s3_client .head_object .return_value = {'ETag' : '"test-etag"' }
152
+ mock_s3control_client = Mock ()
153
+ mock_s3control_client .create_job .return_value = {'JobId' : 'test-job-id' }
154
+
155
+ s3_scenario .s3_client = mock_s3_client
156
+ s3_scenario .s3control_client = mock_s3control_client
157
+
158
+ job_id = s3_scenario .create_s3_batch_job (
159
+ '123456789012' ,
160
+ 'arn:aws:iam::123456789012:role/test-role' ,
161
+ 'arn:aws:s3:::test-bucket/job-manifest.csv' ,
162
+ 'arn:aws:s3:::test-bucket'
163
+ )
164
+
165
+ assert job_id == 'test-job-id'
166
+ mock_s3control_client .create_job .assert_called_once ()
167
+
168
+ @patch ('boto3.client' )
169
+ def test_check_job_failure_reasons (self , mock_boto3_client , s3_scenario ):
170
+ """Test checking job failure reasons."""
171
+ mock_s3control_client = Mock ()
172
+ mock_s3control_client .describe_job .return_value = {
173
+ 'Job' : {
174
+ 'FailureReasons' : ['Reason 1' , 'Reason 2' ]
175
+ }
176
+ }
177
+ s3_scenario .s3control_client = mock_s3control_client
178
+
179
+ reasons = s3_scenario .check_job_failure_reasons ('test-job-id' , '123456789012' )
180
+
181
+ assert reasons == ['Reason 1' , 'Reason 2' ]
182
+
183
+ @patch ('boto3.client' )
184
+ @patch ('time.sleep' )
185
+ def test_wait_for_job_ready_success (self , mock_sleep , mock_boto3_client , s3_scenario ):
186
+ """Test waiting for job to become ready."""
187
+ mock_s3control_client = Mock ()
188
+ mock_s3control_client .describe_job .return_value = {
189
+ 'Job' : {'Status' : 'Ready' }
190
+ }
191
+ s3_scenario .s3control_client = mock_s3control_client
192
+
193
+ result = s3_scenario .wait_for_job_ready ('test-job-id' , '123456789012' )
194
+
195
+ assert result is True
196
+
197
+ @patch ('boto3.client' )
198
+ def test_update_job_priority_success (self , mock_boto3_client , s3_scenario ):
199
+ """Test successful job priority update."""
200
+ mock_s3control_client = Mock ()
201
+ s3_scenario .s3control_client = mock_s3control_client
202
+
203
+ with patch .object (s3_scenario , 'wait_for_job_ready' , return_value = True ):
204
+ s3_scenario .update_job_priority ('test-job-id' , '123456789012' )
205
+
206
+ mock_s3control_client .update_job_priority .assert_called_once ()
207
+ mock_s3control_client .update_job_status .assert_called_once ()
208
+
209
+ @patch ('boto3.client' )
210
+ def test_cleanup_resources (self , mock_boto3_client , s3_scenario ):
211
+ """Test resource cleanup."""
212
+ mock_s3_client = Mock ()
213
+ mock_s3_client .list_objects_v2 .return_value = {
214
+ 'Contents' : [{'Key' : 'batch-op-reports/report1.csv' }]
215
+ }
216
+ s3_scenario .s3_client = mock_s3_client
217
+
218
+ file_names = ['test-file.txt' ]
219
+ s3_scenario .cleanup_resources ('test-bucket' , file_names )
220
+
221
+ assert mock_s3_client .delete_object .call_count == 2 # file + report
222
+ mock_s3_client .delete_bucket .assert_called_once_with (Bucket = 'test-bucket' )
223
+
224
+
225
+ class TestUtilityFunctions :
226
+ """Test cases for utility functions."""
227
+
228
+ @patch ('s3_batch.input' , return_value = 'c' )
229
+ def test_wait_for_input_valid (self , mock_input ):
230
+ """Test wait_for_input with valid input."""
231
+ from s3_batch import wait_for_input
232
+ wait_for_input () # Should not raise exception
233
+
234
+ @patch ('s3_batch.input' , side_effect = ['invalid' , 'c' ])
235
+ def test_wait_for_input_invalid_then_valid (self , mock_input ):
236
+ """Test wait_for_input with invalid then valid input."""
237
+ from s3_batch import wait_for_input
238
+ wait_for_input () # Should not raise exception
239
+
240
+ def test_setup_resources (self ):
241
+ """Test setup_resources function."""
242
+ mock_scenario = Mock ()
243
+
244
+ manifest_location , report_bucket_arn = setup_resources (
245
+ mock_scenario , 'test-bucket' , ['file1.txt' , 'file2.txt' ]
246
+ )
247
+
248
+ assert manifest_location == 'arn:aws:s3:::test-bucket/job-manifest.csv'
249
+ assert report_bucket_arn == 'arn:aws:s3:::test-bucket'
250
+ mock_scenario .create_bucket .assert_called_once_with ('test-bucket' )
251
+ mock_scenario .upload_files_to_bucket .assert_called_once ()
252
+
253
+
254
+ class TestErrorHandling :
255
+ """Test cases for error handling scenarios."""
256
+
257
+ @pytest .fixture
258
+ def s3_scenario (self ):
259
+ """Create S3BatchScenario instance for testing."""
260
+ return S3BatchScenario ('us-west-2' )
261
+
262
+ @patch ('boto3.client' )
263
+ def test_create_bucket_client_error (self , mock_boto3_client , s3_scenario ):
264
+ """Test bucket creation with ClientError."""
265
+ mock_s3_client = Mock ()
266
+ mock_s3_client .create_bucket .side_effect = ClientError (
267
+ {'Error' : {'Code' : 'BucketAlreadyExists' , 'Message' : 'Bucket exists' }},
268
+ 'CreateBucket'
269
+ )
270
+ s3_scenario .s3_client = mock_s3_client
271
+
272
+ with pytest .raises (ClientError ):
273
+ s3_scenario .create_bucket ('test-bucket' )
274
+
275
+ @patch ('boto3.client' )
276
+ def test_create_s3_batch_job_client_error (self , mock_boto3_client , s3_scenario ):
277
+ """Test S3 batch job creation with ClientError."""
278
+ mock_s3_client = Mock ()
279
+ mock_s3_client .head_object .side_effect = ClientError (
280
+ {'Error' : {'Code' : 'NoSuchKey' , 'Message' : 'Key not found' }},
281
+ 'HeadObject'
282
+ )
283
+ s3_scenario .s3_client = mock_s3_client
284
+
285
+ with pytest .raises (ClientError ):
286
+ s3_scenario .create_s3_batch_job (
287
+ '123456789012' ,
288
+ 'arn:aws:iam::123456789012:role/test-role' ,
289
+ 'arn:aws:s3:::test-bucket/job-manifest.csv' ,
290
+ 'arn:aws:s3:::test-bucket'
291
+ )
0 commit comments