44#include < aws/s3/S3Client.h>
55#include < aws/s3/model/GetObjectRequest.h>
66#include < aws/s3/model/GetObjectResult.h>
7+ #include < aws/s3/model/HeadObjectRequest.h>
8+ #include < aws/s3/model/HeadObjectResult.h>
79#include < aws/transfer/TransferManager.h>
810#include < aws/testing/AwsTestHelpers.h>
911#include < aws/testing/MemoryTesting.h>
1012#include < sstream>
13+ #include < fstream>
1114
1215using namespace Aws ;
1316using namespace Aws ::S3;
@@ -36,11 +39,87 @@ class MockS3Client : public S3Client {
3639 }
3740};
3841
42+ class MockMultipartS3Client : public S3Client {
43+ public:
44+ Aws::String FULL_OBJECT_CHECKSUM; // "SBi/K+1ooBg="
45+ MockMultipartS3Client (Aws::String expected_checksum) : S3Client() {
46+ FULL_OBJECT_CHECKSUM = expected_checksum;
47+ };
48+
49+ HeadObjectOutcome HeadObject (const HeadObjectRequest&) const override {
50+ HeadObjectResult result;
51+ result.SetContentLength (78643200 );
52+ result.SetChecksumCRC64NVME (FULL_OBJECT_CHECKSUM);
53+ result.SetChecksumType (Aws::S3::Model::ChecksumType::FULL_OBJECT); // This is key!
54+ result.SetETag (" \" test-etag-12345\" " ); // Add ETag
55+ return HeadObjectOutcome (std::move (result));
56+ }
57+
58+ GetObjectOutcome GetObject (const GetObjectRequest& request) const override {
59+ GetObjectResult result;
60+
61+ const uint64_t totalSize = 78643200 ;
62+ const uint64_t partSize = 5242880 ;
63+ const std::vector<Aws::String> checksums = {
64+ " wAQOkgd/LJk=" , " zfmsUj6AZfs=" , " oyENjcGDHcY=" , " wAQOkgd/LJk=" , " zfmsUj6AZfs=" ,
65+ " oyENjcGDHcY=" , " wAQOkgd/LJk=" , " zfmsUj6AZfs=" , " oyENjcGDHcY=" , " wAQOkgd/LJk=" ,
66+ " zfmsUj6AZfs=" , " oyENjcGDHcY=" , " wAQOkgd/LJk=" , " zfmsUj6AZfs=" , " oyENjcGDHcY="
67+ };
68+
69+ if (request.RangeHasBeenSet ()) {
70+ auto range = request.GetRange ();
71+ size_t dashPos = range.find (' -' );
72+ uint64_t start = std::stoull (range.substr (6 , dashPos - 6 ).c_str ());
73+ uint64_t end = std::stoull (range.substr (dashPos + 1 ).c_str ());
74+ uint64_t size = end - start + 1 ;
75+
76+ int partNum = static_cast <int >(start) / partSize;
77+ if (partNum < 15 ) {
78+ result.SetContentRange (Aws::String (" bytes " ) + Aws::String (std::to_string (start).c_str ()) + " -" + Aws::String (std::to_string (end).c_str ()) + " /" + Aws::String (std::to_string (totalSize).c_str ()));
79+ result.SetChecksumCRC64NVME (checksums[partNum]);
80+ result.SetContentLength (size);
81+ result.SetETag (Aws::String (" \" part-etag-" ) + Aws::String (std::to_string (partNum).c_str ()) + " \" " );
82+
83+ // Call the response stream factory if provided
84+ if (request.GetResponseStreamFactory ()) {
85+ auto responseStream = request.GetResponseStreamFactory ()();
86+
87+ // Write part-specific data to the response stream
88+ char partChar = ' A' + (partNum % 3 );
89+ for (uint64_t i = 0 ; i < size; ++i) {
90+ responseStream->put (partChar);
91+ }
92+ responseStream->flush ();
93+
94+ // Simulate data received callback to track bytes transferred
95+ if (request.GetDataReceivedEventHandler ()) {
96+ request.GetDataReceivedEventHandler ()(nullptr , nullptr , size);
97+ }
98+
99+ result.ReplaceBody (responseStream);
100+ } else {
101+ // Fallback for non-factory requests
102+ auto stream = Aws::New<std::stringstream>(ALLOCATION_TAG);
103+ char partChar = ' A' + (partNum % 3 );
104+ for (uint64_t i = 0 ; i < size; ++i) {
105+ stream->put (partChar);
106+ }
107+ stream->seekg (0 , std::ios::beg);
108+ result.ReplaceBody (stream);
109+ }
110+ }
111+ }
112+
113+ return GetObjectOutcome (std::move (result));
114+ }
115+ };
116+
39117class TransferUnitTest : public testing ::Test {
40118protected:
41119 void SetUp () override {
42120 executor = Aws::MakeShared<PooledThreadExecutor>(ALLOCATION_TAG, 1 );
43121 mockS3Client = Aws::MakeShared<MockS3Client>(ALLOCATION_TAG);
122+ mockMultipartS3Client = Aws::MakeShared<MockMultipartS3Client>(ALLOCATION_TAG, " SBi/K+1ooBg=" );
44123 }
45124
46125 static void SetUpTestSuite () {
@@ -53,6 +132,7 @@ class TransferUnitTest : public testing::Test {
53132
54133 std::shared_ptr<PooledThreadExecutor> executor;
55134 std::shared_ptr<MockS3Client> mockS3Client;
135+ std::shared_ptr<MockMultipartS3Client> mockMultipartS3Client;
56136 static SDKOptions _options;
57137};
58138
@@ -72,4 +152,80 @@ TEST_F(TransferUnitTest, ContentValidationShouldFail) {
72152 handle->WaitUntilFinished ();
73153
74154 EXPECT_EQ (TransferStatus::FAILED, handle->GetStatus ());
155+ }
156+
157+ TEST_F (TransferUnitTest, MultipartDownloadTest) {
158+ TransferManagerConfiguration config (executor.get ());
159+ config.s3Client = mockMultipartS3Client;
160+ config.bufferSize = 5242880 ; // 5MB to ensure multipart
161+ auto transferManager = TransferManager::Create (config);
162+
163+ // Create a temporary file for download since multipart needs seekable stream
164+ std::string tempFile;
165+ #ifdef _WIN32
166+ char tempPath[MAX_PATH];
167+ GetTempPathA (MAX_PATH, tempPath);
168+ tempFile = std::string (tempPath) + " test_download_" + std::to_string (rand ());
169+ #else
170+ tempFile = " /tmp/test_download_" + std::to_string (rand ());
171+ #endif
172+ auto createStreamFn = [tempFile]() -> Aws::IOStream* {
173+ return Aws::New<Aws::FStream>(ALLOCATION_TAG, tempFile.c_str (),
174+ std::ios_base::out | std::ios_base::in |
175+ std::ios_base::binary | std::ios_base::trunc);
176+ };
177+
178+ // Download the full 78MB file
179+ auto handle = transferManager->DownloadFile (" test-bucket" , " test-key" , createStreamFn);
180+ handle->WaitUntilFinished ();
181+
182+ // Test multipart download functionality - should PASS with correct checksum
183+ EXPECT_TRUE (handle->IsMultipart ());
184+ EXPECT_EQ (78643200u , handle->GetBytesTotalSize ());
185+ EXPECT_EQ (15u , handle->GetCompletedParts ().size ());
186+ EXPECT_EQ (0u , handle->GetFailedParts ().size ());
187+ EXPECT_EQ (0u , handle->GetPendingParts ().size ());
188+ EXPECT_EQ (TransferStatus::COMPLETED, handle->GetStatus ()); // Should PASS
189+
190+ // Clean up
191+ std::remove (tempFile.c_str ());
192+ }
193+
194+ TEST_F (TransferUnitTest, MultipartDownloadTest_Fail) {
195+ TransferManagerConfiguration config (executor.get ());
196+ auto mockFailClient = Aws::MakeShared<MockMultipartS3Client>(ALLOCATION_TAG, " WRONG_CHECKSUM=" );
197+ config.s3Client = mockFailClient;
198+ config.bufferSize = 5242880 ; // 5MB to ensure multipart
199+ auto transferManager = TransferManager::Create (config);
200+
201+ // Create a temporary file for download since multipart needs seekable stream
202+ std::string tempFile;
203+ #ifdef _WIN32
204+ char tempPath[MAX_PATH];
205+ GetTempPathA (MAX_PATH, tempPath);
206+ tempFile = std::string (tempPath) + " test_download_" + std::to_string (rand ());
207+ #else
208+ tempFile = " /tmp/test_download_" + std::to_string (rand ());
209+ #endif
210+ auto createStreamFn = [tempFile]() -> Aws::IOStream* {
211+ return Aws::New<Aws::FStream>(ALLOCATION_TAG, tempFile.c_str (),
212+ std::ios_base::out | std::ios_base::in |
213+ std::ios_base::binary | std::ios_base::trunc);
214+ };
215+
216+ // Download the full 78MB file
217+ auto handle = transferManager->DownloadFile (" test-bucket" , " test-key" , createStreamFn);
218+ handle->WaitUntilFinished ();
219+
220+ // Test multipart download functionality - should FAIL with wrong checksum
221+ EXPECT_TRUE (handle->IsMultipart ());
222+ EXPECT_EQ (78643200u , handle->GetBytesTotalSize ());
223+ EXPECT_EQ (15u , handle->GetCompletedParts ().size ());
224+ EXPECT_EQ (0u , handle->GetFailedParts ().size ());
225+ EXPECT_EQ (0u , handle->GetPendingParts ().size ());
226+ EXPECT_EQ (TransferStatus::FAILED, handle->GetStatus ()); // Should FAIL due to wrong checksum
227+ EXPECT_EQ (" Full-object checksum validation failed" , handle->GetLastError ().GetMessage ());
228+
229+ // Clean up
230+ std::remove (tempFile.c_str ());
75231}
0 commit comments