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
26using BPrivate::BDateTime;
27using BPrivate::Network::BBorrow;
28using BPrivate::Network::BExclusiveBorrow;
29using BPrivate::Network::BHttpFields;
30using BPrivate::Network::BHttpMethod;
31using BPrivate::Network::BHttpRequest;
32using BPrivate::Network::BHttpResult;
33using BPrivate::Network::BHttpSession;
34using BPrivate::Network::BHttpTime;
35using BPrivate::Network::BHttpTimeFormat;
36using BPrivate::Network::BNetworkRequestError;
37using BPrivate::Network::format_http_time;
38using BPrivate::Network::make_exclusive_borrow;
39using BPrivate::Network::parse_http_time;
40
41using namespace std::literals;
42
43// Logger settings
44constexpr bool LOG_ENABLED = true;
45constexpr bool LOG_TO_CONSOLE = false;
46
47
48HttpProtocolTest::HttpProtocolTest()
49{
50}
51
52
53void
54HttpProtocolTest::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
233void
234HttpProtocolTest::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
278constexpr 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
285void
286HttpProtocolTest::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
314void
315HttpProtocolTest::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
356HttpProtocolTest::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>
376class ObserverHelper : public BLooper
377{
378public:
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
394HttpIntegrationTest::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
414void
415HttpIntegrationTest::setUp()
416{
417	CPPUNIT_ASSERT_EQUAL_MESSAGE("Starting up test server", B_OK, fTestServer.Start());
418}
419
420
421void
422HttpIntegrationTest::tearDown()
423{
424	if (fLogger) {
425		fLogger->Lock();
426		fLogger->Quit();
427	}
428}
429
430
431/* static */ void
432HttpIntegrationTest::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
488void
489HttpIntegrationTest::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
518static 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
527constexpr 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
536void
537HttpIntegrationTest::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
562void
563HttpIntegrationTest::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
579void
580HttpIntegrationTest::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
604static 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
610void
611HttpIntegrationTest::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
637void
638HttpIntegrationTest::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
663void
664HttpIntegrationTest::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
684void
685HttpIntegrationTest::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
697void
698HttpIntegrationTest::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
716static 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
741static 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
757void
758HttpIntegrationTest::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