libreoffice-online/test/HttpRequestTests.cpp
Ashod Nakashian 2bb07adb54 wsd: http: simplify syncRequest to return Response
Since the return value of syncRequest and syncDownload
was derived from Response anyway, there is no reason
to have multiple values for callers to look at.

This simplifies the API.

Change-Id: I0f136e515dd0ef6eda84f6a7cd662b260809d2f1
Signed-off-by: Ashod Nakashian <ashod.nakashian@collabora.co.uk>
2021-04-07 11:36:10 -04:00

505 lines
18 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <config.h>
#include "HttpTestServer.hpp"
#include <Poco/Net/AcceptCertificateHandler.h>
#include <Poco/Net/InvalidCertificateHandler.h>
#include <Poco/Net/SSLManager.h>
#include <Poco/Net/HTTPClientSession.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/StreamCopier.h>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <test/lokassert.hpp>
#if ENABLE_SSL
#include "Ssl.hpp"
#include <net/SslSocket.hpp>
#endif
#include <net/ServerSocket.hpp>
#include <net/DelaySocket.hpp>
#include <net/HttpRequest.hpp>
#include <FileUtil.hpp>
#include <Util.hpp>
#include <helpers.hpp>
/// When enabled, in addition to the loopback
/// server, an external server will be used
/// to check for regressions.
#define ENABLE_EXTERNAL_REGRESSION_CHECK
/// http::Request unit-tests.
/// FIXME: use loopback and avoid depending on external services.
/// Currently we need to rely on external services to validate
/// the implementation.
class HttpRequestTests final : public CPPUNIT_NS::TestFixture
{
CPPUNIT_TEST_SUITE(HttpRequestTests);
CPPUNIT_TEST(testInvalidURI);
CPPUNIT_TEST(testSimpleGet);
CPPUNIT_TEST(testSimpleGetSync);
// CPPUNIT_TEST(test500GetStatuses); // Slow.
// CPPUNIT_TEST(testSimplePost);
CPPUNIT_TEST(testTimeout);
CPPUNIT_TEST(testOnFinished_Complete);
CPPUNIT_TEST(testOnFinished_Timeout);
CPPUNIT_TEST_SUITE_END();
void testInvalidURI();
void testSimpleGet();
void testSimpleGetSync();
void test500GetStatuses();
void testSimplePost();
void testTimeout();
void testOnFinished_Complete();
void testOnFinished_Timeout();
static constexpr std::chrono::seconds DefTimeoutSeconds{ 5 };
int _port;
SocketPoll _pollServerThread;
std::shared_ptr<ServerSocket> _socket;
static const int SimulatedLatencyMs = 0;
public:
HttpRequestTests()
: _port(9990)
, _pollServerThread("HttpServerPoll")
{
#if ENABLE_SSL
Poco::Net::initializeSSL();
// Just accept the certificate anyway for testing purposes
Poco::SharedPtr<Poco::Net::InvalidCertificateHandler> invalidCertHandler
= new Poco::Net::AcceptCertificateHandler(false);
Poco::Net::Context::Params sslParams;
Poco::Net::Context::Ptr sslContext
= new Poco::Net::Context(Poco::Net::Context::CLIENT_USE, sslParams);
Poco::Net::SSLManager::instance().initializeClient(nullptr, invalidCertHandler, sslContext);
#endif
}
~HttpRequestTests()
{
#if ENABLE_SSL
Poco::Net::uninitializeSSL();
#endif
}
class ServerSocketFactory final : public SocketFactory
{
std::shared_ptr<Socket> create(const int physicalFd) override
{
int fd = physicalFd;
#if !MOBILEAPP
if (HttpRequestTests::SimulatedLatencyMs > 0)
fd = Delay::create(HttpRequestTests::SimulatedLatencyMs, physicalFd);
#endif
// #if ENABLE_SSL
// using SocketType = SslStreamSocket;
// #else
using SocketType = StreamSocket;
// #endif
return StreamSocket::create<SocketType>(fd, false,
std::make_shared<ServerRequestHandler>());
}
};
void setUp()
{
LOG_INF("HttpRequestTests::setUp");
std::shared_ptr<SocketFactory> factory = std::make_shared<ServerSocketFactory>();
for (int i = 0; i < 40; ++i, ++_port)
{
// Try listening on this port.
LOG_INF("HttpRequestTests::setUp: creating socket to listen on port " << _port);
_socket = ServerSocket::create(ServerSocket::Type::Local, _port, Socket::Type::IPv4,
_pollServerThread, factory);
if (_socket)
break;
}
_pollServerThread.startThread();
_pollServerThread.insertNewSocket(_socket);
}
void tearDown()
{
LOG_INF("HttpRequestTests::tearDown");
_pollServerThread.stop();
}
};
constexpr std::chrono::seconds HttpRequestTests::DefTimeoutSeconds;
void HttpRequestTests::testInvalidURI()
{
// Cannot create from a blank URI.
LOK_ASSERT(http::Session::createHttp(std::string()) == nullptr);
}
void HttpRequestTests::testSimpleGet()
{
const char* Host = "example.com";
const char* URL = "/";
// Start the polling thread.
SocketPoll pollThread("HttpAsyncReqPoll");
pollThread.startThread();
http::Request httpRequest(URL);
static constexpr http::Session::Protocol Protocols[]
= { http::Session::Protocol::HttpUnencrypted, http::Session::Protocol::HttpSsl };
for (const http::Session::Protocol protocol : Protocols)
{
if (protocol == http::Session::Protocol::HttpSsl)
{
#if ENABLE_SSL
if (!SslContext::isInitialized())
#endif
continue; // Skip SSL, it's not enabled.
}
auto httpSession = http::Session::create(Host, protocol);
std::condition_variable cv;
std::mutex mutex;
bool timedout = true;
httpSession->setFinishedHandler([&](const std::shared_ptr<http::Session>&) {
std::lock_guard<std::mutex> lock(mutex);
timedout = false;
cv.notify_all();
});
httpSession->asyncRequest(httpRequest, pollThread);
// Use Poco to get the same URL in parallel.
const bool secure = (protocol == http::Session::Protocol::HttpSsl);
const int port = (protocol == http::Session::Protocol::HttpSsl ? 443 : 80);
const auto pocoResponse = helpers::pocoGet(secure, Host, port, URL);
std::unique_lock<std::mutex> lock(mutex);
cv.wait_for(lock, DefTimeoutSeconds);
const std::shared_ptr<const http::Response> httpResponse = httpSession->response();
LOK_ASSERT_EQUAL_MESSAGE("Timed out waiting for the onFinished handler", false, timedout);
LOK_ASSERT(httpResponse->state() == http::Response::State::Complete);
LOK_ASSERT(!httpResponse->statusLine().httpVersion().empty());
LOK_ASSERT(!httpResponse->statusLine().reasonPhrase().empty());
LOK_ASSERT_EQUAL(200U, httpResponse->statusLine().statusCode());
LOK_ASSERT(httpResponse->statusLine().statusCategory()
== http::StatusLine::StatusCodeClass::Successful);
LOK_ASSERT_EQUAL(pocoResponse.second, httpResponse->getBody());
}
pollThread.joinThread();
}
void HttpRequestTests::testSimpleGetSync()
{
constexpr auto testname = "simpleGetSync";
const std::string Host("127.0.0.1");
const int port = _port;
constexpr bool secure = false;
const auto data = Util::rng::getHardRandomHexString(Util::rng::getNext() % 1024);
const auto body = std::string(data.data(), data.size());
const std::string URL = "/echo/" + body;
TST_LOG("Requesting URI: [" << URL << ']');
const auto pocoResponse = helpers::pocoGet(secure, Host, port, URL);
http::Request httpRequest(URL);
auto httpSession = http::Session::create(Host, http::Session::Protocol::HttpUnencrypted, port);
httpSession->setTimeout(std::chrono::seconds(1));
for (int i = 0; i < 5; ++i)
{
TST_LOG("Request #" << i);
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(httpRequest);
LOK_ASSERT(httpResponse->done());
LOK_ASSERT(httpResponse->state() == http::Response::State::Complete);
LOK_ASSERT(!httpResponse->statusLine().httpVersion().empty());
LOK_ASSERT(!httpResponse->statusLine().reasonPhrase().empty());
LOK_ASSERT_EQUAL(200U, httpResponse->statusLine().statusCode());
LOK_ASSERT(httpResponse->statusLine().statusCategory()
== http::StatusLine::StatusCodeClass::Successful);
LOK_ASSERT_EQUAL(std::string("HTTP/1.1"), httpResponse->statusLine().httpVersion());
LOK_ASSERT_EQUAL(std::string("OK"), httpResponse->statusLine().reasonPhrase());
LOK_ASSERT_EQUAL(pocoResponse.second, httpResponse->getBody());
LOK_ASSERT_EQUAL(body, httpResponse->getBody());
}
}
/// Compare the response from Poco with ours.
/// @checkReasonPhrase controls whether we compare the Reason Phrase too or not.
/// This is useful for when a status code is recognized by one and not the other.
/// @checkBody controls whether we compare the body content or not.
/// This is useful when we don't care about the content of the body, just that
/// there is some content at all or not.
static void compare(const Poco::Net::HTTPResponse& pocoResponse, const std::string& pocoBody,
const http::Response& httpResponse, bool checkReasonPhrase, bool checkBody)
{
LOK_ASSERT_EQUAL_MESSAGE("Response state", httpResponse.state(),
http::Response::State::Complete);
LOK_ASSERT(!httpResponse.statusLine().httpVersion().empty());
LOK_ASSERT(!httpResponse.statusLine().reasonPhrase().empty());
if (checkBody)
LOK_ASSERT_EQUAL_MESSAGE("Body", pocoBody, httpResponse.getBody());
else
LOK_ASSERT_EQUAL_MESSAGE("Body empty?", pocoBody.empty(), httpResponse.getBody().empty());
LOK_ASSERT_EQUAL_MESSAGE("Status Code", static_cast<unsigned>(pocoResponse.getStatus()),
httpResponse.statusLine().statusCode());
if (checkReasonPhrase)
LOK_ASSERT_EQUAL_MESSAGE("Reason Phrase", Util::toLower(pocoResponse.getReason()),
Util::toLower(httpResponse.statusLine().reasonPhrase()));
else
LOK_ASSERT_EQUAL_MESSAGE("Reason Phrase empty?", pocoResponse.getReason().empty(),
httpResponse.statusLine().reasonPhrase().empty());
LOK_ASSERT_EQUAL_MESSAGE("hasContentLength", pocoResponse.hasContentLength(),
httpResponse.header().hasContentLength());
if (checkBody && pocoResponse.hasContentLength())
LOK_ASSERT_EQUAL_MESSAGE("ContentLength", pocoResponse.getContentLength(),
httpResponse.header().getContentLength());
}
/// This test requests specific *reponse* codes from
/// the server to test the handling of all possible
/// response status codes.
/// It exercises a few hundred requests/responses.
void HttpRequestTests::test500GetStatuses()
{
constexpr auto Host = "httpbin.org";
constexpr bool secure = false;
constexpr int port = 80;
// Start the polling thread.
SocketPoll pollThread("HttpAsyncReqPoll");
pollThread.startThread();
auto httpSession = http::Session::createHttp(Host);
httpSession->setTimeout(DefTimeoutSeconds);
std::condition_variable cv;
std::mutex mutex;
bool timedout = true;
httpSession->setFinishedHandler([&](const std::shared_ptr<http::Session>&) {
std::lock_guard<std::mutex> lock(mutex);
timedout = false;
cv.notify_all();
});
http::StatusLine::StatusCodeClass statusCodeClasses[]
= { http::StatusLine::StatusCodeClass::Informational,
http::StatusLine::StatusCodeClass::Successful,
http::StatusLine::StatusCodeClass::Redirection,
http::StatusLine::StatusCodeClass::Client_Error,
http::StatusLine::StatusCodeClass::Server_Error };
int curStatusCodeClass = -1;
for (unsigned statusCode = 100; statusCode < 512; ++statusCode)
{
const std::string url = "/status/" + std::to_string(statusCode);
http::Request httpRequest;
httpRequest.setUrl(url);
timedout = true; // Assume we timed out until we prove otherwise.
httpSession->asyncRequest(httpRequest, pollThread);
// Get via Poco in parallel.
std::pair<std::shared_ptr<Poco::Net::HTTPResponse>, std::string> pocoResponse;
if (statusCode > 100)
pocoResponse = helpers::pocoGet(secure, Host, port, url);
#ifdef ENABLE_EXTERNAL_REGRESSION_CHECK
std::pair<std::shared_ptr<Poco::Net::HTTPResponse>, std::string> pocoResponseExt;
if (statusCode > 100)
pocoResponseExt = helpers::pocoGet(secure, "httpbin.org", 80, url);
#endif
const std::shared_ptr<const http::Response> httpResponse = httpSession->response();
std::unique_lock<std::mutex> lock(mutex);
cv.wait_for(lock, DefTimeoutSeconds, [&]() { return httpResponse->done(); });
LOK_ASSERT_EQUAL(http::Response::State::Complete, httpResponse->state());
LOK_ASSERT(!httpResponse->statusLine().httpVersion().empty());
LOK_ASSERT(!httpResponse->statusLine().reasonPhrase().empty());
if (statusCode % 100 == 0)
++curStatusCodeClass;
LOK_ASSERT(httpResponse->statusLine().statusCategory()
== statusCodeClasses[curStatusCodeClass]);
LOK_ASSERT_EQUAL(statusCode, httpResponse->statusLine().statusCode());
// Poco throws exception "No message received" for 1xx Status Codes.
if (statusCode > 100)
{
compare(*pocoResponse.first, pocoResponse.second, *httpResponse, true, true);
#ifdef ENABLE_EXTERNAL_REGRESSION_CHECK
// These Status Codes are not recognized by httpbin.org,
// so we get "unknown" and must skip comparing them.
const bool checkReasonPhrase
= (statusCode != 103 && statusCode != 208 && statusCode != 413 && statusCode != 414
&& statusCode != 416 && statusCode != 421 && statusCode != 425
&& statusCode != 440 && statusCode != 508 && statusCode != 511);
const bool checkBody = (statusCode != 402 && statusCode != 418);
compare(*pocoResponseExt.first, pocoResponseExt.second, *httpResponse,
checkReasonPhrase, checkBody);
#endif
}
}
pollThread.joinThread();
}
void HttpRequestTests::testSimplePost()
{
const std::string Host = "httpbin.org";
const char* URL = "/post";
// Start the polling thread.
SocketPoll pollThread("HttpAsyncReqPoll");
pollThread.startThread();
http::Request httpRequest(URL, http::Request::VERB_POST);
// Write the test data to file.
const char data[] = "abcd-qwerty!!!";
const std::string path = FileUtil::getSysTempDirectoryPath() + "/test_http_post";
std::ofstream ofs(path, std::ios::binary);
ofs.write(data, sizeof(data) - 1); // Don't write the terminating null.
ofs.close();
httpRequest.setBodyFile(path);
auto httpSession = http::Session::createHttp(Host);
httpSession->setTimeout(DefTimeoutSeconds);
std::condition_variable cv;
std::mutex mutex;
bool timedout = true;
httpSession->setFinishedHandler([&](const std::shared_ptr<http::Session>&) {
std::lock_guard<std::mutex> lock(mutex);
timedout = false;
cv.notify_all();
});
httpSession->asyncRequest(httpRequest, pollThread);
std::unique_lock<std::mutex> lock(mutex);
cv.wait_for(lock, DefTimeoutSeconds);
const std::shared_ptr<const http::Response> httpResponse = httpSession->response();
LOK_ASSERT(httpResponse->state() == http::Response::State::Complete);
LOK_ASSERT(!httpResponse->statusLine().httpVersion().empty());
LOK_ASSERT(!httpResponse->statusLine().reasonPhrase().empty());
LOK_ASSERT_EQUAL(200U, httpResponse->statusLine().statusCode());
LOK_ASSERT(httpResponse->statusLine().statusCategory()
== http::StatusLine::StatusCodeClass::Successful);
const std::string body = httpResponse->getBody();
LOK_ASSERT(!body.empty());
std::cerr << "[" << body << "]\n";
LOK_ASSERT(body.find(data) != std::string::npos);
pollThread.joinThread();
}
void HttpRequestTests::testTimeout()
{
const char* Host = "www.example.com";
const char* URL = "/";
http::Request httpRequest(URL);
auto httpSession = http::Session::createHttp(Host);
httpSession->setTimeout(std::chrono::milliseconds(1)); // Very short interval.
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(httpRequest);
LOK_ASSERT(httpResponse->done());
LOK_ASSERT(httpResponse->state() == http::Response::State::Timeout);
}
void HttpRequestTests::testOnFinished_Complete()
{
const char* Host = "www.example.com";
const char* URL = "/";
http::Request httpRequest(URL);
auto httpSession = http::Session::createHttp(Host);
bool completed = false;
httpSession->setFinishedHandler([&](const std::shared_ptr<http::Session>& session) {
LOK_ASSERT(session->response()->done());
LOK_ASSERT(session->response()->state() == http::Response::State::Complete);
completed = true;
return true;
});
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(httpRequest);
LOK_ASSERT(completed);
LOK_ASSERT(httpResponse->done());
LOK_ASSERT(httpResponse->state() == http::Response::State::Complete);
}
void HttpRequestTests::testOnFinished_Timeout()
{
const char* Host = "www.example.com";
const char* URL = "/";
http::Request httpRequest(URL);
auto httpSession = http::Session::createHttp(Host);
httpSession->setTimeout(std::chrono::milliseconds(1)); // Very short interval.
bool completed = false;
httpSession->setFinishedHandler([&](const std::shared_ptr<http::Session>& session) {
LOK_ASSERT(session->response()->done());
LOK_ASSERT(session->response()->state() == http::Response::State::Timeout);
completed = true;
return true;
});
const std::shared_ptr<const http::Response> httpResponse
= httpSession->syncRequest(httpRequest);
LOK_ASSERT(completed);
LOK_ASSERT(httpResponse->done());
LOK_ASSERT(httpResponse->state() == http::Response::State::Timeout);
}
CPPUNIT_TEST_SUITE_REGISTRATION(HttpRequestTests);
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */