11# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22# SPDX-License-Identifier: Apache-2.0
33
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+
76import json
7+ import pytest
8+ from unittest .mock import Mock , patch
9+ from botocore .exceptions import ClientError
810
911from s3_batch import CloudFormationHelper , S3BatchScenario , setup_resources
1012
@@ -20,7 +22,7 @@ def cfn_helper(self):
2022 @patch ('boto3.client' )
2123 def test_init (self , mock_boto3_client ):
2224 """Test CloudFormationHelper initialization."""
23- helper = CloudFormationHelper ('us-east-1' )
25+ CloudFormationHelper ('us-east-1' )
2426 mock_boto3_client .assert_called_with ('cloudformation' , region_name = 'us-east-1' )
2527
2628 @patch ('boto3.client' )
@@ -29,14 +31,17 @@ def test_deploy_cloudformation_stack_success(self, mock_boto3_client, cfn_helper
2931 mock_client = Mock ()
3032 mock_boto3_client .return_value = mock_client
3133 cfn_helper .cfn_client = mock_client
32-
3334 with patch .object (cfn_helper , '_wait_for_stack_completion' ):
3435 cfn_helper .deploy_cloudformation_stack ('test-stack' )
35-
3636 mock_client .create_stack .assert_called_once ()
3737 call_args = mock_client .create_stack .call_args
3838 assert call_args [1 ]['StackName' ] == 'test-stack'
3939 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' ]
4045
4146 @patch ('boto3.client' )
4247 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
4853 )
4954 mock_boto3_client .return_value = mock_client
5055 cfn_helper .cfn_client = mock_client
51-
5256 with pytest .raises (ClientError ):
5357 cfn_helper .deploy_cloudformation_stack ('test-stack' )
5458
@@ -61,7 +65,7 @@ def test_get_stack_outputs_success(self, mock_boto3_client, cfn_helper):
6165 'Outputs' : [
6266 {'OutputKey' : 'S3BatchRoleArn' , 'OutputValue' : 'arn:aws:iam::123456789012:role/test-role' }
6367 ]
64- }]
68+ }]
6569 }
6670 mock_boto3_client .return_value = mock_client
6771 cfn_helper .cfn_client = mock_client
@@ -82,6 +86,7 @@ def test_destroy_cloudformation_stack_success(self, mock_boto3_client, cfn_helpe
8286 mock_client .delete_stack .assert_called_once_with (StackName = 'test-stack' )
8387
8488
89+
8590class TestS3BatchScenario :
8691 """Test cases for S3BatchScenario class."""
8792
@@ -164,6 +169,10 @@ def test_create_s3_batch_job_success(self, mock_boto3_client, s3_scenario):
164169
165170 assert job_id == 'test-job-id'
166171 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
167176
168177 @patch ('boto3.client' )
169178 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
193202 result = s3_scenario .wait_for_job_ready ('test-job-id' , '123456789012' )
194203
195204 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
196219
197220 @patch ('boto3.client' )
198221 def test_update_job_priority_success (self , mock_boto3_client , s3_scenario ):
199222 """Test successful job priority update."""
200223 mock_s3control_client = Mock ()
224+ mock_s3control_client .describe_job .return_value = {
225+ 'Job' : {'Status' : 'Suspended' }
226+ }
201227 s3_scenario .s3control_client = mock_s3control_client
202228
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' )
205230
206231 mock_s3control_client .update_job_priority .assert_called_once ()
207232 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 ()
208267
209268 @patch ('boto3.client' )
210269 def test_cleanup_resources (self , mock_boto3_client , s3_scenario ):
@@ -228,12 +287,14 @@ class TestUtilityFunctions:
228287 @patch ('s3_batch.input' , return_value = 'c' )
229288 def test_wait_for_input_valid (self , mock_input ):
230289 """Test wait_for_input with valid input."""
290+ # pylint: disable=import-outside-toplevel
231291 from s3_batch import wait_for_input
232292 wait_for_input () # Should not raise exception
233293
234294 @patch ('s3_batch.input' , side_effect = ['invalid' , 'c' ])
235295 def test_wait_for_input_invalid_then_valid (self , mock_input ):
236296 """Test wait_for_input with invalid then valid input."""
297+ # pylint: disable=import-outside-toplevel
237298 from s3_batch import wait_for_input
238299 wait_for_input () # Should not raise exception
239300
0 commit comments