python-boto3/tests/functional/test_s3.py

562 lines
19 KiB
Python
Raw Permalink Normal View History

2015-11-27 23:25:33 +01:00
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
2021-09-22 18:34:33 +02:00
# https://aws.amazon.com/apache2.0/
2015-11-27 23:25:33 +01:00
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
2022-12-12 18:00:59 +01:00
import io
2018-07-11 07:39:36 +02:00
import botocore
2016-11-09 01:23:44 +01:00
import botocore.stub
2022-05-26 01:13:54 +02:00
import pytest
2018-07-11 07:39:36 +02:00
from botocore.config import Config
2016-05-22 04:03:29 +02:00
from botocore.stub import Stubber
2015-11-27 23:25:33 +01:00
import boto3.session
2016-11-09 01:23:44 +01:00
from boto3.s3.transfer import TransferConfig
2022-05-26 01:13:54 +02:00
from tests import unittest
2015-11-27 23:25:33 +01:00
class TestS3MethodInjection(unittest.TestCase):
def test_transfer_methods_injected_to_client(self):
session = boto3.session.Session(region_name='us-west-2')
client = session.client('s3')
2021-10-04 19:51:32 +02:00
assert hasattr(client, 'upload_file')
assert hasattr(client, 'download_file')
assert hasattr(client, 'copy')
2015-11-27 23:25:33 +01:00
def test_bucket_resource_has_load_method(self):
session = boto3.session.Session(region_name='us-west-2')
bucket = session.resource('s3').Bucket('fakebucket')
2021-10-04 19:51:32 +02:00
assert hasattr(bucket, 'load')
2015-11-27 23:25:33 +01:00
def test_transfer_methods_injected_to_bucket(self):
bucket = boto3.resource('s3').Bucket('my_bucket')
2021-10-04 19:51:32 +02:00
assert hasattr(bucket, 'upload_file')
assert hasattr(bucket, 'download_file')
assert hasattr(bucket, 'copy')
2015-11-27 23:25:33 +01:00
def test_transfer_methods_injected_to_object(self):
obj = boto3.resource('s3').Object('my_bucket', 'my_key')
2021-10-04 19:51:32 +02:00
assert hasattr(obj, 'upload_file')
assert hasattr(obj, 'download_file')
assert hasattr(obj, 'copy')
2016-11-09 01:23:44 +01:00
class BaseTransferTest(unittest.TestCase):
def setUp(self):
self.session = boto3.session.Session(
2022-05-26 01:13:54 +02:00
aws_access_key_id='foo',
aws_secret_access_key='bar',
region_name='us-west-2',
)
2016-11-09 01:23:44 +01:00
self.s3 = self.session.resource('s3')
self.stubber = Stubber(self.s3.meta.client)
self.bucket = 'mybucket'
self.key = 'mykey'
self.upload_id = 'uploadid'
self.etag = '"example0etag"'
self.progress = 0
self.progress_times_called = 0
def stub_head(self, content_length=4, expected_params=None):
head_response = {
'AcceptRanges': 'bytes',
'ContentLength': content_length,
'ContentType': 'binary/octet-stream',
'ETag': self.etag,
'Metadata': {},
'ResponseMetadata': {
'HTTPStatusCode': 200,
2022-05-26 01:13:54 +02:00
},
2016-11-09 01:23:44 +01:00
}
if expected_params is None:
2022-05-26 01:13:54 +02:00
expected_params = {'Bucket': self.bucket, 'Key': self.key}
2016-11-09 01:23:44 +01:00
self.stubber.add_response(
2022-05-26 01:13:54 +02:00
method='head_object',
service_response=head_response,
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def stub_create_multipart_upload(self):
# Add the response and assert params for CreateMultipartUpload
create_upload_response = {
"Bucket": self.bucket,
"Key": self.key,
2022-05-26 01:13:54 +02:00
"UploadId": self.upload_id,
2016-11-09 01:23:44 +01:00
}
expected_params = {
"Bucket": self.bucket,
"Key": self.key,
}
self.stubber.add_response(
method='create_multipart_upload',
service_response=create_upload_response,
2022-05-26 01:13:54 +02:00
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def stub_complete_multipart_upload(self, parts):
complete_upload_response = {
"Location": "us-west-2",
"Bucket": self.bucket,
"Key": self.key,
2022-05-26 01:13:54 +02:00
"ETag": self.etag,
2016-11-09 01:23:44 +01:00
}
expected_params = {
"Bucket": self.bucket,
"Key": self.key,
2022-05-26 01:13:54 +02:00
"MultipartUpload": {"Parts": parts},
"UploadId": self.upload_id,
2016-11-09 01:23:44 +01:00
}
self.stubber.add_response(
method='complete_multipart_upload',
service_response=complete_upload_response,
2022-05-26 01:13:54 +02:00
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
class TestCopy(BaseTransferTest):
def setUp(self):
2022-05-26 01:13:54 +02:00
super().setUp()
2016-11-09 01:23:44 +01:00
self.copy_source = {'Bucket': 'foo', 'Key': 'bar'}
def stub_single_part_copy(self):
self.stub_head(expected_params=self.copy_source)
self.stub_copy_object()
def stub_multipart_copy(self, part_size, num_parts):
# Set the HEAD to return the total size
total_size = part_size * num_parts
self.stub_head(
2022-05-26 01:13:54 +02:00
content_length=total_size, expected_params=self.copy_source
)
2016-11-09 01:23:44 +01:00
self.stub_create_multipart_upload()
# Add the responses for each UploadPartCopy
parts = []
for i in range(num_parts):
# Fill in the parts
part_number = i + 1
2022-05-26 01:13:54 +02:00
copy_range = "bytes={}-{}".format(
i * part_size, i * part_size + (part_size - 1)
2016-11-09 01:23:44 +01:00
)
self.stub_copy_part(part_number=part_number, copy_range=copy_range)
parts.append({'ETag': self.etag, 'PartNumber': part_number})
self.stub_complete_multipart_upload(parts)
def stub_copy_object(self):
copy_response = {
2022-05-26 01:13:54 +02:00
'CopyObjectResult': {'ETag': self.etag},
'ResponseMetadata': {'HTTPStatusCode': 200},
2016-11-09 01:23:44 +01:00
}
expected_params = {
"Bucket": self.bucket,
"Key": self.key,
2022-05-26 01:13:54 +02:00
"CopySource": self.copy_source,
2016-11-09 01:23:44 +01:00
}
self.stubber.add_response(
2022-05-26 01:13:54 +02:00
method='copy_object',
service_response=copy_response,
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def stub_copy_part(self, part_number, copy_range):
copy_part_response = {
2022-05-26 01:13:54 +02:00
"CopyPartResult": {"ETag": self.etag},
'ResponseMetadata': {'HTTPStatusCode': 200},
2016-11-09 01:23:44 +01:00
}
expected_params = {
"Bucket": self.bucket,
"Key": self.key,
"CopySource": self.copy_source,
"UploadId": self.upload_id,
"PartNumber": part_number,
2022-05-26 01:13:54 +02:00
"CopySourceRange": copy_range,
2016-11-09 01:23:44 +01:00
}
self.stubber.add_response(
2022-05-26 01:13:54 +02:00
method='upload_part_copy',
service_response=copy_part_response,
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def test_client_copy(self):
self.stub_single_part_copy()
with self.stubber:
response = self.s3.meta.client.copy(
2022-05-26 01:13:54 +02:00
self.copy_source, self.bucket, self.key
)
2016-11-09 01:23:44 +01:00
# The response will be none on a successful transfer.
2021-10-04 19:51:32 +02:00
assert response is None
2016-11-09 01:23:44 +01:00
def test_bucket_copy(self):
self.stub_single_part_copy()
bucket = self.s3.Bucket(self.bucket)
with self.stubber:
response = bucket.copy(self.copy_source, self.key)
# The response will be none on a successful transfer.
2021-10-04 19:51:32 +02:00
assert response is None
2016-11-09 01:23:44 +01:00
def test_object_copy(self):
self.stub_single_part_copy()
obj = self.s3.Object(self.bucket, self.key)
with self.stubber:
response = obj.copy(self.copy_source)
2021-10-04 19:51:32 +02:00
assert response is None
2016-11-09 01:23:44 +01:00
def test_copy_progress(self):
2022-05-26 01:13:54 +02:00
chunksize = 8 * (1024**2)
2016-11-09 01:23:44 +01:00
self.stub_multipart_copy(chunksize, 3)
transfer_config = TransferConfig(
2022-05-26 01:13:54 +02:00
multipart_chunksize=chunksize,
multipart_threshold=1,
max_concurrency=1,
)
2016-11-09 01:23:44 +01:00
def progress_callback(amount):
self.progress += amount
self.progress_times_called += 1
with self.stubber:
self.s3.meta.client.copy(
2022-05-26 01:13:54 +02:00
Bucket=self.bucket,
Key=self.key,
CopySource=self.copy_source,
Config=transfer_config,
Callback=progress_callback,
)
2016-11-09 01:23:44 +01:00
# Assert that the progress callback was called the correct number of
# times with the correct amounts.
2021-10-04 19:51:32 +02:00
assert self.progress_times_called == 3
assert self.progress == chunksize * 3
2016-11-09 01:23:44 +01:00
class TestUploadFileobj(BaseTransferTest):
def setUp(self):
2022-05-26 01:13:54 +02:00
super().setUp()
2022-12-12 18:00:59 +01:00
self.contents = io.BytesIO(b'foo\n')
2016-11-09 01:23:44 +01:00
def stub_put_object(self):
put_object_response = {
"ETag": self.etag,
2022-05-26 01:13:54 +02:00
"ResponseMetadata": {"HTTPStatusCode": 200},
2016-11-09 01:23:44 +01:00
}
expected_params = {
"Bucket": self.bucket,
"Key": self.key,
2022-05-26 01:13:54 +02:00
"Body": botocore.stub.ANY,
2016-11-09 01:23:44 +01:00
}
self.stubber.add_response(
2022-05-26 01:13:54 +02:00
method='put_object',
service_response=put_object_response,
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def stub_upload_part(self, part_number):
upload_part_response = {
'ETag': self.etag,
2022-05-26 01:13:54 +02:00
'ResponseMetadata': {'HTTPStatusCode': 200},
2016-11-09 01:23:44 +01:00
}
expected_params = {
"Bucket": self.bucket,
"Key": self.key,
"Body": botocore.stub.ANY,
"PartNumber": part_number,
2022-05-26 01:13:54 +02:00
"UploadId": self.upload_id,
2016-11-09 01:23:44 +01:00
}
self.stubber.add_response(
2022-05-26 01:13:54 +02:00
method='upload_part',
service_response=upload_part_response,
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def stub_multipart_upload(self, num_parts):
self.stub_create_multipart_upload()
# Add the responses for each UploadPartCopy
parts = []
for i in range(num_parts):
# Fill in the parts
part_number = i + 1
self.stub_upload_part(part_number=part_number)
parts.append({'ETag': self.etag, 'PartNumber': part_number})
self.stub_complete_multipart_upload(parts)
def test_client_upload(self):
self.stub_put_object()
with self.stubber:
# The stubber will assert that all the right parameters are called.
self.s3.meta.client.upload_fileobj(
2022-05-26 01:13:54 +02:00
Fileobj=self.contents, Bucket=self.bucket, Key=self.key
)
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
def test_raises_value_error_on_invalid_fileobj(self):
with self.stubber:
2021-10-04 19:51:32 +02:00
with pytest.raises(ValueError):
2016-11-09 01:23:44 +01:00
self.s3.meta.client.upload_fileobj(
2022-05-26 01:13:54 +02:00
Fileobj='foo', Bucket=self.bucket, Key=self.key
)
2016-11-09 01:23:44 +01:00
def test_bucket_upload(self):
self.stub_put_object()
bucket = self.s3.Bucket(self.bucket)
with self.stubber:
# The stubber will assert that all the right parameters are called.
bucket.upload_fileobj(Fileobj=self.contents, Key=self.key)
self.stubber.assert_no_pending_responses()
def test_object_upload(self):
self.stub_put_object()
obj = self.s3.Object(self.bucket, self.key)
with self.stubber:
# The stubber will assert that all the right parameters are called.
obj.upload_fileobj(Fileobj=self.contents)
self.stubber.assert_no_pending_responses()
def test_multipart_upload(self):
2022-05-26 01:13:54 +02:00
chunksize = 8 * (1024**2)
2022-12-12 18:00:59 +01:00
contents = io.BytesIO(b'0' * (chunksize * 3))
2016-11-09 01:23:44 +01:00
self.stub_multipart_upload(num_parts=3)
transfer_config = TransferConfig(
2022-05-26 01:13:54 +02:00
multipart_chunksize=chunksize,
multipart_threshold=1,
max_concurrency=1,
)
2016-11-09 01:23:44 +01:00
with self.stubber:
# The stubber will assert that all the right parameters are called.
self.s3.meta.client.upload_fileobj(
2022-05-26 01:13:54 +02:00
Fileobj=contents,
Bucket=self.bucket,
Key=self.key,
Config=transfer_config,
)
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
class TestDownloadFileobj(BaseTransferTest):
def setUp(self):
2022-05-26 01:13:54 +02:00
super().setUp()
2016-11-09 01:23:44 +01:00
self.contents = b'foo'
2022-12-12 18:00:59 +01:00
self.fileobj = io.BytesIO()
2016-11-09 01:23:44 +01:00
def stub_single_part_download(self):
self.stub_head(content_length=len(self.contents))
self.stub_get_object(self.contents)
def stub_get_object(self, full_contents, start_byte=0, end_byte=None):
"""
Stubs out the get_object operation.
:param full_contents: The FULL contents of the object
:param start_byte: The first byte to grab.
:param end_byte: The last byte to grab.
"""
get_object_response = {}
expected_params = {}
contents = full_contents
end_byte_range = end_byte
# If the start byte is set and the end byte is not, the end byte is
# the last byte.
if start_byte != 0 and end_byte is None:
end_byte = len(full_contents) - 1
# The range on get object where the the end byte is the last byte
# should set the input range as e.g. Range='bytes=3-'
if end_byte == len(full_contents) - 1:
end_byte_range = ''
# If this is a ranged get, ContentRange needs to be returned,
# contents needs to be pruned, and Range needs to be an expected param.
if end_byte is not None:
2022-05-26 01:13:54 +02:00
contents = full_contents[start_byte : end_byte + 1]
part_range = f'bytes={start_byte}-{end_byte_range}'
content_range = 'bytes={}-{}/{}'.format(
start_byte, end_byte, len(full_contents)
)
2016-11-09 01:23:44 +01:00
get_object_response['ContentRange'] = content_range
expected_params['Range'] = part_range
2022-05-26 01:13:54 +02:00
get_object_response.update(
{
"AcceptRanges": "bytes",
"ETag": self.etag,
"ContentLength": len(contents),
"ContentType": "binary/octet-stream",
2022-12-12 18:00:59 +01:00
"Body": io.BytesIO(contents),
2022-05-26 01:13:54 +02:00
"ResponseMetadata": {"HTTPStatusCode": 200},
2016-11-09 01:23:44 +01:00
}
2022-05-26 01:13:54 +02:00
)
expected_params.update({"Bucket": self.bucket, "Key": self.key})
2016-11-09 01:23:44 +01:00
self.stubber.add_response(
2022-05-26 01:13:54 +02:00
method='get_object',
service_response=get_object_response,
expected_params=expected_params,
)
2016-11-09 01:23:44 +01:00
def stub_multipart_download(self, contents, part_size, num_parts):
self.stub_head(content_length=len(contents))
for i in range(num_parts):
start_byte = i * part_size
end_byte = (i + 1) * part_size - 1
self.stub_get_object(
2022-05-26 01:13:54 +02:00
full_contents=contents,
start_byte=start_byte,
end_byte=end_byte,
)
2016-11-09 01:23:44 +01:00
def test_client_download(self):
self.stub_single_part_download()
with self.stubber:
self.s3.meta.client.download_fileobj(
2022-05-26 01:13:54 +02:00
Bucket=self.bucket, Key=self.key, Fileobj=self.fileobj
)
2016-11-09 01:23:44 +01:00
2021-10-04 19:51:32 +02:00
assert self.fileobj.getvalue() == self.contents
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
def test_raises_value_error_on_invalid_fileobj(self):
with self.stubber:
2021-10-04 19:51:32 +02:00
with pytest.raises(ValueError):
2016-11-09 01:23:44 +01:00
self.s3.meta.client.download_fileobj(
2022-05-26 01:13:54 +02:00
Bucket=self.bucket, Key=self.key, Fileobj='foo'
)
2016-11-09 01:23:44 +01:00
def test_bucket_download(self):
self.stub_single_part_download()
bucket = self.s3.Bucket(self.bucket)
with self.stubber:
bucket.download_fileobj(Key=self.key, Fileobj=self.fileobj)
2021-10-04 19:51:32 +02:00
assert self.fileobj.getvalue() == self.contents
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
def test_object_download(self):
self.stub_single_part_download()
obj = self.s3.Object(self.bucket, self.key)
with self.stubber:
obj.download_fileobj(Fileobj=self.fileobj)
2021-10-04 19:51:32 +02:00
assert self.fileobj.getvalue() == self.contents
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
def test_multipart_download(self):
self.contents = b'A' * 55
self.stub_multipart_download(
2022-05-26 01:13:54 +02:00
contents=self.contents, part_size=5, num_parts=11
)
2016-11-09 01:23:44 +01:00
transfer_config = TransferConfig(
2022-05-26 01:13:54 +02:00
multipart_chunksize=5, multipart_threshold=1, max_concurrency=1
)
2016-11-09 01:23:44 +01:00
with self.stubber:
self.s3.meta.client.download_fileobj(
2022-05-26 01:13:54 +02:00
Bucket=self.bucket,
Key=self.key,
Fileobj=self.fileobj,
Config=transfer_config,
)
2016-11-09 01:23:44 +01:00
2021-10-04 19:51:32 +02:00
assert self.fileobj.getvalue() == self.contents
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
def test_download_progress(self):
self.contents = b'A' * 55
self.stub_multipart_download(
2022-05-26 01:13:54 +02:00
contents=self.contents, part_size=5, num_parts=11
)
2016-11-09 01:23:44 +01:00
transfer_config = TransferConfig(
2022-05-26 01:13:54 +02:00
multipart_chunksize=5, multipart_threshold=1, max_concurrency=1
)
2016-11-09 01:23:44 +01:00
def progress_callback(amount):
self.progress += amount
self.progress_times_called += 1
with self.stubber:
self.s3.meta.client.download_fileobj(
2022-05-26 01:13:54 +02:00
Bucket=self.bucket,
Key=self.key,
Fileobj=self.fileobj,
Config=transfer_config,
Callback=progress_callback,
)
2016-11-09 01:23:44 +01:00
# Assert that the progress callback was called the correct number of
# times with the correct amounts.
2021-10-04 19:51:32 +02:00
assert self.progress_times_called == 11
assert self.progress == 55
2016-11-09 01:23:44 +01:00
self.stubber.assert_no_pending_responses()
2016-05-22 04:03:29 +02:00
class TestS3ObjectSummary(unittest.TestCase):
def setUp(self):
self.session = boto3.session.Session(
2022-05-26 01:13:54 +02:00
aws_access_key_id='foo',
aws_secret_access_key='bar',
region_name='us-west-2',
)
2016-05-22 04:03:29 +02:00
self.s3 = self.session.resource('s3')
self.obj_summary = self.s3.ObjectSummary('my_bucket', 'my_key')
self.obj_summary_size = 12
self.stubber = Stubber(self.s3.meta.client)
self.stubber.activate()
self.stubber.add_response(
method='head_object',
service_response={
2022-05-26 01:13:54 +02:00
'ContentLength': self.obj_summary_size,
'ETag': 'my-etag',
'ContentType': 'binary',
2016-05-22 04:03:29 +02:00
},
2022-05-26 01:13:54 +02:00
expected_params={'Bucket': 'my_bucket', 'Key': 'my_key'},
2016-05-22 04:03:29 +02:00
)
def tearDown(self):
self.stubber.deactivate()
def test_has_load(self):
2021-10-04 19:51:32 +02:00
# Validate load was injected onto ObjectSummary.
assert hasattr(self.obj_summary, 'load')
2016-05-22 04:03:29 +02:00
def test_autoloads_correctly(self):
# In HeadObject the parameter returned is ContentLength, this
# should get mapped to Size of ListObject since the resource uses
# the shape returned to by ListObjects.
2021-10-04 19:51:32 +02:00
assert self.obj_summary.size == self.obj_summary_size
2016-05-22 04:03:29 +02:00
def test_cannot_access_other_non_related_parameters(self):
# Even though an HeadObject was used to load this, it should
# only expose the attributes from its shape defined in ListObjects.
2021-10-04 19:51:32 +02:00
assert not hasattr(self.obj_summary, 'content_length')
2018-07-11 07:39:36 +02:00
class TestServiceResource(unittest.TestCase):
def setUp(self):
self.session = boto3.session.Session()
def test_unsigned_signature_version_is_not_corrupted(self):
config = Config(signature_version=botocore.UNSIGNED)
resource = self.session.resource('s3', config=config)
2021-10-04 19:51:32 +02:00
sig_version = resource.meta.client.meta.config.signature_version
assert sig_version is botocore.UNSIGNED