aboutsummaryrefslogtreecommitdiffstats
path: root/src/quicktestutils/qml/testhttpserver.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/quicktestutils/qml/testhttpserver.cpp')
-rw-r--r--src/quicktestutils/qml/testhttpserver.cpp441
1 files changed, 441 insertions, 0 deletions
diff --git a/src/quicktestutils/qml/testhttpserver.cpp b/src/quicktestutils/qml/testhttpserver.cpp
new file mode 100644
index 0000000000..4dfaf5acba
--- /dev/null
+++ b/src/quicktestutils/qml/testhttpserver.cpp
@@ -0,0 +1,441 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "testhttpserver_p.h"
+#include <QTcpSocket>
+#include <QDebug>
+#include <QFile>
+#include <QTimer>
+#include <QTest>
+#include <QQmlFile>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+\internal
+\class TestHTTPServer
+\brief provides a very, very basic HTTP server for testing.
+
+Inside the test case, an instance of TestHTTPServer should be created, with the
+appropriate port to listen on. The server will listen on the localhost interface.
+
+Directories to serve can then be added to server, which will be added as "roots".
+Each root can be added as a Normal, Delay or Disconnect root. Requests for files
+within a Normal root are returned immediately. Request for files within a Delay
+root are delayed for 500ms, and then served. Requests for files within a Disconnect
+directory cause the server to disconnect immediately. A request for a file that isn't
+found in any root will return a 404 error.
+
+If you have the following directory structure:
+
+\code
+disconnect/disconnectTest.qml
+files/main.qml
+files/Button.qml
+files/content/WebView.qml
+slowFiles/slowMain.qml
+\endcode
+it can be added like this:
+\code
+TestHTTPServer server;
+QVERIFY2(server.listen(14445), qPrintable(server.errorString()));
+server.serveDirectory("disconnect", TestHTTPServer::Disconnect);
+server.serveDirectory("files");
+server.serveDirectory("slowFiles", TestHTTPServer::Delay);
+\endcode
+
+The following request urls will then result in the appropriate action:
+\table
+\header \li URL \li Action
+\row \li http://localhost:14445/disconnectTest.qml \li Disconnection
+\row \li http://localhost:14445/main.qml \li main.qml returned immediately
+\row \li http://localhost:14445/Button.qml \li Button.qml returned immediately
+\row \li http://localhost:14445/content/WebView.qml \li content/WebView.qml returned immediately
+\row \li http://localhost:14445/slowMain.qml \li slowMain.qml returned after 500ms
+\endtable
+*/
+
+static QList<QByteArrayView> ignoredHeaders = {
+ "HTTP2-Settings", // We ignore this
+ "Upgrade", // We ignore this as well
+};
+
+static QUrl localHostUrl(quint16 port)
+{
+ QUrl url;
+ url.setScheme(QStringLiteral("http"));
+ url.setHost(QStringLiteral("127.0.0.1"));
+ url.setPort(port);
+ return url;
+}
+
+TestHTTPServer::TestHTTPServer()
+ : m_state(AwaitingHeader)
+{
+ QObject::connect(&m_server, &QTcpServer::newConnection, this, &TestHTTPServer::newConnection);
+}
+
+bool TestHTTPServer::listen()
+{
+ return m_server.listen(QHostAddress::LocalHost, 0);
+}
+
+QUrl TestHTTPServer::baseUrl() const
+{
+ return localHostUrl(m_server.serverPort());
+}
+
+quint16 TestHTTPServer::port() const
+{
+ return m_server.serverPort();
+}
+
+QUrl TestHTTPServer::url(const QString &documentPath) const
+{
+ return baseUrl().resolved(documentPath);
+}
+
+QString TestHTTPServer::urlString(const QString &documentPath) const
+{
+ return url(documentPath).toString();
+}
+
+QString TestHTTPServer::errorString() const
+{
+ return m_server.errorString();
+}
+
+bool TestHTTPServer::serveDirectory(const QString &dir, Mode mode)
+{
+ m_directories.append(qMakePair(dir, mode));
+ return true;
+}
+
+/*
+ Add an alias, so that if filename is requested and does not exist,
+ alias may be returned.
+*/
+void TestHTTPServer::addAlias(const QString &filename, const QString &alias)
+{
+ m_aliases.insert(filename, alias);
+}
+
+void TestHTTPServer::addRedirect(const QString &filename, const QString &redirectName)
+{
+ m_redirects.insert(filename, redirectName);
+}
+
+void TestHTTPServer::registerFileNameForContentSubstitution(const QString &fileName)
+{
+ m_contentSubstitutedFileNames.insert(fileName);
+}
+
+bool TestHTTPServer::wait(const QUrl &expect, const QUrl &reply, const QUrl &body)
+{
+ m_state = AwaitingHeader;
+ m_data.clear();
+
+ QFile expectFile(QQmlFile::urlToLocalFileOrQrc(expect));
+ if (!expectFile.open(QIODevice::ReadOnly))
+ return false;
+
+ QFile replyFile(QQmlFile::urlToLocalFileOrQrc(reply));
+ if (!replyFile.open(QIODevice::ReadOnly))
+ return false;
+
+ m_bodyData = QByteArray();
+ if (body.isValid()) {
+ QFile bodyFile(QQmlFile::urlToLocalFileOrQrc(body));
+ if (!bodyFile.open(QIODevice::ReadOnly))
+ return false;
+ m_bodyData = bodyFile.readAll();
+ }
+
+ const QByteArray serverHostUrl
+ = QByteArrayLiteral("127.0.0.1:")+ QByteArray::number(m_server.serverPort());
+
+ QByteArray line;
+ bool headers_done = false;
+ while (!(line = expectFile.readLine()).isEmpty()) {
+ line.replace('\r', "");
+ if (headers_done) {
+ m_waitData.body.append(line);
+ } else if (line.at(0) == '\n') {
+ headers_done = true;
+ } else if (line.endsWith("{{Ignore}}\n")) {
+ m_waitData.headerPrefixes.append(line.left(line.size() - strlen("{{Ignore}}\n")));
+ } else {
+ line.replace("{{ServerHostUrl}}", serverHostUrl);
+ m_waitData.headerExactMatches.append(line);
+ }
+ }
+
+ m_replyData = replyFile.readAll();
+
+ if (!m_replyData.endsWith('\n'))
+ m_replyData.append('\n');
+ m_replyData.append("Content-length: ");
+ m_replyData.append(QByteArray::number(m_bodyData.size()));
+ m_replyData.append("\n\n");
+
+ for (int ii = 0; ii < m_replyData.size(); ++ii) {
+ if (m_replyData.at(ii) == '\n' && (!ii || m_replyData.at(ii - 1) != '\r')) {
+ m_replyData.insert(ii, '\r');
+ ++ii;
+ }
+ }
+ m_replyData.append(m_bodyData);
+
+ return true;
+}
+
+bool TestHTTPServer::hasFailed() const
+{
+ return m_state == Failed;
+}
+
+void TestHTTPServer::newConnection()
+{
+ QTcpSocket *socket = m_server.nextPendingConnection();
+ if (!socket)
+ return;
+
+ if (!m_directories.isEmpty())
+ m_dataCache.insert(socket, QByteArray());
+
+ QObject::connect(socket, &QAbstractSocket::disconnected, this, &TestHTTPServer::disconnected);
+ QObject::connect(socket, &QIODevice::readyRead, this, &TestHTTPServer::readyRead);
+}
+
+void TestHTTPServer::disconnected()
+{
+ QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
+ if (!socket)
+ return;
+
+ m_dataCache.remove(socket);
+ for (int ii = 0; ii < m_toSend.size(); ++ii) {
+ if (m_toSend.at(ii).first == socket) {
+ m_toSend.removeAt(ii);
+ --ii;
+ }
+ }
+ socket->disconnect();
+ socket->deleteLater();
+}
+
+void TestHTTPServer::readyRead()
+{
+ QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
+ if (!socket || socket->state() == QTcpSocket::ClosingState)
+ return;
+
+ if (!m_directories.isEmpty()) {
+ serveGET(socket, socket->readAll());
+ return;
+ }
+
+ if (m_state == Failed || (m_waitData.body.isEmpty() && m_waitData.headerExactMatches.size() == 0)) {
+ qWarning() << "TestHTTPServer: Unexpected data" << socket->readAll();
+ return;
+ }
+
+ if (m_state == AwaitingHeader) {
+ QByteArray line;
+ while (!(line = socket->readLine()).isEmpty()) {
+ line.replace('\r', "");
+ if (line.at(0) == '\n') {
+ m_state = AwaitingData;
+ m_data += socket->readAll();
+ break;
+ } else {
+ bool prefixFound = false;
+ for (const QByteArray &prefix : m_waitData.headerPrefixes) {
+ if (line.startsWith(prefix)) {
+ prefixFound = true;
+ break;
+ }
+ }
+ for (QByteArrayView ignore : ignoredHeaders) {
+ if (line.startsWith(ignore)) {
+ prefixFound = true;
+ break;
+ }
+ }
+
+ if (!prefixFound && !m_waitData.headerExactMatches.contains(line)) {
+ qWarning() << "TestHTTPServer: Unexpected header:" << line
+ << "\nExpected exact headers: " << m_waitData.headerExactMatches
+ << "\nExpected header prefixes: " << m_waitData.headerPrefixes;
+ m_state = Failed;
+ socket->disconnectFromHost();
+ return;
+ }
+ }
+ }
+ } else {
+ m_data += socket->readAll();
+ }
+
+ if (!m_data.isEmpty() || m_waitData.body.isEmpty()) {
+ if (m_waitData.body != m_data) {
+ qWarning() << "TestHTTPServer: Unexpected data" << m_data << "\nExpected: " << m_waitData.body;
+ m_state = Failed;
+ } else {
+ socket->write(m_replyData);
+ }
+ socket->disconnectFromHost();
+ }
+}
+
+bool TestHTTPServer::reply(QTcpSocket *socket, const QByteArray &fileNameIn)
+{
+ const QString fileName = QLatin1String(fileNameIn);
+ if (m_redirects.contains(fileName)) {
+ const QByteArray response
+ = "HTTP/1.1 302 Found\r\nContent-length: 0\r\nContent-type: text/html; charset=UTF-8\r\nLocation: "
+ + m_redirects.value(fileName).toUtf8() + "\r\n\r\n";
+ socket->write(response);
+ return true;
+ }
+
+ for (int ii = 0; ii < m_directories.size(); ++ii) {
+ const QString &dir = m_directories.at(ii).first;
+ const Mode mode = m_directories.at(ii).second;
+
+ QString dirFile = dir + QLatin1Char('/') + fileName;
+
+ if (!QFile::exists(dirFile)) {
+ const QHash<QString, QString>::const_iterator it = m_aliases.constFind(fileName);
+ if (it != m_aliases.constEnd())
+ dirFile = dir + QLatin1Char('/') + it.value();
+ }
+
+ QFile file(dirFile);
+ if (file.open(QIODevice::ReadOnly)) {
+
+ if (mode == Disconnect)
+ return true;
+
+ QByteArray data = file.readAll();
+ if (m_contentSubstitutedFileNames.contains(QLatin1Char('/') + fileName))
+ data.replace(QByteArrayLiteral("{{ServerBaseUrl}}"), baseUrl().toString().toUtf8());
+
+ QByteArray response
+ = "HTTP/1.0 200 OK\r\nContent-type: text/html; charset=UTF-8\r\nContent-length: ";
+ response += QByteArray::number(data.size());
+ response += "\r\n\r\n";
+ response += data;
+
+ if (mode == Delay) {
+ m_toSend.append(qMakePair(socket, response));
+ QTimer::singleShot(500, this, &TestHTTPServer::sendOne);
+ return false;
+ } else {
+ socket->write(response);
+ return true;
+ }
+ }
+ }
+
+ socket->write("HTTP/1.0 404 Not found\r\nContent-type: text/html; charset=UTF-8\r\n\r\n");
+
+ return true;
+}
+
+void TestHTTPServer::sendDelayedItem()
+{
+ sendOne();
+}
+
+void TestHTTPServer::sendOne()
+{
+ if (!m_toSend.isEmpty()) {
+ m_toSend.first().first->write(m_toSend.first().second);
+ m_toSend.first().first->close();
+ m_toSend.removeFirst();
+ }
+}
+
+void TestHTTPServer::serveGET(QTcpSocket *socket, const QByteArray &data)
+{
+ const QHash<QTcpSocket *, QByteArray>::iterator it = m_dataCache.find(socket);
+ if (it == m_dataCache.end())
+ return;
+
+ QByteArray &total = it.value();
+ total.append(data);
+
+ if (total.contains("\n\r\n")) {
+ bool close = true;
+ if (total.startsWith("GET /")) {
+ const int space = total.indexOf(' ', 4);
+ if (space != -1)
+ close = reply(socket, total.mid(5, space - 5));
+ }
+ m_dataCache.erase(it);
+ if (close)
+ socket->disconnectFromHost();
+ }
+}
+
+ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QString &dir, TestHTTPServer::Mode mode) :
+ m_port(0)
+{
+ m_dirs[dir] = mode;
+ start();
+}
+
+ThreadedTestHTTPServer::ThreadedTestHTTPServer(const QHash<QString, TestHTTPServer::Mode> &dirs) :
+ m_dirs(dirs), m_port(0)
+{
+ start();
+}
+
+ThreadedTestHTTPServer::~ThreadedTestHTTPServer()
+{
+ quit();
+ wait();
+}
+
+QUrl ThreadedTestHTTPServer::baseUrl() const
+{
+ return localHostUrl(m_port);
+}
+
+QUrl ThreadedTestHTTPServer::url(const QString &documentPath) const
+{
+ return baseUrl().resolved(documentPath);
+}
+
+QString ThreadedTestHTTPServer::urlString(const QString &documentPath) const
+{
+ return url(documentPath).toString();
+}
+
+void ThreadedTestHTTPServer::run()
+{
+ TestHTTPServer server;
+ {
+ QMutexLocker locker(&m_mutex);
+ QVERIFY2(server.listen(), qPrintable(server.errorString()));
+ m_port = server.port();
+ for (QHash<QString, TestHTTPServer::Mode>::ConstIterator i = m_dirs.constBegin();
+ i != m_dirs.constEnd(); ++i) {
+ server.serveDirectory(i.key(), i.value());
+ }
+ m_condition.wakeAll();
+ }
+ exec();
+}
+
+void ThreadedTestHTTPServer::start()
+{
+ QMutexLocker locker(&m_mutex);
+ QThread::start();
+ m_condition.wait(&m_mutex);
+}
+
+QT_END_NAMESPACE
+
+#include "moc_testhttpserver_p.cpp"