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>
This commit is contained in:
Mårten Nordheim 2024-03-11 17:36:18 +01:00
parent abed1a41e1
commit 68ceb847d6
4 changed files with 370 additions and 0 deletions

View File

@ -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()

View File

@ -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
)

View File

@ -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

View File

@ -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"