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
HttpProtocolTest()48 HttpProtocolTest::HttpProtocolTest()
49 {
50 }
51
52
53 void
HttpFieldsTest()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
HttpMethodTest()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
HttpRequestTest()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
HttpTimeTest()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
AddTests(BTestSuite & parent)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:
ObserverHelper()379 ObserverHelper()
380 :
381 BLooper("ObserverHelper")
382 {
383 }
384
MessageReceived(BMessage * msg)385 void MessageReceived(BMessage* msg) override { messages.emplace_back(*msg); }
386
387 std::vector<BMessage> messages;
388 };
389
390
391 // HttpIntegrationTest
392
393
HttpIntegrationTest(TestServerMode mode)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
setUp()415 HttpIntegrationTest::setUp()
416 {
417 CPPUNIT_ASSERT_EQUAL_MESSAGE("Starting up test server", B_OK, fTestServer.Start());
418 }
419
420
421 void
tearDown()422 HttpIntegrationTest::tearDown()
423 {
424 if (fLogger) {
425 fLogger->Lock();
426 fLogger->Quit();
427 }
428 }
429
430
431 /* static */ void
AddTests(BTestSuite & parent)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
HostAndNetworkFailTest()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
GetTest()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
GetWithBufferTest()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
HeadTest()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
NoContentTest()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
AutoRedirectTest()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
BasicAuthTest()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
StopOnErrorTest()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
RequestCancelTest()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
PostTest()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