Imported Upstream version 17.22.1
[platform/upstream/libzypp.git] / tests / zyppng / media / EvDownloader_test.cc
1 #include <zypp/zyppng/base/EventDispatcher>
2 #include <zypp/zyppng/media/network/downloader.h>
3 #include <zypp/zyppng/media/network/networkrequesterror.h>
4 #include <zypp/zyppng/media/network/networkrequestdispatcher.h>
5 #include <zypp/zyppng/media/network/request.h>
6 #include <zypp/TmpPath.h>
7 #include <zypp/PathInfo.h>
8 #include <zypp/ZConfig.h>
9 #include <iostream>
10 #include <fstream>
11 #include <random>
12 #include "WebServer.h"
13
14 #include <boost/test/unit_test.hpp>
15 #include <boost/test/data/test_case.hpp>
16
17 #define BOOST_TEST_REQ_SUCCESS(REQ) \
18   do { \
19       BOOST_REQUIRE_MESSAGE( REQ->state() == zyppng::Download::Success, zypp::str::Format(" %1% != zyppng::Download::Success (%2%)") % REQ->state() % REQ->errorString() ); \
20       BOOST_REQUIRE_EQUAL( REQ->lastRequestError().type(), zyppng::NetworkRequestError::NoError ); \
21       BOOST_REQUIRE( REQ->errorString().empty() ); \
22   } while(false)
23
24 #define BOOST_TEST_REQ_FAILED(REQ) \
25   do { \
26       BOOST_REQUIRE_EQUAL( REQ->state(), zyppng::Download::Failed ); \
27       BOOST_REQUIRE_NE( REQ->lastRequestError().type(), zyppng::NetworkRequestError::NoError ); \
28       BOOST_REQUIRE( !REQ->errorString().empty() ); \
29   } while(false)
30
31 namespace bdata = boost::unit_test::data;
32
33 bool withSSL[] = {true, false};
34
35 //read all contents of a file into a string)
36 std::string readFile ( const zypp::Pathname &file )
37 {
38   if ( ! zypp::PathInfo( file ).isFile() ) {
39     return std::string();
40   }
41   std::ifstream istr( file.asString().c_str() );
42   if ( ! istr ) {
43     return std::string();
44   }
45
46   std::string str((std::istreambuf_iterator<char>(istr)),
47     std::istreambuf_iterator<char>());
48   return str;
49 }
50
51
52 BOOST_DATA_TEST_CASE( dltest_basic, bdata::make( withSSL ), withSSL)
53 {
54   auto ev = zyppng::EventDispatcher::createMain();
55
56   zyppng::Downloader downloader;
57
58   //make sure the data here is big enough to cross the threshold of 256 bytes so we get a progress signal emitted and not only the alive signal.
59   std::string dummyContent = "This is just some dummy content,\nto test downloading and signals.\n"
60                              "This is just some dummy content,\nto test downloading and signals.\n"
61                              "This is just some dummy content,\nto test downloading and signals.\n"
62                              "This is just some dummy content,\nto test downloading and signals.\n";
63
64   WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"data"/"dummywebroot").c_str(), 10001, withSSL );
65   web.addRequestHandler("getData", WebServer::makeResponse("200", dummyContent ) );
66   BOOST_REQUIRE( web.start() );
67
68   zypp::filesystem::TmpFile targetFile;
69   zyppng::Url weburl (web.url());
70   weburl.setPathName("/handler/getData");
71   zyppng::TransferSettings set = web.transferSettings();
72
73   bool gotStarted = false;
74   bool gotFinished = false;
75   bool gotProgress = false;
76   bool gotAlive = false;
77   off_t lastProgress = 0;
78   off_t totalDL = 0;
79
80   std::vector<zyppng::Download::State> allStates;
81
82
83   zyppng::Download::Ptr dl = downloader.downloadFile(  weburl, targetFile.path(), dummyContent.length() );
84   dl->settings() = set;
85
86   dl->sigFinished().connect([&]( zyppng::Download & ){
87     gotFinished = true;
88     ev->quit();
89   });
90
91   dl->sigStarted().connect([&]( zyppng::Download & ){
92     gotStarted = true;
93   });
94
95   dl->sigAlive().connect([&]( zyppng::Download &, off_t ){
96     gotAlive = true;
97   });
98
99   dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
100     allStates.push_back( state );
101   });
102
103   dl->sigProgress().connect([&]( zyppng::Download &, off_t dltotal, off_t dlnow ){
104     gotProgress = true;
105     lastProgress = dlnow;
106     totalDL = dltotal;
107   });
108   dl->start();
109   ev->run();
110
111   BOOST_TEST_REQ_SUCCESS( dl );
112   BOOST_REQUIRE( gotStarted );
113   BOOST_REQUIRE( gotFinished );
114   BOOST_REQUIRE( gotProgress );
115   BOOST_REQUIRE( gotAlive );
116   BOOST_REQUIRE_EQUAL( totalDL, dummyContent.length() );
117   BOOST_REQUIRE_EQUAL( lastProgress, dummyContent.length() );
118   BOOST_REQUIRE ( allStates == std::vector<zyppng::Download::State>({zyppng::Download::Initializing,zyppng::Download::Running, zyppng::Download::Success}) );
119 }
120
121
122 struct MirrorSet
123 {
124   std::string name; //<dataset name, used only in debug output if the test fails
125   std::string handlerPath; //< the webhandler path used to query the resource
126   std::vector< std::pair<int, std::string> > mirrors; //all mirrors injected into the metalink file
127   int expectedFileDownloads; //< how many downloads are direct file downloads
128   int expectedHandlerDownloads; //< how many started downloads are handler requests
129   std::vector<zyppng::Download::State> expectedStates;
130   bool expectSuccess; //< should the download work out?
131
132   std::ostream & operator<<( std::ostream & str ) const {
133     str << "MirrorSet{ " << name << " }";
134     return str;
135   }
136 };
137
138 namespace boost{ namespace test_tools{ namespace tt_detail{
139 template<>
140 struct print_log_value< MirrorSet > {
141     void    operator()( std::ostream& str, MirrorSet const& set) {
142       set.operator<<( str );
143     }
144 };
145 }}}
146
147 std::vector< MirrorSet > generateMirr ()
148 {
149   std::vector< MirrorSet > res;
150
151   //all mirrors good:
152   res.push_back( MirrorSet() );
153   res.back().name = "All good mirrors";
154   res.back().expectSuccess = true;
155   res.back().expectedHandlerDownloads  = 1;
156   res.back().expectedFileDownloads  = 9;
157   res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
158   for ( int i = 100 ; i >= 10; i -= 10 )
159     res.back().mirrors.push_back( std::make_pair( i, "/test.txt") );
160
161   //no mirrors:
162   res.push_back( MirrorSet() );
163   res.back().name = "Empty mirrors";
164   res.back().expectSuccess = true;
165   res.back().expectedHandlerDownloads  = 10;
166   res.back().expectedFileDownloads  = 0;
167   res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
168
169   //only broken mirrors:
170   res.push_back( MirrorSet() );
171   res.back().name = "All broken mirrors";
172   res.back().expectSuccess = true;
173   res.back().expectedHandlerDownloads  = 2; //has to fall back to url handler download
174   res.back().expectedFileDownloads  = 10; //should try all mirrors and fail
175   res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Running, zyppng::Download::Success};
176   for ( int i = 100 ; i >= 10; i -= 10 )
177     res.back().mirrors.push_back( std::make_pair( i, "/doesnotexist.txt") );
178
179   //some broken mirrors:
180   res.push_back( MirrorSet() );
181   res.back().name = "Some broken mirrors less URLs than blocks";
182   res.back().expectSuccess = true;
183   res.back().expectedHandlerDownloads = 1;
184   res.back().expectedFileDownloads = 9 + 3; // 3 should fail due to broken mirrors
185   res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
186   for ( int i = 10 ; i >= 5; i-- ) {
187     if ( i % 2 ) {
188       res.back().mirrors.push_back( std::make_pair( i*10, "/doesnotexist.txt") );
189     } else {
190       res.back().mirrors.push_back( std::make_pair( i*10, "/test.txt") );
191     }
192   }
193
194   //some broken mirrors with more URLs than blocks:
195   res.push_back( MirrorSet() );
196   res.back().name = "Some broken mirrors more URLs than blocks";
197   res.back().expectSuccess = true;
198   res.back().expectedHandlerDownloads = 1;
199   //its not really possible to know how many times the downloads will fail, there are
200   //5 broken mirrors in the set, but if a working mirror is done before the last broken
201   //URL is picked from the dataset not all broken URLs will be used
202   res.back().expectedFileDownloads  = -1;
203   res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
204   for ( int i = 10 ; i >= 1; i-- ) {
205     if ( i % 2 ) {
206       res.back().mirrors.push_back( std::make_pair( i*10, "/doesnotexist.txt") );
207     } else {
208       res.back().mirrors.push_back( std::make_pair( i*10, "/test.txt") );
209     }
210   }
211
212   //mirrors where some return a invalid block
213   res.push_back( MirrorSet() );
214   res.back().name = "Some mirrors return broken blocks";
215   res.back().expectSuccess = true;
216   res.back().expectedHandlerDownloads  = 1;
217   //its not really possible to know how many times the downloads will fail, there are
218   //5 broken mirrors in the set, but if a working mirror is done before the last broken
219   //URL is picked from the dataset not all broken URLs will be used
220   res.back().expectedFileDownloads  = -1;
221   res.back().expectedStates = {zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success};
222   for ( int i = 10 ; i >= 1; i-- ) {
223     if ( i % 2 ) {
224       res.back().mirrors.push_back( std::make_pair( i*10, "/handler/random") );
225     } else {
226       res.back().mirrors.push_back( std::make_pair( i*10, "/test.txt") );
227     }
228   }
229   return res;
230 }
231
232
233 //create one URL line for a metalink template file
234 std::string makeUrl ( int pref, const zyppng::Url &url )
235 {
236   return ( zypp::str::Format( "<url preference=\"%1%\" location=\"de\" type=\"%2%\">%3%</url>" ) % pref % url.getScheme() % url );
237 };
238
239
240 static bool requestWantsMetaLink ( const WebServer::Request &req )
241 {
242   auto it = req.params.find( "HTTP_ACCEPT" );
243   if ( it != req.params.end() ) {
244     if ( (*it).second.find("application/metalink+xml")  != std::string::npos ||
245          (*it).second.find("application/metalink4+xml") != std::string::npos ) {
246       return true;
247     }
248   }
249   return false;
250 }
251
252 //creates a request handler for the Mock WebServer that returns the metalink data
253 //specified in \a data if the request has the metalink accept handler
254 WebServer::RequestHandler makeMetaFileHandler ( const std::string *data )
255 {
256   return [ data ]( WebServer::Request &req ){
257     if ( requestWantsMetaLink( req ) ) {
258       req.rout << WebServer::makeResponseString( "200", { "Content-Type: application/metalink+xml; charset=utf-8\r\n" }, *data );
259       return;
260     }
261     req.rout << "Location: /test.txt\r\n\r\n";
262     return;
263   };
264 };
265
266 //creates a request handler for the Mock WebServer that returns a junk block of
267 //data for a range request, otherwise relocates the request
268 WebServer::RequestHandler makeJunkBlockHandler ( )
269 {
270   return [ ]( WebServer::Request &req ){
271     auto it = req.params.find( "HTTP_RANGE" );
272     if ( it != req.params.end() && zypp::str::startsWith( it->second, "bytes=" ) ) {
273         //bytes=786432-1048575
274         std::string range = it->second.substr( 6 ); //remove bytes=
275         size_t dash = range.find_first_of( "-" );
276         off_t start = -1;
277         off_t end = -1;
278         if ( dash != std::string::npos ) {
279           start = std::stoll( range.substr( 0, dash ) );
280           end   = std::stoll( range.substr( dash+1 ) );
281         }
282
283         if ( start != -1 && end != -1 ) {
284           std::string block;
285           for ( off_t curr = 0; curr < ( end - start); curr++ ) {
286             block += 'a';
287           }
288           req.rout << "Status: 206 Partial Content\r\n"
289                    << "Accept-Ranges: bytes\r\n"
290                    << "Content-Length: "<<( end - start)<<"\r\n\r\n"
291                    << block;
292           return;
293         }
294     }
295     req.rout << "Location: /test.txt\r\n\r\n";
296     return;
297   };
298 };
299
300 int maxConcurrentDLs[] = { 1, 2, 4, 8, 10, 15 };
301
302 BOOST_DATA_TEST_CASE( test1, bdata::make( generateMirr() ) * bdata::make( withSSL ) * bdata::make( maxConcurrentDLs )  , elem, withSSL, maxDLs )
303 {
304   //each URL in the metalink file has a preference , a schema and of course the URL, we need to adapt those to our test setup
305   //so we generate the file on the fly from a template in the test data
306   std::string metaTempl = readFile ( zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader/test.txt.meta" );
307   BOOST_REQUIRE( !metaTempl.empty() );
308
309   auto ev = zyppng::EventDispatcher::createMain();
310
311   WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader").c_str(), 10001, withSSL );
312   BOOST_REQUIRE( web.start() );
313
314   zypp::filesystem::TmpFile targetFile;
315   zyppng::Downloader downloader;
316   downloader.requestDispatcher()->setMaximumConcurrentConnections( maxDLs );
317
318   //first metalink download, generate a fully valid one
319   zyppng::Url weburl (web.url());
320   weburl.setPathName("/handler/test.txt");
321
322   std::string urls;
323   if ( elem.mirrors.size() ) {
324     for ( const auto &mirr : elem.mirrors ) {
325       zyppng::Url mirrUrl (web.url());
326       mirrUrl.setPathName( mirr.second );
327       urls += makeUrl( mirr.first, mirrUrl ) + "\n";
328     }
329   }
330
331   std::string metaFile = zypp::str::Format( metaTempl ) % urls;
332   web.addRequestHandler("test.txt", makeMetaFileHandler( &metaFile ) );
333   web.addRequestHandler("random", makeJunkBlockHandler( ) );
334
335   int expectedDownloads = elem.expectedHandlerDownloads + elem.expectedFileDownloads;
336   int startedDownloads = 0;
337   int finishedDownloads = 0;
338   bool downloadHadError = false;
339   bool gotProgress = false;
340   bool gotAlive = false;
341   bool gotMultiDLState = false;
342   std::vector<zyppng::Download::State> allStates;
343   off_t gotTotal = 0;
344   off_t lastProgress = 0;
345
346   int countHandlerReq = 0; //the requests made to the handler slot
347   int countFileReq = 0;    //the requests made to the file directly, e.g. a mirror read from the metalink file
348
349   auto dl = downloader.downloadFile( weburl, targetFile );
350   dl->settings() = web.transferSettings();
351
352   dl->dispatcher().sigDownloadStarted().connect( [&]( zyppng::NetworkRequestDispatcher &, zyppng::NetworkRequest &req){
353     startedDownloads++;
354     if ( req.url() == weburl )
355       countHandlerReq++;
356     else
357       countFileReq++;
358   });
359
360   dl->dispatcher().sigDownloadFinished().connect( [&]( zyppng::NetworkRequestDispatcher &, zyppng::NetworkRequest &req ){
361     finishedDownloads++;
362
363     if ( !downloadHadError )
364       downloadHadError = req.hasError();
365   });
366
367   dl->sigFinished().connect([&]( zyppng::Download & ){
368     ev->quit();
369   });
370
371   dl->sigAlive().connect([&]( zyppng::Download &, off_t dlnow ){
372     gotAlive = true;
373     lastProgress = dlnow;
374   });
375
376   dl->sigProgress().connect([&]( zyppng::Download &, off_t dltotal, off_t dlnow ){
377     gotProgress = true;
378     gotTotal = dltotal;
379     lastProgress = dlnow;
380   });
381
382   dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
383     if ( state == zyppng::Download::RunningMulti )
384       gotMultiDLState = true;
385   });
386
387   dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
388     allStates.push_back( state );
389   });
390
391   dl->start();
392   ev->run();
393
394   //std::cout << dl->errorString() << std::endl;
395
396   if ( elem.expectSuccess )
397     BOOST_TEST_REQ_SUCCESS( dl );
398   else
399     BOOST_TEST_REQ_FAILED ( dl );
400
401   if ( elem.expectedHandlerDownloads > -1 && elem.expectedFileDownloads > -1 )
402     BOOST_REQUIRE_EQUAL( startedDownloads, expectedDownloads );
403
404   BOOST_REQUIRE_EQUAL( startedDownloads, finishedDownloads );
405   BOOST_REQUIRE( gotAlive );
406   BOOST_REQUIRE( gotProgress );
407   BOOST_REQUIRE( gotMultiDLState );
408   BOOST_REQUIRE_EQUAL( lastProgress, 2148018 );
409   BOOST_REQUIRE_EQUAL( lastProgress, gotTotal );
410
411   if ( elem.expectedHandlerDownloads > -1 )
412     BOOST_REQUIRE_EQUAL( countHandlerReq, elem.expectedHandlerDownloads );
413
414   if ( elem.expectedFileDownloads > -1 )
415     BOOST_REQUIRE_EQUAL( countFileReq, elem.expectedFileDownloads );
416
417   if ( !elem.expectedStates.empty() )
418     BOOST_REQUIRE( elem.expectedStates == allStates );
419 }
420
421 //tests:
422 // - broken cert
423 // - correct expected filesize
424 // - invalid filesize
425 // - password handling and propagation
426
427
428 //creates a request handler that requires a authentication to work
429 WebServer::RequestHandler createAuthHandler ( )
430 {
431   return [ ]( WebServer::Request &req ){
432     //Basic dGVzdDp0ZXN0
433     auto it = req.params.find( "HTTP_AUTHORIZATION" );
434     bool authorized = false;
435     if ( it != req.params.end() )
436       authorized = ( it->second == "Basic dGVzdDp0ZXN0" );
437
438     if ( !authorized ) {
439       req.rout << "Status: 401 Unauthorized\r\n"
440                   "Content-Type: text/html; charset=utf-8\r\n"
441                   "WWW-Authenticate: Basic realm=\"User Visible Realm\", charset=\"UTF-8\" \r\n"
442                   "\r\n"
443                   "Sorry you are not authorized.";
444       return;
445     }
446
447     if ( requestWantsMetaLink( req ) ) {
448       it = req.params.find( "HTTPS" );
449       if ( it != req.params.end() && it->second == "on" ) {
450         req.rout << "Status: 307 Temporary Redirect\r\n"
451                  << "Cache-Control: no-cache\r\n"
452                  << "Location: /auth-https.meta\r\n\r\n";
453       } else {
454         req.rout << "Status: 307 Temporary Redirect\r\n"
455                  << "Cache-Control: no-cache\r\n"
456                  << "Location: /auth-http.meta\r\n\r\n";
457       }
458       return;
459     }
460     req.rout << "Status: 307 Temporary Redirect\r\n"
461                 "Location: /test.txt\r\n\r\n";
462     return;
463   };
464 };
465
466 BOOST_DATA_TEST_CASE( dltest_auth, bdata::make( withSSL ), withSSL )
467 {
468   //don't write or read creds from real settings dir
469   zypp::filesystem::TmpDir repoManagerRoot;
470   zypp::ZConfig::instance().setRepoManagerRoot( repoManagerRoot.path() );
471
472   auto ev = zyppng::EventDispatcher::createMain();
473
474   zyppng::Downloader downloader;
475
476   WebServer web((zypp::Pathname(TESTS_SRC_DIR)/"/zyppng/data/downloader").c_str(), 10001, withSSL );
477   BOOST_REQUIRE( web.start() );
478
479   zypp::filesystem::TmpFile targetFile;
480   zyppng::Url weburl (web.url());
481   weburl.setPathName("/handler/test.txt");
482   zyppng::TransferSettings set = web.transferSettings();
483
484   web.addRequestHandler( "test.txt", createAuthHandler() );
485   web.addRequestHandler( "quit", [ &ev ]( WebServer::Request & ){ ev->quit();} );
486
487   {
488     auto dl = downloader.downloadFile( weburl, targetFile.path() );
489     dl->settings() = set;
490
491     dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
492       ev->quit();
493     });
494
495     dl->start();
496     ev->run();
497     BOOST_TEST_REQ_FAILED( dl );
498     BOOST_REQUIRE_EQUAL( dl->lastRequestError().type(), zyppng::NetworkRequestError::Unauthorized );
499   }
500
501   {
502     auto dl = downloader.downloadFile( weburl, targetFile.path() );
503     dl->settings() = set;
504
505     int gotAuthRequest = 0;
506
507     dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
508       ev->quit();
509     });
510
511     dl->sigAuthRequired().connect( [&]( zyppng::Download &, zyppng::NetworkAuthData &auth, const std::string &availAuth ){
512       gotAuthRequest++;
513       if ( gotAuthRequest >= 2 )
514         return;
515       auth.setUsername("wrong");
516       auth.setPassword("credentials");
517     });
518
519     dl->start();
520     ev->run();
521     BOOST_TEST_REQ_FAILED( dl );
522     BOOST_REQUIRE_EQUAL( gotAuthRequest, 2 );
523     BOOST_REQUIRE_EQUAL( dl->lastRequestError().type(), zyppng::NetworkRequestError::AuthFailed );
524   }
525
526   {
527     int gotAuthRequest = 0;
528     std::vector<zyppng::Download::State> allStates;
529     auto dl = downloader.downloadFile( weburl, targetFile.path() );
530     dl->settings() = set;
531
532     dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
533       ev->quit();
534     });
535
536     dl->sigAuthRequired().connect( [&]( zyppng::Download &, zyppng::NetworkAuthData &auth, const std::string &availAuth ){
537       gotAuthRequest++;
538       auth.setUsername("test");
539       auth.setPassword("test");
540     });
541
542     dl->sigStateChanged().connect([&]( zyppng::Download &, zyppng::Download::State state ){
543       allStates.push_back( state );
544     });
545
546
547     dl->start();
548     ev->run();
549     BOOST_TEST_REQ_SUCCESS( dl );
550     BOOST_REQUIRE_EQUAL( gotAuthRequest, 1 );
551     BOOST_REQUIRE ( allStates == std::vector<zyppng::Download::State>({zyppng::Download::Initializing, zyppng::Download::RunningMulti, zyppng::Download::Success}) );
552   }
553
554   {
555     //the creds should be in the credential manager now, we should not need to specify them again in the slot
556     bool gotAuthRequest = false;
557     auto dl = downloader.downloadFile( weburl, targetFile.path() );
558     dl->settings() = set;
559
560     dl->sigFinished( ).connect([ &ev ]( zyppng::Download & ){
561       ev->quit();
562     });
563
564     dl->sigAuthRequired().connect( [&]( zyppng::Download &, zyppng::NetworkAuthData &auth, const std::string &availAuth ){
565       gotAuthRequest = true;
566     });
567
568     dl->start();
569     ev->run();
570     BOOST_TEST_REQ_SUCCESS( dl );
571     BOOST_REQUIRE( !gotAuthRequest );
572   }
573 }