@@ -37,7 +37,7 @@ TEST(DownloadResultContract, CarriesTransferAndResponseMetadata) {
3737}
3838
3939// Test download_to_file against a real HTTPS endpoint.
40- // Uses httpbin .org which returns known-size responses.
40+ // Uses httpbingo .org (a maintained httpbin work-alike) which returns known-size responses.
4141
4242class DownloadToFileTest : public ::testing::Test {
4343protected:
@@ -67,7 +67,7 @@ TEST_F(DownloadToFileTest, BasicDownloadWithProgress) {
6767 int callCount = 0 ;
6868
6969 auto result = client.download_to_file (
70- " https://httpbin .org/bytes/100" ,
70+ " https://httpbingo .org/bytes/100" ,
7171 dest,
7272 [&](std::int64_t total, std::int64_t downloaded) {
7373 lastTotal = total;
@@ -97,7 +97,7 @@ TEST_F(DownloadToFileTest, ProgressIncrementsMonotonically) {
9797 std::vector<std::int64_t > downloadedValues;
9898
9999 auto result = client.download_to_file (
100- " https://httpbin .org/bytes/51200" ,
100+ " https://httpbingo .org/bytes/51200" ,
101101 dest,
102102 [&](std::int64_t total, std::int64_t downloaded) {
103103 (void )total;
@@ -128,9 +128,9 @@ TEST_F(DownloadToFileTest, FollowsRedirects) {
128128
129129 auto dest = tmpDir / " redirected.bin" ;
130130
131- // httpbin /redirect-to redirects to the given URL
131+ // httpbingo /redirect-to redirects to the given URL
132132 auto result = client.download_to_file (
133- " https://httpbin .org/redirect-to?url=https%3A%2F%2Fhttpbin .org%2Fbytes%2F50" ,
133+ " https://httpbingo .org/redirect-to?url=https%3A%2F%2Fhttpbingo .org%2Fbytes%2F50" ,
134134 dest
135135 );
136136
@@ -149,7 +149,7 @@ TEST_F(DownloadToFileTest, NoProgressCallbackStillWorks) {
149149 auto dest = tmpDir / " no_progress.bin" ;
150150
151151 auto result = client.download_to_file (
152- " https://httpbin .org/bytes/200" ,
152+ " https://httpbingo .org/bytes/200" ,
153153 dest
154154 );
155155
@@ -167,7 +167,7 @@ TEST_F(DownloadToFileTest, Http404ReturnsError) {
167167 auto dest = tmpDir / " not_found.bin" ;
168168
169169 auto result = client.download_to_file (
170- " https://httpbin .org/status/404" ,
170+ " https://httpbingo .org/status/404" ,
171171 dest
172172 );
173173
@@ -186,14 +186,194 @@ TEST_F(DownloadToFileTest, TotalBytesKnownForContentLength) {
186186 std::int64_t reportedTotal = -1 ;
187187
188188 auto result = client.download_to_file (
189- " https://httpbin .org/bytes/1024" ,
189+ " https://httpbingo .org/bytes/1024" ,
190190 dest,
191191 [&](std::int64_t total, [[maybe_unused]] std::int64_t downloaded) {
192192 if (reportedTotal < 0 ) reportedTotal = total;
193193 }
194194 );
195195
196196 ASSERT_TRUE (result.ok ()) << " Error: " << result.error ;
197- // httpbin /bytes/N returns Content-Length: N
197+ // httpbingo /bytes/N returns Content-Length: N
198198 EXPECT_EQ (reportedTotal, 1024 );
199199}
200+
201+ // Test download_to_file_parallel against endpoints with known Range behavior.
202+ // httpbingo.org /range/N honors Range (206) with deterministic content;
203+ // /bytes/N ignores Range (200), which exercises the fallback path.
204+
205+ class ParallelDownloadTest : public ::testing::Test {
206+ protected:
207+ std::filesystem::path tmpDir;
208+
209+ void SetUp () override {
210+ https::Socket::platform_init ();
211+ tmpDir = std::filesystem::temp_directory_path () / " tinyhttps_parallel_test" ;
212+ std::filesystem::create_directories (tmpDir);
213+ }
214+ void TearDown () override {
215+ std::error_code ec;
216+ std::filesystem::remove_all (tmpDir, ec);
217+ }
218+ };
219+
220+ TEST_F (ParallelDownloadTest, SegmentedResultMatchesSequential) {
221+ const std::string url = " https://httpbingo.org/range/65536" ;
222+
223+ https::HttpClient seqClient ({});
224+ auto seqDest = tmpDir / " seq.bin" ;
225+ auto seq = seqClient.download_to_file (url, seqDest);
226+ ASSERT_TRUE (seq.ok ()) << " Sequential error: " << seq.error ;
227+
228+ https::HttpClientConfig cfg;
229+ cfg.connectTimeoutMs = 15000 ;
230+ cfg.readTimeoutMs = 30000 ;
231+ cfg.maxConnectionsPerFile = 4 ;
232+ cfg.minSegmentBytes = 1024 ; // small so test files actually split
233+ https::HttpClient parClient (cfg);
234+
235+ auto parDest = tmpDir / " par.bin" ;
236+ auto par = parClient.download_to_file_parallel (url, parDest);
237+ ASSERT_TRUE (par.ok ()) << " Parallel error: " << par.error ;
238+
239+ EXPECT_EQ (par.bytesWritten , 65536 );
240+ ASSERT_TRUE (par.expectedBytes .has_value ());
241+ EXPECT_EQ (*par.expectedBytes , 65536 );
242+ EXPECT_EQ (std::filesystem::file_size (parDest), 65536u );
243+
244+ std::ifstream seqFile (seqDest, std::ios::binary);
245+ std::ifstream parFile (parDest, std::ios::binary);
246+ std::string seqContent{ std::istreambuf_iterator<char >(seqFile),
247+ std::istreambuf_iterator<char >() };
248+ std::string parContent{ std::istreambuf_iterator<char >(parFile),
249+ std::istreambuf_iterator<char >() };
250+ EXPECT_EQ (parContent, seqContent)
251+ << " Segmented download content differs from sequential" ;
252+ }
253+
254+ TEST_F (ParallelDownloadTest, MoreSegmentsThanConnections) {
255+ // maxSegments decoupled from connection count (aria2 -s): 16 segments
256+ // pulled by only 2 workers.
257+ https::HttpClientConfig cfg;
258+ cfg.connectTimeoutMs = 15000 ;
259+ cfg.readTimeoutMs = 30000 ;
260+ cfg.maxConnectionsPerFile = 2 ;
261+ cfg.maxSegments = 16 ;
262+ cfg.minSegmentBytes = 1024 ;
263+ https::HttpClient client (cfg);
264+
265+ auto dest = tmpDir / " decoupled.bin" ;
266+ auto result = client.download_to_file_parallel (
267+ " https://httpbingo.org/range/32768" , dest);
268+
269+ ASSERT_TRUE (result.ok ()) << " Error: " << result.error ;
270+ EXPECT_EQ (result.bytesWritten , 32768 );
271+ EXPECT_EQ (std::filesystem::file_size (dest), 32768u );
272+ }
273+
274+ TEST_F (ParallelDownloadTest, ProgressIsMonotonic) {
275+ https::HttpClientConfig cfg;
276+ cfg.connectTimeoutMs = 15000 ;
277+ cfg.readTimeoutMs = 30000 ;
278+ cfg.maxConnectionsPerFile = 4 ;
279+ cfg.minSegmentBytes = 1024 ;
280+ https::HttpClient client (cfg);
281+
282+ auto dest = tmpDir / " monotonic.bin" ;
283+ std::vector<std::int64_t > values;
284+ std::int64_t reportedTotal = -1 ;
285+
286+ auto result = client.download_to_file_parallel (
287+ " https://httpbingo.org/range/131072" ,
288+ dest,
289+ [&](std::int64_t total, std::int64_t downloaded) {
290+ reportedTotal = total;
291+ values.push_back (downloaded);
292+ }
293+ );
294+
295+ ASSERT_TRUE (result.ok ()) << " Error: " << result.error ;
296+ EXPECT_EQ (reportedTotal, 131072 );
297+ ASSERT_FALSE (values.empty ());
298+ EXPECT_EQ (values.back (), 131072 );
299+ for (std::size_t i = 1 ; i < values.size (); ++i) {
300+ EXPECT_GT (values[i], values[i - 1 ])
301+ << " Parallel progress not monotonic at index " << i;
302+ }
303+ }
304+
305+ TEST_F (ParallelDownloadTest, FallsBackWhenServerIgnoresRange) {
306+ // /bytes/N does not honor Range — probe gets 200 and the body is
307+ // streamed out over the single probe connection.
308+ https::HttpClientConfig cfg;
309+ cfg.connectTimeoutMs = 15000 ;
310+ cfg.readTimeoutMs = 30000 ;
311+ cfg.maxConnectionsPerFile = 4 ;
312+ cfg.minSegmentBytes = 1024 ;
313+ https::HttpClient client (cfg);
314+
315+ auto dest = tmpDir / " fallback.bin" ;
316+ auto result = client.download_to_file_parallel (
317+ " https://httpbingo.org/bytes/4096" , dest);
318+
319+ ASSERT_TRUE (result.ok ()) << " Error: " << result.error ;
320+ EXPECT_EQ (result.statusCode , 200 );
321+ EXPECT_EQ (result.bytesWritten , 4096 );
322+ EXPECT_EQ (std::filesystem::file_size (dest), 4096u );
323+ }
324+
325+ TEST_F (ParallelDownloadTest, SmallFileFallsBackToSequential) {
326+ // File smaller than minSegmentBytes — not worth splitting.
327+ https::HttpClientConfig cfg;
328+ cfg.connectTimeoutMs = 15000 ;
329+ cfg.readTimeoutMs = 30000 ;
330+ cfg.maxConnectionsPerFile = 4 ;
331+ cfg.minSegmentBytes = 1 << 20 ;
332+ https::HttpClient client (cfg);
333+
334+ auto dest = tmpDir / " small.bin" ;
335+ auto result = client.download_to_file_parallel (
336+ " https://httpbingo.org/range/2048" , dest);
337+
338+ ASSERT_TRUE (result.ok ()) << " Error: " << result.error ;
339+ EXPECT_EQ (result.statusCode , 200 );
340+ EXPECT_EQ (result.bytesWritten , 2048 );
341+ }
342+
343+ TEST_F (ParallelDownloadTest, FollowsRedirectBeforeSegmenting) {
344+ https::HttpClientConfig cfg;
345+ cfg.connectTimeoutMs = 15000 ;
346+ cfg.readTimeoutMs = 30000 ;
347+ cfg.maxConnectionsPerFile = 4 ;
348+ cfg.minSegmentBytes = 1024 ;
349+ https::HttpClient client (cfg);
350+
351+ auto dest = tmpDir / " redirected.bin" ;
352+ auto result = client.download_to_file_parallel (
353+ " https://httpbingo.org/redirect-to?url=https%3A%2F%2Fhttpbingo.org%2Frange%2F8192" ,
354+ dest);
355+
356+ ASSERT_TRUE (result.ok ()) << " Error: " << result.error ;
357+ EXPECT_EQ (result.bytesWritten , 8192 );
358+ EXPECT_EQ (result.finalUrl , " https://httpbingo.org/range/8192" );
359+ }
360+
361+ TEST_F (ParallelDownloadTest, CancellationAbortsWorkers) {
362+ https::HttpClientConfig cfg;
363+ cfg.connectTimeoutMs = 15000 ;
364+ cfg.readTimeoutMs = 30000 ;
365+ cfg.maxConnectionsPerFile = 4 ;
366+ cfg.minSegmentBytes = 1024 ;
367+ https::HttpClient client (cfg);
368+
369+ auto dest = tmpDir / " cancelled.bin" ;
370+ auto result = client.download_to_file_parallel (
371+ " https://httpbingo.org/range/524288" , // httpbingo /range caps at 512KiB
372+ dest,
373+ nullptr ,
374+ [] { return true ; } // cancel immediately
375+ );
376+
377+ EXPECT_FALSE (result.ok ());
378+ EXPECT_EQ (result.error , " cancelled" );
379+ }
0 commit comments