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:
parent
abed1a41e1
commit
68ceb847d6
@ -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()
|
||||
|
@ -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
|
||||
)
|
246
tests/auto/network/access/qnetworkreply_local/minihttpserver.h
Normal file
246
tests/auto/network/access/qnetworkreply_local/minihttpserver.h
Normal 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
|
@ -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"
|
Loading…
x
Reference in New Issue
Block a user