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 +#endif + +#include + +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> 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 +#include + +#include +#include +#include +#include + +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 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 +#include +#if QT_CONFIG(ssl) +#include +#endif +#include +#include +#include + +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 +#include +#ifndef QT_NO_SSL +#include +#endif +#include +#include + +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("baseUrl"); + QTest::addColumn("requestPath"); + QTest::addColumn("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"