xref: /haiku/src/tests/kits/net/netservices2/HttpProtocolTest.cpp (revision 71e29bbeea760848317843508c711f2a0b446fbb)
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