1
1
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
# SPDX-License-Identifier: Apache-2.0
3
3
4
- import pytest
5
- from unittest .mock import Mock , patch , MagicMock
6
- from botocore .exceptions import ClientError , WaiterError
4
+ """Unit tests for S3 batch operations module."""
5
+
7
6
import json
7
+ import pytest
8
+ from unittest .mock import Mock , patch
9
+ from botocore .exceptions import ClientError
8
10
9
11
from s3_batch import CloudFormationHelper , S3BatchScenario , setup_resources
10
12
@@ -20,7 +22,7 @@ def cfn_helper(self):
20
22
@patch ('boto3.client' )
21
23
def test_init (self , mock_boto3_client ):
22
24
"""Test CloudFormationHelper initialization."""
23
- helper = CloudFormationHelper ('us-east-1' )
25
+ CloudFormationHelper ('us-east-1' )
24
26
mock_boto3_client .assert_called_with ('cloudformation' , region_name = 'us-east-1' )
25
27
26
28
@patch ('boto3.client' )
@@ -29,14 +31,17 @@ def test_deploy_cloudformation_stack_success(self, mock_boto3_client, cfn_helper
29
31
mock_client = Mock ()
30
32
mock_boto3_client .return_value = mock_client
31
33
cfn_helper .cfn_client = mock_client
32
-
33
34
with patch .object (cfn_helper , '_wait_for_stack_completion' ):
34
35
cfn_helper .deploy_cloudformation_stack ('test-stack' )
35
-
36
36
mock_client .create_stack .assert_called_once ()
37
37
call_args = mock_client .create_stack .call_args
38
38
assert call_args [1 ]['StackName' ] == 'test-stack'
39
39
assert 'CAPABILITY_IAM' in call_args [1 ]['Capabilities' ]
40
+
41
+ # Verify the template includes AmazonS3FullAccess policy
42
+ template_body = json .loads (call_args [1 ]['TemplateBody' ])
43
+ assert 'ManagedPolicyArns' in template_body ['Resources' ]['S3BatchRole' ]['Properties' ]
44
+ assert 'arn:aws:iam::aws:policy/AmazonS3FullAccess' in template_body ['Resources' ]['S3BatchRole' ]['Properties' ]['ManagedPolicyArns' ]
40
45
41
46
@patch ('boto3.client' )
42
47
def test_deploy_cloudformation_stack_failure (self , mock_boto3_client , cfn_helper ):
@@ -48,7 +53,6 @@ def test_deploy_cloudformation_stack_failure(self, mock_boto3_client, cfn_helper
48
53
)
49
54
mock_boto3_client .return_value = mock_client
50
55
cfn_helper .cfn_client = mock_client
51
-
52
56
with pytest .raises (ClientError ):
53
57
cfn_helper .deploy_cloudformation_stack ('test-stack' )
54
58
@@ -61,7 +65,7 @@ def test_get_stack_outputs_success(self, mock_boto3_client, cfn_helper):
61
65
'Outputs' : [
62
66
{'OutputKey' : 'S3BatchRoleArn' , 'OutputValue' : 'arn:aws:iam::123456789012:role/test-role' }
63
67
]
64
- }]
68
+ }]
65
69
}
66
70
mock_boto3_client .return_value = mock_client
67
71
cfn_helper .cfn_client = mock_client
@@ -82,6 +86,7 @@ def test_destroy_cloudformation_stack_success(self, mock_boto3_client, cfn_helpe
82
86
mock_client .delete_stack .assert_called_once_with (StackName = 'test-stack' )
83
87
84
88
89
+
85
90
class TestS3BatchScenario :
86
91
"""Test cases for S3BatchScenario class."""
87
92
@@ -164,6 +169,10 @@ def test_create_s3_batch_job_success(self, mock_boto3_client, s3_scenario):
164
169
165
170
assert job_id == 'test-job-id'
166
171
mock_s3control_client .create_job .assert_called_once ()
172
+
173
+ # Verify ConfirmationRequired is set to False
174
+ call_args = mock_s3control_client .create_job .call_args
175
+ assert call_args [1 ]['ConfirmationRequired' ] is False
167
176
168
177
@patch ('boto3.client' )
169
178
def test_check_job_failure_reasons (self , mock_boto3_client , s3_scenario ):
@@ -193,18 +202,68 @@ def test_wait_for_job_ready_success(self, mock_sleep, mock_boto3_client, s3_scen
193
202
result = s3_scenario .wait_for_job_ready ('test-job-id' , '123456789012' )
194
203
195
204
assert result is True
205
+
206
+ @patch ('boto3.client' )
207
+ @patch ('time.sleep' )
208
+ def test_wait_for_job_ready_suspended (self , mock_sleep , mock_boto3_client , s3_scenario ):
209
+ """Test waiting for job with Suspended status."""
210
+ mock_s3control_client = Mock ()
211
+ mock_s3control_client .describe_job .return_value = {
212
+ 'Job' : {'Status' : 'Suspended' }
213
+ }
214
+ s3_scenario .s3control_client = mock_s3control_client
215
+
216
+ result = s3_scenario .wait_for_job_ready ('test-job-id' , '123456789012' )
217
+
218
+ assert result is True
196
219
197
220
@patch ('boto3.client' )
198
221
def test_update_job_priority_success (self , mock_boto3_client , s3_scenario ):
199
222
"""Test successful job priority update."""
200
223
mock_s3control_client = Mock ()
224
+ mock_s3control_client .describe_job .return_value = {
225
+ 'Job' : {'Status' : 'Suspended' }
226
+ }
201
227
s3_scenario .s3control_client = mock_s3control_client
202
228
203
- with patch .object (s3_scenario , 'wait_for_job_ready' , return_value = True ):
204
- s3_scenario .update_job_priority ('test-job-id' , '123456789012' )
229
+ s3_scenario .update_job_priority ('test-job-id' , '123456789012' )
205
230
206
231
mock_s3control_client .update_job_priority .assert_called_once ()
207
232
mock_s3control_client .update_job_status .assert_called_once ()
233
+
234
+ @patch ('boto3.client' )
235
+ def test_update_job_priority_with_ready_status (self , mock_boto3_client , s3_scenario ):
236
+ """Test job priority update with Ready status."""
237
+ mock_s3control_client = Mock ()
238
+ mock_s3control_client .describe_job .return_value = {
239
+ 'Job' : {'Status' : 'Ready' }
240
+ }
241
+ s3_scenario .s3control_client = mock_s3control_client
242
+
243
+ s3_scenario .update_job_priority ('test-job-id' , '123456789012' )
244
+
245
+ mock_s3control_client .update_job_priority .assert_called_once ()
246
+ mock_s3control_client .update_job_status .assert_called_once ()
247
+
248
+ @patch ('boto3.client' )
249
+ def test_update_job_priority_error_handling (self , mock_boto3_client , s3_scenario ):
250
+ """Test error handling in job priority update."""
251
+ mock_s3control_client = Mock ()
252
+ mock_s3control_client .describe_job .return_value = {
253
+ 'Job' : {'Status' : 'Suspended' }
254
+ }
255
+ mock_s3control_client .update_job_priority .side_effect = ClientError (
256
+ {'Error' : {'Code' : 'InvalidRequest' , 'Message' : 'Cannot update priority' }},
257
+ 'UpdateJobPriority'
258
+ )
259
+ mock_s3control_client .update_job_status = Mock ()
260
+ s3_scenario .s3control_client = mock_s3control_client
261
+
262
+ # Should not raise exception due to error handling
263
+ s3_scenario .update_job_priority ('test-job-id' , '123456789012' )
264
+
265
+ # Should still try to activate the job even if priority update fails
266
+ mock_s3control_client .update_job_status .assert_called_once ()
208
267
209
268
@patch ('boto3.client' )
210
269
def test_cleanup_resources (self , mock_boto3_client , s3_scenario ):
@@ -228,12 +287,14 @@ class TestUtilityFunctions:
228
287
@patch ('s3_batch.input' , return_value = 'c' )
229
288
def test_wait_for_input_valid (self , mock_input ):
230
289
"""Test wait_for_input with valid input."""
290
+ # pylint: disable=import-outside-toplevel
231
291
from s3_batch import wait_for_input
232
292
wait_for_input () # Should not raise exception
233
293
234
294
@patch ('s3_batch.input' , side_effect = ['invalid' , 'c' ])
235
295
def test_wait_for_input_invalid_then_valid (self , mock_input ):
236
296
"""Test wait_for_input with invalid then valid input."""
297
+ # pylint: disable=import-outside-toplevel
237
298
from s3_batch import wait_for_input
238
299
wait_for_input () # Should not raise exception
239
300
0 commit comments