Add REST client convenience wrappers
[ChangeLog][QtNetwork][QRestAccessManager] Added new convenience classes QRestAccessManager and QRestReply for typical RESTful client application usage Task-number: QTBUG-114637 Task-number: QTBUG-114701 Change-Id: I65057e56bf27f365b54bfd528565efd5f09386aa Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io> Reviewed-by: Mate Barany <mate.barany@qt.io> Reviewed-by: Marc Mutz <marc.mutz@qt.io>bb10
parent
f587ba1036
commit
e560adef21
|
|
@ -135,6 +135,8 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http
|
|||
access/qnetworkreplyhttpimpl.cpp access/qnetworkreplyhttpimpl_p.h
|
||||
access/qnetworkrequestfactory.cpp access/qnetworkrequestfactory_p.h
|
||||
access/qnetworkrequestfactory.h
|
||||
access/qrestaccessmanager.cpp access/qrestaccessmanager.h access/qrestaccessmanager_p.h
|
||||
access/qrestreply.cpp access/qrestreply.h access/qrestreply_p.h
|
||||
socket/qhttpsocketengine.cpp socket/qhttpsocketengine_p.h
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,819 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#include "qrestaccessmanager.h"
|
||||
#include "qrestaccessmanager_p.h"
|
||||
#include "qrestreply.h"
|
||||
|
||||
#include <QtNetwork/qhttpmultipart.h>
|
||||
#include <QtNetwork/qnetworkaccessmanager.h>
|
||||
#include <QtNetwork/qnetworkreply.h>
|
||||
|
||||
#if QT_CONFIG(ssl)
|
||||
#include <QtNetwork/qsslsocket.h>
|
||||
#endif
|
||||
|
||||
#include <QtCore/qjsondocument.h>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtCore/qloggingcategory.h>
|
||||
#include <QtCore/qthread.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcQrest, "qt.network.access.rest")
|
||||
|
||||
/*!
|
||||
|
||||
\class QRestAccessManager
|
||||
\brief The QRestAccessManager is a networking convenience class for RESTful
|
||||
client applications.
|
||||
\since 6.7
|
||||
|
||||
\ingroup network
|
||||
\inmodule QtNetwork
|
||||
\reentrant
|
||||
|
||||
QRestAccessManager provides a networking API for typical REST client
|
||||
applications. It provides the means to issue HTTP requests such as GET
|
||||
and POST. The responses to these requests can be handled with traditional
|
||||
Qt signal and slot mechanisms, as well as by providing callbacks
|
||||
directly - see \l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
The class is a wrapper on top of QNetworkAccessManager, and it both amends
|
||||
convenience methods and omits typically less used features. These
|
||||
features are still accessible by configuring the underlying
|
||||
QNetworkAccessManager directly. QRestAccessManager is closely related to
|
||||
the QRestReply class, which it returns when issuing network requests.
|
||||
|
||||
QRestAccessManager and related QRestReply classes can only be used in the
|
||||
thread they live in. For further information see
|
||||
\l {QObject#Thread Affinity}{QObject thread affinity} documentation.
|
||||
|
||||
\section1 Issuing Network Requests and Handling Replies
|
||||
|
||||
Network requests are initiated with a function call corresponding to
|
||||
the desired HTTP method, such as \c get() and \c post().
|
||||
|
||||
\section2 Using Signals and Slots
|
||||
|
||||
The function returns a QRestReply* object, whose signals can be used
|
||||
to follow up on the completion of the request in a traditional
|
||||
Qt-signals-and-slots way.
|
||||
|
||||
Here's an example of how you could send a GET request and handle the
|
||||
response:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 0
|
||||
|
||||
\section2 Using Callbacks and Context Objects
|
||||
|
||||
The functions also take a context object of QObject (subclass) type
|
||||
and a callback function as parameters. The callback takes one QRestReply*
|
||||
as a parameter. The callback can be any callable, incl. a
|
||||
pointer-to-member-function..
|
||||
|
||||
These callbacks are invoked when the QRestReply has finished processing
|
||||
(also in the case the processing finished due to an error).
|
||||
|
||||
The context object can be \c nullptr, although, generally speaking,
|
||||
this is discouraged. Using a valid context object ensures that if the
|
||||
context object is destroyed during request processing, the callback will
|
||||
not be called. Stray callbacks which access a destroyed context is a source
|
||||
of application misbehavior.
|
||||
|
||||
Here's an example of how you could send a GET request and check the
|
||||
response:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 1
|
||||
|
||||
Many of the functions take in data for sending to a server. The data is
|
||||
supplied as the second parameter after the request.
|
||||
|
||||
Here's an example of how you could send a POST request and check the
|
||||
response:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 2
|
||||
|
||||
\section2 Supported data types
|
||||
|
||||
The following table summarizes the methods and the supported data types.
|
||||
\c X means support.
|
||||
|
||||
\table
|
||||
\header
|
||||
\li Data type
|
||||
\li \c get()
|
||||
\li \c post()
|
||||
\li \c put()
|
||||
\li \c head()
|
||||
\li \c deleteResource()
|
||||
\row
|
||||
\li No data
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\li X
|
||||
\li X
|
||||
\row
|
||||
\li QByteArray
|
||||
\li X
|
||||
\li X
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\row
|
||||
\li QJsonObject *)
|
||||
\li X
|
||||
\li X
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\row
|
||||
\li QJsonArray *)
|
||||
\li -
|
||||
\li X
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\row
|
||||
\li QVariantMap **)
|
||||
\li -
|
||||
\li X
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\row
|
||||
\li QHttpMultiPart
|
||||
\li -
|
||||
\li X
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\row
|
||||
\li QIODevice
|
||||
\li X
|
||||
\li X
|
||||
\li X
|
||||
\li -
|
||||
\li -
|
||||
\endtable
|
||||
|
||||
*) QJsonObject and QJsonArray are sent in \l QJsonDocument::Compact format,
|
||||
and the \c Content-Type header is set to \c {application/json} if the
|
||||
\c Content-Type header was not set
|
||||
|
||||
**) QVariantMap is converted to and treated as a QJsonObject
|
||||
|
||||
\sa QRestReply, QNetworkRequestFactory, QNetworkAccessManager
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestAccessManager::authenticationRequired(QRestReply *reply,
|
||||
QAuthenticator *authenticator)
|
||||
|
||||
This signal is emitted when the final server requires authentication.
|
||||
The authentication relates to the provided \a reply instance, and any
|
||||
credentials are to be filled in the provided \a authenticator instance.
|
||||
|
||||
See \l QNetworkAccessManager::authenticationRequired() for details.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestAccessManager::proxyAuthenticationRequired(
|
||||
const QNetworkProxy &proxy, QAuthenticator *authenticator)
|
||||
|
||||
This signal is emitted when a proxy authentication requires action.
|
||||
The proxy details are in \a proxy object, and any credentials are
|
||||
to be filled in the provided \a authenticator object.
|
||||
|
||||
See \l QNetworkAccessManager::proxyAuthenticationRequired() for details.
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestAccessManager::requestFinished(QRestReply *reply)
|
||||
|
||||
This signal is emitted whenever a pending network reply is
|
||||
finished. \a reply parameter will contain a pointer to the
|
||||
reply that has just finished. This signal is emitted in tandem
|
||||
with the QRestReply::finished() signal. QRestReply provides
|
||||
functions for checking the status of the request, as well as for
|
||||
acquiring any received data.
|
||||
|
||||
\note Do not delete \a reply object in the slot connected to this
|
||||
signal. Use deleteLater() if needed. See also \l deletesRepliesOnFinished().
|
||||
|
||||
\sa QRestReply::finished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
|
||||
const QNetworkRequest &request,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
Issues an \c {HTTP GET} based on \a request.
|
||||
|
||||
The optional \a callback and \a context object can be provided for
|
||||
handling the request completion as illustrated below:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 3
|
||||
|
||||
Alternatively the signals of the returned QRestReply* object can be
|
||||
used. For further information see
|
||||
\l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
\sa QRestReply, QRestReply::finished(),
|
||||
QRestAccessManager::requestFinished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
|
||||
const QNetworkRequest &request, const QByteArray &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
Issues an \c {HTTP GET} based on \a request and provided \a data.
|
||||
|
||||
The optional \a callback and \a context object can be provided for
|
||||
handling the request completion as illustrated below:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 4
|
||||
|
||||
Alternatively the signals of the returned QRestReply* object can be
|
||||
used. For further information see
|
||||
\l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
\sa QRestReply, QRestReply::finished(),
|
||||
QRestAccessManager::requestFinished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
|
||||
const QNetworkRequest &request, const QJsonObject &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get(
|
||||
const QNetworkRequest &request, QIODevice *data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
|
||||
const QNetworkRequest &request, const QJsonObject &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
Issues an \c {HTTP POST} based on \a request.
|
||||
|
||||
The optional \a callback and \a context object can be provided for
|
||||
handling the request completion as illustrated below:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 5
|
||||
|
||||
Alternatively, the signals of the returned QRestReply* object can be
|
||||
used. For further information see
|
||||
\l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
The \c post() method always requires \a data parameter. The following
|
||||
data types are supported:
|
||||
\list
|
||||
\li QByteArray
|
||||
\li QJsonObject *)
|
||||
\li QJsonArray *)
|
||||
\li QVariantMap **)
|
||||
\li QHttpMultiPart*
|
||||
\li QIODevice*
|
||||
\endlist
|
||||
|
||||
*) Sent in \l QJsonDocument::Compact format, and the
|
||||
\c Content-Type header is set to \c {application/json} if the
|
||||
\c Content-Type header was not set
|
||||
**) QVariantMap is converted to and treated as a QJsonObject
|
||||
|
||||
\sa QRestReply, QRestReply::finished(),
|
||||
QRestAccessManager::requestFinished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
|
||||
const QNetworkRequest &request, const QJsonArray &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
|
||||
const QNetworkRequest &request, const QVariantMap &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
|
||||
const QNetworkRequest &request, const QByteArray &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
|
||||
const QNetworkRequest &request, QHttpMultiPart *data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post(
|
||||
const QNetworkRequest &request, QIODevice *data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
|
||||
const QNetworkRequest &request, const QJsonObject &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
Issues an \c {HTTP PUT} based on \a request.
|
||||
|
||||
The optional \a callback and \a context object can be provided for
|
||||
handling the request completion as illustrated below:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 6
|
||||
|
||||
Alternatively the signals of the returned QRestReply* object can be
|
||||
used. For further information see
|
||||
\l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
The \c put() method always requires \a data parameter. The following
|
||||
data types are supported:
|
||||
\list
|
||||
\li QByteArray
|
||||
\li QJsonObject *)
|
||||
\li QJsonArray *)
|
||||
\li QVariantMap **)
|
||||
\li QHttpMultiPart*
|
||||
\li QIODevice*
|
||||
\endlist
|
||||
|
||||
*) Sent in \l QJsonDocument::Compact format, and the
|
||||
\c Content-Type header is set to \c {application/json} if the
|
||||
\c Content-Type header was not set
|
||||
**) QVariantMap is converted to and treated as a QJsonObject
|
||||
|
||||
\sa QRestReply, QRestReply::finished(), QRestAccessManager::requestFinished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
|
||||
const QNetworkRequest &request, const QJsonArray &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
|
||||
const QNetworkRequest &request, const QVariantMap &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
|
||||
const QNetworkRequest &request, const QByteArray &data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
|
||||
const QNetworkRequest &request, QHttpMultiPart *data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put(
|
||||
const QNetworkRequest &request, QIODevice *data,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
\overload
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::head(
|
||||
const QNetworkRequest &request,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
Issues an \c {HTTP HEAD} based on \a request.
|
||||
|
||||
The optional \a callback and \a context object can be provided for
|
||||
handling the request completion as illustrated below:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 7
|
||||
|
||||
Alternatively the signals of the returned QRestReply* object can be
|
||||
used. For further information see
|
||||
\l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
\c head() request does not support providing data.
|
||||
|
||||
\sa QRestReply, QRestReply::finished(),
|
||||
QRestAccessManager::requestFinished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::deleteResource(
|
||||
const QNetworkRequest &request,
|
||||
const ContextTypeForFunctor<Functor> *context,
|
||||
Functor &&callback)
|
||||
|
||||
Issues an \c {HTTP DELETE} based on \a request.
|
||||
|
||||
The optional \a callback and \a context object can be provided for
|
||||
handling the request completion as illustrated below:
|
||||
|
||||
\snippet code/src_network_access_qrestaccessmanager.cpp 8
|
||||
|
||||
Alternatively the signals of the returned QRestReply* object can be
|
||||
used. For further information see
|
||||
\l {Issuing Network Requests and Handling Replies}.
|
||||
|
||||
\c deleteResource() request does not support providing data.
|
||||
|
||||
\sa QRestReply, QRestReply::finished(),
|
||||
QRestAccessManager::requestFinished()
|
||||
*/
|
||||
|
||||
/*
|
||||
Memory management/object ownership:
|
||||
- QRestAM is parent of QNAM and QRestReplies
|
||||
- QRestReplies are parents of QNetworkReplies
|
||||
*/
|
||||
|
||||
/*!
|
||||
Constructs a QRestAccessManager and sets \a parent as the parent object.
|
||||
*/
|
||||
QRestAccessManager::QRestAccessManager(QObject *parent)
|
||||
: QObject(*new QRestAccessManagerPrivate, parent)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
d->ensureNetworkAccessManager();
|
||||
}
|
||||
|
||||
/*!
|
||||
Destroys the QRestAccessManager object and frees up any
|
||||
resources, including any unfinished QRestReply objects.
|
||||
*/
|
||||
QRestAccessManager::~QRestAccessManager()
|
||||
= default;
|
||||
|
||||
/*!
|
||||
Returns whether QRestAccessManager is currently configured to automatically
|
||||
delete replies once they have finished. By default this is \c true.
|
||||
|
||||
\sa setDeletesRepliesOnFinished()
|
||||
*/
|
||||
bool QRestAccessManager::deletesRepliesOnFinished() const
|
||||
{
|
||||
Q_D(const QRestAccessManager);
|
||||
return d->deletesRepliesOnFinished;
|
||||
}
|
||||
|
||||
/*!
|
||||
Enables or disables automatic deletion of QRestReply instances
|
||||
once the request has finished, according to the provided
|
||||
\a autoDelete parameter. The deletion is done with deleteLater()
|
||||
so that using the replies in directly-connected slots or callbacks is safe.
|
||||
|
||||
\sa deletesRepliesOnFinished()
|
||||
*/
|
||||
void QRestAccessManager::setDeletesRepliesOnFinished(bool autoDelete)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
d->deletesRepliesOnFinished = autoDelete;
|
||||
}
|
||||
|
||||
/*!
|
||||
Aborts all unfinished network requests. Calling this function is same
|
||||
as calling QRestReply::abort() for all individual unfinished requests.
|
||||
|
||||
\sa QRestReply::abort(), QNetworkReply::abort()
|
||||
*/
|
||||
void QRestAccessManager::abortRequests()
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
|
||||
// Make copy of the reply container, as it might get modified when
|
||||
// aborting individual requests if they finish immediately
|
||||
const auto requests = d->activeRequests;
|
||||
for (const auto &[req, _] : requests.asKeyValueRange())
|
||||
req->abort();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the underlying QNetworkAccessManager instance. The instance
|
||||
can be used for accessing less-frequently used features and configurations.
|
||||
|
||||
\sa QNetworkAccessManager
|
||||
*/
|
||||
QNetworkAccessManager *QRestAccessManager::networkAccessManager() const
|
||||
{
|
||||
Q_D(const QRestAccessManager);
|
||||
return d->qnam;
|
||||
}
|
||||
|
||||
QRestAccessManagerPrivate::QRestAccessManagerPrivate()
|
||||
= default;
|
||||
|
||||
QRestAccessManagerPrivate::~QRestAccessManagerPrivate()
|
||||
{
|
||||
if (!activeRequests.isEmpty()) {
|
||||
qCWarning(lcQrest, "Access manager destroyed while %lld requests were still in progress",
|
||||
qlonglong(activeRequests.size()));
|
||||
}
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
|
||||
const QJsonObject &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); },
|
||||
data, request, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
|
||||
const QJsonArray &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); },
|
||||
data, request, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
|
||||
const QVariantMap &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
return postWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
|
||||
const QByteArray &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
|
||||
QHttpMultiPart *data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request,
|
||||
QIODevice *data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::getNoDataImpl(const QNetworkRequest &request,
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->get(request); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request,
|
||||
const QByteArray &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request,
|
||||
const QJsonObject &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&](auto req, auto json) { return d->qnam->get(req, json); },
|
||||
data, request, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request,
|
||||
QIODevice *data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::deleteResourceNoDataImpl(const QNetworkRequest &request,
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->deleteResource(request); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::headNoDataImpl(const QNetworkRequest &request,
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->head(request); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
|
||||
const QJsonObject &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); },
|
||||
data, request, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
|
||||
const QJsonArray &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); },
|
||||
data, request, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
|
||||
const QVariantMap &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
return putWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
|
||||
const QByteArray &data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request,
|
||||
QHttpMultiPart *data, const QObject *context,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, QIODevice *data,
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_D(QRestAccessManager);
|
||||
return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot);
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManagerPrivate::createActiveRequest(QNetworkReply *networkReply,
|
||||
const QObject *contextObject,
|
||||
QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
Q_Q(QRestAccessManager);
|
||||
Q_ASSERT(networkReply);
|
||||
auto restReply = new QRestReply(networkReply, q);
|
||||
QtPrivate::SlotObjSharedPtr slotPtr(QtPrivate::SlotObjUniquePtr{slot}); // adopts
|
||||
activeRequests.insert(restReply, CallerInfo{contextObject, slotPtr});
|
||||
|
||||
// If context object is provided, use it with connect => context object lifecycle is considered
|
||||
const QObject *context = contextObject ? contextObject : q;
|
||||
QObject::connect(networkReply, &QNetworkReply::finished, context, [restReply, this]() {
|
||||
handleReplyFinished(restReply);
|
||||
});
|
||||
// Safe guard in case reply is destroyed before it's finished
|
||||
QObject::connect(restReply, &QRestReply::destroyed, q, [restReply, this]() {
|
||||
activeRequests.remove(restReply);
|
||||
});
|
||||
// If context object is destroyed, clean up any possible replies it had associated with it
|
||||
if (contextObject) {
|
||||
QObject::connect(contextObject, &QObject::destroyed, q, [restReply, this]() {
|
||||
activeRequests.remove(restReply);
|
||||
});
|
||||
}
|
||||
return restReply;
|
||||
}
|
||||
|
||||
void QRestAccessManagerPrivate::verifyThreadAffinity(const QObject *contextObject)
|
||||
{
|
||||
Q_Q(QRestAccessManager);
|
||||
if (QThread::currentThread() != q->thread()) {
|
||||
qCWarning(lcQrest, "QRestAccessManager can only be called in the thread it belongs to");
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
if (contextObject && (contextObject->thread() != q->thread())) {
|
||||
qCWarning(lcQrest, "QRestAccessManager: the context object must reside in the same thread");
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
}
|
||||
|
||||
void QRestAccessManagerPrivate::ensureNetworkAccessManager()
|
||||
{
|
||||
Q_Q(QRestAccessManager);
|
||||
if (!qnam) {
|
||||
qnam = new QNetworkAccessManager(q);
|
||||
connect(qnam, &QNetworkAccessManager::authenticationRequired, this,
|
||||
&QRestAccessManagerPrivate::handleAuthenticationRequired);
|
||||
#ifndef QT_NO_NETWORKPROXY
|
||||
QObject::connect(qnam, &QNetworkAccessManager::proxyAuthenticationRequired,
|
||||
q, &QRestAccessManager::proxyAuthenticationRequired);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void QRestAccessManagerPrivate::handleReplyFinished(QRestReply *restReply)
|
||||
{
|
||||
Q_Q(QRestAccessManager);
|
||||
|
||||
auto request = activeRequests.find(restReply);
|
||||
if (request == activeRequests.end()) {
|
||||
qCWarning(lcQrest, "Unexpected reply received, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
CallerInfo caller = request.value();
|
||||
activeRequests.erase(request);
|
||||
|
||||
if (caller.slot) {
|
||||
// Callback was provided. If we have context object, use it.
|
||||
// For clarity: being here with a context object means it has not been destroyed
|
||||
// while the request has been in progress
|
||||
void *argv[] = { nullptr, &restReply };
|
||||
QObject *context = caller.contextObject
|
||||
? const_cast<QObject*>(caller.contextObject) : nullptr;
|
||||
caller.slot->call(context, argv);
|
||||
}
|
||||
if (restReply->hasError())
|
||||
emit restReply->errorOccurred(restReply);
|
||||
emit restReply->finished(restReply);
|
||||
emit q->requestFinished(restReply);
|
||||
|
||||
if (deletesRepliesOnFinished)
|
||||
restReply->deleteLater();
|
||||
}
|
||||
|
||||
void QRestAccessManagerPrivate::handleAuthenticationRequired(QNetworkReply *networkReply,
|
||||
QAuthenticator *authenticator)
|
||||
{
|
||||
Q_Q(QRestAccessManager);
|
||||
QRestReply *restReply = restReplyFromNetworkReply(networkReply);
|
||||
if (restReply)
|
||||
emit q->authenticationRequired(restReply, authenticator);
|
||||
else
|
||||
qCWarning(lcQrest, "No matching QRestReply for authentication, ignoring.");
|
||||
}
|
||||
|
||||
QRestReply *QRestAccessManagerPrivate::restReplyFromNetworkReply(QNetworkReply *networkReply)
|
||||
{
|
||||
for (const auto &[restReply,_] : activeRequests.asKeyValueRange()) {
|
||||
if (restReply->networkReply() == networkReply)
|
||||
return restReply;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include "moc_qrestaccessmanager.cpp"
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QRESTACCESSMANAGER_H
|
||||
#define QRESTACCESSMANAGER_H
|
||||
|
||||
#include <QtNetwork/qnetworkaccessmanager.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QRestReply;
|
||||
|
||||
#define QREST_METHOD_WITH_DATA(METHOD, DATA) \
|
||||
public: \
|
||||
template <typename Functor, if_compatible_callback<Functor> = true> \
|
||||
QRestReply *METHOD(const QNetworkRequest &request, DATA data, \
|
||||
const ContextTypeForFunctor<Functor> *context, \
|
||||
Functor &&callback) \
|
||||
{ \
|
||||
return METHOD##WithDataImpl(request, data, context, \
|
||||
QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \
|
||||
} \
|
||||
QRestReply *METHOD(const QNetworkRequest &request, DATA data) \
|
||||
{ \
|
||||
return METHOD##WithDataImpl(request, data, nullptr, nullptr); \
|
||||
} \
|
||||
private: \
|
||||
QRestReply *METHOD##WithDataImpl(const QNetworkRequest &request, DATA data, \
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot); \
|
||||
/* end */
|
||||
|
||||
|
||||
#define QREST_METHOD_NO_DATA(METHOD) \
|
||||
public: \
|
||||
template <typename Functor, if_compatible_callback<Functor> = true> \
|
||||
QRestReply *METHOD(const QNetworkRequest &request, \
|
||||
const ContextTypeForFunctor<Functor> *context, \
|
||||
Functor &&callback) \
|
||||
{ \
|
||||
return METHOD##NoDataImpl(request, context, \
|
||||
QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \
|
||||
} \
|
||||
QRestReply *METHOD(const QNetworkRequest &request) \
|
||||
{ \
|
||||
return METHOD##NoDataImpl(request, nullptr, nullptr); \
|
||||
} \
|
||||
private: \
|
||||
QRestReply *METHOD##NoDataImpl(const QNetworkRequest &request, \
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot); \
|
||||
/* end */
|
||||
|
||||
class QRestAccessManagerPrivate;
|
||||
class Q_NETWORK_EXPORT QRestAccessManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using CallbackPrototype = void(*)(QRestReply*);
|
||||
template <typename Functor>
|
||||
using ContextTypeForFunctor = typename QtPrivate::ContextTypeForFunctor<Functor>::ContextType;
|
||||
template <typename Functor>
|
||||
using if_compatible_callback = std::enable_if_t<
|
||||
QtPrivate::AreFunctionsCompatible<CallbackPrototype, Functor>::value, bool>;
|
||||
public:
|
||||
explicit QRestAccessManager(QObject *parent = nullptr);
|
||||
~QRestAccessManager() override;
|
||||
|
||||
QNetworkAccessManager *networkAccessManager() const;
|
||||
|
||||
bool deletesRepliesOnFinished() const;
|
||||
void setDeletesRepliesOnFinished(bool autoDelete);
|
||||
|
||||
void abortRequests();
|
||||
|
||||
QREST_METHOD_NO_DATA(deleteResource)
|
||||
QREST_METHOD_NO_DATA(head)
|
||||
QREST_METHOD_NO_DATA(get)
|
||||
QREST_METHOD_WITH_DATA(get, const QByteArray &)
|
||||
QREST_METHOD_WITH_DATA(get, const QJsonObject &)
|
||||
QREST_METHOD_WITH_DATA(get, QIODevice *)
|
||||
QREST_METHOD_WITH_DATA(post, const QJsonObject &)
|
||||
QREST_METHOD_WITH_DATA(post, const QJsonArray &)
|
||||
QREST_METHOD_WITH_DATA(post, const QVariantMap &)
|
||||
QREST_METHOD_WITH_DATA(post, const QByteArray &)
|
||||
QREST_METHOD_WITH_DATA(post, QHttpMultiPart *)
|
||||
QREST_METHOD_WITH_DATA(post, QIODevice *)
|
||||
QREST_METHOD_WITH_DATA(put, const QJsonObject &)
|
||||
QREST_METHOD_WITH_DATA(put, const QJsonArray &)
|
||||
QREST_METHOD_WITH_DATA(put, const QVariantMap &)
|
||||
QREST_METHOD_WITH_DATA(put, const QByteArray &)
|
||||
QREST_METHOD_WITH_DATA(put, QHttpMultiPart *)
|
||||
QREST_METHOD_WITH_DATA(put, QIODevice *)
|
||||
|
||||
Q_SIGNALS:
|
||||
#ifndef QT_NO_NETWORKPROXY
|
||||
void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator);
|
||||
#endif
|
||||
void authenticationRequired(QRestReply *reply, QAuthenticator *authenticator);
|
||||
void requestFinished(QRestReply *reply);
|
||||
|
||||
private:
|
||||
Q_DECLARE_PRIVATE(QRestAccessManager)
|
||||
Q_DISABLE_COPY(QRestAccessManager)
|
||||
};
|
||||
|
||||
#undef QREST_METHOD_NO_DATA
|
||||
#undef QREST_METHOD_WITH_DATA
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QRESTACCESSMANAGER_P_H
|
||||
#define QRESTACCESSMANAGER_P_H
|
||||
|
||||
//
|
||||
// W A R N I N G
|
||||
// -------------
|
||||
//
|
||||
// This file is not part of the Qt API. It exists for the convenience
|
||||
// of the Network Access API. This header file may change from
|
||||
// version to version without notice, or even be removed.
|
||||
//
|
||||
// We mean it.
|
||||
//
|
||||
|
||||
#include "qrestaccessmanager.h"
|
||||
#include "private/qobject_p.h"
|
||||
|
||||
#include <QtNetwork/qnetworkaccessmanager.h>
|
||||
|
||||
#include <QtCore/qjsonarray.h>
|
||||
#include <QtCore/qhash.h>
|
||||
#include <QtCore/qjsondocument.h>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QRestReply;
|
||||
class QRestAccessManagerPrivate : public QObjectPrivate
|
||||
{
|
||||
public:
|
||||
QRestAccessManagerPrivate();
|
||||
~QRestAccessManagerPrivate() override;
|
||||
|
||||
void ensureNetworkAccessManager();
|
||||
|
||||
QRestReply *createActiveRequest(QNetworkReply *networkReply, const QObject *contextObject,
|
||||
QtPrivate::QSlotObjectBase *slot);
|
||||
|
||||
void removeActiveRequest(QRestReply *restReply);
|
||||
void handleReplyFinished(QRestReply *restReply);
|
||||
void handleAuthenticationRequired(QNetworkReply *networkReply, QAuthenticator *authenticator);
|
||||
QRestReply *restReplyFromNetworkReply(QNetworkReply *networkReply);
|
||||
|
||||
template<typename Functor>
|
||||
QRestReply *executeRequest(Functor requestOperation,
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
verifyThreadAffinity(context);
|
||||
QNetworkReply *reply = requestOperation();
|
||||
return createActiveRequest(reply, context, slot);
|
||||
}
|
||||
|
||||
template<typename Functor, typename Json>
|
||||
QRestReply *executeRequest(Functor requestOperation, Json jsonData,
|
||||
const QNetworkRequest &request,
|
||||
const QObject *context, QtPrivate::QSlotObjectBase *slot)
|
||||
{
|
||||
verifyThreadAffinity(context);
|
||||
QNetworkRequest req(request);
|
||||
if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) {
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader,
|
||||
QLatin1StringView{"application/json"});
|
||||
}
|
||||
QJsonDocument json(jsonData);
|
||||
QNetworkReply *reply = requestOperation(req, json.toJson(QJsonDocument::Compact));
|
||||
return createActiveRequest(reply, context, slot);
|
||||
}
|
||||
|
||||
void verifyThreadAffinity(const QObject *contextObject);
|
||||
|
||||
struct CallerInfo {
|
||||
const QObject *contextObject = nullptr;
|
||||
QtPrivate::SlotObjSharedPtr slot;
|
||||
};
|
||||
QHash<QRestReply*, CallerInfo> activeRequests;
|
||||
|
||||
QNetworkAccessManager *qnam = nullptr;
|
||||
bool deletesRepliesOnFinished = true;
|
||||
Q_DECLARE_PUBLIC(QRestAccessManager)
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#include "qrestreply.h"
|
||||
#include "qrestreply_p.h"
|
||||
|
||||
#include <QtCore/qjsonarray.h>
|
||||
#include <QtCore/qjsondocument.h>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtCore/qlatin1stringmatcher.h>
|
||||
#include <QtCore/qloggingcategory.h>
|
||||
#include <QtCore/qstringconverter.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcQrest)
|
||||
|
||||
/*!
|
||||
\class QRestReply
|
||||
\since 6.7
|
||||
\brief QRestReply is the class for following up the requests sent with
|
||||
QRestAccessManager.
|
||||
|
||||
\reentrant
|
||||
\ingroup network
|
||||
\inmodule QtNetwork
|
||||
|
||||
QRestReply is a convenience class for typical RESTful client
|
||||
applications. It wraps the more detailed QNetworkReply and provides
|
||||
convenience methods for data and status handling.
|
||||
|
||||
\sa QRestAccessManager, QNetworkReply
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestReply::finished(QRestReply *reply)
|
||||
|
||||
This signal is emitted when \a reply has finished processing. This
|
||||
signal is emitted also in cases when the reply finished due to network
|
||||
or protocol errors (the server did not reply with an HTTP status).
|
||||
|
||||
\sa isFinished(), httpStatus(), error()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestReply::errorOccurred(QRestReply *reply)
|
||||
|
||||
This signal is emitted if, while processing \a reply, an error occurs that
|
||||
is considered to be a network/protocol error. These errors are
|
||||
disctinct from HTTP error responses such as \c {500 Internal Server Error}.
|
||||
This signal is emitted together with the
|
||||
finished() signal, and often connecting to that is sufficient.
|
||||
|
||||
\sa finished(), isFinished(), httpStatus(), error()
|
||||
*/
|
||||
|
||||
QRestReply::QRestReply(QNetworkReply *reply, QObject *parent)
|
||||
: QObject(*new QRestReplyPrivate, parent)
|
||||
{
|
||||
Q_D(QRestReply);
|
||||
Q_ASSERT(reply);
|
||||
d->networkReply = reply;
|
||||
// Reparent so that destruction of QRestReply destroys QNetworkReply
|
||||
reply->setParent(this);
|
||||
}
|
||||
|
||||
/*!
|
||||
Destroys this QRestReply object.
|
||||
|
||||
\sa abort()
|
||||
*/
|
||||
QRestReply::~QRestReply()
|
||||
= default;
|
||||
|
||||
/*!
|
||||
Returns a pointer to the underlying QNetworkReply wrapped by this object.
|
||||
*/
|
||||
QNetworkReply *QRestReply::networkReply() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
return d->networkReply;
|
||||
}
|
||||
|
||||
/*!
|
||||
Aborts the network operation immediately. The finished() signal
|
||||
will be emitted.
|
||||
|
||||
\sa QRestAccessManager::abortRequests() QNetworkReply::abort()
|
||||
*/
|
||||
void QRestReply::abort()
|
||||
{
|
||||
Q_D(QRestReply);
|
||||
d->networkReply->abort();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the received data as a QJsonObject. Requires the reply to be
|
||||
finished.
|
||||
|
||||
The returned value is wrapped in \c std::optional. If the conversion
|
||||
from the received data fails (empty data or JSON parsing error),
|
||||
\c std::nullopt is returned.
|
||||
|
||||
Calling this function consumes the received data, and any further calls
|
||||
to get response data will return empty.
|
||||
|
||||
This function returns \c {std::nullopt} and will not consume
|
||||
any data if the reply is not finished.
|
||||
|
||||
\sa jsonArray(), body(), text(), finished(), isFinished()
|
||||
*/
|
||||
std::optional<QJsonObject> QRestReply::json()
|
||||
{
|
||||
Q_D(QRestReply);
|
||||
if (!isFinished()) {
|
||||
qCWarning(lcQrest, "Attempt to read json() of an unfinished reply, ignoring.");
|
||||
return std::nullopt;
|
||||
}
|
||||
const QJsonDocument json = d->replyDataToJson();
|
||||
return json.isObject() ? std::optional{json.object()} : std::nullopt;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the received data as a QJsonArray. Requires the reply to be
|
||||
finished.
|
||||
|
||||
The returned value is wrapped in \c std::optional. If the conversion
|
||||
from the received data fails (empty data or JSON parsing error),
|
||||
\c std::nullopt is returned.
|
||||
|
||||
Calling this function consumes the received data, and any further calls
|
||||
to get response data will return empty.
|
||||
|
||||
This function returns \c {std::nullopt} and will not consume
|
||||
any data if the reply is not finished.
|
||||
|
||||
\sa json(), body(), text(), finished(), isFinished()
|
||||
*/
|
||||
std::optional<QJsonArray> QRestReply::jsonArray()
|
||||
{
|
||||
Q_D(QRestReply);
|
||||
if (!isFinished()) {
|
||||
qCWarning(lcQrest, "Attempt to read jsonArray() of an unfinished reply, ignoring.");
|
||||
return std::nullopt;
|
||||
}
|
||||
const QJsonDocument json = d->replyDataToJson();
|
||||
return json.isArray() ? std::optional{json.array()} : std::nullopt;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the received data as a QByteArray.
|
||||
|
||||
Calling this function consumes the data received so far, and any further
|
||||
calls to get response data will return empty until further data has been
|
||||
received.
|
||||
|
||||
\sa json(), text()
|
||||
*/
|
||||
QByteArray QRestReply::body()
|
||||
{
|
||||
Q_D(QRestReply);
|
||||
return d->networkReply->readAll();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the received data as a QString. Requires the reply to be finished.
|
||||
|
||||
The received data is decoded into a QString (UTF-16). The decoding
|
||||
uses the \e Content-Type header's \e charset parameter to determine the
|
||||
source encoding, if available. If the encoding information is not
|
||||
available or not supported by \l QStringConverter, UTF-8 is used as a
|
||||
default.
|
||||
|
||||
Calling this function consumes the received data, and any further calls
|
||||
to get response data will return empty.
|
||||
|
||||
This function returns a default-constructed value and will not consume
|
||||
any data if the reply is not finished.
|
||||
|
||||
\sa json(), body(), isFinished(), finished()
|
||||
*/
|
||||
QString QRestReply::text()
|
||||
{
|
||||
Q_D(QRestReply);
|
||||
if (!isFinished()) {
|
||||
qCWarning(lcQrest, "Attempt to read text() of an unfinished reply, ignoring.");
|
||||
return {};
|
||||
}
|
||||
QByteArray data = d->networkReply->readAll();
|
||||
if (data.isEmpty())
|
||||
return {};
|
||||
|
||||
const QByteArray charset = d->contentCharset();
|
||||
QStringDecoder decoder(charset);
|
||||
if (!decoder.isValid()) { // the decoder may not support the mimetype's charset
|
||||
qCWarning(lcQrest, "Charset \"%s\" is not supported, defaulting to UTF-8",
|
||||
charset.constData());
|
||||
decoder = QStringDecoder(QStringDecoder::Utf8);
|
||||
}
|
||||
return decoder(data);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the HTTP status received in the server response.
|
||||
The value is \e 0 if not available (the status line has not been received,
|
||||
yet).
|
||||
|
||||
\note The HTTP status is reported as indicated by the received HTTP
|
||||
response. It is possible that an error() occurs after receiving the status,
|
||||
for instance due to network disconnection while receiving a long response.
|
||||
These potential subsequent errors are not represented by the reported
|
||||
HTTP status.
|
||||
|
||||
\sa isSuccess(), hasError(), error()
|
||||
*/
|
||||
int QRestReply::httpStatus() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
return d->networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
}
|
||||
|
||||
/*!
|
||||
\fn bool QRestReply::isSuccess() const
|
||||
|
||||
Returns whether the HTTP status is between 200..299 and no
|
||||
further errors have occurred while receiving the response (for example
|
||||
abrupt disconnection while receiving the body data). This function
|
||||
is a convenient way to check whether the response is considered successful.
|
||||
|
||||
\sa httpStatus(), hasError(), error()
|
||||
*/
|
||||
|
||||
/*!
|
||||
Returns whether the HTTP status is between 200..299.
|
||||
|
||||
\sa isSuccess(), httpStatus(), hasError(), error()
|
||||
*/
|
||||
bool QRestReply::isHttpStatusSuccess() const
|
||||
{
|
||||
const int status = httpStatus();
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns whether an error has occurred. This includes errors such as
|
||||
network and protocol errors, but excludes cases where the server
|
||||
successfully responded with an HTTP error status (for example
|
||||
\c {500 Internal Server Error}). Use \l httpStatus() or
|
||||
\l isHttpStatusSuccess() to get the HTTP status information.
|
||||
|
||||
\sa httpStatus(), isSuccess(), error(), errorString()
|
||||
*/
|
||||
bool QRestReply::hasError() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
return d->hasNonHttpError();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the last error, if any. The errors include
|
||||
errors such as network and protocol errors, but exclude
|
||||
cases when the server successfully responded with an HTTP status.
|
||||
|
||||
\sa httpStatus(), isSuccess(), hasError(), errorString()
|
||||
*/
|
||||
QNetworkReply::NetworkError QRestReply::error() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
if (!hasError())
|
||||
return QNetworkReply::NetworkError::NoError;
|
||||
return d->networkReply->error();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns a human-readable description of the last network error.
|
||||
|
||||
\sa httpStatus(), isSuccess(), hasError(), error()
|
||||
*/
|
||||
QString QRestReply::errorString() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
if (hasError())
|
||||
return d->networkReply->errorString();
|
||||
return {};
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns whether the network request has finished.
|
||||
*/
|
||||
bool QRestReply::isFinished() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
return d->networkReply->isFinished();
|
||||
}
|
||||
|
||||
QRestReplyPrivate::QRestReplyPrivate()
|
||||
= default;
|
||||
|
||||
QRestReplyPrivate::~QRestReplyPrivate()
|
||||
= default;
|
||||
|
||||
QByteArray QRestReplyPrivate::contentCharset() const
|
||||
{
|
||||
// Content-type consists of mimetype and optional parameters, of which one may be 'charset'
|
||||
// Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5
|
||||
// and RFC 2045 section 5.1
|
||||
//
|
||||
// text/plain; charset=utf-8
|
||||
// text/plain; charset=utf-8;version=1.7
|
||||
// text/plain; charset = utf-8
|
||||
// text/plain; charset ="utf-8"
|
||||
QByteArray contentTypeValue =
|
||||
networkReply->header(QNetworkRequest::KnownHeaders::ContentTypeHeader).toByteArray();
|
||||
// Default to the most commonly used UTF-8.
|
||||
QByteArray charset{"UTF-8"};
|
||||
|
||||
QList<QByteArray> parameters = contentTypeValue.split(';');
|
||||
if (parameters.size() >= 2) { // Need at least one parameter in addition to the mimetype itself
|
||||
parameters.removeFirst(); // Exclude the mimetype itself, only interested in parameters
|
||||
QLatin1StringMatcher matcher("charset="_L1, Qt::CaseSensitivity::CaseInsensitive);
|
||||
qsizetype matchIndex = -1;
|
||||
for (auto ¶meter : parameters) {
|
||||
// Remove whitespaces and parantheses
|
||||
const QByteArray curated = parameter.replace(" ", "").replace("\"","");
|
||||
// Check for match
|
||||
matchIndex = matcher.indexIn(QLatin1String(curated.constData()));
|
||||
if (matchIndex >= 0) {
|
||||
charset = curated.sliced(matchIndex + 8); // 8 is size of "charset="
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return charset;
|
||||
}
|
||||
|
||||
// Returns true if there's an error that isn't appropriately indicated by the HTTP status
|
||||
bool QRestReplyPrivate::hasNonHttpError() const
|
||||
{
|
||||
const int status = networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (status > 0) {
|
||||
// The HTTP status is set upon receiving the response headers, but the
|
||||
// connection might still fail later while receiving the body data.
|
||||
return networkReply->error() == QNetworkReply::RemoteHostClosedError;
|
||||
}
|
||||
return networkReply->error() != QNetworkReply::NoError;
|
||||
}
|
||||
|
||||
QJsonDocument QRestReplyPrivate::replyDataToJson()
|
||||
{
|
||||
QJsonParseError parseError;
|
||||
const QByteArray data = networkReply->readAll();
|
||||
const QJsonDocument json = QJsonDocument::fromJson(data, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
qCDebug(lcQrest) << "Response data not JSON:" << parseError.errorString()
|
||||
<< "at" << parseError.offset << data;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include "moc_qrestreply.cpp"
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QRESTREPLY_H
|
||||
#define QRESTREPLY_H
|
||||
|
||||
#include <QtNetwork/qnetworkreply.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QRestReplyPrivate;
|
||||
class Q_NETWORK_EXPORT QRestReply : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
~QRestReply() override;
|
||||
|
||||
QNetworkReply *networkReply() const;
|
||||
|
||||
std::optional<QJsonObject> json();
|
||||
std::optional<QJsonArray> jsonArray();
|
||||
QByteArray body();
|
||||
QString text();
|
||||
|
||||
bool isSuccess() const
|
||||
{
|
||||
return !hasError() && isHttpStatusSuccess();
|
||||
}
|
||||
int httpStatus() const;
|
||||
bool isHttpStatusSuccess() const;
|
||||
|
||||
bool hasError() const;
|
||||
QNetworkReply::NetworkError error() const;
|
||||
QString errorString() const;
|
||||
|
||||
bool isFinished() const;
|
||||
|
||||
public Q_SLOTS:
|
||||
void abort();
|
||||
|
||||
Q_SIGNALS:
|
||||
void finished(QRestReply *reply);
|
||||
void errorOccurred(QRestReply *reply);
|
||||
|
||||
private:
|
||||
friend class QRestAccessManagerPrivate;
|
||||
explicit QRestReply(QNetworkReply *reply, QObject *parent = nullptr);
|
||||
Q_DECLARE_PRIVATE(QRestReply)
|
||||
Q_DISABLE_COPY_MOVE(QRestReply)
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // QRESTREPLY_H
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
|
||||
#ifndef QRESTREPLY_P_H
|
||||
#define QRESTREPLY_P_H
|
||||
|
||||
//
|
||||
// W A R N I N G
|
||||
// -------------
|
||||
//
|
||||
// This file is not part of the Qt API. It exists for the convenience
|
||||
// of the Network Access API. This header file may change from
|
||||
// version to version without notice, or even be removed.
|
||||
//
|
||||
// We mean it.
|
||||
//
|
||||
|
||||
#include "private/qobject_p.h"
|
||||
#include <QtNetwork/qnetworkreply.h>
|
||||
#include <QtCore/qjsondocument.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QRestReplyPrivate : public QObjectPrivate
|
||||
{
|
||||
public:
|
||||
QRestReplyPrivate();
|
||||
~QRestReplyPrivate() override;
|
||||
|
||||
QNetworkReply *networkReply = nullptr;
|
||||
|
||||
QByteArray contentCharset() const;
|
||||
bool hasNonHttpError() const;
|
||||
QJsonDocument replyDataToJson();
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
//! [0]
|
||||
QRestReply *reply = manager->get(request);
|
||||
QObject::connect(reply, &QRestReply::finished, this, &MyClass::handleFinished);
|
||||
//! [0]
|
||||
|
||||
|
||||
//! [1]
|
||||
// With lambda
|
||||
manager->get(request, this, [this](QRestReply *reply) {
|
||||
if (reply->isSuccess()) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
// With member function
|
||||
manager->get(request, this, &MyClass::handleFinished);
|
||||
//! [1]
|
||||
|
||||
|
||||
//! [2]
|
||||
QJsonObject myJson;
|
||||
// ...
|
||||
manager->post(request, myJson, this, [this](QRestReply *reply) {
|
||||
if (!reply->isSuccess()) {
|
||||
// ...
|
||||
}
|
||||
if (std::optional<QJsonObject> json = reply->json()) {
|
||||
// use *json
|
||||
}
|
||||
});
|
||||
//! [2]
|
||||
|
||||
|
||||
//! [3]
|
||||
manager->get(request, this, [this](QRestReply *reply) {
|
||||
if (!reply->isSuccess())
|
||||
// handle error
|
||||
if (std::optional<QJsonObject> json = reply->json())
|
||||
// use *json
|
||||
});
|
||||
//! [3]
|
||||
|
||||
|
||||
//! [4]
|
||||
manager->get(request, myData, this, [this](QRestReply *reply) {
|
||||
if (reply->isSuccess())
|
||||
// ...
|
||||
});
|
||||
//! [4]
|
||||
|
||||
|
||||
//! [5]
|
||||
manager->post(request, myData, this, [this](QRestReply *reply) {
|
||||
if (reply->isSuccess())
|
||||
// ...
|
||||
});
|
||||
//! [5]
|
||||
|
||||
|
||||
//! [6]
|
||||
manager->put(request, myData, this, [this](QRestReply *reply) {
|
||||
if (reply->isSuccess())
|
||||
// ...
|
||||
});
|
||||
//! [6]
|
||||
|
||||
|
||||
//! [7]
|
||||
manager->head(request, this, [this](QRestReply *reply) {
|
||||
if (reply->isSuccess())
|
||||
// ...
|
||||
});
|
||||
//! [7]
|
||||
|
||||
|
||||
//! [8]
|
||||
manager->deleteResource(request, this, [this](QRestReply *reply) {
|
||||
if (reply->isSuccess())
|
||||
// ...
|
||||
});
|
||||
//! [8]
|
||||
|
||||
|
|
@ -11,6 +11,7 @@ add_subdirectory(qnetworkrequestfactory)
|
|||
add_subdirectory(qnetworkreply)
|
||||
add_subdirectory(qnetworkcachemetadata)
|
||||
add_subdirectory(qabstractnetworkcache)
|
||||
add_subdirectory(qrestaccessmanager)
|
||||
if(QT_FEATURE_private_tests)
|
||||
add_subdirectory(qhttpheaderparser)
|
||||
add_subdirectory(qhttpnetworkconnection)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright (C) 2023 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
qt_internal_add_test(tst_qrestaccessmanager
|
||||
SOURCES
|
||||
tst_qrestaccessmanager.cpp
|
||||
httptestserver.cpp httptestserver_p.h
|
||||
LIBRARIES
|
||||
Qt::Network
|
||||
Qt::CorePrivate
|
||||
)
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
// 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::setHandler(const Handler &handler) {
|
||||
m_handler = handler;
|
||||
}
|
||||
|
||||
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 (m_request.headers.contains("Host")) {
|
||||
const auto parts = m_request.headers["Host"].split(':');
|
||||
m_request.url.setHost(parts.at(0));
|
||||
if (parts.size() == 2)
|
||||
m_request.url.setPort(parts.at(1).toUInt());
|
||||
}
|
||||
HttpData response;
|
||||
// Inform the testcase about request and ask for response data
|
||||
m_handler(m_request, response);
|
||||
|
||||
if (response.respond) {
|
||||
QByteArray responseMessage;
|
||||
responseMessage += "HTTP/1.1 ";
|
||||
responseMessage += QByteArray::number(response.status);
|
||||
responseMessage += CRLF;
|
||||
// Insert headers if any
|
||||
for (const auto &[name,value] : response.headers.asKeyValueRange()) {
|
||||
responseMessage += name;
|
||||
responseMessage += value;
|
||||
responseMessage += CRLF;
|
||||
}
|
||||
responseMessage += CRLF;
|
||||
responseMessage += response.body;
|
||||
|
||||
/*
|
||||
qDebug() << "HTTPTestServer received request"
|
||||
<< "\nMethod:" << m_request.method
|
||||
<< "\nHeaders:" << m_request.headers
|
||||
<< "\nBody:" << m_request.body;
|
||||
qDebug() << "HTTPTestServer sends response:" << responseMessage;
|
||||
*/
|
||||
m_socket->write(responseMessage);
|
||||
}
|
||||
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 == "POST")
|
||||
method = Method::Post;
|
||||
else if (fragment == "DELETE")
|
||||
method = Method::Delete;
|
||||
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.insert(std::move(key), std::move(value));
|
||||
fragment.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HttpTestServer::readBody(QTcpSocket *socket)
|
||||
{
|
||||
qint64 bytesLeft = 0;
|
||||
if (m_request.headers.contains("Content-Length")) {
|
||||
bool conversionResult;
|
||||
bytesLeft = m_request.headers["Content-Length"].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;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// 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 <QtCore/qmap.h>
|
||||
#include <QtCore/qurl.h>
|
||||
|
||||
// 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;
|
||||
bool respond = true;
|
||||
QByteArray body;
|
||||
QByteArray method;
|
||||
quint16 port = 0;
|
||||
QPair<quint8, quint8> version;
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
};
|
||||
|
||||
// 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,
|
||||
Post,
|
||||
Delete,
|
||||
} 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)>;
|
||||
void setHandler(const Handler &handler);
|
||||
|
||||
private slots:
|
||||
void handleConnected();
|
||||
void handleDataAvailable();
|
||||
|
||||
private:
|
||||
QTcpSocket *m_socket = nullptr;
|
||||
HttpData m_request;
|
||||
Handler m_handler = nullptr;
|
||||
};
|
||||
|
||||
#endif // QRESTACCESSSMANAGER_HTTPTESTSERVER_P_H
|
||||
|
|
@ -0,0 +1,794 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include "httptestserver_p.h"
|
||||
|
||||
#include <QtNetwork/qhttpmultipart.h>
|
||||
#include <QtNetwork/qrestaccessmanager.h>
|
||||
#include <QtNetwork/qauthenticator.h>
|
||||
#include <QtNetwork/qrestreply.h>
|
||||
|
||||
#include <QTest>
|
||||
#include <QtTest/qsignalspy.h>
|
||||
|
||||
#include <QtCore/qbuffer.h>
|
||||
#include <QtCore/qjsonobject.h>
|
||||
#include <QtCore/qjsondocument.h>
|
||||
#include <QtCore/qjsonarray.h>
|
||||
#include <QtCore/qstringconverter.h>
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
class tst_QRestAccessManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
|
||||
void initialization();
|
||||
void destruction();
|
||||
void callbacks();
|
||||
void threading();
|
||||
void networkRequestReply();
|
||||
void abort();
|
||||
void authentication();
|
||||
void errors();
|
||||
void body();
|
||||
void json();
|
||||
void text();
|
||||
|
||||
private:
|
||||
void memberHandler(QRestReply *reply);
|
||||
|
||||
friend class Transient;
|
||||
QList<QRestReply*> m_expectedReplies;
|
||||
QList<QRestReply*> m_actualReplies;
|
||||
};
|
||||
|
||||
void tst_QRestAccessManager::initTestCase()
|
||||
{
|
||||
qRegisterMetaType<QRestReply*>(); // For QSignalSpy
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::initialization()
|
||||
{
|
||||
QRestAccessManager manager;
|
||||
QVERIFY(manager.networkAccessManager());
|
||||
QCOMPARE(manager.deletesRepliesOnFinished(), true);
|
||||
}
|
||||
|
||||
#define VERIFY_REPLY_OK(METHOD) \
|
||||
QTRY_VERIFY(replyFromServer); \
|
||||
QCOMPARE(serverSideRequest.method, METHOD); \
|
||||
QVERIFY(replyFromServer->isSuccess()); \
|
||||
QVERIFY(!replyFromServer->hasError()); \
|
||||
replyFromServer->deleteLater(); \
|
||||
replyFromServer = nullptr; \
|
||||
|
||||
void tst_QRestAccessManager::networkRequestReply()
|
||||
{
|
||||
// A basic test for each HTTP method against the local testserver.
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
request.setRawHeader("Content-Type"_ba, "text/plain"); // To silence missing content-type warn
|
||||
QRestReply *replyFromServer = 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([&](HttpData request, HttpData &response) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
|
||||
});
|
||||
auto callback = [&](QRestReply *reply) { replyFromServer = reply; };
|
||||
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};
|
||||
|
||||
// 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, 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);
|
||||
|
||||
// POST
|
||||
manager.post(request, byteArrayData, this, callback);
|
||||
VERIFY_REPLY_OK(methodPOST);
|
||||
QCOMPARE(serverSideRequest.body, byteArrayData);
|
||||
|
||||
manager.post(request, jsonObjectData, this, callback);
|
||||
VERIFY_REPLY_OK(methodPOST);
|
||||
QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
|
||||
|
||||
manager.post(request, 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, jsonObjectData, this, callback);
|
||||
VERIFY_REPLY_OK(methodPUT);
|
||||
QCOMPARE(QJsonDocument::fromJson(serverSideRequest.body).object(), jsonObjectData);
|
||||
|
||||
manager.put(request, 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);
|
||||
|
||||
//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.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
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::abort()
|
||||
{
|
||||
// Test aborting requests
|
||||
QRestAccessManager manager;
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
|
||||
QSignalSpy finishedSpy(&manager, &QRestAccessManager::requestFinished);
|
||||
int callbackCount = 0;
|
||||
auto callback = [&](QRestReply*) {
|
||||
callbackCount++;
|
||||
};
|
||||
|
||||
// Abort without any requests
|
||||
manager.abortRequests();
|
||||
QTest::qWait(20);
|
||||
QCOMPARE(finishedSpy.size(), 0);
|
||||
|
||||
// Abort immediately after requesting
|
||||
manager.get(request, this, callback);
|
||||
manager.abortRequests();
|
||||
QTRY_COMPARE(callbackCount, 1);
|
||||
QTRY_COMPARE(finishedSpy.size(), 1);
|
||||
|
||||
// Abort after request has been sent out
|
||||
server.setHandler([&](HttpData, HttpData &response) {
|
||||
response.respond = false;
|
||||
manager.abortRequests();
|
||||
});
|
||||
manager.get(request, this, callback);
|
||||
QTRY_COMPARE(callbackCount, 2);
|
||||
QTRY_COMPARE(finishedSpy.size(), 2);
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::memberHandler(QRestReply *reply)
|
||||
{
|
||||
m_actualReplies.append(reply);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
QRestAccessManager manager;
|
||||
|
||||
manager.setDeletesRepliesOnFinished(false); // Don't autodelete so we can compare results later
|
||||
QNetworkRequest request{u"i_dont_exist"_s}; // Will result in ProtocolUnknown error
|
||||
QSignalSpy managerFinishedSpy(&manager, &QRestAccessManager::requestFinished);
|
||||
|
||||
auto lambdaHandler = [this](QRestReply *reply) { m_actualReplies.append(reply); };
|
||||
QRestReply *reply = nullptr;
|
||||
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([](QNetworkReply*){})); // Wrong parameter type
|
||||
static_assert(!isCompatibleCallback([](const QString &){})); // Wrong parameter type
|
||||
static_assert(!isCompatibleCallback([](QRestReply*, QString){})); // Too many parameters
|
||||
|
||||
// -- Test without data
|
||||
// Without callback
|
||||
reply = manager.get(request);
|
||||
QCOMPARE(reply->isFinished(), false); // Test this once here
|
||||
m_expectedReplies.append(reply);
|
||||
QObject::connect(reply, &QRestReply::finished, lambdaHandler);
|
||||
|
||||
// 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);}));
|
||||
// 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);}));
|
||||
// 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());
|
||||
QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size());
|
||||
for (auto reply: m_actualReplies) {
|
||||
QVERIFY(!reply->isSuccess());
|
||||
QVERIFY(reply->hasError());
|
||||
QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
|
||||
QCOMPARE(reply->isFinished(), true);
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_actualReplies.clear();
|
||||
m_expectedReplies.clear();
|
||||
managerFinishedSpy.clear();
|
||||
|
||||
// -- Test with data
|
||||
reply = manager.post(request, data);
|
||||
m_expectedReplies.append(reply);
|
||||
QObject::connect(reply, &QRestReply::finished, lambdaHandler);
|
||||
|
||||
// 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);}));
|
||||
// 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);}));
|
||||
// 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());
|
||||
QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size());
|
||||
for (auto reply: m_actualReplies) {
|
||||
QVERIFY(!reply->isSuccess());
|
||||
QVERIFY(reply->hasError());
|
||||
QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
|
||||
QCOMPARE(reply->isFinished(), true);
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_actualReplies.clear();
|
||||
m_expectedReplies.clear();
|
||||
managerFinishedSpy.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
|
||||
reply = manager.get(request, data);
|
||||
m_expectedReplies.append(reply);
|
||||
QObject::connect(reply, &QRestReply::finished, lambdaHandler);
|
||||
// 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);}));
|
||||
// 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);}));
|
||||
// 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());
|
||||
QTRY_COMPARE(managerFinishedSpy.size(), m_actualReplies.size());
|
||||
for (auto reply: m_actualReplies) {
|
||||
QVERIFY(!reply->isSuccess());
|
||||
QVERIFY(reply->hasError());
|
||||
QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
|
||||
QCOMPARE(reply->isFinished(), true);
|
||||
reply->deleteLater();
|
||||
}
|
||||
m_actualReplies.clear();
|
||||
m_expectedReplies.clear();
|
||||
managerFinishedSpy.clear();
|
||||
}
|
||||
|
||||
class RestWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RestWorker(QObject *parent = nullptr) : QObject(parent)
|
||||
{
|
||||
m_manager = new QRestAccessManager(this);
|
||||
}
|
||||
QRestAccessManager *m_manager;
|
||||
|
||||
public slots:
|
||||
void issueRestRequest()
|
||||
{
|
||||
QNetworkRequest request{u"i_dont_exist"_s};
|
||||
m_manager->get(request, this, [this](QRestReply *reply){
|
||||
emit result(reply->body());
|
||||
});
|
||||
}
|
||||
signals:
|
||||
void result(const QByteArray &data);
|
||||
};
|
||||
|
||||
void tst_QRestAccessManager::threading()
|
||||
{
|
||||
// QRestAccessManager and QRestReply are only allowed to use in the thread they live in.
|
||||
|
||||
// A "sanity test" for checking that there are no problems with running the QRestAM
|
||||
// in another thread.
|
||||
QThread restWorkThread;
|
||||
RestWorker restWorker;
|
||||
restWorker.moveToThread(&restWorkThread);
|
||||
|
||||
QList<QByteArray> results;
|
||||
QObject::connect(&restWorker, &RestWorker::result, this, [&](const QByteArray &data){
|
||||
results.append(data);
|
||||
});
|
||||
restWorkThread.start();
|
||||
|
||||
QMetaObject::invokeMethod(&restWorker, &RestWorker::issueRestRequest);
|
||||
QTRY_COMPARE(results.size(), 1);
|
||||
restWorkThread.quit();
|
||||
restWorkThread.wait();
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::destruction()
|
||||
{
|
||||
QRestAccessManager *manager = new QRestAccessManager;
|
||||
manager->setDeletesRepliesOnFinished(false); // Don't autodelete so we can compare results later
|
||||
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); };
|
||||
|
||||
// Delete reply immediately, make sure nothing bad happens and that there is no callback
|
||||
QRestReply *reply = manager->get(request, this, handler);
|
||||
delete reply;
|
||||
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");
|
||||
delete manager;
|
||||
QTest::qWait(20);
|
||||
QCOMPARE(m_actualReplies.size(), m_expectedReplies.size()); // Both should be 0
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::authentication()
|
||||
{
|
||||
// Test the case where server responds with '401' (authentication required).
|
||||
// The QRestAM emits an authenticationRequired signal, which is used to the username/password.
|
||||
// The QRestAM/QNAM underneath then automatically resends the request.
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
QRestReply *replyFromServer = nullptr;
|
||||
|
||||
HttpData serverSideRequest;
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
if (!request.headers.contains("Authorization"_ba)) {
|
||||
response.status = 401;
|
||||
response.headers.insert("WWW-Authenticate: "_ba, "Basic realm=\"secret_place\""_ba);
|
||||
} else {
|
||||
response.status = 200;
|
||||
}
|
||||
serverSideRequest = request; // store for checking later the 'Authorization' header value
|
||||
});
|
||||
|
||||
QObject::connect(&manager, &QRestAccessManager::authenticationRequired, this,
|
||||
[](QRestReply*, QAuthenticator *authenticator) {
|
||||
authenticator->setUser(u"a_user"_s);
|
||||
authenticator->setPassword(u"a_password"_s);
|
||||
});
|
||||
|
||||
// Issue a GET request without any authorization data.
|
||||
int finishedCount = 0;
|
||||
manager.get(request, this, [&](QRestReply *reply) {
|
||||
finishedCount++;
|
||||
replyFromServer = reply;
|
||||
});
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
// Server and QRestAM/QNAM exchange req/res twice, but finished() should be emitted just once
|
||||
QCOMPARE(finishedCount, 1);
|
||||
QCOMPARE(serverSideRequest.headers["Authorization"_ba], "Basic YV91c2VyOmFfcGFzc3dvcmQ="_ba);
|
||||
}
|
||||
|
||||
#define VERIFY_HTTP_ERROR_STATUS(STATUS) \
|
||||
serverSideResponse.status = STATUS; \
|
||||
reply = manager.get(request); \
|
||||
QObject::connect(reply, &QRestReply::errorOccurred, this, \
|
||||
[&](){ errorSignalReceived = true; }); \
|
||||
QTRY_VERIFY(reply->isFinished()); \
|
||||
QVERIFY(!errorSignalReceived); \
|
||||
QVERIFY(!reply->hasError()); \
|
||||
QCOMPARE(reply->httpStatus(), serverSideResponse.status); \
|
||||
QCOMPARE(reply->error(), QNetworkReply::NetworkError::NoError); \
|
||||
QVERIFY(!reply->isSuccess()); \
|
||||
reply->deleteLater(); \
|
||||
|
||||
void tst_QRestAccessManager::errors()
|
||||
{
|
||||
// Tests the distinction between HTTP and other (network/protocol) errors
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
QRestReply *reply = nullptr;
|
||||
bool errorSignalReceived = false;
|
||||
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
server.setHandler([&](HttpData, HttpData &response) {
|
||||
response = serverSideResponse;
|
||||
});
|
||||
|
||||
// Test few HTTP statuses in different categories
|
||||
VERIFY_HTTP_ERROR_STATUS(103) // QNetworkReply::NoError
|
||||
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
|
||||
reply = manager.get({}); // Empty url
|
||||
QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; });
|
||||
QTRY_VERIFY(reply->isFinished());
|
||||
QTRY_VERIFY(errorSignalReceived);
|
||||
QVERIFY(reply->hasError());
|
||||
QVERIFY(!reply->isSuccess());
|
||||
QCOMPARE(reply->error(), QNetworkReply::ProtocolUnknownError);
|
||||
reply->deleteLater();
|
||||
errorSignalReceived = false;
|
||||
|
||||
reply = manager.get(QNetworkRequest{{"http://non-existent.foo.bar.test"}});
|
||||
QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; });
|
||||
QTRY_VERIFY(reply->isFinished());
|
||||
QTRY_VERIFY(errorSignalReceived);
|
||||
QVERIFY(reply->hasError());
|
||||
QVERIFY(!reply->isSuccess());
|
||||
QCOMPARE(reply->error(), QNetworkReply::HostNotFoundError);
|
||||
reply->deleteLater();
|
||||
errorSignalReceived = false;
|
||||
|
||||
reply = manager.get(request);
|
||||
QObject::connect(reply, &QRestReply::errorOccurred, this, [&](){ errorSignalReceived = true; });
|
||||
reply->abort();
|
||||
QTRY_VERIFY(reply->isFinished());
|
||||
QTRY_VERIFY(errorSignalReceived);
|
||||
QVERIFY(reply->hasError());
|
||||
QVERIFY(!reply->isSuccess());
|
||||
QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError);
|
||||
reply->deleteLater();
|
||||
errorSignalReceived = false;
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::body()
|
||||
{
|
||||
// Test using QRestReply::body() data accessor
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
QRestReply *replyFromServer = nullptr;
|
||||
|
||||
HttpData serverSideRequest; // The request data the server received
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
});
|
||||
|
||||
serverSideResponse.status = 200;
|
||||
serverSideResponse.body = "some_data"_ba;
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
QCOMPARE(replyFromServer->body(), serverSideResponse.body);
|
||||
QCOMPARE(replyFromServer->httpStatus(), serverSideResponse.status);
|
||||
QVERIFY(!replyFromServer->hasError());
|
||||
QVERIFY(replyFromServer->isSuccess());
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
|
||||
serverSideResponse.body = ""_ba; // Empty
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
QCOMPARE(replyFromServer->body(), serverSideResponse.body);
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
|
||||
serverSideResponse.status = 500;
|
||||
serverSideResponse.body = "some_other_data"_ba;
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
QCOMPARE(replyFromServer->body(), serverSideResponse.body);
|
||||
QCOMPARE(replyFromServer->httpStatus(), serverSideResponse.status);
|
||||
QVERIFY(!replyFromServer->hasError());
|
||||
QVERIFY(!replyFromServer->isSuccess());
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::json()
|
||||
{
|
||||
// Test using QRestReply::json() and jsonArray() data accessors
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
QRestReply *replyFromServer = nullptr;
|
||||
QJsonObject responseJsonObject;
|
||||
QJsonArray responseJsonArray;
|
||||
|
||||
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) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
});
|
||||
|
||||
// Test receiving valid json object
|
||||
serverSideResponse.body = "{\"key1\":\"value1\",""\"key2\":\"value2\"}\n"_ba;
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
responseJsonObject = *replyFromServer->json();
|
||||
QCOMPARE(responseJsonObject["key1"], "value1");
|
||||
QCOMPARE(responseJsonObject["key2"], "value2");
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
|
||||
// Test receiving an invalid json object
|
||||
serverSideResponse.body = "foobar"_ba;
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
QVERIFY(!replyFromServer->json().has_value()); // std::nullopt returned
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
|
||||
// Test receiving valid json array
|
||||
serverSideResponse.body = "[\"foo\", \"bar\"]\n"_ba;
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
responseJsonArray = *replyFromServer->jsonArray();
|
||||
QCOMPARE(responseJsonArray.size(), 2);
|
||||
QCOMPARE(responseJsonArray[0].toString(), "foo"_L1);
|
||||
QCOMPARE(responseJsonArray[1].toString(), "bar"_L1);
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
|
||||
// Test receiving an invalid json array
|
||||
serverSideResponse.body = "foobar"_ba;
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
QVERIFY(!replyFromServer->jsonArray().has_value()); // std::nullopt returned
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
}
|
||||
|
||||
#define VERIFY_TEXT_REPLY_OK \
|
||||
QTRY_VERIFY(replyFromServer); \
|
||||
responseString = replyFromServer->text(); \
|
||||
QCOMPARE(responseString, sourceString); \
|
||||
replyFromServer->deleteLater(); \
|
||||
replyFromServer = nullptr; \
|
||||
|
||||
void tst_QRestAccessManager::text()
|
||||
{
|
||||
// Test using QRestReply::text() data accessor with various text encodings
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
QRestReply *replyFromServer = 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) {
|
||||
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.insert("Content-Type:"_ba, "text/plain; charset=UTF-8"_ba);
|
||||
serverSideResponse.body = encUTF8(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
VERIFY_TEXT_REPLY_OK;
|
||||
|
||||
// Successful UTF-16
|
||||
serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-16"_ba);
|
||||
serverSideResponse.body = encUTF16(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
VERIFY_TEXT_REPLY_OK;
|
||||
|
||||
// Successful UTF-16, parameter case insensitivity
|
||||
serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; chARset=uTf-16"_ba);
|
||||
serverSideResponse.body = encUTF16(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
VERIFY_TEXT_REPLY_OK;
|
||||
|
||||
// Successful UTF-32
|
||||
serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-32"_ba);
|
||||
serverSideResponse.body = encUTF32(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
VERIFY_TEXT_REPLY_OK;
|
||||
|
||||
// Successful UTF-32 with spec-wise allowed extra content in the Content-Type header value
|
||||
serverSideResponse.headers.insert("Content-Type:"_ba,
|
||||
"text/plain; charset = \"UTF-32\";extraparameter=bar"_ba);
|
||||
serverSideResponse.body = encUTF32(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
VERIFY_TEXT_REPLY_OK;
|
||||
|
||||
// Unsuccessful UTF-32, wrong encoding indicated (indicated charset UTF-32 but data is UTF-8)
|
||||
serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=UTF-32"_ba);
|
||||
serverSideResponse.body = encUTF8(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
responseString = replyFromServer->text();
|
||||
QCOMPARE_NE(responseString, sourceString);
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
|
||||
// Unsupported encoding, defaults to UTF-8
|
||||
serverSideResponse.headers.insert("Content-Type:"_ba, "text/plain; charset=foo"_ba);
|
||||
serverSideResponse.body = encUTF8(sourceString);
|
||||
manager.get(request, this, [&](QRestReply *reply) { replyFromServer = reply; });
|
||||
QTRY_VERIFY(replyFromServer);
|
||||
QTest::ignoreMessage(QtWarningMsg, "Charset \"foo\" is not supported, defaulting to UTF-8");
|
||||
responseString = replyFromServer->text();
|
||||
QCOMPARE(responseString, sourceString);
|
||||
replyFromServer->deleteLater();
|
||||
replyFromServer = nullptr;
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QRestAccessManager)
|
||||
#include "tst_qrestaccessmanager.moc"
|
||||
Loading…
Reference in New Issue