1 /* 2 * Copyright 2022 Haiku Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Niels Sascha Reedijk, niels.reedijk@gmail.com 7 */ 8 9 #include "HttpProtocolTest.h" 10 11 #include <cppunit/TestAssert.h> 12 #include <cppunit/TestCaller.h> 13 #include <cppunit/TestSuite.h> 14 #include <tools/cppunit/ThreadedTestCaller.h> 15 16 #include <DateTime.h> 17 #include <ExclusiveBorrow.h> 18 #include <HttpFields.h> 19 #include <HttpRequest.h> 20 #include <HttpResult.h> 21 #include <HttpTime.h> 22 #include <Looper.h> 23 #include <NetServicesDefs.h> 24 #include <Url.h> 25 26 using BPrivate::BDateTime; 27 using BPrivate::Network::BBorrow; 28 using BPrivate::Network::BExclusiveBorrow; 29 using BPrivate::Network::BHttpFields; 30 using BPrivate::Network::BHttpMethod; 31 using BPrivate::Network::BHttpRequest; 32 using BPrivate::Network::BHttpResult; 33 using BPrivate::Network::BHttpSession; 34 using BPrivate::Network::BHttpTime; 35 using BPrivate::Network::BHttpTimeFormat; 36 using BPrivate::Network::BNetworkRequestError; 37 using BPrivate::Network::format_http_time; 38 using BPrivate::Network::make_exclusive_borrow; 39 using BPrivate::Network::parse_http_time; 40 41 using namespace std::literals; 42 43 // Logger settings 44 constexpr bool LOG_ENABLED = true; 45 constexpr bool LOG_TO_CONSOLE = false; 46 47 48 HttpProtocolTest::HttpProtocolTest() 49 { 50 } 51 52 53 void 54 HttpProtocolTest::HttpFieldsTest() 55 { 56 // Header field name validation (ignore value validation) 57 { 58 auto fields = BHttpFields(); 59 try { 60 auto validFieldName = "Content-Encoding"sv; 61 fields.AddField(validFieldName, "value"sv); 62 } catch (...) { 63 CPPUNIT_FAIL("Unexpected exception when passing valid field name"); 64 } 65 try { 66 auto invalidFieldName = "Cóntênt_Éncõdìng"; 67 fields.AddField(invalidFieldName, "value"sv); 68 CPPUNIT_FAIL("Creating a header with an invalid name did not raise an exception"); 69 } catch (const BHttpFields::InvalidInput& e) { 70 // success 71 } 72 } 73 // Header field value validation (ignore name validation) 74 { 75 auto fields = BHttpFields(); 76 try { 77 auto validFieldValue = "VálìdF|êldValue"sv; 78 fields.AddField("Field"sv, validFieldValue); 79 } catch (...) { 80 CPPUNIT_FAIL("Unexpected exception when passing valid field value"); 81 } 82 try { 83 auto invalidFieldValue = "Invalid\tField\0Value"; 84 fields.AddField("Field"sv, invalidFieldValue); 85 CPPUNIT_FAIL("Creating a header with an invalid value did not raise an exception"); 86 } catch (const BHttpFields::InvalidInput& e) { 87 // success 88 } 89 } 90 91 // Header line parsing validation 92 { 93 auto fields = BHttpFields(); 94 try { 95 BString noWhiteSpace("Connection:close"); 96 fields.AddField(noWhiteSpace); 97 BString extraWhiteSpace("Connection: close\t\t \t"); 98 fields.AddField(extraWhiteSpace); 99 for (const auto& field: fields) { 100 std::string_view name = field.Name(); 101 CPPUNIT_ASSERT_EQUAL("Connection"sv, name); 102 CPPUNIT_ASSERT_EQUAL("close"sv, field.Value()); 103 } 104 } catch (const BHttpFields::InvalidInput& e) { 105 CPPUNIT_FAIL(e.input.String()); 106 CPPUNIT_FAIL("Unexpected exception when adding a header with an valid value"); 107 } 108 109 try { 110 BString noSeparator("Connection close"); 111 fields.AddField(noSeparator); 112 } catch (const BHttpFields::InvalidInput& e) { 113 // success 114 } catch (...) { 115 CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid value"); 116 } 117 118 try { 119 BString noName = (":close"); 120 fields.AddField(noName); 121 } catch (const BHttpFields::InvalidInput& e) { 122 // success 123 } catch (...) { 124 CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid value"); 125 } 126 127 try { 128 BString noValue = ("Connection :"); 129 fields.AddField(noValue); 130 } catch (const BHttpFields::InvalidInput& e) { 131 // success 132 } catch (...) { 133 CPPUNIT_FAIL("Unexpected exception when creating a header with an invalid value"); 134 } 135 } 136 137 // Header field name case insensitive comparison 138 { 139 BHttpFields fields = BHttpFields(); 140 fields.AddField("content-type"sv, "value"sv); 141 CPPUNIT_ASSERT(fields[0].Name() == "content-type"sv); 142 CPPUNIT_ASSERT(fields[0].Name() == "Content-Type"sv); 143 CPPUNIT_ASSERT(fields[0].Name() == "cOnTeNt-TyPe"sv); 144 CPPUNIT_ASSERT(fields[0].Name() != "content_type"sv); 145 CPPUNIT_ASSERT(fields[0].Name() == BString{"Content-Type"}); 146 } 147 148 // Set up a generic set of headers for further use 149 const BHttpFields defaultFields = {{"Host"sv, "haiku-os.org"sv}, {"Accept"sv, "*/*"sv}, 150 {"Set-Cookie"sv, "qwerty=494793ddkl; Domain=haiku-os.co.uk"sv}, 151 {"Set-Cookie"sv, "afbzyi=0kdnke0lyv; Domain=haiku-os.co.uk"sv}, 152 {}, // Empty; should be ignored by the constructor 153 {"Accept-Encoding"sv, "gzip"sv}}; 154 155 // Validate std::initializer_list constructor 156 CPPUNIT_ASSERT_EQUAL(5, defaultFields.CountFields()); 157 158 // Test copying and moving 159 { 160 BHttpFields copiedFields = defaultFields; 161 CPPUNIT_ASSERT_EQUAL(copiedFields.CountFields(), defaultFields.CountFields()); 162 for (size_t i = 0; i < defaultFields.CountFields(); i++) { 163 std::string_view copiedName = copiedFields[i].Name(); 164 CPPUNIT_ASSERT(defaultFields[i].Name() == copiedName); 165 CPPUNIT_ASSERT_EQUAL(defaultFields[i].Value(), copiedFields[i].Value()); 166 } 167 168 BHttpFields movedFields(std::move(copiedFields)); 169 CPPUNIT_ASSERT_EQUAL(movedFields.CountFields(), defaultFields.CountFields()); 170 for (size_t i = 0; i < movedFields.CountFields(); i++) { 171 std::string_view defaultName = defaultFields[i].Name(); 172 CPPUNIT_ASSERT(movedFields[i].Name() == defaultName); 173 CPPUNIT_ASSERT_EQUAL(movedFields[i].Value(), defaultFields[i].Value()); 174 } 175 176 CPPUNIT_ASSERT_EQUAL(copiedFields.CountFields(), 0); 177 } 178 179 // Test query and modification tools 180 { 181 BHttpFields fields = defaultFields; 182 // test order of adding fields (in order of construction) 183 fields.AddField("Set-Cookie"sv, "vfxdrm=9lpqrsvxm; Domain=haiku-os.co.uk"sv); 184 // query for Set-Cookie should find the first in the list 185 auto it = fields.FindField("Set-Cookie"sv); 186 CPPUNIT_ASSERT(it != fields.end()); 187 CPPUNIT_ASSERT((*it).Name() == "Set-Cookie"sv); 188 CPPUNIT_ASSERT_EQUAL(defaultFields[2].Value(), (*it).Value()); 189 190 // the last item should be the newly insterted one 191 it = fields.end(); 192 it--; 193 CPPUNIT_ASSERT(it != fields.begin()); 194 CPPUNIT_ASSERT((*it).Name() == "Set-Cookie"sv); 195 CPPUNIT_ASSERT_EQUAL("vfxdrm=9lpqrsvxm; Domain=haiku-os.co.uk"sv, (*it).Value()); 196 197 // the item before should be the Accept-Encoding one 198 it--; 199 CPPUNIT_ASSERT(it != fields.begin()); 200 CPPUNIT_ASSERT((*it).Name() == "Accept-Encoding"sv); 201 202 // remove the Accept-Encoding entry by iterator 203 fields.RemoveField(it); 204 CPPUNIT_ASSERT_EQUAL(fields.CountFields(), defaultFields.CountFields()); 205 // remove the Set-Cookie entries by name 206 fields.RemoveField("Set-Cookie"sv); 207 CPPUNIT_ASSERT_EQUAL(fields.CountFields(), 2); 208 // test MakeEmpty 209 fields.MakeEmpty(); 210 CPPUNIT_ASSERT_EQUAL(fields.CountFields(), 0); 211 } 212 213 // Iterate through the fields using a constant iterator 214 { 215 const BHttpFields fields = {{"key1"sv, "value1"sv}, {"key2"sv, "value2"sv}, 216 {"key3"sv, "value3"sv}, {"key4"sv, "value4"sv}}; 217 218 auto count = 0L; 219 for (const auto& field: fields) { 220 count++; 221 auto key = BString("key"); 222 auto value = BString("value"); 223 key << count; 224 value << count; 225 CPPUNIT_ASSERT_EQUAL(std::string_view(key.String()), field.Name()); 226 CPPUNIT_ASSERT_EQUAL(value, BString(field.Value().data(), field.Value().length())); 227 } 228 CPPUNIT_ASSERT_EQUAL(count, 4); 229 } 230 } 231 232 233 void 234 HttpProtocolTest::HttpMethodTest() 235 { 236 using namespace std::literals; 237 238 // Default methods 239 { 240 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Get).Method(), "GET"sv); 241 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Head).Method(), "HEAD"sv); 242 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Post).Method(), "POST"sv); 243 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Put).Method(), "PUT"sv); 244 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Delete).Method(), "DELETE"sv); 245 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Connect).Method(), "CONNECT"sv); 246 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Options).Method(), "OPTIONS"sv); 247 CPPUNIT_ASSERT_EQUAL(BHttpMethod(BHttpMethod::Trace).Method(), "TRACE"sv); 248 } 249 250 // Valid custom method 251 { 252 try { 253 auto method = BHttpMethod("PATCH"sv); 254 CPPUNIT_ASSERT_EQUAL(method.Method(), "PATCH"sv); 255 } catch (...) { 256 CPPUNIT_FAIL("Unexpected error when creating valid method"); 257 } 258 } 259 260 // Invalid empty method 261 try { 262 auto method = BHttpMethod(""); 263 CPPUNIT_FAIL("Creating an empty method was succesful unexpectedly"); 264 } catch (BHttpMethod::InvalidMethod&) { 265 // success 266 } 267 268 // Method with invalid characters (arabic translation of GET) 269 try { 270 auto method = BHttpMethod("جلب"); 271 CPPUNIT_FAIL("Creating a method with invalid characters was succesful unexpectedly"); 272 } catch (BHttpMethod::InvalidMethod&) { 273 // success 274 } 275 } 276 277 278 constexpr std::string_view kExpectedRequestText = "GET / HTTP/1.1\r\n" 279 "Host: www.haiku-os.org\r\n" 280 "Accept-Encoding: gzip\r\n" 281 "Connection: close\r\n" 282 "Api-Key: 01234567890abcdef\r\n\r\n"; 283 284 285 void 286 HttpProtocolTest::HttpRequestTest() 287 { 288 // Basic test 289 BHttpRequest request; 290 CPPUNIT_ASSERT(request.IsEmpty()); 291 auto url = BUrl("https://www.haiku-os.org"); 292 request.SetUrl(url); 293 CPPUNIT_ASSERT(request.Url() == url); 294 295 // Add Invalid HTTP fields (should throw) 296 try { 297 BHttpFields invalidField = {{"Host"sv, "haiku-os.org"sv}}; 298 request.SetFields(invalidField); 299 CPPUNIT_FAIL("Should not be able to add the invalid \"Host\" field to a request"); 300 } catch (BHttpFields::InvalidInput& e) { 301 // Correct; do nothing 302 } 303 304 // Add valid HTTP field 305 BHttpFields validField = {{"Api-Key"sv, "01234567890abcdef"}}; 306 request.SetFields(validField); 307 308 // Validate header serialization 309 BString header = request.HeaderToString(); 310 CPPUNIT_ASSERT(header.Compare(kExpectedRequestText.data(), kExpectedRequestText.size()) == 0); 311 } 312 313 314 void 315 HttpProtocolTest::HttpTimeTest() 316 { 317 const std::vector<BString> kValidTimeStrings 318 = {"Sun, 07 Dec 2003 16:01:00 GMT", "Sun, 07 Dec 2003 16:01:00", 319 "Sunday, 07-Dec-03 16:01:00 GMT", "Sunday, 07-Dec-03 16:01:00 GMT", 320 "Sunday, 07-Dec-2003 16:01:00", "Sunday, 07-Dec-2003 16:01:00 GMT", 321 "Sunday, 07-Dec-2003 16:01:00 UTC", "Sun Dec 7 16:01:00 2003"}; 322 const BDateTime kExpectedDateTime = {BDate{2003, 12, 7}, BTime{16, 01, 0}}; 323 324 for (const auto& timeString: kValidTimeStrings) { 325 CPPUNIT_ASSERT(kExpectedDateTime == parse_http_time(timeString)); 326 } 327 328 const std::vector<BString> kInvalidTimeStrings = { 329 "Sun, 07 Dec 2003", // Date only 330 "Sun, 07 Dec 2003 16:01:00 BST", // Invalid timezone 331 "On Sun, 07 Dec 2003 16:01:00 GMT", // Extra data in front of the string 332 }; 333 334 for (const auto& timeString: kInvalidTimeStrings) { 335 try { 336 parse_http_time(timeString); 337 BString errorMessage = "Expected exception with invalid timestring: "; 338 errorMessage.Append(timeString); 339 CPPUNIT_FAIL(errorMessage.String()); 340 } catch (const BHttpTime::InvalidInput& e) { 341 // expected exception; continue 342 } 343 } 344 345 // Validate format_http_time() 346 CPPUNIT_ASSERT_EQUAL( 347 BString("Sun, 07 Dec 2003 16:01:00 GMT"), format_http_time(kExpectedDateTime)); 348 CPPUNIT_ASSERT_EQUAL(BString("Sunday, 07-Dec-03 16:01:00 GMT"), 349 format_http_time(kExpectedDateTime, BHttpTimeFormat::RFC850)); 350 CPPUNIT_ASSERT_EQUAL(BString("Sun Dec 7 16:01:00 2003"), 351 format_http_time(kExpectedDateTime, BHttpTimeFormat::AscTime)); 352 } 353 354 355 /* static */ void 356 HttpProtocolTest::AddTests(BTestSuite& parent) 357 { 358 CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpProtocolTest"); 359 360 suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>( 361 "HttpProtocolTest::HttpFieldsTest", &HttpProtocolTest::HttpFieldsTest)); 362 suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>( 363 "HttpProtocolTest::HttpMethodTest", &HttpProtocolTest::HttpMethodTest)); 364 suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>( 365 "HttpProtocolTest::HttpRequestTest", &HttpProtocolTest::HttpRequestTest)); 366 suite.addTest(new CppUnit::TestCaller<HttpProtocolTest>( 367 "HttpProtocolTest::HttpTimeTest", &HttpProtocolTest::HttpTimeTest)); 368 369 parent.addTest("HttpProtocolTest", &suite); 370 } 371 372 373 // Observer test 374 375 #include <iostream> 376 class ObserverHelper : public BLooper 377 { 378 public: 379 ObserverHelper() 380 : 381 BLooper("ObserverHelper") 382 { 383 } 384 385 void MessageReceived(BMessage* msg) override { messages.emplace_back(*msg); } 386 387 std::vector<BMessage> messages; 388 }; 389 390 391 // HttpIntegrationTest 392 393 394 HttpIntegrationTest::HttpIntegrationTest(TestServerMode mode) 395 : 396 fTestServer(mode) 397 { 398 // increase number of concurrent connections to 4 (from 2) 399 fSession.SetMaxConnectionsPerHost(4); 400 401 if constexpr (LOG_ENABLED) { 402 fLogger = new HttpDebugLogger(); 403 fLogger->SetConsoleLogging(LOG_TO_CONSOLE); 404 if (mode == TestServerMode::Http) 405 fLogger->SetFileLogging("http-messages.log"); 406 else 407 fLogger->SetFileLogging("https-messages.log"); 408 fLogger->Run(); 409 fLoggerMessenger.SetTo(fLogger); 410 } 411 } 412 413 414 void 415 HttpIntegrationTest::setUp() 416 { 417 CPPUNIT_ASSERT_EQUAL_MESSAGE("Starting up test server", B_OK, fTestServer.Start()); 418 } 419 420 421 void 422 HttpIntegrationTest::tearDown() 423 { 424 if (fLogger) { 425 fLogger->Lock(); 426 fLogger->Quit(); 427 } 428 } 429 430 431 /* static */ void 432 HttpIntegrationTest::AddTests(BTestSuite& parent) 433 { 434 // Http 435 { 436 CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpIntegrationTest"); 437 438 HttpIntegrationTest* httpIntegrationTest = new HttpIntegrationTest(TestServerMode::Http); 439 BThreadedTestCaller<HttpIntegrationTest>* testCaller 440 = new BThreadedTestCaller<HttpIntegrationTest>("HttpTest::", httpIntegrationTest); 441 442 // HTTP 443 testCaller->addThread( 444 "HostAndNetworkFailTest", &HttpIntegrationTest::HostAndNetworkFailTest); 445 testCaller->addThread("GetTest", &HttpIntegrationTest::GetTest); 446 testCaller->addThread("GetWithBufferTest", &HttpIntegrationTest::GetWithBufferTest); 447 testCaller->addThread("HeadTest", &HttpIntegrationTest::HeadTest); 448 testCaller->addThread("NoContentTest", &HttpIntegrationTest::NoContentTest); 449 testCaller->addThread("AutoRedirectTest", &HttpIntegrationTest::AutoRedirectTest); 450 testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest); 451 testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest); 452 testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest); 453 testCaller->addThread("PostTest", &HttpIntegrationTest::PostTest); 454 455 suite.addTest(testCaller); 456 parent.addTest("HttpIntegrationTest", &suite); 457 } 458 459 // Https 460 { 461 CppUnit::TestSuite& suite = *new CppUnit::TestSuite("HttpsIntegrationTest"); 462 463 HttpIntegrationTest* httpsIntegrationTest = new HttpIntegrationTest(TestServerMode::Https); 464 BThreadedTestCaller<HttpIntegrationTest>* testCaller 465 = new BThreadedTestCaller<HttpIntegrationTest>("HttpsTest::", httpsIntegrationTest); 466 467 // HTTPS 468 testCaller->addThread( 469 "HostAndNetworkFailTest", &HttpIntegrationTest::HostAndNetworkFailTest); 470 testCaller->addThread("GetTest", &HttpIntegrationTest::GetTest); 471 testCaller->addThread("GetWithBufferTest", &HttpIntegrationTest::GetWithBufferTest); 472 testCaller->addThread("HeadTest", &HttpIntegrationTest::HeadTest); 473 testCaller->addThread("NoContentTest", &HttpIntegrationTest::NoContentTest); 474 testCaller->addThread("AutoRedirectTest", &HttpIntegrationTest::AutoRedirectTest); 475 // testCaller->addThread("BasicAuthTest", &HttpIntegrationTest::BasicAuthTest); 476 // Skip BasicAuthTest for HTTPS: it seems like it does not close the socket properly, 477 // raising a SSL EOF error. 478 testCaller->addThread("StopOnErrorTest", &HttpIntegrationTest::StopOnErrorTest); 479 testCaller->addThread("RequestCancelTest", &HttpIntegrationTest::RequestCancelTest); 480 testCaller->addThread("PostTest", &HttpIntegrationTest::PostTest); 481 482 suite.addTest(testCaller); 483 parent.addTest("HttpsIntegrationTest", &suite); 484 } 485 } 486 487 488 void 489 HttpIntegrationTest::HostAndNetworkFailTest() 490 { 491 // Test hostname resolution fail 492 { 493 auto request = BHttpRequest(BUrl("http://doesnotexist/")); 494 auto result = fSession.Execute(std::move(request)); 495 try { 496 result.Status(); 497 CPPUNIT_FAIL("Expecting exception when trying to connect to invalid hostname"); 498 } catch (const BNetworkRequestError& e) { 499 CPPUNIT_ASSERT_EQUAL(BNetworkRequestError::HostnameError, e.Type()); 500 } 501 } 502 503 // Test connection error fail 504 { 505 // FIXME: find a better way to get an unused local port, instead of hardcoding one 506 auto request = BHttpRequest(BUrl("http://localhost:59445/")); 507 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 508 try { 509 result.Status(); 510 CPPUNIT_FAIL("Expecting exception when trying to connect to invalid hostname"); 511 } catch (const BNetworkRequestError& e) { 512 CPPUNIT_ASSERT_EQUAL(BNetworkRequestError::NetworkError, e.Type()); 513 } 514 } 515 } 516 517 518 static const BHttpFields kExpectedGetFields = { 519 {"Server"sv, "Test HTTP Server for Haiku"sv}, 520 {"Date"sv, "Sun, 09 Feb 2020 19:32:42 GMT"sv}, 521 {"Content-Type"sv, "text/plain"sv}, 522 {"Content-Length"sv, "107"sv}, 523 {"Content-Encoding"sv, "gzip"sv}, 524 }; 525 526 527 constexpr std::string_view kExpectedGetBody = {"Path: /\r\n" 528 "\r\n" 529 "Headers:\r\n" 530 "--------\r\n" 531 "Host: 127.0.0.1:PORT\r\n" 532 "Accept-Encoding: gzip\r\n" 533 "Connection: close\r\n"}; 534 535 536 void 537 HttpIntegrationTest::GetTest() 538 { 539 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/")); 540 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 541 try { 542 auto receivedFields = result.Fields(); 543 544 CPPUNIT_ASSERT_EQUAL_MESSAGE("Mismatch in number of headers", 545 kExpectedGetFields.CountFields(), receivedFields.CountFields()); 546 for (auto& field: receivedFields) { 547 auto expectedField = kExpectedGetFields.FindField(field.Name()); 548 if (expectedField == kExpectedGetFields.end()) 549 CPPUNIT_FAIL("Could not find expected field in response headers"); 550 551 CPPUNIT_ASSERT_EQUAL(field.Value(), (*expectedField).Value()); 552 } 553 auto receivedBody = result.Body().text; 554 CPPUNIT_ASSERT(receivedBody.has_value()); 555 CPPUNIT_ASSERT_EQUAL(kExpectedGetBody, receivedBody.value().String()); 556 } catch (const BPrivate::Network::BError& e) { 557 CPPUNIT_FAIL(e.DebugMessage().String()); 558 } 559 } 560 561 562 void 563 HttpIntegrationTest::GetWithBufferTest() 564 { 565 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/")); 566 auto body = make_exclusive_borrow<BMallocIO>(); 567 auto result = fSession.Execute(std::move(request), BBorrow<BDataIO>(body), fLoggerMessenger); 568 try { 569 result.Body(); 570 auto bodyString 571 = std::string(reinterpret_cast<const char*>(body->Buffer()), body->BufferLength()); 572 CPPUNIT_ASSERT_EQUAL(kExpectedGetBody, bodyString); 573 } catch (const BPrivate::Network::BError& e) { 574 CPPUNIT_FAIL(e.DebugMessage().String()); 575 } 576 } 577 578 579 void 580 HttpIntegrationTest::HeadTest() 581 { 582 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/")); 583 request.SetMethod(BHttpMethod::Head); 584 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 585 try { 586 auto receivedFields = result.Fields(); 587 CPPUNIT_ASSERT_EQUAL_MESSAGE("Mismatch in number of headers", 588 kExpectedGetFields.CountFields(), receivedFields.CountFields()); 589 for (auto& field: receivedFields) { 590 auto expectedField = kExpectedGetFields.FindField(field.Name()); 591 if (expectedField == kExpectedGetFields.end()) 592 CPPUNIT_FAIL("Could not find expected field in response headers"); 593 594 CPPUNIT_ASSERT_EQUAL(field.Value(), (*expectedField).Value()); 595 } 596 597 CPPUNIT_ASSERT(result.Body().text->Length() == 0); 598 } catch (const BPrivate::Network::BError& e) { 599 CPPUNIT_FAIL(e.DebugMessage().String()); 600 } 601 } 602 603 604 static const BHttpFields kExpectedNoContentFields = { 605 {"Server"sv, "Test HTTP Server for Haiku"sv}, 606 {"Date"sv, "Sun, 09 Feb 2020 19:32:42 GMT"sv}, 607 }; 608 609 610 void 611 HttpIntegrationTest::NoContentTest() 612 { 613 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/204")); 614 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 615 try { 616 auto receivedStatus = result.Status(); 617 CPPUNIT_ASSERT_EQUAL(204, receivedStatus.code); 618 619 auto receivedFields = result.Fields(); 620 CPPUNIT_ASSERT_EQUAL_MESSAGE("Mismatch in number of headers", 621 kExpectedNoContentFields.CountFields(), receivedFields.CountFields()); 622 for (auto& field: receivedFields) { 623 auto expectedField = kExpectedNoContentFields.FindField(field.Name()); 624 if (expectedField == kExpectedNoContentFields.end()) 625 CPPUNIT_FAIL("Could not find expected field in response headers"); 626 627 CPPUNIT_ASSERT_EQUAL(field.Value(), (*expectedField).Value()); 628 } 629 630 CPPUNIT_ASSERT(result.Body().text->Length() == 0); 631 } catch (const BPrivate::Network::BError& e) { 632 CPPUNIT_FAIL(e.DebugMessage().String()); 633 } 634 } 635 636 637 void 638 HttpIntegrationTest::AutoRedirectTest() 639 { 640 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/302")); 641 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 642 try { 643 auto receivedFields = result.Fields(); 644 645 CPPUNIT_ASSERT_EQUAL_MESSAGE("Mismatch in number of headers", 646 kExpectedGetFields.CountFields(), receivedFields.CountFields()); 647 for (auto& field: receivedFields) { 648 auto expectedField = kExpectedGetFields.FindField(field.Name()); 649 if (expectedField == kExpectedGetFields.end()) 650 CPPUNIT_FAIL("Could not find expected field in response headers"); 651 652 CPPUNIT_ASSERT_EQUAL(field.Value(), (*expectedField).Value()); 653 } 654 auto receivedBody = result.Body().text; 655 CPPUNIT_ASSERT(receivedBody.has_value()); 656 CPPUNIT_ASSERT_EQUAL(kExpectedGetBody, receivedBody.value().String()); 657 } catch (const BPrivate::Network::BError& e) { 658 CPPUNIT_FAIL(e.DebugMessage().String()); 659 } 660 } 661 662 663 void 664 HttpIntegrationTest::BasicAuthTest() 665 { 666 // Basic Authentication 667 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/auth/basic/walter/secret")); 668 request.SetAuthentication({"walter", "secret"}); 669 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 670 CPPUNIT_ASSERT(result.Status().code == 200); 671 672 // Basic Authentication with incorrect credentials 673 try { 674 request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/auth/basic/walter/secret")); 675 request.SetAuthentication({"invaliduser", "invalidpassword"}); 676 result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 677 CPPUNIT_ASSERT(result.Status().code == 401); 678 } catch (const BPrivate::Network::BError& e) { 679 CPPUNIT_FAIL(e.DebugMessage().String()); 680 } 681 } 682 683 684 void 685 HttpIntegrationTest::StopOnErrorTest() 686 { 687 // Test the Stop on Error functionality 688 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/400")); 689 request.SetStopOnError(true); 690 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 691 CPPUNIT_ASSERT(result.Status().code == 400); 692 CPPUNIT_ASSERT(result.Fields().CountFields() == 0); 693 CPPUNIT_ASSERT(result.Body().text->Length() == 0); 694 } 695 696 697 void 698 HttpIntegrationTest::RequestCancelTest() 699 { 700 // Test the cancellation functionality 701 // TODO: this test potentially fails if the case is executed before the cancellation is 702 // processed. In practise, the cancellation always comes first. When the server 703 // supports a wait parameter, then this test can be made more robust. 704 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/")); 705 auto result = fSession.Execute(std::move(request), nullptr, fLoggerMessenger); 706 fSession.Cancel(result); 707 try { 708 result.Body(); 709 CPPUNIT_FAIL("Expected exception because request was cancelled"); 710 } catch (const BNetworkRequestError& e) { 711 CPPUNIT_ASSERT(e.Type() == BNetworkRequestError::Canceled); 712 } 713 } 714 715 716 static const BString kPostText 717 = "The MIT License\n" 718 "\n" 719 "Copyright (c) <year> <copyright holders>\n" 720 "\n" 721 "Permission is hereby granted, free of charge, to any person obtaining a copy\n" 722 "of this software and associated documentation files (the \"Software\"), to deal\n" 723 "in the Software without restriction, including without limitation the rights\n" 724 "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n" 725 "copies of the Software, and to permit persons to whom the Software is\n" 726 "furnished to do so, subject to the following conditions:\n" 727 "\n" 728 "The above copyright notice and this permission notice shall be included in\n" 729 "all copies or substantial portions of the Software.\n" 730 "\n" 731 "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n" 732 "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n" 733 "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n" 734 "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n" 735 "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n" 736 "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n" 737 "THE SOFTWARE.\n" 738 "\n"; 739 740 741 static BString kExpectedPostBody = BString().SetToFormat("Path: /post\r\n" 742 "\r\n" 743 "Headers:\r\n" 744 "--------\r\n" 745 "Host: 127.0.0.1:PORT\r\n" 746 "Accept-Encoding: gzip\r\n" 747 "Connection: close\r\n" 748 "Content-Type: text/plain\r\n" 749 "Content-Length: 1083\r\n" 750 "\r\n" 751 "Request body:\r\n" 752 "-------------\r\n" 753 "%s\r\n", 754 kPostText.String()); 755 756 757 void 758 HttpIntegrationTest::PostTest() 759 { 760 using namespace BPrivate::Network::UrlEvent; 761 using namespace BPrivate::Network::UrlEventData; 762 763 auto postBody = std::make_unique<BMallocIO>(); 764 postBody->Write(kPostText.String(), kPostText.Length()); 765 postBody->Seek(0, SEEK_SET); 766 auto request = BHttpRequest(BUrl(fTestServer.BaseUrl(), "/post")); 767 request.SetMethod(BHttpMethod::Post); 768 request.SetRequestBody(std::move(postBody), "text/plain", kPostText.Length()); 769 770 auto observer = new ObserverHelper(); 771 observer->Run(); 772 773 auto result = fSession.Execute(std::move(request), nullptr, BMessenger(observer)); 774 775 CPPUNIT_ASSERT(result.Body().text.has_value()); 776 CPPUNIT_ASSERT_EQUAL(kExpectedPostBody.Length(), result.Body().text.value().Length()); 777 CPPUNIT_ASSERT(result.Body().text.value() == kExpectedPostBody); 778 779 usleep(2000); // give some time to catch up on receiving all messages 780 781 observer->Lock(); 782 while (observer->IsMessageWaiting()) { 783 observer->Unlock(); 784 usleep(1000); // give some time to catch up on receiving all messages 785 observer->Lock(); 786 } 787 788 // Assert that the messages have the right contents. 789 CPPUNIT_ASSERT_MESSAGE( 790 "Expected at least 8 observer messages for this request.", observer->messages.size() >= 8); 791 792 uint32 previousMessage = 0; 793 for (const auto& message: observer->messages) { 794 auto id = observer->messages[0].GetInt32(BPrivate::Network::UrlEventData::Id, -1); 795 CPPUNIT_ASSERT_EQUAL_MESSAGE("message Id does not match", result.Identity(), id); 796 797 if (message.what == BPrivate::Network::UrlEvent::DebugMessage) { 798 // ignore debug messages 799 continue; 800 } 801 802 switch (previousMessage) { 803 case 0: 804 CPPUNIT_ASSERT_MESSAGE( 805 "message should be HostNameResolved", HostNameResolved == message.what); 806 break; 807 808 case HostNameResolved: 809 CPPUNIT_ASSERT_MESSAGE( 810 "message should be ConnectionOpened", ConnectionOpened == message.what); 811 break; 812 813 case ConnectionOpened: 814 CPPUNIT_ASSERT_MESSAGE( 815 "message should be UploadProgress", UploadProgress == message.what); 816 [[fallthrough]]; 817 818 case UploadProgress: 819 switch (message.what) { 820 case UploadProgress: 821 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::NumBytes data", 822 message.HasInt64(NumBytes)); 823 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::TotalBytes data", 824 message.HasInt64(TotalBytes)); 825 CPPUNIT_ASSERT_MESSAGE("UrlEventData::TotalBytes size does not match", 826 kPostText.Length() == message.GetInt64(TotalBytes, 0)); 827 break; 828 case ResponseStarted: 829 break; 830 default: 831 CPPUNIT_FAIL("Expected UploadProgress or ResponseStarted message"); 832 } 833 break; 834 835 case ResponseStarted: 836 CPPUNIT_ASSERT_MESSAGE("message should be HttpStatus", HttpStatus == message.what); 837 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::HttpStatusCode data", 838 message.HasInt16(HttpStatusCode)); 839 break; 840 841 case HttpStatus: 842 CPPUNIT_ASSERT_MESSAGE("message should be HttpFields", HttpFields == message.what); 843 break; 844 845 case HttpFields: 846 CPPUNIT_ASSERT_MESSAGE( 847 "message should be DownloadProgress", DownloadProgress == message.what); 848 [[fallthrough]]; 849 850 case DownloadProgress: 851 case BytesWritten: 852 switch (message.what) { 853 case DownloadProgress: 854 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::NumBytes data", 855 message.HasInt64(NumBytes)); 856 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::TotalBytes data", 857 message.HasInt64(TotalBytes)); 858 break; 859 case BytesWritten: 860 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::NumBytes data", 861 message.HasInt64(NumBytes)); 862 break; 863 case RequestCompleted: 864 CPPUNIT_ASSERT_MESSAGE("message must have UrlEventData::Success data", 865 message.HasBool(Success)); 866 CPPUNIT_ASSERT_MESSAGE( 867 "UrlEventData::Success must be true", message.GetBool(Success)); 868 break; 869 default: 870 CPPUNIT_FAIL("Expected DownloadProgress, BytesWritten or HttpStatus " 871 "message"); 872 } 873 break; 874 875 default: 876 CPPUNIT_FAIL("Unexpected message"); 877 } 878 previousMessage = message.what; 879 } 880 881 observer->Quit(); 882 } 883