diff options
| author | Mårten Nordheim <marten.nordheim@qt.io> | 2024-03-11 17:36:18 +0100 |
|---|---|---|
| committer | Mårten Nordheim <marten.nordheim@qt.io> | 2024-05-16 19:40:03 +0200 |
| commit | 68ceb847d6f780b14eabb95f26f8c2a78d13113d (patch) | |
| tree | 4b7ce0e246b5448c56775cb9ed9a3cdf99ec1f32 | |
| parent | abed1a41e1dfc6b1cc6eaa7cc8072c6638230811 (diff) | |
QNetworkReply: Add a separate test for self-contained tests
Nothing there should rely on the network testing server.
Part of this will also be testing the local/unix domain socket support.
Task-number: QTBUG-102855
Change-Id: Icf3b0bd9370ec62e003862caf4cd3ed38d875bac
Reviewed-by: Mate Barany <mate.barany@qt.io>
4 files changed, 370 insertions, 0 deletions
diff --git a/tests/auto/network/access/CMakeLists.txt b/tests/auto/network/access/CMakeLists.txt index 3ae66e13088..d1130f832e7 100644 --- a/tests/auto/network/access/CMakeLists.txt +++ b/tests/auto/network/access/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(qnetworkreply) add_subdirectory(qnetworkcachemetadata) add_subdirectory(qabstractnetworkcache) if(QT_FEATURE_http) + add_subdirectory(qnetworkreply_local) add_subdirectory(qnetworkrequestfactory) add_subdirectory(qrestaccessmanager) endif() diff --git a/tests/auto/network/access/qnetworkreply_local/CMakeLists.txt b/tests/auto/network/access/qnetworkreply_local/CMakeLists.txt new file mode 100644 index 00000000000..13a60afb134 --- /dev/null +++ b/tests/auto/network/access/qnetworkreply_local/CMakeLists.txt @@ -0,0 +1,9 @@ +qt_internal_add_test(tst_qnetworkreply_local + SOURCES + minihttpserver.h + tst_qnetworkreply_local.cpp + LIBRARIES + Qt::CorePrivate + Qt::NetworkPrivate + BUNDLE_ANDROID_OPENSSL_LIBS +) diff --git a/tests/auto/network/access/qnetworkreply_local/minihttpserver.h b/tests/auto/network/access/qnetworkreply_local/minihttpserver.h new file mode 100644 index 00000000000..eb0697a6f84 --- /dev/null +++ b/tests/auto/network/access/qnetworkreply_local/minihttpserver.h @@ -0,0 +1,246 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef MINIHTTPSERVER_H +#define MINIHTTPSERVER_H + +#include <QtNetwork/qtnetworkglobal.h> + +#include <QtNetwork/qtcpserver.h> +#include <QtNetwork/qtcpsocket.h> +#include <QtNetwork/qlocalsocket.h> +#if QT_CONFIG(ssl) +# include <QtNetwork/qsslsocket.h> +#endif +#if QT_CONFIG(localserver) +# include <QtNetwork/qlocalserver.h> +#endif + +#include <QtCore/qpointer.h> +#include <QtCore/qhash.h> + +#include <utility> + +static inline QByteArray default200Response() +{ + return QByteArrayLiteral("HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 12\r\n" + "\r\n" + "Hello World!"); +} +class MiniHttpServerV2 : public QObject +{ + Q_OBJECT + +public: + struct State; + +#if QT_CONFIG(localserver) + void bind(QLocalServer *server) + { + Q_ASSERT(!localServer); + localServer = server; + connect(server, &QLocalServer::newConnection, this, + &MiniHttpServerV2::incomingLocalConnection); + } +#endif + + void bind(QTcpServer *server) + { + Q_ASSERT(!tcpServer); + tcpServer = server; + connect(server, &QTcpServer::pendingConnectionAvailable, this, + &MiniHttpServerV2::incomingConnection); + } + + void setDataToTransmit(QByteArray data) { dataToTransmit = std::move(data); } + + void clearServerState() + { + auto copy = std::exchange(clientStates, {}); + for (auto [socket, _] : copy.asKeyValueRange()) { + if (auto *tcpSocket = qobject_cast<QTcpSocket *>(socket)) + tcpSocket->disconnectFromHost(); + else if (auto *localSocket = qobject_cast<QLocalSocket *>(socket)) + localSocket->disconnectFromServer(); + else + Q_UNREACHABLE_RETURN(); + socket->deleteLater(); + } + } + + bool hasPendingConnections() const + { + return +#if QT_CONFIG(localserver) + (localServer && localServer->hasPendingConnections()) || +#endif + (tcpServer && tcpServer->hasPendingConnections()); + } + + QString addressForScheme(QStringView scheme) const + { + using namespace Qt::StringLiterals; + if (scheme.startsWith("unix"_L1) || scheme.startsWith("local"_L1)) { +#if QT_CONFIG(localserver) + if (localServer) + return localServer->serverName(); +#endif + } else if (scheme == "http"_L1) { + if (tcpServer) + return "%1:%2"_L1.arg(tcpServer->serverAddress().toString(), + QString::number(tcpServer->serverPort())); + } + return {}; + } + + QList<State> peerStates() const { return clientStates.values(); } + +protected: +#if QT_CONFIG(localserver) + void incomingLocalConnection() + { + auto *socket = localServer->nextPendingConnection(); + connectSocketSignals(socket); + } +#endif + + void incomingConnection() + { + auto *socket = tcpServer->nextPendingConnection(); + connectSocketSignals(socket); + } + + void reply(QIODevice *socket) + { + Q_ASSERT(socket); + if (dataToTransmit.isEmpty()) { + emit socket->bytesWritten(0); // emulate having written the data + return; + } + if (!stopTransfer) + socket->write(dataToTransmit); + } + +private: + void connectSocketSignals(QIODevice *socket) + { + connect(socket, &QIODevice::readyRead, this, [this, socket]() { readyReadSlot(socket); }); + connect(socket, &QIODevice::bytesWritten, this, + [this, socket]() { bytesWrittenSlot(socket); }); +#if QT_CONFIG(ssl) + if (auto *sslSocket = qobject_cast<QSslSocket *>(socket)) + connect(sslSocket, &QSslSocket::sslErrors, this, &MiniHttpServerV2::slotSslErrors); +#endif + + if (auto *tcpSocket = qobject_cast<QTcpSocket *>(socket)) { + connect(tcpSocket, &QAbstractSocket::errorOccurred, this, &MiniHttpServerV2::slotError); + } else if (auto *localSocket = qobject_cast<QLocalSocket *>(socket)) { + connect(localSocket, &QLocalSocket::errorOccurred, this, + [this](QLocalSocket::LocalSocketError error) { + slotError(QAbstractSocket::SocketError(error)); + }); + } else { + Q_UNREACHABLE_RETURN(); + } + } + + void parseContentLength(State &st, QByteArrayView header) + { + qsizetype index = header.indexOf("\r\ncontent-length:"); + if (index == -1) + return; + st.foundContentLength = true; + + index += sizeof("\r\ncontent-length:") - 1; + const auto *end = std::find(header.cbegin() + index, header.cend(), '\r'); + QByteArrayView num = header.mid(index, std::distance(header.cbegin() + index, end)); + bool ok = false; + st.contentLength = num.toInt(&ok); + if (!ok) + st.contentLength = -1; + } + +private slots: +#if QT_CONFIG(ssl) + void slotSslErrors(const QList<QSslError> &errors) + { + QTcpSocket *currentClient = qobject_cast<QTcpSocket *>(sender()); + Q_ASSERT(currentClient); + qDebug() << "slotSslErrors" << currentClient->errorString() << errors; + } +#endif + void slotError(QAbstractSocket::SocketError err) + { + QTcpSocket *currentClient = qobject_cast<QTcpSocket *>(sender()); + Q_ASSERT(currentClient); + qDebug() << "slotError" << err << currentClient->errorString(); + } + +public slots: + + void readyReadSlot(QIODevice *socket) + { + if (stopTransfer) + return; + State &st = clientStates[socket]; + st.receivedData += socket->readAll(); + const qsizetype doubleEndlPos = st.receivedData.indexOf("\r\n\r\n"); + + if (doubleEndlPos != -1) { + const qsizetype endOfHeader = doubleEndlPos + 4; + st.contentRead = st.receivedData.size() - endOfHeader; + + if (!st.checkedContentLength) { + parseContentLength(st, QByteArrayView(st.receivedData).first(endOfHeader)); + st.checkedContentLength = true; + } + + if (st.contentRead < st.contentLength) + return; + + // multiple requests incoming, remove the bytes of the current one + if (multiple) + st.receivedData.remove(0, endOfHeader); + + reply(socket); + } + } + + void bytesWrittenSlot(QIODevice *socket) + { + // Disconnect and delete in next cycle (else Windows clients will fail with + // RemoteHostClosedError). + if (doClose && socket->bytesToWrite() == 0) { + disconnect(socket, nullptr, this, nullptr); + socket->deleteLater(); + } + } + +private: + QByteArray dataToTransmit = default200Response(); + + QTcpServer *tcpServer = nullptr; +#if QT_CONFIG(localserver) + QLocalServer *localServer = nullptr; +#endif + + QHash<QIODevice *, State> clientStates; + +public: + struct State + { + QByteArray receivedData; + qsizetype contentLength = 0; + qsizetype contentRead = 0; + bool checkedContentLength = false; + bool foundContentLength = false; + }; + + bool doClose = true; + bool multiple = false; + bool stopTransfer = false; +}; + +#endif // MINIHTTPSERVER_H diff --git a/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp b/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp new file mode 100644 index 00000000000..729605d9c86 --- /dev/null +++ b/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp @@ -0,0 +1,114 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtNetwork/qtnetworkglobal.h> + +#include <QtTest/qtest.h> + +#include <QtNetwork/qnetworkreply.h> +#include <QtNetwork/qnetworkaccessmanager.h> + +#include "minihttpserver.h" + +using namespace Qt::StringLiterals; + +/* + The tests here are meant to be self-contained, using servers in the same + process if needed. This enables externals to more easily run the tests too. +*/ +class tst_QNetworkReply_local : public QObject +{ + Q_OBJECT +private slots: + void initTestCase_data(); + + void get(); + void post(); +}; + +void tst_QNetworkReply_local::initTestCase_data() +{ + QTest::addColumn<QString>("scheme"); + + QTest::newRow("http") << "http"; +#if QT_CONFIG(localserver) + QTest::newRow("unix") << "unix+http"; + QTest::newRow("local") << "local+http"; // equivalent to unix, but test that it works +#endif +} + +static std::unique_ptr<MiniHttpServerV2> getServerForCurrentScheme() +{ + auto server = std::make_unique<MiniHttpServerV2>(); + QFETCH_GLOBAL(QString, scheme); + if (scheme.startsWith("unix"_L1) || scheme.startsWith("local"_L1)) { +#if QT_CONFIG(localserver) + QLocalServer *localServer = new QLocalServer(server.get()); + localServer->listen(u"qt_networkreply_test_"_s + % QLatin1StringView(QTest::currentTestFunction()) + % QString::number(QCoreApplication::applicationPid())); + server->bind(localServer); +#endif + } else if (scheme == "http") { + QTcpServer *tcpServer = new QTcpServer(server.get()); + tcpServer->listen(QHostAddress::LocalHost, 0); + server->bind(tcpServer); + } + return server; +} + +static QUrl getUrlForCurrentScheme(MiniHttpServerV2 *server) +{ + QFETCH_GLOBAL(QString, scheme); + const QString address = server->addressForScheme(scheme); + const QString urlString = QLatin1StringView("%1://%2").arg(scheme, address); + return { urlString }; +} + +void tst_QNetworkReply_local::get() +{ + std::unique_ptr<MiniHttpServerV2> server = getServerForCurrentScheme(); + const QUrl url = getUrlForCurrentScheme(server.get()); + + QNetworkAccessManager manager; + std::unique_ptr<QNetworkReply> reply(manager.get(QNetworkRequest(url))); + + const bool res = QTest::qWaitFor([reply = reply.get()] { return reply->isFinished(); }); + QVERIFY(res); + + QCOMPARE(reply->readAll(), QByteArray("Hello World!")); + QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); +} + +void tst_QNetworkReply_local::post() +{ + std::unique_ptr<MiniHttpServerV2> server = getServerForCurrentScheme(); + const QUrl url = getUrlForCurrentScheme(server.get()); + + QNetworkAccessManager manager; + const QByteArray payload = "Hello from the other side!"_ba; + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + std::unique_ptr<QNetworkReply> reply(manager.post(req, payload)); + + const bool res = QTest::qWaitFor([reply = reply.get()] { return reply->isFinished(); }); + QVERIFY(res); + + QCOMPARE(reply->readAll(), QByteArray("Hello World!")); + QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); + + auto states = server->peerStates(); + QCOMPARE(states.size(), 1); + + const auto &firstRequest = states.at(0); + + QVERIFY(firstRequest.checkedContentLength); + QCOMPARE(firstRequest.contentLength, payload.size()); + QCOMPARE_GT(firstRequest.receivedData.size(), payload.size() + 4); + QCOMPARE(firstRequest.receivedData.last(payload.size() + 4), "\r\n\r\n" + payload); +} + +QTEST_MAIN(tst_QNetworkReply_local) + +#include "tst_qnetworkreply_local.moc" +#include "moc_minihttpserver.cpp" |
