Skip to content

Commit 65684f5

Browse files
aBurmeseDevrlhagerm
authored andcommitted
Fix job activation and update tests
1 parent 574188e commit 65684f5

File tree

2 files changed

+125
-31
lines changed

2 files changed

+125
-31
lines changed

python/example_code/s3/scenarios/batch/s3_batch.py

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

4+
# snippet-start:[python.example_code.s3.S3Batch.scenario]
45
"""
56
This module provides functionality for AWS S3 Batch Operations.
67
It includes classes for managing CloudFormation stacks and S3 batch scenarios.
@@ -57,6 +58,9 @@ def deploy_cloudformation_stack(self, stack_name):
5758
}
5859
]
5960
},
61+
"ManagedPolicyArns": [
62+
"arn:aws:iam::aws:policy/AmazonS3FullAccess"
63+
],
6064
"Policies": [
6165
{
6266
"PolicyName": "S3BatchOperationsPolicy",
@@ -66,18 +70,8 @@ def deploy_cloudformation_stack(self, stack_name):
6670
{
6771
"Effect": "Allow",
6872
"Action": [
69-
"s3:GetObject",
70-
"s3:PutObject",
71-
"s3:PutObjectTagging",
72-
"s3:GetObjectTagging"
73-
],
74-
"Resource": "*"
75-
},
76-
{
77-
"Effect": "Allow",
78-
"Action": [
79-
"s3:GetObject",
80-
"s3:PutObject"
73+
"s3:*",
74+
"s3-object-lambda:*"
8175
],
8276
"Resource": "*"
8377
}
@@ -336,11 +330,12 @@ def create_s3_batch_job(self, account_id, role_arn, manifest_location,
336330
Priority=10,
337331
RoleArn=role_arn,
338332
Description='Batch job for tagging objects',
339-
ConfirmationRequired=True
333+
# Set to False to avoid confirmation requirement
334+
ConfirmationRequired=False
340335
)
341336
job_id = response['JobId']
342337
print(f"Created batch job with ID: {job_id}")
343-
print("Job requires confirmation before starting...")
338+
print("Job created and should start automatically")
344339
return job_id
345340
except ClientError as e:
346341
print(f"Error creating batch job: {e}")
@@ -403,6 +398,11 @@ def wait_for_job_ready(self, job_id, account_id, desired_status='Ready'):
403398
print(f"Current job status: {current_status}")
404399
if current_status == desired_status:
405400
return True
401+
# For jobs with ConfirmationRequired=True, they start in Suspended state
402+
# and need to be activated
403+
if current_status == 'Suspended':
404+
print("Job is in Suspended state, can proceed with activation")
405+
return True
406406
if current_status in ['Active', 'Failed', 'Cancelled', 'Complete']:
407407
print(f"Job is in {current_status} state, cannot update priority")
408408
if 'FailureReasons' in response['Job']:
@@ -431,21 +431,40 @@ def update_job_priority(self, job_id, account_id):
431431
ClientError: If updating job priority fails
432432
"""
433433
try:
434-
if self.wait_for_job_ready(job_id, account_id):
434+
# Check current job status
435+
response = self.s3control_client.describe_job(
436+
AccountId=account_id,
437+
JobId=job_id
438+
)
439+
current_status = response['Job']['Status']
440+
print(f"Current job status before update: {current_status}")
441+
print(f"Full job details: {response['Job']}")
442+
443+
# First try to update the job priority
444+
try:
435445
self.s3control_client.update_job_priority(
436446
AccountId=account_id,
437447
JobId=job_id,
438448
Priority=20
439449
)
440-
print(f"Updated priority for job {job_id}")
450+
print(f"Successfully updated priority for job {job_id}")
451+
except ClientError as e:
452+
print(f"Warning: Could not update job priority: {e}")
453+
# Continue anyway to try activating the job
454+
455+
# Then try to activate the job
456+
try:
441457
self.s3control_client.update_job_status(
442458
AccountId=account_id,
443459
JobId=job_id,
444460
RequestedJobStatus='Active'
445461
)
446-
print("Job confirmed and started")
447-
else:
448-
print("Could not update job priority as job is not in Ready state")
462+
print(f"Successfully activated job {job_id}")
463+
except ClientError as e:
464+
print(f"Error activating job: {e}")
465+
if 'Message' in str(e):
466+
print(f"Detailed error message: {e.response.get('Message', '')}")
467+
raise
449468
except ClientError as e:
450469
print(f"Error updating job priority: {e}")
451470
raise
@@ -579,8 +598,21 @@ def main():
579598
raise ValueError("Job failed, stopping execution")
580599

581600
wait_for_input()
582-
print("\n2. Updating job priority...")
583-
scenario.update_job_priority(job_id, account_id)
601+
print("\n2. Checking job status...")
602+
# Get current job status instead of trying to update priority
603+
response = scenario.s3control_client.describe_job(
604+
AccountId=account_id,
605+
JobId=job_id
606+
)
607+
current_status = response['Job']['Status']
608+
print(f"Current job status: {current_status}")
609+
610+
# Only try to update priority if job is not already active
611+
if current_status not in ['Active', 'Complete']:
612+
print("\nUpdating job priority...")
613+
scenario.update_job_priority(job_id, account_id)
614+
else:
615+
print("Job is already active or complete, no need to update priority.")
584616

585617
print("\nCleanup")
586618
if input(
@@ -599,3 +631,4 @@ def main():
599631

600632
if __name__ == "__main__":
601633
main()
634+
# snippet-end:[python.example_code.s3.S3Batch.scenario]

python/example_code/s3/scenarios/batch/test/test_s3_batch.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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+
76
import json
7+
import pytest
8+
from unittest.mock import Mock, patch
9+
from botocore.exceptions import ClientError
810

911
from 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+
8590
class 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

Comments
 (0)