summaryrefslogtreecommitdiffstats
path: root/tests/auto/network/access/qrestaccessmanager
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/network/access/qrestaccessmanager')
-rw-r--r--tests/auto/network/access/qrestaccessmanager/CMakeLists.txt17
-rw-r--r--tests/auto/network/access/qrestaccessmanager/httptestserver.cpp268
-rw-r--r--tests/auto/network/access/qrestaccessmanager/httptestserver_p.h90
-rw-r--r--tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp876
4 files changed, 1251 insertions, 0 deletions
diff --git a/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt b/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt
new file mode 100644
index 0000000000..614248be28
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/CMakeLists.txt
@@ -0,0 +1,17 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qrestaccessmanager LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+qt_internal_add_test(tst_qrestaccessmanager
+ SOURCES
+ tst_qrestaccessmanager.cpp
+ httptestserver.cpp httptestserver_p.h
+ LIBRARIES
+ Qt::Network
+ Qt::CorePrivate
+)
diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp
new file mode 100644
index 0000000000..25869eb46b
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/httptestserver.cpp
@@ -0,0 +1,268 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "httptestserver_p.h"
+
+#include <QtNetwork/qtcpsocket.h>
+
+#include <QtCore/qcoreapplication.h>
+
+#include <private/qlocale_p.h>
+
+using namespace Qt::StringLiterals;
+
+static constexpr char CRLF[] = "\r\n";
+
+HttpTestServer::HttpTestServer(QObject *parent) : QTcpServer(parent)
+{
+ QObject::connect(this, &QTcpServer::newConnection, this, &HttpTestServer::handleConnected);
+ const auto ok = listen(QHostAddress::LocalHost);
+ Q_ASSERT(ok);
+};
+
+HttpTestServer::~HttpTestServer()
+{
+ if (isListening())
+ close();
+}
+
+QUrl HttpTestServer::url()
+{
+ return QUrl(u"http://127.0.0.1:%1"_s.arg(serverPort()));
+}
+
+void HttpTestServer::handleConnected()
+{
+ Q_ASSERT(!m_socket); // No socket must exist previously, this is a single-connection server
+ m_socket = nextPendingConnection();
+ Q_ASSERT(m_socket);
+ QObject::connect(m_socket, &QTcpSocket::readyRead,
+ this, &HttpTestServer::handleDataAvailable);
+}
+
+void HttpTestServer::handleDataAvailable()
+{
+ Q_ASSERT(m_socket);
+ bool ok = true;
+
+ // Parse the incoming request data into the HttpData object
+ while (m_socket->bytesAvailable()) {
+ if (state == State::ReadingMethod && !(ok = readMethod(m_socket)))
+ qWarning("Invalid Method");
+ if (ok && state == State::ReadingUrl && !(ok = readUrl(m_socket)))
+ qWarning("Invalid URL");
+ if (ok && state == State::ReadingStatus && !(ok = readStatus(m_socket)))
+ qWarning("Invalid Status");
+ if (ok && state == State::ReadingHeader && !(ok = readHeaders(m_socket)))
+ qWarning("Invalid Header");
+ if (ok && state == State::ReadingBody && !(ok = readBody(m_socket)))
+ qWarning("Invalid Body");
+ } // while bytes available
+
+ Q_ASSERT(ok);
+ Q_ASSERT(m_handler);
+ Q_ASSERT(state == State::AllDone);
+
+ if (auto values = m_request.headers.values(
+ QHttpHeaders::WellKnownHeader::Host); !values.empty()) {
+ const auto parts = values.first().split(':');
+ m_request.url.setHost(parts.at(0));
+ if (parts.size() == 2)
+ m_request.url.setPort(parts.at(1).toUInt());
+ }
+ HttpData response;
+ ResponseControl control;
+ // Inform the testcase about request and ask for response data
+ m_handler(m_request, response, control);
+
+ QByteArray responseMessage;
+ responseMessage += "HTTP/1.1 ";
+ responseMessage += QByteArray::number(response.status);
+ responseMessage += CRLF;
+ // Insert headers if any
+ for (const auto &[name,value] : response.headers.toListOfPairs()) {
+ responseMessage += name;
+ responseMessage += ": ";
+ responseMessage += value;
+ responseMessage += CRLF;
+ }
+ responseMessage += CRLF;
+ /*
+ qDebug() << "HTTPTestServer received request"
+ << "\nMethod:" << m_request.method
+ << "\nHeaders:" << m_request.headers
+ << "\nBody:" << m_request.body;
+ */
+ if (control.respond) {
+ if (control.responseChunkSize <= 0) {
+ responseMessage += response.body;
+ // qDebug() << "HTTPTestServer response:" << responseMessage;
+ m_socket->write(responseMessage);
+ } else {
+ // Respond in chunks, first write the headers
+ // qDebug() << "HTTPTestServer response:" << responseMessage;
+ m_socket->write(responseMessage);
+ // Then write bodydata in chunks, while allowing the testcase to process as well
+ QByteArray chunk;
+ while (!response.body.isEmpty()) {
+ chunk = response.body.left(control.responseChunkSize);
+ response.body.remove(0, control.responseChunkSize);
+ // qDebug() << "SERVER writing chunk" << chunk;
+ m_socket->write(chunk);
+ m_socket->flush();
+ m_socket->waitForBytesWritten();
+ // Process events until testcase indicates it's ready for next chunk.
+ // This way we can control the bytes the testcase gets in each chunk
+ control.readyForNextChunk = false;
+ while (!control.readyForNextChunk)
+ QCoreApplication::processEvents();
+ }
+ }
+ }
+ m_socket->disconnectFromHost();
+ m_request = {};
+ m_socket = nullptr; // deleted by QTcpServer during destruction
+ state = State::ReadingMethod;
+ fragment.clear();
+}
+
+bool HttpTestServer::readMethod(QTcpSocket *socket)
+{
+ bool finished = false;
+ while (socket->bytesAvailable() && !finished) {
+ const auto c = socket->read(1).at(0);
+ if (ascii_isspace(c))
+ finished = true;
+ else if (std::isupper(c) && fragment.size() < 8)
+ fragment += c;
+ else
+ return false;
+ }
+ if (finished) {
+ if (fragment == "HEAD")
+ method = Method::Head;
+ else if (fragment == "GET")
+ method = Method::Get;
+ else if (fragment == "PUT")
+ method = Method::Put;
+ else if (fragment == "PATCH")
+ method = Method::Patch;
+ else if (fragment == "POST")
+ method = Method::Post;
+ else if (fragment == "DELETE")
+ method = Method::Delete;
+ else if (fragment == "FOOBAR") // used by custom verb/method tests
+ method = Method::Custom;
+ else
+ qWarning("Invalid operation %s", fragment.data());
+
+ state = State::ReadingUrl;
+ m_request.method = fragment;
+ fragment.clear();
+
+ return method != Method::Unknown;
+ }
+ return true;
+}
+
+bool HttpTestServer::readUrl(QTcpSocket *socket)
+{
+ bool finished = false;
+ while (socket->bytesAvailable() && !finished) {
+ const auto c = socket->read(1).at(0);
+ if (std::isspace(c))
+ finished = true;
+ else
+ fragment += c;
+ }
+ if (finished) {
+ if (!fragment.startsWith('/')) {
+ qWarning("Invalid URL path %s", fragment.constData());
+ return false;
+ }
+ m_request.url = QStringLiteral("http://127.0.0.1:") + QString::number(m_request.port) +
+ QString::fromUtf8(fragment);
+ state = State::ReadingStatus;
+ if (!m_request.url.isValid()) {
+ qWarning("Invalid URL %s", fragment.constData());
+ return false;
+ }
+ fragment.clear();
+ }
+ return true;
+}
+
+bool HttpTestServer::readStatus(QTcpSocket *socket)
+{
+ bool finished = false;
+ while (socket->bytesAvailable() && !finished) {
+ fragment += socket->read(1);
+ if (fragment.endsWith(CRLF)) {
+ finished = true;
+ fragment.resize(fragment.size() - 2);
+ }
+ }
+ if (finished) {
+ if (!std::isdigit(fragment.at(fragment.size() - 3)) ||
+ fragment.at(fragment.size() - 2) != '.' ||
+ !std::isdigit(fragment.at(fragment.size() - 1))) {
+ qWarning("Invalid version");
+ return false;
+ }
+ m_request.version = std::pair(fragment.at(fragment.size() - 3) - '0',
+ fragment.at(fragment.size() - 1) - '0');
+ state = State::ReadingHeader;
+ fragment.clear();
+ }
+ return true;
+}
+
+bool HttpTestServer::readHeaders(QTcpSocket *socket)
+{
+ while (socket->bytesAvailable()) {
+ fragment += socket->read(1);
+ if (fragment.endsWith(CRLF)) {
+ if (fragment == CRLF) {
+ state = State::ReadingBody;
+ fragment.clear();
+ return true;
+ } else {
+ fragment.chop(2);
+ const int index = fragment.indexOf(':');
+ if (index == -1)
+ return false;
+
+ QByteArray key = fragment.sliced(0, index).trimmed();
+ QByteArray value = fragment.sliced(index + 1).trimmed();
+ m_request.headers.append(key, value);
+ fragment.clear();
+ }
+ }
+ }
+ return true;
+}
+
+bool HttpTestServer::readBody(QTcpSocket *socket)
+{
+ qint64 bytesLeft = 0;
+ if (auto values = m_request.headers.values(
+ QHttpHeaders::WellKnownHeader::ContentLength); !values.empty()) {
+ bool conversionResult;
+ bytesLeft = values.first().toInt(&conversionResult);
+ if (!conversionResult)
+ return false;
+ fragment.resize(bytesLeft);
+ }
+ while (bytesLeft) {
+ qint64 got = socket->read(&fragment.data()[fragment.size() - bytesLeft], bytesLeft);
+ if (got < 0)
+ return false; // error
+ bytesLeft -= got;
+ if (bytesLeft)
+ qApp->processEvents();
+ }
+ fragment.swap(m_request.body);
+ state = State::AllDone;
+ return true;
+}
+
diff --git a/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h b/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h
new file mode 100644
index 0000000000..0a94b2c8a6
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/httptestserver_p.h
@@ -0,0 +1,90 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#ifndef QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
+#define QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
+
+#include <QtNetwork/qtcpserver.h>
+#include <QtNetwork/qhttpheaders.h>
+
+#include <QtCore/qmap.h>
+#include <QtCore/qurl.h>
+
+#include <functional>
+
+// This struct is used for parsing the incoming network request data into, as well
+// as getting the response data from the testcase
+struct HttpData {
+ QUrl url;
+ int status = 0;
+ QByteArray body;
+ QByteArray method;
+ quint16 port = 0;
+ QPair<quint8, quint8> version;
+ QHttpHeaders headers;
+};
+
+struct ResponseControl
+{
+ bool respond = true;
+ qsizetype responseChunkSize = -1;
+ bool readyForNextChunk = true;
+};
+
+// Simple HTTP server. Currently supports only one concurrent connection
+class HttpTestServer : public QTcpServer
+{
+ Q_OBJECT
+
+public:
+ explicit HttpTestServer(QObject *parent = nullptr);
+ ~HttpTestServer() override;
+
+ // Returns this server's URL for the testcase to send requests to
+ QUrl url();
+
+ enum class State {
+ ReadingMethod,
+ ReadingUrl,
+ ReadingStatus,
+ ReadingHeader,
+ ReadingBody,
+ AllDone
+ } state = State::ReadingMethod;
+
+ enum class Method {
+ Unknown,
+ Head,
+ Get,
+ Put,
+ Patch,
+ Post,
+ Delete,
+ Custom,
+ } method = Method::Unknown;
+
+ // Parsing helpers for incoming data => HttpData
+ bool readMethod(QTcpSocket *socket);
+ bool readUrl(QTcpSocket *socket);
+ bool readStatus(QTcpSocket *socket);
+ bool readHeaders(QTcpSocket *socket);
+ bool readBody(QTcpSocket *socket);
+ // Parsing-time buffer in case data is received a small chunk at a time (readyRead())
+ QByteArray fragment;
+
+ // Settable callback for testcase. Gives the received request data, and takes in response data
+ using Handler = std::function<void(const HttpData &request, HttpData &response,
+ ResponseControl &control)>;
+ void setHandler(Handler handler) { m_handler = std::move(handler); }
+
+private slots:
+ void handleConnected();
+ void handleDataAvailable();
+
+private:
+ QTcpSocket *m_socket = nullptr;
+ HttpData m_request;
+ Handler m_handler = nullptr;
+};
+
+#endif // QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
diff --git a/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp b/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp
new file mode 100644
index 0000000000..d6bdda76ca
--- /dev/null
+++ b/tests/auto/network/access/qrestaccessmanager/tst_qrestaccessmanager.cpp
@@ -0,0 +1,876 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "httptestserver_p.h"
+
+#if QT_CONFIG(http)
+#include <QtNetwork/qhttpmultipart.h>
+#endif
+#include <QtNetwork/qrestaccessmanager.h>
+#include <QtNetwork/qauthenticator.h>
+#include <QtNetwork/qnetworkreply.h>
+#include <QtNetwork/qnetworkrequestfactory.h>
+#include <QtNetwork/qrestreply.h>
+
+#include <QTest>
+#include <QtTest/qsignalspy.h>
+
+#include <QtCore/private/qglobal_p.h> // for access to Qt's feature system
+#include <QtCore/qbuffer.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qstringconverter.h>
+
+using namespace Qt::StringLiterals;
+using namespace std::chrono_literals;
+
+using Header = QHttpHeaders::WellKnownHeader;
+
+class tst_QRestAccessManager : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initialization();
+ void destruction();
+ void callbacks();
+#if QT_CONFIG(http)
+ void requests();
+#endif
+ void reply();
+ void errors();
+ void body();
+ void json();
+ void text();
+ void textStreaming();
+
+private:
+ void memberHandler(QRestReply &reply);
+
+ friend class Transient;
+ QList<QNetworkReply*> m_expectedReplies;
+ QList<QNetworkReply*> m_actualReplies;
+};
+
+void tst_QRestAccessManager::initialization()
+{
+ QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QRestAccessManager: QNetworkAccesManager is nullptr");
+ QRestAccessManager manager1(nullptr);
+ QVERIFY(!manager1.networkAccessManager());
+
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager2(&qnam);
+ QVERIFY(manager2.networkAccessManager());
+}
+
+void tst_QRestAccessManager::reply()
+{
+ QNetworkAccessManager qnam;
+
+ QNetworkReply *nr = qnam.get(QNetworkRequest(QUrl{"someurl"}));
+ QRestReply rr1(nr);
+ QCOMPARE(rr1.networkReply(), nr);
+
+ // Move-construct
+ QRestReply rr2(std::move(rr1));
+ QCOMPARE(rr2.networkReply(), nr);
+
+ // Move-assign
+ QTest::ignoreMessage(QtMsgType::QtWarningMsg, "QRestReply: QNetworkReply is nullptr");
+ QRestReply rr3(nullptr);
+ rr3 = std::move(rr2);
+ QCOMPARE(rr3.networkReply(), nr);
+}
+
+#define VERIFY_REPLY_OK(METHOD) \
+{ \
+ QTRY_VERIFY(networkReply); \
+ QRestReply restReply(networkReply); \
+ QCOMPARE(serverSideRequest.method, METHOD); \
+ QVERIFY(restReply.isSuccess()); \
+ QVERIFY(!restReply.hasError()); \
+ networkReply->deleteLater(); \
+ networkReply = nullptr; \
+}
+
+#if QT_CONFIG(http)
+void tst_QRestAccessManager::requests()
+{
+ // A basic test for each HTTP method against the local testserver.
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ request.setRawHeader("Content-Type"_ba, "text/plain"); // To silence missing content-type warn
+ QNetworkReply *networkReply = nullptr;
+ std::unique_ptr<QHttpMultiPart> multiPart;
+ QHttpPart part;
+ part.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"text\""));
+ part.setBody("multipart_text");
+ QByteArray ioDeviceData{"io_device_data"_ba};
+ QBuffer bufferIoDevice(&ioDeviceData);
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.status = 200;
+ server.setHandler([&](const HttpData &request, HttpData &response, ResponseControl&) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+
+ });
+ auto callback = [&](QRestReply &reply) { networkReply = reply.networkReply(); };
+ const QByteArray byteArrayData{"some_data"_ba};
+ const QJsonObject jsonObjectData{{"key1", "value1"}, {"key2", "value2"}};
+ const QJsonArray jsonArrayData{{"arrvalue1", "arrvalue2", QJsonObject{{"key1", "value1"}}}};
+ const QVariantMap variantMapData{{"key1", "value1"}, {"key2", "value2"}};
+ const QByteArray methodDELETE{"DELETE"_ba};
+ const QByteArray methodHEAD{"HEAD"_ba};
+ const QByteArray methodPOST{"POST"_ba};
+ const QByteArray methodGET{"GET"_ba};
+ const QByteArray methodPUT{"PUT"_ba};
+ const QByteArray methodPATCH{"PATCH"_ba};
+ const QByteArray methodCUSTOM{"FOOBAR"_ba};
+
+ // DELETE
+ manager.deleteResource(request, this, callback);
+ VERIFY_REPLY_OK(methodDELETE);
+ QCOMPARE(serverSideRequest.body, ""_ba);
+
+ // HEAD
+ manager.head(request, this, callback);
+ VERIFY_REPLY_OK(methodHEAD);
+ QCOMPARE(serverSideRequest.body, ""_ba);
+
+ // GET
+ manager.get(request, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(serverSideRequest.body, ""_ba);
+
+ manager.get(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.get(request, QJsonDocument{jsonObjectData}, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.get(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodGET);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ // CUSTOM
+ manager.sendCustomRequest(request, methodCUSTOM, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodCUSTOM);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.sendCustomRequest(request, methodCUSTOM, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodCUSTOM);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ multiPart.reset(new QHttpMultiPart(QHttpMultiPart::FormDataType));
+ multiPart->append(part);
+ manager.sendCustomRequest(request, methodCUSTOM, multiPart.get(), this, callback);
+ VERIFY_REPLY_OK(methodCUSTOM);
+ QVERIFY(serverSideRequest.body.contains("--boundary"_ba));
+ QVERIFY(serverSideRequest.body.contains("multipart_text"_ba));
+
+ // POST
+ manager.post(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.post(request, QJsonDocument{jsonObjectData}, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.post(request, QJsonDocument{jsonArrayData}, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData);
+
+ manager.post(request, variantMapData, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType);
+ multiPart->append(part);
+ manager.post(request, multiPart.get(), this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QVERIFY(serverSideRequest.body.contains("--boundary"_ba));
+ QVERIFY(serverSideRequest.body.contains("multipart_text"_ba));
+
+ manager.post(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodPOST);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ // PUT
+ manager.put(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.put(request, QJsonDocument{jsonObjectData}, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.put(request, QJsonDocument{jsonArrayData}, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData);
+
+ manager.put(request, variantMapData, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType);
+ multiPart->append(part);
+ manager.put(request, multiPart.get(), this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QVERIFY(serverSideRequest.body.contains("--boundary"_ba));
+ QVERIFY(serverSideRequest.body.contains("multipart_text"_ba));
+
+ manager.put(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodPUT);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ // PATCH
+ manager.patch(request, byteArrayData, this, callback);
+ VERIFY_REPLY_OK(methodPATCH);
+ QCOMPARE(serverSideRequest.body, byteArrayData);
+
+ manager.patch(request, QJsonDocument{jsonObjectData}, this, callback);
+ VERIFY_REPLY_OK(methodPATCH);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.patch(request, QJsonDocument{jsonArrayData}, this, callback);
+ VERIFY_REPLY_OK(methodPATCH);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).array(), jsonArrayData);
+
+ manager.patch(request, variantMapData, this, callback);
+ VERIFY_REPLY_OK(methodPATCH);
+ QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
+
+ manager.patch(request, &bufferIoDevice, this, callback);
+ VERIFY_REPLY_OK(methodPATCH);
+ QCOMPARE(serverSideRequest.body, ioDeviceData);
+
+ //These must NOT compile
+ //manager.get(request, [](){}); // callback without context object
+ //manager.get(request, ""_ba, [](){}); // callback without context object
+ //manager.get(request, QString()); // wrong datatype
+ //manager.get(request, 123); // wrong datatype
+ //manager.post(request, QString()); // wrong datatype
+ //manager.put(request, 123); // wrong datatype
+ //manager.post(request); // data is required
+ //manager.put(request, QString()); // wrong datatype
+ //manager.put(request); // data is required
+ //manager.patch(request, 123); // wrong datatype
+ //manager.patch(request, QString()); // wrong datatype
+ //manager.patch(request); // data is required
+ //manager.deleteResource(request, "f"_ba); // data not allowed
+ //manager.head(request, "f"_ba); // data not allowed
+ //manager.post(request, ""_ba, this, [](int param){}); // Wrong callback signature
+ //manager.get(request, this, [](int param){}); // Wrong callback signature
+ //manager.sendCustomRequest(request, this, [](){}); // No verb && no data
+ //manager.sendCustomRequest(request, "FOOBAR", this, [](){}); // No verb || no data
+}
+#endif
+
+void tst_QRestAccessManager::memberHandler(QRestReply &reply)
+{
+ m_actualReplies.append(reply.networkReply());
+}
+
+// Class that is destroyed during an active request.
+// Used to test that the callbacks won't be called in these cases
+class Transient : public QObject
+{
+ Q_OBJECT
+public:
+ explicit Transient(tst_QRestAccessManager *test) : QObject(test), m_test(test) {}
+
+ void memberHandler(QRestReply &reply)
+ {
+ m_test->m_actualReplies.append(reply.networkReply());
+ }
+
+private:
+ tst_QRestAccessManager *m_test = nullptr;
+};
+
+template <typename Functor, std::enable_if_t<
+ QtPrivate::AreFunctionsCompatible<void(*)(QRestReply&), Functor>::value, bool> = true>
+inline constexpr bool isCompatibleCallback(Functor &&) { return true; }
+
+template <typename Functor, std::enable_if_t<
+ !QtPrivate::AreFunctionsCompatible<void(*)(QRestReply&), Functor>::value, bool> = true,
+ typename = void>
+inline constexpr bool isCompatibleCallback(Functor &&) { return false; }
+
+void tst_QRestAccessManager::callbacks()
+{
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+
+ QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error
+
+ auto lambdaHandler = [this](QRestReply &reply) { m_actualReplies.append(reply.networkReply()); };
+ Transient *transient = nullptr;
+ QByteArray data{"some_data"};
+
+ // Compile-time tests for callback signatures
+ static_assert(isCompatibleCallback([](QRestReply&){})); // Correct signature
+ static_assert(isCompatibleCallback(lambdaHandler));
+ static_assert(isCompatibleCallback(&Transient::memberHandler));
+ static_assert(isCompatibleCallback([](){})); // Less parameters are allowed
+
+ static_assert(!isCompatibleCallback([](QString){})); // Wrong parameter type
+ static_assert(!isCompatibleCallback([](QRestReply*){})); // Wrong parameter type
+ static_assert(!isCompatibleCallback([](const QString &){})); // Wrong parameter type
+ static_assert(!isCompatibleCallback([](QRestReply&, QString){})); // Too many parameters
+
+ // -- Test without data
+ // Without callback using signals and slot
+ QNetworkReply* reply = manager.get(request);
+ m_expectedReplies.append(reply);
+ QObject::connect(reply, &QNetworkReply::finished, this,
+ [this, reply](){m_actualReplies.append(reply);});
+
+ // With lambda callback, without context object
+ m_expectedReplies.append(manager.get(request, nullptr, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, nullptr,
+ [this](QRestReply &reply){m_actualReplies.append(reply.networkReply());}));
+ // With lambda callback and context object
+ m_expectedReplies.append(manager.get(request, this, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, this,
+ [this](QRestReply &reply){m_actualReplies.append(reply.networkReply());}));
+ // With member callback and context object
+ m_expectedReplies.append(manager.get(request, this, &tst_QRestAccessManager::memberHandler));
+ // With context object that is destroyed, there should be no callback or eg. crash.
+ transient = new Transient(this);
+ manager.get(request, transient, &Transient::memberHandler); // Reply not added to expecteds
+ delete transient;
+
+ // Let requests finish
+ QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size());
+ for (auto reply: m_actualReplies) {
+ QRestReply restReply(reply);
+ QVERIFY(!restReply.isSuccess());
+ QVERIFY(restReply.hasError());
+ QCOMPARE(restReply.error(), QNetworkReply::ProtocolUnknownError);
+ QCOMPARE(restReply.networkReply()->isFinished(), true);
+ restReply.networkReply()->deleteLater();
+ }
+ m_actualReplies.clear();
+ m_expectedReplies.clear();
+
+ // -- Test with data
+ // With lambda callback, without context object
+ m_expectedReplies.append(manager.post(request, data, nullptr, lambdaHandler));
+ m_expectedReplies.append(manager.post(request, data, nullptr,
+ [this](QRestReply &reply){m_actualReplies.append(reply.networkReply());}));
+ // With lambda callback and context object
+ m_expectedReplies.append(manager.post(request, data, this, lambdaHandler));
+ m_expectedReplies.append(manager.post(request, data, this,
+ [this](QRestReply &reply){m_actualReplies.append(reply.networkReply());}));
+ // With member callback and context object
+ m_expectedReplies.append(manager.post(request, data,
+ this, &tst_QRestAccessManager::memberHandler));
+ // With context object that is destroyed, there should be no callback or eg. crash
+ transient = new Transient(this);
+ manager.post(request, data, transient, &Transient::memberHandler); // Note: reply not expected
+ delete transient;
+
+ // Let requests finish
+ QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size());
+ for (auto reply: m_actualReplies) {
+ QRestReply restReply(reply);
+ QVERIFY(!restReply.isSuccess());
+ QVERIFY(restReply.hasError());
+ QCOMPARE(restReply.error(), QNetworkReply::ProtocolUnknownError);
+ QCOMPARE(restReply.networkReply()->isFinished(), true);
+ reply->deleteLater();
+ }
+ m_actualReplies.clear();
+ m_expectedReplies.clear();
+
+ // -- Test GET with data separately, as GET provides methods that are usable with and
+ // without data, and fairly easy to get the qrestaccessmanager.h template SFINAE subtly wrong.
+ // With lambda callback, without context object
+ m_expectedReplies.append(manager.get(request, data, nullptr, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, data, nullptr,
+ [this](QRestReply &reply){m_actualReplies.append(reply.networkReply());}));
+ // With lambda callback and context object
+ m_expectedReplies.append(manager.get(request, data, this, lambdaHandler));
+ m_expectedReplies.append(manager.get(request, data, this,
+ [this](QRestReply &reply){m_actualReplies.append(reply.networkReply());}));
+ // With member callback and context object
+ m_expectedReplies.append(manager.get(request, data,
+ this, &tst_QRestAccessManager::memberHandler));
+ // With context object that is destroyed, there should be no callback or eg. crash
+ transient = new Transient(this);
+ manager.get(request, data, transient, &Transient::memberHandler); // Reply not added
+ delete transient;
+
+ // Let requests finish
+ QTRY_COMPARE(m_actualReplies.size(), m_expectedReplies.size());
+ for (auto reply: m_actualReplies) {
+ QRestReply restReply(reply);
+ QVERIFY(!restReply.isSuccess());
+ QVERIFY(restReply.hasError());
+ QCOMPARE(restReply.error(), QNetworkReply::ProtocolUnknownError);
+ QCOMPARE(restReply.networkReply()->isFinished(), true);
+ restReply.networkReply()->deleteLater();
+ }
+ m_actualReplies.clear();
+ m_expectedReplies.clear();
+}
+
+void tst_QRestAccessManager::destruction()
+{
+ std::unique_ptr<QNetworkAccessManager> qnam = std::make_unique<QNetworkAccessManager>();
+ std::unique_ptr<QRestAccessManager> manager = std::make_unique<QRestAccessManager>(qnam.get());
+ QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error
+ m_expectedReplies.clear();
+ m_actualReplies.clear();
+ auto handler = [this](QRestReply &reply) { m_actualReplies.append(reply.networkReply()); };
+
+ // Delete reply immediately, make sure nothing bad happens and that there is no callback
+ QNetworkReply *networkReply = manager->get(request, this, handler);
+ delete networkReply;
+ QTest::qWait(20); // allow some time for the callback to arrive (it shouldn't)
+ QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0
+
+ // Delete access manager immediately after request, make sure nothing bad happens
+ manager->get(request, this, handler);
+ manager->post(request, "data"_ba, this, handler);
+ QTest::ignoreMessage(QtWarningMsg, "Access manager destroyed while 2 requests were still"
+ " in progress");
+ manager.reset();
+ QTest::qWait(20);
+ QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0
+
+ // Destroy the underlying QNAM while requests in progress
+ manager = std::make_unique<QRestAccessManager>(qnam.get());
+ manager->get(request, this, handler);
+ manager->post(request, "data"_ba, this, handler);
+ qnam.reset();
+ QTest::qWait(20);
+ QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0
+}
+
+#define VERIFY_HTTP_ERROR_STATUS(STATUS) \
+{ \
+ serverSideResponse.status = STATUS; \
+ QRestReply restReply(manager.get(request)); \
+ QTRY_VERIFY(restReply.networkReply()->isFinished()); \
+ QVERIFY(!restReply.hasError()); \
+ QCOMPARE(restReply.httpStatus(), serverSideResponse.status); \
+ QCOMPARE(restReply.error(), QNetworkReply::NetworkError::NoError); \
+ QVERIFY(!restReply.isSuccess()); \
+ restReply.networkReply()->deleteLater(); \
+} \
+
+void tst_QRestAccessManager::errors()
+{
+ // Tests the distinction between HTTP and other (network/protocol) errors
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+
+ HttpData serverSideResponse; // The response data the server responds with
+ server.setHandler([&](const HttpData &, HttpData &response, ResponseControl &) {
+ response = serverSideResponse;
+ });
+
+ // Test few HTTP statuses in different categories
+ VERIFY_HTTP_ERROR_STATUS(301) // QNetworkReply::ProtocolUnknownError
+ VERIFY_HTTP_ERROR_STATUS(302) // QNetworkReply::ProtocolUnknownError
+ VERIFY_HTTP_ERROR_STATUS(400) // QNetworkReply::ProtocolInvalidOperationError
+ VERIFY_HTTP_ERROR_STATUS(401) // QNetworkReply::AuthenticationRequiredEror
+ VERIFY_HTTP_ERROR_STATUS(402) // QNetworkReply::UnknownContentError
+ VERIFY_HTTP_ERROR_STATUS(403) // QNetworkReply::ContentAccessDenied
+ VERIFY_HTTP_ERROR_STATUS(404) // QNetworkReply::ContentNotFoundError
+ VERIFY_HTTP_ERROR_STATUS(405) // QNetworkReply::ContentOperationNotPermittedError
+ VERIFY_HTTP_ERROR_STATUS(406) // QNetworkReply::UnknownContentError
+ VERIFY_HTTP_ERROR_STATUS(407) // QNetworkReply::ProxyAuthenticationRequiredError
+ VERIFY_HTTP_ERROR_STATUS(408) // QNetworkReply::UnknownContentError
+ VERIFY_HTTP_ERROR_STATUS(409) // QNetworkReply::ContentConflictError
+ VERIFY_HTTP_ERROR_STATUS(410) // QNetworkReply::ContentGoneError
+ VERIFY_HTTP_ERROR_STATUS(500) // QNetworkReply::InternalServerError
+ VERIFY_HTTP_ERROR_STATUS(501) // QNetworkReply::OperationNotImplementedError
+ VERIFY_HTTP_ERROR_STATUS(502) // QNetworkReply::UnknownServerError
+ VERIFY_HTTP_ERROR_STATUS(503) // QNetworkReply::ServiceUnavailableError
+ VERIFY_HTTP_ERROR_STATUS(504) // QNetworkReply::UnknownServerError
+ VERIFY_HTTP_ERROR_STATUS(505) // QNetworkReply::UnknownServerError
+
+ {
+ // Test that actual network/protocol errors come through
+ QRestReply restReply(manager.get({})); // Empty url
+ QTRY_VERIFY(restReply.networkReply()->isFinished());
+ QVERIFY(restReply.hasError());
+ QVERIFY(!restReply.isSuccess());
+ QCOMPARE(restReply.error(), QNetworkReply::ProtocolUnknownError);
+ restReply.networkReply()->deleteLater();
+ }
+
+ {
+ QRestReply restReply(manager.get(QNetworkRequest{{"http://non-existent.foo.bar.test"}}));
+ QTRY_VERIFY(restReply.networkReply()->isFinished());
+ QVERIFY(restReply.hasError());
+ QVERIFY(!restReply.isSuccess());
+ QCOMPARE(restReply.error(), QNetworkReply::HostNotFoundError);
+ restReply.networkReply()->deleteLater();
+ }
+
+ {
+ QRestReply restReply(manager.get(request));
+ restReply.networkReply()->abort();
+ QTRY_VERIFY(restReply.networkReply()->isFinished());
+ QVERIFY(restReply.hasError());
+ QVERIFY(!restReply.isSuccess());
+ QCOMPARE(restReply.error(), QNetworkReply::OperationCanceledError);
+ restReply.networkReply()->deleteLater();
+ }
+}
+
+void tst_QRestAccessManager::body()
+{
+ // Test using QRestReply::body() data accessor
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QNetworkReply *networkReply = nullptr;
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ server.setHandler([&](const HttpData &request, HttpData &response, ResponseControl&) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+ });
+
+ {
+ serverSideResponse.status = 200;
+ serverSideResponse.body = "some_data"_ba;
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); });
+ QTRY_VERIFY(networkReply);
+ QRestReply restReply(networkReply);
+ QCOMPARE(restReply.readBody(), serverSideResponse.body);
+ QCOMPARE(restReply.httpStatus(), serverSideResponse.status);
+ QVERIFY(!restReply.hasError());
+ QVERIFY(restReply.isSuccess());
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ }
+
+ {
+ serverSideResponse.status = 200;
+ serverSideResponse.body = ""_ba; // Empty
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); });
+ QTRY_VERIFY(networkReply);
+ QRestReply restReply(networkReply);
+ QCOMPARE(restReply.readBody(), serverSideResponse.body);
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ }
+
+ {
+ serverSideResponse.status = 500;
+ serverSideResponse.body = "some_other_data"_ba;
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); });
+ QTRY_VERIFY(networkReply);
+ QRestReply restReply(networkReply);
+ QCOMPARE(restReply.readBody(), serverSideResponse.body);
+ QCOMPARE(restReply.httpStatus(), serverSideResponse.status);
+ QVERIFY(!restReply.hasError());
+ QVERIFY(!restReply.isSuccess());
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ }
+}
+
+void tst_QRestAccessManager::json()
+{
+ // Tests using QRestReply::readJson()
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QNetworkReply *networkReply = nullptr;
+ QJsonDocument responseJsonDocument;
+ std::optional<QJsonDocument> json;
+ QJsonParseError parseError;
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.status = 200;
+ server.setHandler([&](const HttpData &request, HttpData &response, ResponseControl&) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+ });
+
+ {
+ // Test receiving valid json object
+ serverSideResponse.body = "{\"key1\":\"value1\",""\"key2\":\"value2\"}\n"_ba;
+ networkReply = manager.get(request);
+ // Read unfinished reply
+ QVERIFY(!networkReply->isFinished());
+ QTest::ignoreMessage(QtWarningMsg, "readJson() called on an unfinished reply, ignoring");
+ parseError.error = QJsonParseError::ParseError::DocumentTooLarge; // Reset to impossible value
+ QRestReply restReply(networkReply);
+ QVERIFY(!restReply.readJson(&parseError));
+ QCOMPARE(parseError.error, QJsonParseError::ParseError::NoError);
+ // Read finished reply
+ QTRY_VERIFY(networkReply->isFinished());
+ parseError.error = QJsonParseError::ParseError::DocumentTooLarge;
+ json = restReply.readJson(&parseError);
+ QVERIFY(json);
+ QCOMPARE(parseError.error, QJsonParseError::ParseError::NoError);
+ responseJsonDocument = *json;
+ QVERIFY(responseJsonDocument.isObject());
+ QCOMPARE(responseJsonDocument["key1"], "value1");
+ QCOMPARE(responseJsonDocument["key2"], "value2");
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ }
+
+ {
+ // Test receiving an invalid json object
+ serverSideResponse.body = "foobar"_ba;
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); });
+ QTRY_VERIFY(networkReply);
+ QRestReply restReply(networkReply);
+ parseError.error = QJsonParseError::ParseError::DocumentTooLarge;
+ const auto json = restReply.readJson(&parseError);
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ QCOMPARE_EQ(json, std::nullopt);
+ QCOMPARE_NE(parseError.error, QJsonParseError::ParseError::NoError);
+ QCOMPARE_NE(parseError.error, QJsonParseError::ParseError::DocumentTooLarge);
+ QCOMPARE_GT(parseError.offset, 0);
+ }
+
+ {
+ // Test receiving valid json array
+ serverSideResponse.body = "[\"foo\", \"bar\"]\n"_ba;
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); });
+ QTRY_VERIFY(networkReply);
+ QRestReply restReply(networkReply);
+ parseError.error = QJsonParseError::ParseError::DocumentTooLarge;
+ json = restReply.readJson(&parseError);
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ QCOMPARE(parseError.error, QJsonParseError::ParseError::NoError);
+ QVERIFY(json);
+ responseJsonDocument = *json;
+ QVERIFY(responseJsonDocument.isArray());
+ QCOMPARE(responseJsonDocument.array().size(), 2);
+ QCOMPARE(responseJsonDocument[0].toString(), "foo"_L1);
+ QCOMPARE(responseJsonDocument[1].toString(), "bar"_L1);
+ }
+}
+
+#define VERIFY_TEXT_REPLY_OK \
+{ \
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); }); \
+ QTRY_VERIFY(networkReply); \
+ QRestReply restReply(networkReply); \
+ responseString = restReply.readText(); \
+ networkReply->deleteLater(); \
+ networkReply = nullptr; \
+ QCOMPARE(responseString, sourceString); \
+}
+
+#define VERIFY_TEXT_REPLY_ERROR(WARNING_MESSAGE) \
+{ \
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); }); \
+ QTRY_VERIFY(networkReply); \
+ QTest::ignoreMessage(QtWarningMsg, WARNING_MESSAGE); \
+ QRestReply restReply(networkReply); \
+ responseString = restReply.readText(); \
+ networkReply->deleteLater(); \
+ networkReply = nullptr; \
+ QVERIFY(responseString.isEmpty()); \
+}
+
+void tst_QRestAccessManager::text()
+{
+ // Test using QRestReply::text() data accessor with various text encodings
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+ QNetworkReply *networkReply = nullptr;
+ QJsonObject responseJsonObject;
+
+ QStringEncoder encUTF8("UTF-8");
+ QStringEncoder encUTF16("UTF-16");
+ QStringEncoder encUTF32("UTF-32");
+ QString responseString;
+
+ HttpData serverSideRequest; // The request data the server received
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.status = 200;
+ server.setHandler([&](const HttpData &request, HttpData &response, ResponseControl&) {
+ serverSideRequest = request;
+ response = serverSideResponse;
+ });
+
+ const QString sourceString("this is a string"_L1);
+
+ // Charset parameter of Content-Type header may specify non-UTF-8 character encoding.
+ //
+ // QString is UTF-16, and in the tests below we encode the response data to various
+ // charset encodings (into byte arrays). When we get the response data, the text()
+ // should consider the indicated charset and convert it to an UTF-16 QString => the returned
+ // QString from text() should match with the original (UTF-16) QString.
+
+ // Successful UTF-8 (explicit)
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-8"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-8 (obfuscated)
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=\"UT\\F-8\""_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-8 (empty charset)
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=\"\""_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-8 (implicit)
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-16
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-16"_ba);
+ serverSideResponse.body = encUTF16(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-16, parameter case insensitivity
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; chARset=uTf-16"_ba);
+ serverSideResponse.body = encUTF16(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-32
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-32"_ba);
+ serverSideResponse.body = encUTF32(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-32 with spec-wise allowed extra trailing content in the Content-Type header value
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType,
+ "text(this is a \\)comment)/ (this (too)) plain; charset = \"UTF-32\";extraparameter=bar"_ba);
+ serverSideResponse.body = encUTF32(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ // Successful UTF-32 with spec-wise allowed extra leading content in the Content-Type header value
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType,
+ "text/plain; extraparameter=bar;charset = \"UT\\F-32\""_ba);
+ serverSideResponse.body = encUTF32(sourceString);
+ VERIFY_TEXT_REPLY_OK;
+
+ {
+ // Unsuccessful UTF-32, wrong encoding indicated (indicated UTF-32 but data is UTF-8)
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-32"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); });
+ QTRY_VERIFY(networkReply);
+ QRestReply restReply(networkReply);
+ responseString = restReply.readText();
+ QCOMPARE_NE(responseString, sourceString);
+ networkReply->deleteLater();
+ networkReply = nullptr;
+ }
+
+ // Unsupported encoding
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=foo"_ba);
+ serverSideResponse.body = encUTF8(sourceString);
+ VERIFY_TEXT_REPLY_ERROR("readText(): Charset \"foo\" is not supported")
+
+ // Broken UTF-8
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-8"_ba);
+ serverSideResponse.body = "\xF0\x28\x8C\x28\xA0\xB0\xC0\xD0"; // invalid characters
+ VERIFY_TEXT_REPLY_ERROR("readText(): Decoding error occurred");
+}
+
+void tst_QRestAccessManager::textStreaming()
+{
+ // Tests textual data received in chunks
+ QNetworkAccessManager qnam;
+ QRestAccessManager manager(&qnam);
+ HttpTestServer server;
+ QTRY_VERIFY(server.isListening());
+ QNetworkRequest request(server.url());
+
+ // Create long text data
+ const QString expectedData = u"사랑abcd€fghiklmnΩpqrstuvwx愛사랑A사랑BCD€FGHIJKLMNΩPQRsTUVWXYZ愛"_s;
+ QString cumulativeReceivedText;
+ QStringEncoder encUTF8("UTF-8");
+ ResponseControl *responseControl = nullptr;
+
+ HttpData serverSideResponse; // The response data the server responds with
+ serverSideResponse.headers.removeAll(Header::ContentType);
+ serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-8"_ba);
+ serverSideResponse.body = encUTF8(expectedData);
+ serverSideResponse.status = 200;
+
+ server.setHandler([&](const HttpData &, HttpData &response, ResponseControl &control) {
+ response = serverSideResponse;
+ responseControl = &control; // store for later
+ control.responseChunkSize = 5; // tell testserver to send data in chunks of this size
+ });
+
+ {
+ QRestReply restReply(manager.get(request));
+ QObject::connect(restReply.networkReply(), &QNetworkReply::readyRead, this, [&]() {
+ cumulativeReceivedText += restReply.readText();
+ // Tell testserver that test is ready for next chunk
+ responseControl->readyForNextChunk = true;
+ });
+ QTRY_VERIFY(restReply.networkReply()->isFinished());
+ QCOMPARE(cumulativeReceivedText, expectedData);
+ restReply.networkReply()->deleteLater();
+ }
+
+ {
+ cumulativeReceivedText.clear();
+ // Broken UTF-8 characters after first five ok characters
+ serverSideResponse.body =
+ "12345"_ba + "\xF0\x28\x8C\x28\xA0\xB0\xC0\xD0" + "abcde"_ba;
+ QRestReply restReply(manager.get(request));
+ QObject::connect(restReply.networkReply(), &QNetworkReply::readyRead, this, [&]() {
+ static bool firstTime = true;
+ if (!firstTime) // First text part is without warnings
+ QTest::ignoreMessage(QtWarningMsg, "readText(): Decoding error occurred");
+ firstTime = false;
+ cumulativeReceivedText += restReply.readText();
+ // Tell testserver that test is ready for next chunk
+ responseControl->readyForNextChunk = true;
+ });
+ QTRY_VERIFY(restReply.networkReply()->isFinished());
+ QCOMPARE(cumulativeReceivedText, "12345"_ba);
+ restReply.networkReply()->deleteLater();
+ }
+}
+
+QTEST_MAIN(tst_QRestAccessManager)
+#include "tst_qrestaccessmanager.moc"