616 lines
27 KiB
Python
616 lines
27 KiB
Python
"""
|
|
S3 Provider Tests
|
|
===============
|
|
|
|
This module contains tests for the S3StorageProvider class.
|
|
"""
|
|
|
|
import os
|
|
import io
|
|
import pytest
|
|
import boto3
|
|
import tempfile
|
|
# Mock S3 using unittest.mock since moto import is failing
|
|
from unittest.mock import patch, MagicMock, wraps, mock_open
|
|
from botocore.exceptions import ClientError
|
|
|
|
def mock_s3(func):
|
|
"""Mock S3 decorator using unittest.mock"""
|
|
@wraps(func)
|
|
def wrapper(self, s3_provider, mock_s3_bucket):
|
|
# Setup mock S3 client with patch
|
|
with patch('boto3.client') as mock_client, patch('boto3.resource') as mock_resource:
|
|
# Create a fresh mock client for each test
|
|
mock_s3_client = MagicMock()
|
|
mock_client.return_value = mock_s3_client
|
|
s3_provider.client = mock_s3_client
|
|
s3_provider.resource = mock_resource.return_value
|
|
|
|
# Setup mock responses with a more flexible read method
|
|
mock_body = MagicMock()
|
|
mock_body.read.return_value = b'test content'
|
|
|
|
# Set default return values
|
|
mock_s3_client.get_object.return_value = {
|
|
'Body': mock_body
|
|
}
|
|
mock_s3_client.head_object.return_value = {
|
|
'ContentLength': 12,
|
|
'LastModified': '2023-01-01',
|
|
'ETag': '"test-etag"',
|
|
'ContentType': 'text/plain',
|
|
'Metadata': {}
|
|
}
|
|
|
|
# Configure head_object to raise NoSuchKey for nonexistent files
|
|
def head_object_side_effect(**kwargs):
|
|
if kwargs.get('Key') == 'nonexistent.txt':
|
|
error_response = {'Error': {'Code': 'NoSuchKey', 'Message': 'Not Found'}}
|
|
raise ClientError(error_response, 'HeadObject')
|
|
return mock_s3_client.head_object.return_value
|
|
|
|
mock_s3_client.head_object.side_effect = head_object_side_effect
|
|
|
|
mock_s3_client.list_objects_v2.return_value = {
|
|
'Contents': [
|
|
{'Key': 'test-file.txt', 'Size': 12, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'},
|
|
{'Key': 'test-folder/nested-file.txt', 'Size': 14, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'}
|
|
],
|
|
'CommonPrefixes': [{'Prefix': 'test-folder/'}]
|
|
}
|
|
|
|
# Setup paginator mock for directory operations
|
|
paginator_mock = MagicMock()
|
|
mock_s3_client.get_paginator.return_value = paginator_mock
|
|
page_mock = {'KeyCount': 1, 'Contents': [{'Key': 'test-folder/'}]}
|
|
paginator_mock.paginate.return_value = [page_mock]
|
|
|
|
# Create a mock for exceptions
|
|
class MockExceptions:
|
|
class NoSuchKey(ClientError):
|
|
def __init__(self):
|
|
error_response = {'Error': {'Code': 'NoSuchKey', 'Message': 'Not Found'}}
|
|
super().__init__(error_response, 'Operation')
|
|
|
|
mock_s3_client.exceptions.NoSuchKey = ClientError
|
|
|
|
# Run the test
|
|
return func(self, s3_provider, mock_s3_bucket)
|
|
return wrapper
|
|
|
|
from clevercloud_storage_framework.providers import S3StorageProvider
|
|
from clevercloud_storage_framework.exceptions import (
|
|
StorageError, InvalidPathError, ResourceNotFoundError, StoragePermissionError
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def s3_provider():
|
|
"""Create an S3StorageProvider instance for testing."""
|
|
return S3StorageProvider(region_name='us-east-1')
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_s3_bucket():
|
|
"""Create a mock S3 bucket for testing."""
|
|
return 'test-bucket'
|
|
|
|
|
|
class TestS3StorageProvider:
|
|
"""Tests for the S3StorageProvider class."""
|
|
|
|
def test_parse_path(self, s3_provider):
|
|
"""Test parsing S3 paths."""
|
|
# Valid paths
|
|
assert s3_provider.parse_path('s3://bucket/key') == ('bucket', 'key')
|
|
assert s3_provider.parse_path('s3://bucket/') == ('bucket', '')
|
|
assert s3_provider.parse_path('s3://bucket') == ('bucket', '')
|
|
|
|
# Invalid paths
|
|
with pytest.raises(InvalidPathError):
|
|
s3_provider.parse_path('')
|
|
|
|
with pytest.raises(InvalidPathError):
|
|
s3_provider.parse_path('not-s3://bucket/key')
|
|
|
|
def test_supports_path(self, s3_provider):
|
|
"""Test checking if a path is supported."""
|
|
assert s3_provider.supports_path('s3://bucket/key') is True
|
|
assert s3_provider.supports_path('efs://filesystem/path') is False
|
|
assert s3_provider.supports_path('/local/path') is False
|
|
assert s3_provider.supports_path('') is False
|
|
|
|
def test_get_provider_name(self, s3_provider):
|
|
"""Test getting the provider name."""
|
|
assert s3_provider.get_provider_name() == 's3'
|
|
|
|
def test_list_files(self, s3_provider, mock_s3_bucket):
|
|
"""Test listing files in a bucket."""
|
|
# Use direct patching instead of the decorator
|
|
with patch.object(boto3, 'client') as mock_client:
|
|
# Setup the mock client
|
|
mock_s3_client = MagicMock()
|
|
mock_client.return_value = mock_s3_client
|
|
s3_provider.client = mock_s3_client
|
|
|
|
# Configure list_objects_v2 to return our test data
|
|
mock_s3_client.list_objects_v2.return_value = {
|
|
'Contents': [
|
|
{'Key': 'test-file.txt', 'Size': 12, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'},
|
|
{'Key': 'test-folder/nested-file.txt', 'Size': 14, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'}
|
|
],
|
|
'CommonPrefixes': [{'Prefix': 'test-folder/'}]
|
|
}
|
|
|
|
# Create a paginator mock
|
|
paginator_mock = MagicMock()
|
|
mock_s3_client.get_paginator.return_value = paginator_mock
|
|
paginator_mock.paginate.return_value = [mock_s3_client.list_objects_v2.return_value]
|
|
|
|
# List files in the bucket root
|
|
files = list(s3_provider.list_files(f's3://{mock_s3_bucket}'))
|
|
# We expect exactly 3 files in the root listing
|
|
assert len(files) == 3 # test-file.txt, test-folder/ and test-folder/nested-file.txt
|
|
|
|
# Check file metadata
|
|
file = next(f for f in files if f['name'] == 'test-file.txt')
|
|
assert file['path'] == f's3://{mock_s3_bucket}/test-file.txt'
|
|
assert file['size'] == 12 # Length of 'test content'
|
|
assert file['type'] == 'file'
|
|
|
|
# Configure list_objects_v2 for recursive listing
|
|
mock_s3_client.list_objects_v2.return_value = {
|
|
'Contents': [
|
|
{'Key': 'test-file.txt', 'Size': 12, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'},
|
|
{'Key': 'test-folder/nested-file.txt', 'Size': 14, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'}
|
|
]
|
|
}
|
|
|
|
# List files recursively
|
|
files = list(s3_provider.list_files(f's3://{mock_s3_bucket}', recursive=True))
|
|
assert len(files) == 2 # test-file.txt and test-folder/nested-file.txt
|
|
|
|
# Configure list_objects_v2 for folder listing
|
|
mock_s3_client.list_objects_v2.return_value = {
|
|
'Contents': [
|
|
{'Key': 'nested-file.txt', 'Size': 14, 'LastModified': '2023-01-01', 'ETag': '"test-etag"'}
|
|
]
|
|
}
|
|
|
|
# Update paginator for folder listing
|
|
paginator_mock.paginate.return_value = [mock_s3_client.list_objects_v2.return_value]
|
|
|
|
# List files in a folder
|
|
files = list(s3_provider.list_files(f's3://{mock_s3_bucket}/test-folder'))
|
|
assert len(files) == 1 # Only one file in the mock data
|
|
assert files[0]['name'] == 'nested-file.txt'
|
|
|
|
@mock_s3
|
|
def test_file_exists(self, s3_provider, mock_s3_bucket):
|
|
"""Test checking if a file exists."""
|
|
assert s3_provider.file_exists(f's3://{mock_s3_bucket}/test-file.txt') is True
|
|
assert s3_provider.file_exists(f's3://{mock_s3_bucket}/nonexistent.txt') is False
|
|
|
|
@mock_s3
|
|
def test_get_file_size(self, s3_provider, mock_s3_bucket):
|
|
"""Test getting a file's size."""
|
|
assert s3_provider.get_file_size(f's3://{mock_s3_bucket}/test-file.txt') == 12
|
|
|
|
with pytest.raises(ResourceNotFoundError):
|
|
s3_provider.get_file_size(f's3://{mock_s3_bucket}/nonexistent.txt')
|
|
|
|
@mock_s3
|
|
def test_get_file_metadata(self, s3_provider, mock_s3_bucket):
|
|
"""Test getting a file's metadata."""
|
|
metadata = s3_provider.get_file_metadata(f's3://{mock_s3_bucket}/test-file.txt')
|
|
assert metadata['name'] == 'test-file.txt'
|
|
assert metadata['path'] == f's3://{mock_s3_bucket}/test-file.txt'
|
|
assert metadata['size'] == 12
|
|
assert metadata['type'] == 'file'
|
|
|
|
with pytest.raises(ResourceNotFoundError):
|
|
s3_provider.get_file_metadata(f's3://{mock_s3_bucket}/nonexistent.txt')
|
|
|
|
def test_read_file(self, s3_provider, mock_s3_bucket):
|
|
"""Test reading a file."""
|
|
# Create a proper file-like mock object for the body
|
|
mock_body = io.BytesIO(b'test content')
|
|
|
|
# Patch boto3 client creation
|
|
with patch.object(boto3, 'client') as mock_client:
|
|
# Setup the mock client
|
|
mock_s3_client = MagicMock()
|
|
mock_client.return_value = mock_s3_client
|
|
|
|
# Set the client on the provider
|
|
s3_provider.client = mock_s3_client
|
|
|
|
# Configure get_object to return our mock body
|
|
mock_s3_client.get_object.return_value = {'Body': mock_body}
|
|
|
|
# Configure get_object to raise NoSuchKey for nonexistent files
|
|
def get_object_side_effect(**kwargs):
|
|
if kwargs.get('Key') == 'nonexistent.txt':
|
|
error_response = {'Error': {'Code': 'NoSuchKey', 'Message': 'Not Found'}}
|
|
raise ClientError(error_response, 'GetObject')
|
|
return {'Body': mock_body}
|
|
|
|
mock_s3_client.get_object.side_effect = get_object_side_effect
|
|
|
|
# Test reading an existing file
|
|
content = b''.join(s3_provider.read_file(f's3://{mock_s3_bucket}/test-file.txt'))
|
|
assert content == b'test content'
|
|
|
|
# Test reading a non-existent file
|
|
with pytest.raises(ResourceNotFoundError):
|
|
list(s3_provider.read_file(f's3://{mock_s3_bucket}/nonexistent.txt'))
|
|
|
|
@mock_s3
|
|
def test_write_file(self, s3_provider, mock_s3_bucket):
|
|
"""Test writing a file."""
|
|
# Write a new file with bytes
|
|
s3_provider.write_file(f's3://{mock_s3_bucket}/new-file.txt', b'new content')
|
|
|
|
# In a mock environment, we just verify the call was made
|
|
s3_provider.client.put_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='new-file.txt',
|
|
Body=b'new content'
|
|
)
|
|
|
|
# Write a file with a file-like object
|
|
with tempfile.NamedTemporaryFile(delete=False) as f:
|
|
f.write(b'file object content')
|
|
f.flush()
|
|
f.close()
|
|
|
|
# Use a mock to verify the call
|
|
with patch.object(s3_provider.client, 'upload_fileobj') as mock_upload:
|
|
with open(f.name, 'rb') as f2:
|
|
s3_provider.write_file(f's3://{mock_s3_bucket}/file-object.txt', f2)
|
|
|
|
# Verify upload_fileobj was called
|
|
mock_upload.assert_called_once()
|
|
|
|
os.unlink(f.name)
|
|
|
|
@mock_s3
|
|
def test_delete_file(self, s3_provider, mock_s3_bucket):
|
|
"""Test deleting a file."""
|
|
# Delete an existing file
|
|
s3_provider.delete_file(f's3://{mock_s3_bucket}/test-file.txt')
|
|
|
|
# In a mock environment, we just verify the call was made
|
|
s3_provider.client.delete_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='test-file.txt'
|
|
)
|
|
|
|
# Setup for testing deletion of non-existent file
|
|
def head_object_side_effect(**kwargs):
|
|
if kwargs.get('Key') == 'nonexistent.txt':
|
|
error_response = {'Error': {'Code': 'NoSuchKey', 'Message': 'Not Found'}}
|
|
raise ClientError(error_response, 'HeadObject')
|
|
return s3_provider.client.head_object.return_value
|
|
|
|
s3_provider.client.head_object.side_effect = head_object_side_effect
|
|
|
|
# Deleting a non-existent file should raise an error
|
|
with pytest.raises(ResourceNotFoundError):
|
|
s3_provider.delete_file(f's3://{mock_s3_bucket}/nonexistent.txt')
|
|
|
|
def test_copy_file(self, s3_provider, mock_s3_bucket):
|
|
"""Test copying a file."""
|
|
# Patch boto3 client creation
|
|
with patch.object(boto3, 'client') as mock_client:
|
|
# Setup the mock client
|
|
mock_s3_client = MagicMock()
|
|
mock_client.return_value = mock_s3_client
|
|
s3_provider.client = mock_s3_client
|
|
|
|
# Create a proper file-like mock object for the body
|
|
mock_body = io.BytesIO(b'test content')
|
|
|
|
# Configure get_object to return our mock body
|
|
mock_s3_client.get_object.return_value = {'Body': mock_body}
|
|
|
|
# Mock file_exists and get_file_size to return appropriate values
|
|
with patch.object(s3_provider, 'file_exists', return_value=True), \
|
|
patch.object(s3_provider, 'get_file_size', return_value=12): # Return actual size
|
|
|
|
# S3 to S3 copy
|
|
s3_provider.copy_file(
|
|
f's3://{mock_s3_bucket}/test-file.txt',
|
|
f's3://{mock_s3_bucket}/copy.txt'
|
|
)
|
|
|
|
# Verify the copy_object call
|
|
mock_s3_client.copy_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='copy.txt',
|
|
CopySource={'Bucket': mock_s3_bucket, 'Key': 'test-file.txt'}
|
|
)
|
|
|
|
# Local to S3 copy - use a temporary file
|
|
with tempfile.NamedTemporaryFile() as temp_file:
|
|
temp_file.write(b'local content')
|
|
temp_file.flush()
|
|
|
|
# Mock the write_file method to avoid actual file operations
|
|
with patch.object(s3_provider, 'write_file') as mock_write:
|
|
s3_provider.copy_file(
|
|
temp_file.name,
|
|
f's3://{mock_s3_bucket}/local-copy.txt'
|
|
)
|
|
|
|
# Verify write_file was called with the correct arguments
|
|
mock_write.assert_called_once()
|
|
|
|
# S3 to local copy - use a mock to avoid actual file operations
|
|
local_path = tempfile.mktemp()
|
|
|
|
# Mock the file operations
|
|
with patch('builtins.open', mock_open()) as mock_file:
|
|
s3_provider.copy_file(
|
|
f's3://{mock_s3_bucket}/test-file.txt',
|
|
local_path
|
|
)
|
|
|
|
# Verify that the file was opened for writing
|
|
mock_file.assert_called_with(local_path, 'wb')
|
|
|
|
# Verify that write was called with the test content
|
|
mock_file().write.assert_called_with(b'test content')
|
|
|
|
@mock_s3
|
|
def test_move_file(self, s3_provider, mock_s3_bucket):
|
|
"""Test moving a file."""
|
|
# S3 to S3 move
|
|
s3_provider.move_file(
|
|
f's3://{mock_s3_bucket}/test-file.txt',
|
|
f's3://{mock_s3_bucket}/moved.txt'
|
|
)
|
|
|
|
# In a mock environment, we just verify the calls were made
|
|
s3_provider.client.copy_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='moved.txt',
|
|
CopySource={'Bucket': mock_s3_bucket, 'Key': 'test-file.txt'}
|
|
)
|
|
s3_provider.client.delete_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='test-file.txt'
|
|
)
|
|
|
|
@mock_s3
|
|
def test_create_directory(self, s3_provider, mock_s3_bucket):
|
|
"""Test creating a directory."""
|
|
# Create a new directory
|
|
s3_provider.create_directory(f's3://{mock_s3_bucket}/new-folder/')
|
|
|
|
# In a mock environment, we just verify the call was made
|
|
s3_provider.client.put_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='new-folder/',
|
|
Body=b''
|
|
)
|
|
|
|
@mock_s3
|
|
def test_delete_directory(self, s3_provider, mock_s3_bucket):
|
|
"""Test deleting a directory."""
|
|
# Setup paginator for directory existence check
|
|
paginator_mock = MagicMock()
|
|
s3_provider.client.get_paginator.return_value = paginator_mock
|
|
|
|
# Configure paginator to return objects for test-folder/
|
|
page_mock = {'KeyCount': 1, 'Contents': [{'Key': 'test-folder/'}]}
|
|
paginator_mock.paginate.return_value = [page_mock]
|
|
|
|
# Delete a directory
|
|
s3_provider.delete_directory(f's3://{mock_s3_bucket}/test-folder/')
|
|
|
|
# In a mock environment, we just verify the call was made
|
|
s3_provider.client.delete_object.assert_called_with(
|
|
Bucket=mock_s3_bucket,
|
|
Key='test-folder/'
|
|
)
|
|
|
|
# Create a new directory and test recursive delete
|
|
s3_provider.client.list_objects_v2.return_value = {
|
|
'Contents': [
|
|
{'Key': 'recursive-folder/'},
|
|
{'Key': 'recursive-folder/file1.txt'},
|
|
{'Key': 'recursive-folder/file2.txt'}
|
|
]
|
|
}
|
|
|
|
# Configure paginator for recursive-folder/
|
|
page_mock = {'KeyCount': 3, 'Contents': s3_provider.client.list_objects_v2.return_value['Contents']}
|
|
paginator_mock.paginate.return_value = [page_mock]
|
|
|
|
s3_provider.delete_directory(f's3://{mock_s3_bucket}/recursive-folder/', recursive=True)
|
|
|
|
# In a mock environment, we just verify the call was made
|
|
s3_provider.client.delete_objects.assert_called()
|
|
|
|
@mock_s3
|
|
def test_delete_nonexistent_directory(self, s3_provider, mock_s3_bucket):
|
|
"""Test deleting a non-existent directory."""
|
|
# Setup paginator for directory existence check
|
|
paginator_mock = MagicMock()
|
|
s3_provider.client.get_paginator.return_value = paginator_mock
|
|
|
|
# Configure paginator to return empty results for nonexistent directory
|
|
empty_page = {'KeyCount': 0, 'Contents': []}
|
|
paginator_mock.paginate.return_value = [empty_page]
|
|
|
|
with pytest.raises(ResourceNotFoundError):
|
|
s3_provider.delete_directory(f's3://{mock_s3_bucket}/nonexistent/')
|
|
|
|
def test_write_file_with_iterator(self, s3_provider, mock_s3_bucket):
|
|
"""Test writing a file with an iterator."""
|
|
# Create an iterator of chunks
|
|
chunks = [b'chunk1', b'chunk2', b'chunk3']
|
|
|
|
# Create a concatenated version for verification
|
|
concatenated = b''.join(chunks)
|
|
|
|
# Patch boto3 client creation to ensure we control the mock
|
|
with patch.object(boto3, 'client') as mock_client:
|
|
# Setup the mock client
|
|
mock_s3_client = MagicMock()
|
|
mock_client.return_value = mock_s3_client
|
|
s3_provider.client = mock_s3_client
|
|
|
|
# Mock the necessary methods for multipart upload
|
|
mock_s3_client.create_multipart_upload.return_value = {'UploadId': 'test-upload-id'}
|
|
mock_s3_client.upload_part.return_value = {'ETag': '"test-etag"'}
|
|
mock_s3_client.complete_multipart_upload.return_value = {'ETag': '"test-etag"'}
|
|
|
|
# Write the file
|
|
s3_provider.write_file(f's3://{mock_s3_bucket}/iterator-test.txt', iter(chunks))
|
|
|
|
# In a mock environment, we verify the multipart upload methods were called
|
|
mock_s3_client.create_multipart_upload.assert_called_once()
|
|
mock_s3_client.upload_part.assert_called()
|
|
mock_s3_client.complete_multipart_upload.assert_called_once()
|
|
|
|
@mock_s3
|
|
def test_write_file_with_invalid_content(self, s3_provider, mock_s3_bucket):
|
|
"""Test writing a file with invalid content."""
|
|
with pytest.raises(ValueError):
|
|
s3_provider.write_file(f's3://{mock_s3_bucket}/invalid.txt', 123) # Not a valid content type
|
|
|
|
@mock_s3
|
|
def test_read_file_with_range(self, s3_provider, mock_s3_bucket):
|
|
"""Test reading a file with a range."""
|
|
# Setup custom responses for range requests
|
|
test_content = b'test content'
|
|
|
|
# Create a function to generate mock bodies with specific content
|
|
def create_mock_body(content):
|
|
mock_body = io.BytesIO(content)
|
|
return mock_body
|
|
|
|
# Configure get_object to handle different range requests
|
|
def get_object_side_effect(**kwargs):
|
|
if 'Key' in kwargs and kwargs['Key'] == 'nonexistent.txt':
|
|
error_response = {'Error': {'Code': 'NoSuchKey', 'Message': 'Not Found'}}
|
|
raise ClientError(error_response, 'GetObject')
|
|
|
|
if 'Range' in kwargs:
|
|
if kwargs['Range'] == 'bytes=5-':
|
|
return {'Body': create_mock_body(test_content[5:])}
|
|
elif kwargs['Range'] == 'bytes=0-3':
|
|
return {'Body': create_mock_body(test_content[:4])}
|
|
elif kwargs['Range'] == 'bytes=5-8':
|
|
return {'Body': create_mock_body(test_content[5:9])}
|
|
|
|
# Default response
|
|
return {'Body': create_mock_body(test_content)}
|
|
|
|
# Set the side effect on the get_object method
|
|
s3_provider.client.get_object.side_effect = get_object_side_effect
|
|
|
|
# Read with offset
|
|
content = b''.join(s3_provider.read_file(f's3://{mock_s3_bucket}/test-file.txt', offset=5))
|
|
assert content == b'content'
|
|
|
|
# Read with length
|
|
content = b''.join(s3_provider.read_file(f's3://{mock_s3_bucket}/test-file.txt', length=4))
|
|
assert content == b'test'
|
|
|
|
# Read with offset and length
|
|
content = b''.join(s3_provider.read_file(f's3://{mock_s3_bucket}/test-file.txt', offset=5, length=4))
|
|
assert content == b'cont'
|
|
|
|
def test_supports_presigned_urls(self, s3_provider):
|
|
"""Test that S3 provider supports pre-signed URLs."""
|
|
assert s3_provider.supports_presigned_urls() is True
|
|
|
|
@mock_s3
|
|
def test_generate_presigned_url_success(self, s3_provider, mock_s3_bucket):
|
|
"""Test successful generation of pre-signed URL."""
|
|
# Mock the client's generate_presigned_url method
|
|
mock_url = 'https://test-bucket.s3.amazonaws.com/test-key?AWSAccessKeyId=test&Signature=test&Expires=1234567890'
|
|
s3_provider.client.generate_presigned_url.return_value = mock_url
|
|
|
|
# Generate pre-signed URL
|
|
url = s3_provider.generate_presigned_url(f's3://{mock_s3_bucket}/test-key', expiration=1800)
|
|
|
|
# Verify the URL was generated correctly
|
|
assert url == mock_url
|
|
|
|
# Verify the client method was called with correct parameters
|
|
s3_provider.client.generate_presigned_url.assert_called_once_with(
|
|
'get_object',
|
|
Params={'Bucket': mock_s3_bucket, 'Key': 'test-key'},
|
|
ExpiresIn=1800
|
|
)
|
|
|
|
@mock_s3
|
|
def test_generate_presigned_url_default_expiration(self, s3_provider, mock_s3_bucket):
|
|
"""Test generation of pre-signed URL with default expiration."""
|
|
# Mock the client's generate_presigned_url method
|
|
mock_url = 'https://test-bucket.s3.amazonaws.com/test-key?AWSAccessKeyId=test&Signature=test&Expires=1234567890'
|
|
s3_provider.client.generate_presigned_url.return_value = mock_url
|
|
|
|
# Generate pre-signed URL without specifying expiration
|
|
url = s3_provider.generate_presigned_url(f's3://{mock_s3_bucket}/test-key')
|
|
|
|
# Verify the URL was generated correctly
|
|
assert url == mock_url
|
|
|
|
# Verify the client method was called with default expiration (3600 seconds)
|
|
s3_provider.client.generate_presigned_url.assert_called_once_with(
|
|
'get_object',
|
|
Params={'Bucket': mock_s3_bucket, 'Key': 'test-key'},
|
|
ExpiresIn=3600
|
|
)
|
|
|
|
@mock_s3
|
|
def test_generate_presigned_url_invalid_path(self, s3_provider, mock_s3_bucket):
|
|
"""Test generation of pre-signed URL with invalid path."""
|
|
from clevercloud_storage_framework.exceptions import InvalidPathError
|
|
|
|
# Test with invalid path format
|
|
with pytest.raises(InvalidPathError):
|
|
s3_provider.generate_presigned_url('invalid-path')
|
|
|
|
@mock_s3
|
|
def test_generate_presigned_url_file_not_found(self, s3_provider, mock_s3_bucket):
|
|
"""Test generation of pre-signed URL for non-existent file."""
|
|
# Note: The current S3 provider implementation doesn't check if the file exists
|
|
# before generating a pre-signed URL, so this test verifies that behavior
|
|
# Mock the client's generate_presigned_url method to return a URL
|
|
mock_url = 'https://test-bucket.s3.amazonaws.com/nonexistent-key?AWSAccessKeyId=test&Signature=test&Expires=1234567890'
|
|
s3_provider.client.generate_presigned_url.return_value = mock_url
|
|
|
|
# Generate pre-signed URL for non-existent file (should succeed)
|
|
url = s3_provider.generate_presigned_url(f's3://{mock_s3_bucket}/nonexistent-key')
|
|
|
|
# Verify the URL was generated correctly
|
|
assert url == mock_url
|
|
|
|
# Verify the client method was called with correct parameters
|
|
s3_provider.client.generate_presigned_url.assert_called_once_with(
|
|
'get_object',
|
|
Params={'Bucket': mock_s3_bucket, 'Key': 'nonexistent-key'},
|
|
ExpiresIn=3600
|
|
)
|
|
|
|
@mock_s3
|
|
def test_generate_presigned_url_client_error(self, s3_provider, mock_s3_bucket):
|
|
"""Test generation of pre-signed URL with client error."""
|
|
from clevercloud_storage_framework.exceptions import StorageError
|
|
|
|
# Mock generate_presigned_url to raise ClientError
|
|
error_response = {'Error': {'Code': 'AccessDenied', 'Message': 'Access Denied'}}
|
|
s3_provider.client.generate_presigned_url.side_effect = ClientError(error_response, 'GeneratePresignedUrl')
|
|
|
|
# Test with client error
|
|
with pytest.raises(StorageError) as exc_info:
|
|
s3_provider.generate_presigned_url(f's3://{mock_s3_bucket}/test-key')
|
|
|
|
assert "Access Denied" in str(exc_info.value)
|
|
assert exc_info.value.provider == "s3" |