877 lines
37 KiB
C++
877 lines
37 KiB
C++
// 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"
|