diff options
author | Juha Vuolle <[email protected]> | 2023-06-02 13:43:42 +0300 |
---|---|---|
committer | Juha Vuolle <[email protected]> | 2023-12-08 15:53:32 +0200 |
commit | f587ba1036164691a0981897397bdcc8f3472438 (patch) | |
tree | f9349d05222b27145df230486eff4add630ca4c7 | |
parent | 925ce9e9084a1a9e3dbd9954fc3bc3117a038915 (diff) |
QNetworkRequestFactory convenience class
The class provides a way to represent server-side service endpoints.
With RESTful applications these endpoints typically have a need for
repeating requests fields such as headers, query parameters,
bearer token, base URL, SSL configuration. This class allows setting
of the repeating parts, while allowing the setting of changing parts
on a per-request basis.
[ChangeLog][QtNetwork][QNetworkRequestFactory] Added a new convenience
class to help with the needs of repeating network request details
imposed by the server-side service endpoints, which is common
with RESTful applications.
Task-number: QTBUG-113814
Change-Id: Iabcfaed786949ffbb0ad0c75297d0db6ecc1a3cc
Reviewed-by: Marc Mutz <[email protected]>
Reviewed-by: Ivan Solovev <[email protected]>
Reviewed-by: Mate Barany <[email protected]>
Reviewed-by: MÃ¥rten Nordheim <[email protected]>
8 files changed, 977 insertions, 0 deletions
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index 949cf3ee210..d1ac08251a5 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -133,6 +133,8 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http access/qhttpprotocolhandler.cpp access/qhttpprotocolhandler_p.h access/qhttpthreaddelegate.cpp access/qhttpthreaddelegate_p.h access/qnetworkreplyhttpimpl.cpp access/qnetworkreplyhttpimpl_p.h + access/qnetworkrequestfactory.cpp access/qnetworkrequestfactory_p.h + access/qnetworkrequestfactory.h socket/qhttpsocketengine.cpp socket/qhttpsocketengine_p.h ) diff --git a/src/network/access/qnetworkrequestfactory.cpp b/src/network/access/qnetworkrequestfactory.cpp new file mode 100644 index 00000000000..3f039515cb1 --- /dev/null +++ b/src/network/access/qnetworkrequestfactory.cpp @@ -0,0 +1,497 @@ +// 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 "qnetworkrequestfactory.h" +#include "qnetworkrequestfactory_p.h" + +#if QT_CONFIG(ssl) +#include <QtNetwork/qsslconfiguration.h> +#endif + +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QNetworkRequestFactoryPrivate) + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(lcQrequestfactory, "qt.network.access.request.factory") + +/*! + \class QNetworkRequestFactory + \since 6.7 + \ingroup shared + \inmodule QtNetwork + + \brief Convenience class for grouping remote server endpoints that share + common network request properties. + + REST servers often have endpoints that require the same headers and other data. + Grouping such endpoints with a QNetworkRequestFactory makes it more + convenient to issue requests to these endpoints; only the typically + varying parts such as \e path and \e query parameters are provided + when creating a new request. + + Basic usage steps of QNetworkRequestFactory are as follows: + \list + \li Instantiation + \li Setting the data common to all requests + \li Issuing requests + \endlist + + An example of usage: + + \snippet code/src_network_access_qnetworkrequestfactory.cpp 0 +*/ + +/*! + Creates a new QNetworkRequestFactory object. + Use setBaseUrl() to set a valid base URL for the requests. + + \sa QNetworkRequestFactory(const QUrl &baseUrl), setBaseUrl() +*/ + +QNetworkRequestFactory::QNetworkRequestFactory() + : d(new QNetworkRequestFactoryPrivate) +{ +} + +/*! + Creates a new QNetworkRequestFactory object, initializing the base URL to + \a baseUrl. The base URL is used to populate subsequent network + requests. + + If the URL contains a \e path component, it will be extracted and used + as a base path in subsequent network requests. This means that any + paths provided when requesting individual requests will be appended + to this base path, as illustrated below: + + \snippet code/src_network_access_qnetworkrequestfactory.cpp 1 + */ +QNetworkRequestFactory::QNetworkRequestFactory(const QUrl &baseUrl) + : d(new QNetworkRequestFactoryPrivate(baseUrl)) +{ +} + +/*! + Destroys this QNetworkRequestFactory object. + */ +QNetworkRequestFactory::~QNetworkRequestFactory() + = default; + +/*! + Creates a copy of \a other. + */ +QNetworkRequestFactory::QNetworkRequestFactory(const QNetworkRequestFactory &other) + = default; + +/*! + Creates a copy of \a other and returns a reference to this factory. + */ +QNetworkRequestFactory &QNetworkRequestFactory::operator=(const QNetworkRequestFactory &other) + = default; + +/*! + \fn QNetworkRequestFactory::QNetworkRequestFactory(QNetworkRequestFactory &&other) noexcept + + Move-constructs the factory from \a other. + + \note The moved-from object \a other is placed in a + partially-formed state, in which the only valid operations are + destruction and assignment of a new value. +*/ + +/*! + \fn QNetworkRequestFactory &QNetworkRequestFactory::operator=(QNetworkRequestFactory &&other) noexcept + + Move-assigns \a other and returns a reference to this factory. + + \note The moved-from object \a other is placed in a + partially-formed state, in which the only valid operations are + destruction and assignment of a new value. + */ + +/*! + \fn void QNetworkRequestFactory::swap(QNetworkRequestFactory &other) + + Swaps this factory with \a other. This operation is + very fast and never fails. + */ + +/*! + \fn bool QNetworkRequestFactory::operator==(const QNetworkRequestFactory &lhs, + const QNetworkRequestFactory &rhs) + + Returns \c true if \a lhs is considered equal with \a rhs, meaning + that all data in the factories match, otherwise returns \c false. + + \note The headers comparison is order-insensitive. + + \sa QNetworkRequestFactory::operator!=() + */ + +/*! + \fn bool QNetworkRequestFactory::operator!=(const QNetworkRequestFactory &lhs, + const QNetworkRequestFactory &rhs) + + Returns \c true if \a lhs is not considered equal with \a rhs. + + \sa QNetworkRequestFactory::operator==() + */ + +/*! + \internal + */ +bool comparesEqual(const QNetworkRequestFactory &lhs, const QNetworkRequestFactory &rhs) noexcept +{ + return lhs.d == rhs.d || lhs.d->equals(*rhs.d); +} + +/*! + Returns the base URL used for the individual requests. + + The base URL may contain a path component. This path is used + as path "prefix" for the paths that are provided when generating + individual requests. + + \sa setBaseUrl() + */ +QUrl QNetworkRequestFactory::baseUrl() const +{ + return d->baseUrl; +} + +/*! + Sets the base URL used in individual requests to \a url. + + \sa baseUrl() + */ +void QNetworkRequestFactory::setBaseUrl(const QUrl &url) +{ + if (d->baseUrl == url) + return; + + d.detach(); + d->baseUrl = url; +} + +#if QT_CONFIG(ssl) +/*! + Returns the SSL configuration set to this factory. The SSL configuration + is set to each individual request. + + \sa setSslConfiguration() + */ +QSslConfiguration QNetworkRequestFactory::sslConfiguration() const +{ + return d->sslConfig; +} + +/*! + Sets the SSL configuration to \a configuration. + + \sa sslConfiguration() + */ +void QNetworkRequestFactory::setSslConfiguration(const QSslConfiguration &configuration) +{ + if (d->sslConfig == configuration) + return; + + d.detach(); + d->sslConfig = configuration; +} +#endif + +/*! + Returns a QNetworkRequest. + + The returned request is filled with the data that this factory + has been configured with. + + \sa request(const QUrlQuery&), request(const QString&, const QUrlQuery&) +*/ + +QNetworkRequest QNetworkRequestFactory::request() const +{ + return d->newRequest(d->requestUrl()); +} + +/*! + Returns a QNetworkRequest. + + The returned request's URL is formed by appending the provided \a path + to the baseUrl (which may itself have a path component). + + \sa request(const QString &, const QUrlQuery &), request(), baseUrl() +*/ +QNetworkRequest QNetworkRequestFactory::request(const QString &path) const +{ + return d->newRequest(d->requestUrl(&path)); +} + +/*! + Returns a QNetworkRequest. + + The returned request's URL is formed by appending the provided \a query + to the baseUrl. + + \sa request(const QString &, const QUrlQuery &), request(), baseUrl() +*/ +QNetworkRequest QNetworkRequestFactory::request(const QUrlQuery &query) const +{ + return d->newRequest(d->requestUrl(nullptr, &query)); +} + +/*! + Returns a QNetworkRequest. + + The returned request's URL is formed by appending the provided \a path + and \a query to the baseUrl (which may itself have a path component). + + If the provided \a path contains query items, they will be combined + with the items in \a query. + + \sa request(const QUrlQuery&), request(), baseUrl() + */ +QNetworkRequest QNetworkRequestFactory::request(const QString &path, const QUrlQuery &query) const +{ + return d->newRequest(d->requestUrl(&path, &query)); +} + +/*! + Sets the headers to \a headers. + + These headers are added to individual requests' headers. + This is a convenience mechanism for setting headers that + repeat across requests. + + \sa headers(), clearHeaders() + */ +void QNetworkRequestFactory::setHeaders(const QHttpHeaders &headers) +{ + d.detach(); + d->headers = headers; +} + +/*! + Returns the currently set headers. + + \sa setHeaders(), clearHeaders() + */ +QHttpHeaders QNetworkRequestFactory::headers() const +{ + return d->headers; +} + +/*! + Clears current headers. + + \sa headers(), setHeaders() +*/ +void QNetworkRequestFactory::clearHeaders() +{ + if (d->headers.isEmpty()) + return; + d.detach(); + d->headers.clear(); +} + +/*! + Returns the bearer token that has been set. + + The bearer token, if present, is used to set the + \c {Authorization: Bearer my_token} header for requests. This is a common + authorization convention and provided as an additional convenience. + + Means to acquire the bearer token varies. Common methods include \c OAuth2 + and the service provider's website/dashboard. It's common that the bearer + token changes over time, for example when updated with a refresh token. + By always re-setting the new token ensures that subsequent requests will + always have the latest, valid, token. + + The presence of the bearer token does not impact the \l headers() + listing. If the \l headers() also lists \c Authorization header, it + will be overwritten. + + \sa setBearerToken(), headers() + */ +QByteArray QNetworkRequestFactory::bearerToken() const +{ + return d->bearerToken; +} + +/*! + Sets the bearer token to \a token. + + \sa bearerToken(), clearBearerToken() +*/ +void QNetworkRequestFactory::setBearerToken(const QByteArray &token) +{ + if (d->bearerToken == token) + return; + + d.detach(); + d->bearerToken = token; +} + +/*! + Clears the bearer token. + + \sa bearerToken() +*/ +void QNetworkRequestFactory::clearBearerToken() +{ + if (d->bearerToken.isEmpty()) + return; + + d.detach(); + d->bearerToken.clear(); +} + +/*! + Returns query parameters that are added to individual requests' query + parameters. The query parameters are added to any potential query + parameters provided with the individual \l request() calls. + + Use cases for using repeating query parameters are server dependent, + but typical examples include language setting \c {?lang=en}, format + specification \c {?format=json}, API version specification + \c {?version=1.0} and API key authentication. + + \sa setQueryParameters(), clearQueryParameters(), request() +*/ +QUrlQuery QNetworkRequestFactory::queryParameters() const +{ + return d->queryParameters; +} + +/*! + Sets \a query parameters that are added to individual requests' query + parameters. + + \sa queryParameters(), clearQueryParameters() + */ +void QNetworkRequestFactory::setQueryParameters(const QUrlQuery &query) +{ + if (d->queryParameters == query) + return; + + d.detach(); + d->queryParameters = query; +} + +/*! + Clears the query parameters. + + \sa queryParameters() +*/ +void QNetworkRequestFactory::clearQueryParameters() +{ + if (d->queryParameters.isEmpty()) + return; + + d.detach(); + d->queryParameters.clear(); +} + +QNetworkRequestFactoryPrivate::QNetworkRequestFactoryPrivate() + = default; + +QNetworkRequestFactoryPrivate::QNetworkRequestFactoryPrivate(const QUrl &baseUrl) + : baseUrl(baseUrl) +{ +} + +QNetworkRequestFactoryPrivate::~QNetworkRequestFactoryPrivate() + = default; + +QNetworkRequest QNetworkRequestFactoryPrivate::newRequest(const QUrl &url) const +{ + QNetworkRequest request; + request.setUrl(url); +#if QT_CONFIG(ssl) + if (!sslConfig.isNull()) + request.setSslConfiguration(sslConfig); +#endif + // Set the header entries to the request. Combine values as there + // may be multiple values per name. Note: this would not necessarily + // produce right result for 'Set-Cookie' header if it has multiple values, + // but since it is a purely server-side (response) header, not relevant here. + const auto headerNames = headers.names(); + for (const auto &name : headerNames) + request.setRawHeader(name, headers.combinedValue(name)); + + constexpr char Bearer[] = "Bearer "; + if (!bearerToken.isEmpty()) + request.setRawHeader("Authorization"_ba, Bearer + bearerToken); + + return request; +} + +QUrl QNetworkRequestFactoryPrivate::requestUrl(const QString *path, + const QUrlQuery *query) const +{ + const QUrl providedPath = path ? QUrl(*path) : QUrl{}; + const QUrlQuery providedQuery = query ? *query : QUrlQuery(); + + if (!providedPath.scheme().isEmpty() || !providedPath.host().isEmpty()) { + qCWarning(lcQrequestfactory, "The provided path %ls may only contain path and query item " + "components, and other parts will be ignored. Set the baseUrl instead", + qUtf16Printable(providedPath.toDisplayString())); + } + + QUrl resultUrl = baseUrl; + QUrlQuery resultQuery(providedQuery); + QString basePath = baseUrl.path(); + // Separate the path and query parameters components on the application-provided path + const QString requestPath{providedPath.path()}; + const QUrlQuery pathQueryItems{providedPath}; + + if (!pathQueryItems.isEmpty()) { + // Add any query items provided as part of the path + const auto items = pathQueryItems.queryItems(QUrl::ComponentFormattingOption::FullyEncoded); + for (const auto &[key, value]: items) + resultQuery.addQueryItem(key, value); + } + + if (!queryParameters.isEmpty()) { + // Add any query items set to this factory + const QList<std::pair<QString,QString>> items = + queryParameters.queryItems(QUrl::ComponentFormattingOption::FullyEncoded); + for (const auto &item: items) + resultQuery.addQueryItem(item.first, item.second); + } + + if (!resultQuery.isEmpty()) + resultUrl.setQuery(resultQuery); + + if (requestPath.isEmpty()) + return resultUrl; + + // Ensure that the "base path" (the path that may be present + // in the baseUrl), and the request path are joined with one '/' + // If both have it, remove one, if neither has it, add one + if (basePath.endsWith(u'/') && requestPath.startsWith(u'/')) + basePath.chop(1); + else if (!requestPath.startsWith(u'/') && !basePath.endsWith(u'/')) + basePath.append(u'/'); + + resultUrl.setPath(basePath.append(requestPath)); + return resultUrl; +} + +bool QNetworkRequestFactoryPrivate::equals( + const QNetworkRequestFactoryPrivate &other) const noexcept +{ + return +#if QT_CONFIG(ssl) + sslConfig == other.sslConfig && +#endif + baseUrl == other.baseUrl && + bearerToken == other.bearerToken && + headers.equals(other.headers) && + queryParameters == other.queryParameters; +} + +QT_END_NAMESPACE diff --git a/src/network/access/qnetworkrequestfactory.h b/src/network/access/qnetworkrequestfactory.h new file mode 100644 index 00000000000..901a61decc5 --- /dev/null +++ b/src/network/access/qnetworkrequestfactory.h @@ -0,0 +1,75 @@ +// 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 QNETWORKREQUESTFACTORY_H +#define QNETWORKREQUESTFACTORY_H + +#include <QtNetwork/qnetworkrequest.h> +#include <QtNetwork/qhttpheaders.h> + +#include <QtCore/qcompare.h> +#include <QtCore/qshareddata.h> +#include <QtCore/qurlquery.h> +#include <QtCore/qurl.h> + +QT_BEGIN_NAMESPACE + +#if QT_CONFIG(ssl) +class QSslConfiguration; +#endif + +class QNetworkRequestFactoryPrivate; +QT_DECLARE_QESDP_SPECIALIZATION_DTOR_WITH_EXPORT(QNetworkRequestFactoryPrivate, Q_NETWORK_EXPORT) + +class QNetworkRequestFactory +{ +public: + Q_NETWORK_EXPORT QNetworkRequestFactory(); + Q_NETWORK_EXPORT explicit QNetworkRequestFactory(const QUrl &baseUrl); + Q_NETWORK_EXPORT ~QNetworkRequestFactory(); + + Q_NETWORK_EXPORT QNetworkRequestFactory(const QNetworkRequestFactory &other); + QNetworkRequestFactory(QNetworkRequestFactory &&other) noexcept = default; + Q_NETWORK_EXPORT QNetworkRequestFactory &operator=(const QNetworkRequestFactory &other); + + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QNetworkRequestFactory) + void swap(QNetworkRequestFactory &other) noexcept { d.swap(other.d); } + + Q_NETWORK_EXPORT QUrl baseUrl() const; + Q_NETWORK_EXPORT void setBaseUrl(const QUrl &url); + +#if QT_CONFIG(ssl) + Q_NETWORK_EXPORT QSslConfiguration sslConfiguration() const; + Q_NETWORK_EXPORT void setSslConfiguration(const QSslConfiguration &configuration); +#endif + + Q_NETWORK_EXPORT QNetworkRequest request() const; + Q_NETWORK_EXPORT QNetworkRequest request(const QUrlQuery &query) const; + Q_NETWORK_EXPORT QNetworkRequest request(const QString &path) const; + Q_NETWORK_EXPORT QNetworkRequest request(const QString &path, const QUrlQuery &query) const; + + Q_NETWORK_EXPORT void setHeaders(const QHttpHeaders &headers); + Q_NETWORK_EXPORT QHttpHeaders headers() const; + Q_NETWORK_EXPORT void clearHeaders(); + + Q_NETWORK_EXPORT QByteArray bearerToken() const; + Q_NETWORK_EXPORT void setBearerToken(const QByteArray &token); + Q_NETWORK_EXPORT void clearBearerToken(); + + Q_NETWORK_EXPORT QUrlQuery queryParameters() const; + Q_NETWORK_EXPORT void setQueryParameters(const QUrlQuery &query); + Q_NETWORK_EXPORT void clearQueryParameters(); + +private: + friend Q_NETWORK_EXPORT bool comparesEqual(const QNetworkRequestFactory &lhs, + const QNetworkRequestFactory &rhs) noexcept; + Q_DECLARE_EQUALITY_COMPARABLE(QNetworkRequestFactory) + + QExplicitlySharedDataPointer<QNetworkRequestFactoryPrivate> d; +}; + +Q_DECLARE_SHARED(QNetworkRequestFactory) + +QT_END_NAMESPACE + +#endif // QNETWORKREQUESTFACTORY_H diff --git a/src/network/access/qnetworkrequestfactory_p.h b/src/network/access/qnetworkrequestfactory_p.h new file mode 100644 index 00000000000..d90d83361bc --- /dev/null +++ b/src/network/access/qnetworkrequestfactory_p.h @@ -0,0 +1,50 @@ +// 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 QNETWORKREQUESTFACTORY_P_H +#define QNETWORKREQUESTFACTORY_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 framework. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtNetwork/qhttpheaders.h> +#include <QtNetwork/qnetworkrequest.h> +#if QT_CONFIG(ssl) +#include <QtNetwork/qsslconfiguration.h> +#endif +#include <QtCore/qshareddata.h> +#include <QtCore/qurl.h> +#include <QtCore/qurlquery.h> + +QT_BEGIN_NAMESPACE + +class QNetworkRequestFactoryPrivate : public QSharedData +{ +public: + QNetworkRequestFactoryPrivate(); + explicit QNetworkRequestFactoryPrivate(const QUrl &baseUrl); + ~QNetworkRequestFactoryPrivate(); + QNetworkRequest newRequest(const QUrl &url) const; + QUrl requestUrl(const QString *path = nullptr, const QUrlQuery *query = nullptr) const; + bool equals(const QNetworkRequestFactoryPrivate &other) const noexcept; + +#if QT_CONFIG(ssl) + QSslConfiguration sslConfig; +#endif + QUrl baseUrl; + QHttpHeaders headers; + QByteArray bearerToken; + QUrlQuery queryParameters; +}; + +QT_END_NAMESPACE + +#endif // QNETWORKREQUESTFACTORY_P_H diff --git a/src/network/doc/snippets/code/src_network_access_qnetworkrequestfactory.cpp b/src/network/doc/snippets/code/src_network_access_qnetworkrequestfactory.cpp new file mode 100644 index 00000000000..f6e0b89858c --- /dev/null +++ b/src/network/doc/snippets/code/src_network_access_qnetworkrequestfactory.cpp @@ -0,0 +1,27 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause +using namespace Qt::StringLiterals; + +//! [0] +// Instantiate a factory somewhere suitable in the application +QNetworkRequestFactory api{{"https://example.com/v1"_L1}}; + +// Set bearer token +api.setBearerToken("my_token"); + +// Issue requests (reply handling omitted for brevity) +manager.get(api.request("models"_L1)); // https://example.com/v1/models +// The conventional leading '/' for the path can be used as well +manager.get(api.request("/models"_L1)); // https://example.com/v1/models +//! [0] + + +//! [1] +// Here the API version v2 is used as the base path: +QNetworkRequestFactory api{{"https://example.com/v2"_L1}}; +// ... +manager.get(api.request("models"_L1)); // https://example.com/v2/models +// Equivalent with a leading '/' +manager.get(api.request("/models"_L1)); // https://example.com/v2/models +//! [1] + diff --git a/tests/auto/network/access/CMakeLists.txt b/tests/auto/network/access/CMakeLists.txt index a3b56646bb4..91c886d3fb0 100644 --- a/tests/auto/network/access/CMakeLists.txt +++ b/tests/auto/network/access/CMakeLists.txt @@ -7,6 +7,7 @@ add_subdirectory(qnetworkcookiejar) add_subdirectory(qnetworkaccessmanager) add_subdirectory(qnetworkcookie) add_subdirectory(qnetworkrequest) +add_subdirectory(qnetworkrequestfactory) add_subdirectory(qnetworkreply) add_subdirectory(qnetworkcachemetadata) add_subdirectory(qabstractnetworkcache) diff --git a/tests/auto/network/access/qnetworkrequestfactory/CMakeLists.txt b/tests/auto/network/access/qnetworkrequestfactory/CMakeLists.txt new file mode 100644 index 00000000000..0b639d254d8 --- /dev/null +++ b/tests/auto/network/access/qnetworkrequestfactory/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +qt_internal_add_test(tst_qnetworkrequestfactory + SOURCES + tst_qnetworkrequestfactory.cpp + LIBRARIES + Qt::Core + Qt::Test + Qt::Network +) diff --git a/tests/auto/network/access/qnetworkrequestfactory/tst_qnetworkrequestfactory.cpp b/tests/auto/network/access/qnetworkrequestfactory/tst_qnetworkrequestfactory.cpp new file mode 100644 index 00000000000..2413065920f --- /dev/null +++ b/tests/auto/network/access/qnetworkrequestfactory/tst_qnetworkrequestfactory.cpp @@ -0,0 +1,314 @@ +// 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 <QtTest/qtest.h> +#include <QtNetwork/qnetworkrequestfactory.h> +#ifndef QT_NO_SSL +#include <QtNetwork/qsslconfiguration.h> +#endif +#include <QtCore/qurlquery.h> +#include <QtCore/qurl.h> + +using namespace Qt::StringLiterals; + +class tst_QNetworkRequestFactory : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void urlAndPath_data(); + void urlAndPath(); + void queryParameters(); + void sslConfiguration(); + void headers(); + void bearerToken(); + void operators(); + +private: + const QUrl url1{u"http://foo.io"_s}; + const QUrl url2{u"http://bar.io"_s}; + const QByteArray bearerToken1{"bearertoken1"}; + const QByteArray bearerToken2{"bearertoken2"}; +}; + +void tst_QNetworkRequestFactory::urlAndPath_data() +{ + QTest::addColumn<QUrl>("baseUrl"); + QTest::addColumn<QString>("requestPath"); + QTest::addColumn<QUrl>("expectedRequestUrl"); + + QUrl base{"http://xyz.io"}; + QUrl result{"http://xyz.io/path/to"}; + QTest::newRow("baseUrl_nopath_noslash_1") << base << u""_s << base; + QTest::newRow("baseUrl_nopath_noslash_2") << base << u"/path/to"_s << result; + QTest::newRow("baseUrl_nopath_noslash_3") << base << u"path/to"_s << result; + + base.setUrl("http://xyz.io/"); + result.setUrl("http://xyz.io/path/to"); + QTest::newRow("baseUrl_nopath_withslash_1") << base << u""_s << base; + QTest::newRow("baseUrl_nopath_withslash_2") << base << u"/path/to"_s << result; + QTest::newRow("baseUrl_nopath_withslash_3") << base << u"path/to"_s << result; + + base.setUrl("http://xyz.io/v1"); + result.setUrl("http://xyz.io/v1/path/to"); + QTest::newRow("baseUrl_withpath_noslash_1") << base << u""_s << base; + QTest::newRow("baseUrl_withpath_noslash_2") << base << u"/path/to"_s << result; + QTest::newRow("baseUrl_withpath_noslash_3") << base << u"path/to"_s << result; + + base.setUrl("http://xyz.io/v1/"); + QTest::newRow("baseUrl_withpath_withslash_1") << base << u""_s << base; + QTest::newRow("baseUrl_withpath_withslash_2") << base << u"/path/to"_s << result; + QTest::newRow("baseUrl_withpath_withslash_3") << base << u"path/to"_s << result; + + // Currently we keep any double '//', but not sure if there is a use case for it, or could + // it be corrected to a single '/' + base.setUrl("http://xyz.io/v1//"); + result.setUrl("http://xyz.io/v1//path/to"); + QTest::newRow("baseUrl_withpath_doubleslash_1") << base << u""_s << base; + QTest::newRow("baseUrl_withpath_doubleslash_2") << base << u"/path/to"_s << result; + QTest::newRow("baseUrl_withpath_doubleslash_3") << base << u"path/to"_s << result; +} + +void tst_QNetworkRequestFactory::urlAndPath() +{ + QFETCH(QUrl, baseUrl); + QFETCH(QString, requestPath); + QFETCH(QUrl, expectedRequestUrl); + + // Set with constructor + QNetworkRequestFactory factory1{baseUrl}; + QCOMPARE(factory1.baseUrl(), baseUrl); + + // Set with setter calls + QNetworkRequestFactory factory2{}; + factory2.setBaseUrl(baseUrl); + QCOMPARE(factory2.baseUrl(), baseUrl); + + // Request path + QNetworkRequest request = factory1.request(); + QCOMPARE(request.url(), baseUrl); // No path was provided for request(), expect baseUrl + request = factory1.request(requestPath); + QCOMPARE(request.url(), expectedRequestUrl); + + // Check the request path didn't change base url + QCOMPARE(factory1.baseUrl(), baseUrl); +} + +void tst_QNetworkRequestFactory::queryParameters() +{ + QNetworkRequestFactory factory({"http://example.com"}); + const QUrlQuery query1{{"q1k", "q1v"}}; + const QUrlQuery query2{{"q2k", "q2v"}}; + + // Set query parameters in request() call + QCOMPARE(factory.request(query1).url(), QUrl{"http://example.com?q1k=q1v"}); + QCOMPARE(factory.request(query2).url(), QUrl{"http://example.com?q2k=q2v"}); + + // Set query parameters into the factory + factory.setQueryParameters(query1); + QUrlQuery resultQuery = factory.queryParameters(); + for (const auto &item: query1.queryItems()) { + QVERIFY(resultQuery.hasQueryItem(item.first)); + QCOMPARE(resultQuery.queryItemValue(item.first), item.second); + } + QCOMPARE(factory.request().url(), QUrl{"http://example.com?q1k=q1v"}); + + // Set query parameters into both request() and factory + QCOMPARE(factory.request(query2).url(), QUrl{"http://example.com?q2k=q2v&q1k=q1v"}); + + // Clear query parameters + factory.clearQueryParameters(); + QVERIFY(factory.queryParameters().isEmpty()); + QCOMPARE(factory.request().url(), QUrl{"http://example.com"}); + + const QString pathWithQuery{"content?raw=1"}; + // Set query parameters in per-request path + QCOMPARE(factory.request(pathWithQuery).url(), + QUrl{"http://example.com/content?raw=1"}); + // Set query parameters in per-request path and the query parameter + QCOMPARE(factory.request(pathWithQuery, query1).url(), + QUrl{"http://example.com/content?q1k=q1v&raw=1"}); + // Set query parameter in per-request path and into the factory + factory.setQueryParameters(query2); + QCOMPARE(factory.request(pathWithQuery).url(), + QUrl{"http://example.com/content?raw=1&q2k=q2v"}); + // Set query parameters in per-request, as additional parameters, and into the factory + QCOMPARE(factory.request(pathWithQuery, query1).url(), + QUrl{"http://example.com/content?q1k=q1v&raw=1&q2k=q2v"}); + + // Test that other than path and query items as part of path are ignored + factory.setQueryParameters(query1); + QRegularExpression re("The provided path*"); + QTest::ignoreMessage(QtMsgType::QtWarningMsg, re); + QCOMPARE(factory.request("https://example2.com").url(), QUrl{"http://example.com?q1k=q1v"}); + QTest::ignoreMessage(QtMsgType::QtWarningMsg, re); + QCOMPARE(factory.request("https://example2.com?q3k=q3v").url(), + QUrl{"http://example.com?q3k=q3v&q1k=q1v"}); +} + +void tst_QNetworkRequestFactory::sslConfiguration() +{ +#ifdef QT_NO_SSL + QSKIP("Skipping SSL tests, not supported by build"); +#else + // Two initially equal factories + QNetworkRequestFactory factory1{url1}; + QNetworkRequestFactory factory2{url1}; + QCOMPARE(factory1, factory2); + + // Make two differing SSL configurations (for this test it's irrelevant how they differ) + QSslConfiguration config1; + config1.setProtocol(QSsl::TlsV1_2); + QSslConfiguration config2; + config2.setProtocol(QSsl::DtlsV1_2); + + // Set configuration and verify that the same config is returned + factory1.setSslConfiguration(config1); + QCOMPARE(factory1.sslConfiguration(), config1); + factory2.setSslConfiguration(config2); + QCOMPARE(factory2.sslConfiguration(), config2); + + // Verify that the factories differ (different SSL config) + QCOMPARE_NE(factory1, factory2); + + // Verify requests are set with appropriate SSL configs + QNetworkRequest request1 = factory1.request(); + QCOMPARE(request1.sslConfiguration(), config1); + QNetworkRequest request2 = factory2.request(); + QCOMPARE(request2.sslConfiguration(), config2); +#endif +} + +void tst_QNetworkRequestFactory::headers() +{ + const QByteArray name1{"headername1"}; + const QByteArray name2{"headername2"}; + const QByteArray value1{"headervalue1"}; + const QByteArray value2{"headervalue2"}; + const QByteArray value3{"headervalue3"}; + + QNetworkRequestFactory factory{url1}; + // Initial state when no headers are set + QVERIFY(factory.headers().isEmpty()); + QVERIFY(factory.headers().values(name1).isEmpty()); + QVERIFY(!factory.headers().has(name1)); + + // Set headers + QHttpHeaders h1; + h1.append(name1, value1); + factory.setHeaders(h1); + QVERIFY(factory.headers().has(name1)); + QCOMPARE(factory.headers().combinedValue(name1), value1); + QCOMPARE(factory.headers().size(), 1); + QVERIFY(factory.headers().values("nonexistent").isEmpty()); + QNetworkRequest request = factory.request(); + QVERIFY(request.hasRawHeader(name1)); + QCOMPARE(request.rawHeader(name1), value1); + + // Check that empty header does not match + QVERIFY(!factory.headers().has(""_ba)); + QVERIFY(factory.headers().values(""_ba).isEmpty()); + + // Clear headers + factory.clearHeaders(); + QVERIFY(factory.headers().isEmpty()); + request = factory.request(); + QVERIFY(!request.hasRawHeader(name1)); + + // Set headers with more entries + h1.clear(); + h1.append(name1, value1); + h1.append(name2, value2); + factory.setHeaders(h1); + QVERIFY(factory.headers().has(name1)); + QVERIFY(factory.headers().has(name2)); + QCOMPARE(factory.headers().combinedValue(name1), value1); + QCOMPARE(factory.headers().combinedValue(name2), value2); + QCOMPARE(factory.headers().size(), 2); + request = factory.request(); + QVERIFY(request.hasRawHeader(name1)); + QVERIFY(request.hasRawHeader(name2)); + QCOMPARE(request.rawHeader(name1), value1); + QCOMPARE(request.rawHeader(name2), value2); + // Append more values to pre-existing header name2 + h1.clear(); + h1.append(name1, value1); + h1.append(name1, value2); + h1.append(name1, value3); + factory.setHeaders(h1); + QVERIFY(factory.headers().has(name1)); + QCOMPARE(factory.headers().combinedValue(name1), value1 + ',' + value2 + ',' + value3); + request = factory.request(); + QVERIFY(request.hasRawHeader(name1)); + QCOMPARE(request.rawHeader(name1), value1 + ',' + value2 + ',' + value3); +} + +void tst_QNetworkRequestFactory::bearerToken() +{ + const auto authHeader = "Authorization"_ba; + QNetworkRequestFactory factory{url1}; + QVERIFY(factory.bearerToken().isEmpty()); + + factory.setBearerToken(bearerToken1); + QCOMPARE(factory.bearerToken(), bearerToken1); + QNetworkRequest request = factory.request(); + QVERIFY(request.hasRawHeader(authHeader)); + QCOMPARE(request.rawHeader(authHeader), "Bearer "_ba + bearerToken1); + + factory.setBearerToken(bearerToken2); + QCOMPARE(factory.bearerToken(), bearerToken2); + request = factory.request(); + QVERIFY(request.hasRawHeader(authHeader)); + QCOMPARE(request.rawHeader(authHeader), "Bearer "_ba + bearerToken2); + + // Set authorization header manually + const auto value = "headervalue"_ba; + QHttpHeaders h1; + h1.append(authHeader, value); + factory.setHeaders(h1); + request = factory.request(); + QVERIFY(request.hasRawHeader(authHeader)); + // bearerToken has precedence over manually set header + QCOMPARE(request.rawHeader(authHeader), "Bearer "_ba + bearerToken2); + // clear bearer token, the manually set header is now used + factory.clearBearerToken(); + request = factory.request(); + QVERIFY(request.hasRawHeader(authHeader)); + QCOMPARE(request.rawHeader(authHeader), value); +} + +void tst_QNetworkRequestFactory::operators() +{ + QNetworkRequestFactory factory1(url1); + + // Copy ctor + QNetworkRequestFactory factory2(factory1); + QCOMPARE(factory2.baseUrl(), factory1.baseUrl()); + + // Copy assignment + QNetworkRequestFactory factory3; + factory3 = factory2; + QCOMPARE(factory3.baseUrl(), factory2.baseUrl()); + + // Move assignment + QNetworkRequestFactory factory4; + factory4 = std::move(factory3); + QCOMPARE(factory4.baseUrl(), factory2.baseUrl()); + + // Verify implicit sharing + factory1.setBaseUrl(url2); + QCOMPARE(factory1.baseUrl(), url2); // changed + QCOMPARE(factory2.baseUrl(), url1); // remains + + // Comparison + QVERIFY(factory2 == factory4); // factory4 was copied + moved, and originates from factory2 + QVERIFY(factory1 != factory2); // factory1 url was changed + + // Move ctor + QNetworkRequestFactory factory5{std::move(factory4)}; + QVERIFY(factory5 == factory2); // the moved factory4 originates from factory2 + QCOMPARE(factory5.baseUrl(), url1); +} + +QTEST_MAIN(tst_QNetworkRequestFactory) +#include "tst_qnetworkrequestfactory.moc" |