// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "httptestserver_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include 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(); void requests(); void reply(); void errors(); void body(); void json(); void text(); void textStreaming(); private: void memberHandler(QRestReply &reply); friend class Transient; QList m_expectedReplies; QList 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; \ } 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 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([&](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::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::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 } 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 ::value, bool> = true> inline constexpr bool isCompatibleCallback(Functor &&) { return true; } template ::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 qnam = std::make_unique(); std::unique_ptr manager = std::make_unique(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(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([&](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([&](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 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([&](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; QVERIFY(!restReply.readJson(&parseError).has_value()); // std::nullopt returned QCOMPARE_NE(parseError.error, QJsonParseError::ParseError::NoError); QCOMPARE_NE(parseError.error, QJsonParseError::ParseError::DocumentTooLarge); QCOMPARE_GT(parseError.offset, 0); networkReply->deleteLater(); networkReply = nullptr; } { // 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); 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); networkReply->deleteLater(); networkReply = nullptr; } } #define VERIFY_TEXT_REPLY_OK \ { \ manager.get(request, this, [&](QRestReply &reply) { networkReply = reply.networkReply(); }); \ QTRY_VERIFY(networkReply); \ QRestReply restReply(networkReply); \ responseString = restReply.readText(); \ QCOMPARE(responseString, sourceString); \ networkReply->deleteLater(); \ networkReply = nullptr; \ } #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(); \ QVERIFY(responseString.isEmpty()); \ networkReply->deleteLater(); \ networkReply = nullptr; \ } 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([&](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 serverSideResponse.headers.append(Header::ContentType, "text/plain; charset=UTF-8"_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 content in the Content-Type header value serverSideResponse.headers.removeAll(Header::ContentType); serverSideResponse.headers.append(Header::ContentType, "text/plain; charset = \"UTF-32\";extraparameter=bar"_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([&](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"