Skip to content

Commit 574188e

Browse files
aBurmeseDevrlhagerm
authored andcommitted
test: Add unit tests for S3 Batch Scenario Python
1 parent d225860 commit 574188e

File tree

2 files changed

+295
-0
lines changed

2 files changed

+295
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pytest>=7.0.0
2+
pytest-mock>=3.10.0
3+
boto3>=1.26.0
4+
botocore>=1.29.0
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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

Comments
 (0)