Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package qt6-networkauth for openSUSE:Factory checked in at 2023-10-13 23:14:19 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/qt6-networkauth (Old) and /work/SRC/openSUSE:Factory/.qt6-networkauth.new.20540 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "qt6-networkauth" Fri Oct 13 23:14:19 2023 rev:24 rq:1116941 version:6.6.0 Changes: -------- --- /work/SRC/openSUSE:Factory/qt6-networkauth/qt6-networkauth.changes 2023-10-02 20:07:27.868958053 +0200 +++ /work/SRC/openSUSE:Factory/.qt6-networkauth.new.20540/qt6-networkauth.changes 2023-10-13 23:14:47.399274081 +0200 @@ -1,0 +2,6 @@ +Tue Oct 10 09:39:54 UTC 2023 - Christophe Marin <christo...@krop.fr> + +- Update to 6.6.0 + * https://www.qt.io/blog/qt-6.6-released + +------------------------------------------------------------------- Old: ---- qtnetworkauth-everywhere-src-6.5.3.tar.xz New: ---- qtnetworkauth-everywhere-src-6.6.0.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ qt6-networkauth.spec ++++++ --- /var/tmp/diff_new_pack.KeHN71/_old 2023-10-13 23:14:49.051334002 +0200 +++ /var/tmp/diff_new_pack.KeHN71/_new 2023-10-13 23:14:49.051334002 +0200 @@ -16,8 +16,8 @@ # -%define real_version 6.5.3 -%define short_version 6.5 +%define real_version 6.6.0 +%define short_version 6.6 %define short_name qtnetworkauth %define tar_name qtnetworkauth-everywhere-src %define tar_suffix %{nil} @@ -28,7 +28,7 @@ %endif # Name: qt6-networkauth%{?pkg_suffix} -Version: 6.5.3 +Version: 6.6.0 Release: 0 Summary: Set of APIs to obtain limited access to online accounts and HTTP services License: GPL-3.0-only WITH Qt-GPL-exception-1.0 ++++++ qtnetworkauth-everywhere-src-6.5.3.tar.xz -> qtnetworkauth-everywhere-src-6.6.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/.cmake.conf new/qtnetworkauth-everywhere-src-6.6.0/.cmake.conf --- old/qtnetworkauth-everywhere-src-6.5.3/.cmake.conf 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/.cmake.conf 2023-10-02 05:05:49.000000000 +0200 @@ -1,3 +1,3 @@ -set(QT_REPO_MODULE_VERSION "6.5.3") +set(QT_REPO_MODULE_VERSION "6.6.0") set(QT_REPO_MODULE_PRERELEASE_VERSION_SEGMENT "alpha1") set(QT_EXTRA_INTERNAL_TARGET_DEFINES "QT_NO_AS_CONST=1") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/.tag new/qtnetworkauth-everywhere-src-6.6.0/.tag --- old/qtnetworkauth-everywhere-src-6.5.3/.tag 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/.tag 2023-10-02 05:05:49.000000000 +0200 @@ -1 +1 @@ -70e2a96d3bfdd8fab6c48a80ee0147a56233b894 +8c7a6264d1ae09d2d0143655642d6f6530701420 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/coin/axivion/ci_config_linux.json new/qtnetworkauth-everywhere-src-6.6.0/coin/axivion/ci_config_linux.json --- old/qtnetworkauth-everywhere-src-6.5.3/coin/axivion/ci_config_linux.json 1970-01-01 01:00:00.000000000 +0100 +++ new/qtnetworkauth-everywhere-src-6.6.0/coin/axivion/ci_config_linux.json 2023-10-02 05:05:49.000000000 +0200 @@ -0,0 +1,59 @@ +{ + "Project": { + "Git": { + "_active": true, + "sourceserver_gitdir": "/data/axivion/databases/$(env:TESTED_MODULE_COIN).git" + }, + "BuildSystemIntegration": { + "child_order": [ + "GCCSetup", + "CMake", + "LinkLibraries" + ] + }, + "CMake": { + "_active": true, + "_copy_from": "CMakeIntegration", + "build_environment": {}, + "build_options": "-j4", + "generate_options": "--fresh", + "generator": "Ninja" + }, + "GCCSetup": { + "_active": true, + "_copy_from": "Command", + "build_command": "gccsetup --cc gcc --cxx g++ --config ../../../axivion/" + }, + "LinkLibraries": { + "_active": true, + "_copy_from": "AxivionLinker", + "input_files": [ + "build/lib/lib*.so*.ir" + ], + "ir": "build/$(env:TESTED_MODULE_COIN).ir" + }, + "Project-GlobalOptions": { + "directory": "../work/qt/$(env:TESTED_MODULE_COIN)", + "ir": "build/$(env:TESTED_MODULE_COIN).ir", + "name": "qt_$(env:TESTED_MODULE_COIN)_dev_$(env:TARGET_OS_COIN)" + } + }, + "Results": { + "Dashboard": { + "dashboard_url": "https://axivion-srv.ci.qt.io/axivion/" + }, + "Database": { + "ci_mode": { + "directory": "/data/axivion/databases" + } + } + }, + "_Format": "1.0", + "_Version": "trunk-9e0ef9c5818", + "_VersionNum": [ + 7, + 6, + 9999, + 11489 + ] +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/dependencies.yaml new/qtnetworkauth-everywhere-src-6.6.0/dependencies.yaml --- old/qtnetworkauth-everywhere-src-6.5.3/dependencies.yaml 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/dependencies.yaml 2023-10-02 05:05:49.000000000 +0200 @@ -1,4 +1,4 @@ dependencies: ../qtbase: - ref: 372eaedc5b8c771c46acc4c96e91bbade4ca3624 + ref: 33f5e985e480283bb0ca9dea5f82643e825ba87c required: true diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauth.cpp new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauth.cpp --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauth.cpp 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauth.cpp 2023-10-02 05:05:49.000000000 +0200 @@ -91,7 +91,8 @@ \value NetworkError Failed to connect to the server. \value ServerError The server answered the - request with an error. + request with an error, or its response was not successfully received + (for example, due to a state mismatch). \value OAuthTokenNotFoundError The server's response to a token request provided no token identifier. @@ -163,6 +164,16 @@ */ /*! + \fn void QAbstractOAuth::requestFailed(const QAbstractOAuth::Error error) + + This signal is emitted to indicate that a request to a server has failed. + The \a error supplied indicates how the request failed. + + \sa QAbstractOAuth2::error() + \sa QAbstractOAuthReplyHandler::tokenRequestErrorOccurred() +*/ + +/*! \fn QNetworkReply *QAbstractOAuth::head(const QUrl &url, const QVariantMap ¶meters) Sends an authenticated HEAD request and returns a new diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauth2.cpp new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauth2.cpp --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauth2.cpp 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauth2.cpp 2023-10-02 05:05:49.000000000 +0200 @@ -79,10 +79,14 @@ /*! \fn QAbstractOAuth2::error(const QString &error, const QString &errorDescription, const QUrl &uri) - Signal emitted when the server responds to the request with an - error: \a error is the name of the error; \a errorDescription describes - the error and \a uri is an optional URI containing more - information about the error. + Signal emitted when the server responds to the authorization request with + an error as defined in \l {https://www.rfc-editor.org/rfc/rfc6749#section-5.2} + {RFC 6749 error response}. + + \a error is the name of the error; \a errorDescription describes the error + and \a uri is an optional URI containing more information about the error. + + \sa QAbstractOAuth::requestFailed() */ /*! diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauthreplyhandler.cpp new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauthreplyhandler.cpp --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauthreplyhandler.cpp 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauthreplyhandler.cpp 2023-10-02 05:05:49.000000000 +0200 @@ -69,6 +69,18 @@ */ /*! + + \fn void QAbstractOAuthReplyHandler::tokenRequestErrorOccurred(QAbstractOAuth::Error error, + const QString& errorString) + + This signal is emitted when a token request or refresh \a error has + occurred. The \a errorString may provide further details on the error. + + \sa QAbstractOAuth::requestFailed() + \since 6.6 +*/ + +/*! \fn void QAbstractOAuthReplyHandler::replyDataReceived(const QByteArray &data) This signal is emitted when an HTTP request finishes and the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauthreplyhandler.h new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauthreplyhandler.h --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qabstractoauthreplyhandler.h 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qabstractoauthreplyhandler.h 2023-10-02 05:05:49.000000000 +0200 @@ -29,6 +29,7 @@ Q_SIGNALS: void callbackReceived(const QVariantMap &values); void tokensReceived(const QVariantMap &tokens); + void tokenRequestErrorOccurred(QAbstractOAuth::Error error, const QString& errorString); void replyDataReceived(const QByteArray &data); void callbackDataReceived(const QByteArray &data); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qoauth2authorizationcodeflow.cpp new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qoauth2authorizationcodeflow.cpp --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qoauth2authorizationcodeflow.cpp 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qoauth2authorizationcodeflow.cpp 2023-10-02 05:05:49.000000000 +0200 @@ -19,6 +19,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + /*! \class QOAuth2AuthorizationCodeFlow \inmodule QtNetworkAuth @@ -61,7 +63,8 @@ using Key = QAbstractOAuth2Private::OAuth2KeyString; if (status != QAbstractOAuth::Status::NotAuthenticated) { - qCWarning(loggingCategory, "Unexpected call"); + qCWarning(loggingCategory) << "Authorization stage: callback in unexpected status:" + << static_cast<int>(status) << ", ignoring the callback"; return; } @@ -71,23 +74,30 @@ const QString code = data.value(Key::code).toString(); const QString receivedState = data.value(Key::state).toString(); if (error.size()) { + // RFC 6749, Section 5.2 Error Response const QString uri = data.value(Key::errorUri).toString(); const QString description = data.value(Key::errorDescription).toString(); - qCWarning(loggingCategory, "AuthenticationError: %s(%s): %s", - qPrintable(error), qPrintable(uri), qPrintable(description)); + qCWarning(loggingCategory, "Authorization stage: AuthenticationError: %s(%s): %s", + qPrintable(error), qPrintable(uri), qPrintable(description)); Q_EMIT q->error(error, description, uri); + // Emit also requestFailed() so that it is a signal for all errors + emit q->requestFailed(QAbstractOAuth::Error::ServerError); return; } + if (code.isEmpty()) { - qCWarning(loggingCategory, "AuthenticationError: Code not received"); + qCWarning(loggingCategory, "Authorization stage: Code not received"); + emit q->requestFailed(QAbstractOAuth::Error::OAuthTokenNotFoundError); return; } if (receivedState.isEmpty()) { - qCWarning(loggingCategory, "State not received"); + qCWarning(loggingCategory, "Authorization stage: State not received"); + emit q->requestFailed(QAbstractOAuth::Error::ServerError); return; } if (state != receivedState) { - qCWarning(loggingCategory, "State mismatch"); + qCWarning(loggingCategory) << "Authorization stage: State mismatch"; + emit q->requestFailed(QAbstractOAuth::Error::ServerError); return; } @@ -105,8 +115,8 @@ using Key = QAbstractOAuth2Private::OAuth2KeyString; if (values.contains(Key::error)) { - const QString error = values.value(Key::error).toString(); - qCWarning(loggingCategory, "Error: %s", qPrintable(error)); + _q_accessTokenRequestFailed(QAbstractOAuth::Error::ServerError, + values.value(Key::error).toString()); return; } @@ -120,7 +130,8 @@ q->setRefreshToken(values.value(Key::refreshToken).toString()); scope = values.value(Key::scope).toString(); if (accessToken.isEmpty()) { - qCWarning(loggingCategory, "Access token not received"); + _q_accessTokenRequestFailed(QAbstractOAuth::Error::OAuthTokenNotFoundError, + "Access token not received"_L1); return; } q->setToken(accessToken); @@ -142,6 +153,23 @@ setStatus(QAbstractOAuth::Status::Granted); } +void QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFailed(QAbstractOAuth::Error error, + const QString& errorString) +{ + Q_Q(QOAuth2AuthorizationCodeFlow); + qCWarning(loggingCategory) << "Token request failed:" << errorString; + // If we were refreshing, reset status to Granted if we have an access token. + // The access token might still be valid, and even if it wouldn't be, + // refreshing can be attempted again. + if (q->status() == QAbstractOAuth::Status::RefreshingToken) { + if (!q->token().isEmpty()) + setStatus(QAbstractOAuth::Status::Granted); + else + setStatus(QAbstractOAuth::Status::NotAuthenticated); + } + emit q->requestFailed(error); +} + void QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate(QNetworkReply *reply, QAuthenticator *authenticator) { @@ -273,8 +301,12 @@ permanent. After a time specified along with the access token when it was obtained, the access token will become invalid. - \b {See also}: - \l {https://tools.ietf.org/html/rfc6749#section-1.5}{Refresh + If refreshing the token fails and an access token exists, the status is + set to QAbstractOAuth::Status::Granted, and to + QAbstractOAuth::Status::NotAuthenticated otherwise. + + \sa QAbstractOAuth::requestFailed() + \sa {https://tools.ietf.org/html/rfc6749#section-1.5}{Refresh Token} */ void QOAuth2AuthorizationCodeFlow::refreshAccessToken() @@ -313,7 +345,7 @@ const QString data = query.toString(QUrl::FullyEncoded); d->currentReply = d->networkAccessManager()->post(request, data.toUtf8()); - d->status = Status::RefreshingToken; + setStatus(Status::RefreshingToken); QNetworkReply *reply = d->currentReply.data(); QAbstractOAuthReplyHandler *handler = replyHandler(); @@ -327,6 +359,10 @@ &QNetworkAccessManager::authenticationRequired, d, &QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate, Qt::UniqueConnection); + QObjectPrivate::connect(d->replyHandler.data(), + &QAbstractOAuthReplyHandler::tokenRequestErrorOccurred, + d, &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFailed, + Qt::UniqueConnection); } /*! @@ -404,6 +440,10 @@ &QNetworkAccessManager::authenticationRequired, d, &QOAuth2AuthorizationCodeFlowPrivate::_q_authenticate, Qt::UniqueConnection); + QObjectPrivate::connect(d->replyHandler.data(), + &QAbstractOAuthReplyHandler::tokenRequestErrorOccurred, + d, &QOAuth2AuthorizationCodeFlowPrivate::_q_accessTokenRequestFailed, + Qt::UniqueConnection); } /*! diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qoauth2authorizationcodeflow_p.h new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qoauth2authorizationcodeflow_p.h --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qoauth2authorizationcodeflow_p.h 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qoauth2authorizationcodeflow_p.h 2023-10-02 05:05:49.000000000 +0200 @@ -39,6 +39,7 @@ void _q_handleCallback(const QVariantMap &data); void _q_accessTokenRequestFinished(const QVariantMap &values); + void _q_accessTokenRequestFailed(QAbstractOAuth::Error error, const QString &errorString = {}); void _q_authenticate(QNetworkReply *reply, QAuthenticator *authenticator); QUrl accessTokenUrl; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qoauthoobreplyhandler.cpp new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qoauthoobreplyhandler.cpp --- old/qtnetworkauth-everywhere-src-6.5.3/src/oauth/qoauthoobreplyhandler.cpp 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/src/oauth/qoauthoobreplyhandler.cpp 2023-10-02 05:05:49.000000000 +0200 @@ -15,6 +15,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + QOAuthOobReplyHandler::QOAuthOobReplyHandler(QObject *parent) : QAbstractOAuthReplyHandler(parent) {} @@ -27,11 +29,12 @@ void QOAuthOobReplyHandler::networkReplyFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { - qCWarning(lcReplyHandler, "%s", qPrintable(reply->errorString())); + emit tokenRequestErrorOccurred(QAbstractOAuth::Error::NetworkError, reply->errorString()); return; } if (reply->header(QNetworkRequest::ContentTypeHeader).isNull()) { - qCWarning(lcReplyHandler, "Empty Content-type header"); + emit tokenRequestErrorOccurred(QAbstractOAuth::Error::NetworkError, + u"Empty Content-type header"_s); return; } const QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).isNull() ? @@ -39,7 +42,7 @@ reply->header(QNetworkRequest::ContentTypeHeader).toString(); const QByteArray data = reply->readAll(); if (data.isEmpty()) { - qCWarning(lcReplyHandler, "No received data"); + emit tokenRequestErrorOccurred(QAbstractOAuth::Error::NetworkError, u"No received data"_s); return; } @@ -54,8 +57,8 @@ || contentType.startsWith(QStringLiteral("text/javascript"))) { const QJsonDocument document = QJsonDocument::fromJson(data); if (!document.isObject()) { - qCWarning(lcReplyHandler, "Received data is not a JSON object: %s", - qPrintable(QString::fromUtf8(data))); + emit tokenRequestErrorOccurred(QAbstractOAuth::Error::ServerError, + u"Received data is not a JSON object: %1"_s.arg(QString::fromUtf8(data))); return; } const QJsonObject object = document.object(); @@ -65,7 +68,8 @@ } ret = object.toVariantMap(); } else { - qCWarning(lcReplyHandler, "Unknown Content-type: %s", qPrintable(contentType)); + emit tokenRequestErrorOccurred(QAbstractOAuth::Error::ServerError, + u"Unknown Content-type %1"_s.arg(contentType)); return; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/qtnetworkauth-everywhere-src-6.5.3/tests/auto/oauth2/tst_oauth2.cpp new/qtnetworkauth-everywhere-src-6.6.0/tests/auto/oauth2/tst_oauth2.cpp --- old/qtnetworkauth-everywhere-src-6.5.3/tests/auto/oauth2/tst_oauth2.cpp 2023-09-24 09:06:35.000000000 +0200 +++ new/qtnetworkauth-everywhere-src-6.6.0/tests/auto/oauth2/tst_oauth2.cpp 2023-10-02 05:05:49.000000000 +0200 @@ -13,6 +13,8 @@ #include "webserver.h" #include "tlswebserver.h" +using namespace Qt::StringLiterals; + class tst_OAuth2 : public QObject { Q_OBJECT @@ -22,6 +24,8 @@ void getToken(); void refreshToken(); void getAndRefreshToken(); + void tokenRequestErrors(); + void authorizationErrors(); void prepareRequest(); #ifndef QT_NO_SSL void setSslConfig(); @@ -39,13 +43,19 @@ return QLatin1String("test"); } + QAbstractOAuth::Error aTokenRequestError = QAbstractOAuth::Error::NoError; + void networkReplyFinished(QNetworkReply *reply) override { QVariantMap data; const auto items = QUrlQuery(reply->readAll()).queryItems(); for (const auto &pair : items) data.insert(pair.first, pair.second); - Q_EMIT tokensReceived(data); + + if (aTokenRequestError == QAbstractOAuth::Error::NoError) + emit tokensReceived(data); + else + emit tokenRequestErrorOccurred(aTokenRequestError, "a token request error"); } void emitCallbackReceived(const QVariantMap &data) @@ -56,6 +66,7 @@ void tst_OAuth2::initTestCase() { + // QLoggingCategory::setFilterRules(QStringLiteral("qt.networkauth* = true")); testDataDir = QFileInfo(QFINDTESTDATA("certs")).absolutePath(); if (testDataDir.isEmpty()) testDataDir = QCoreApplication::applicationDirPath(); @@ -63,6 +74,91 @@ testDataDir += QLatin1String("/"); } +void tst_OAuth2::authorizationErrors() +{ + // This tests failures in authorization stage. For this test we don't need a web server + // as we emit the final (failing) callbackReceived directly. + // Helper to catch the expected warning messages: + constexpr auto expectWarning = [](){ + static const QRegularExpression authStageWarning{"Authorization stage:.*"}; + QTest::ignoreMessage(QtWarningMsg, authStageWarning); + }; + + QOAuth2AuthorizationCodeFlow oauth2; + oauth2.setAuthorizationUrl(QUrl{"authorization"_L1}); + oauth2.setAccessTokenUrl(QUrl{"accessToken"_L1}); + ReplyHandler replyHandler; + oauth2.setReplyHandler(&replyHandler); + + QVariantMap callbackParameters; + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, + &oauth2, [&](const QUrl& /* url */) { + replyHandler.emitCallbackReceived(callbackParameters); + }); + + QSignalSpy requestFailedSpy(&oauth2, &QAbstractOAuth::requestFailed); + QSignalSpy errorSpy(&oauth2, &QAbstractOAuth2::error); + QSignalSpy statusSpy(&oauth2, &QAbstractOAuth2::statusChanged); + auto clearSpies = [&](){ + requestFailedSpy.clear(); + errorSpy.clear(); + statusSpy.clear(); + }; + + // Test error response from the authorization server (RFC 6749 section 5.2) + callbackParameters = {{"error"_L1, "invalid_grant"_L1}, + {"error_description"_L1, "The error description"_L1}, + {"error_uri"_L1, "The error URI"_L1}}; + expectWarning(); + oauth2.grant(); + QTRY_COMPARE(errorSpy.count(), 1); + QTRY_COMPARE(requestFailedSpy.count(), 1); + QCOMPARE(errorSpy.first().at(0).toString(), "invalid_grant"_L1); + QCOMPARE(errorSpy.first().at(1).toString(), "The error description"_L1); + QCOMPARE(errorSpy.first().at(2).toString(), "The error URI"_L1); + QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(), + QAbstractOAuth::Error::ServerError); + QVERIFY(statusSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated); + + // Test not providing authorization code + clearSpies(); + callbackParameters = {{"state"_L1, "thestate"_L1}}; + expectWarning(); + oauth2.grant(); + QTRY_COMPARE(requestFailedSpy.count(), 1); + QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(), + QAbstractOAuth::Error::OAuthTokenNotFoundError); + QCOMPARE(errorSpy.count(), 0); + QVERIFY(statusSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated); + + // Test not providing a state + clearSpies(); + callbackParameters = {{"code"_L1, "thecode"_L1}}; + expectWarning(); + oauth2.grant(); + QTRY_COMPARE(requestFailedSpy.count(), 1); + QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(), + QAbstractOAuth::Error::ServerError); + QCOMPARE(errorSpy.count(), 0); + QVERIFY(statusSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated); + + // Test state mismatch (here we use "thestate" while the actual, expected, state is a + // random generated string varying each run + clearSpies(); + callbackParameters = {{"code"_L1, "thecode"_L1}, {"state"_L1, "thestate"_L1}}; + expectWarning(); + oauth2.grant(); + QTRY_COMPARE(requestFailedSpy.count(), 1); + QCOMPARE(requestFailedSpy.first().at(0).value<QAbstractOAuth::Error>(), + QAbstractOAuth::Error::ServerError); + QCOMPARE(errorSpy.count(), 0); + QVERIFY(statusSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated); +} + void tst_OAuth2::getToken() { WebServer webServer([](const WebServer::HttpRequest &request, QTcpSocket *socket) { @@ -163,6 +259,122 @@ QCOMPARE(oauth2.token(), QLatin1String("refresh_token")); } +void tst_OAuth2::tokenRequestErrors() +{ + // This test tests the token acquisition and refreshing errors. + // Helper to catch the expected warning messages: + constexpr auto expectWarning = [](){ + static const QRegularExpression tokenWarning{"Token request failed:.*"}; + QTest::ignoreMessage(QtWarningMsg, tokenWarning); + }; + + QByteArray accessTokenResponse; // Varying reply for the auth server + WebServer authServer([&](const WebServer::HttpRequest &request, QTcpSocket *socket) { + if (request.url.path() == QLatin1String("/accessToken")) + socket->write(accessTokenResponse); + }); + + QOAuth2AuthorizationCodeFlow oauth2; + oauth2.setAuthorizationUrl(authServer.url(QLatin1String("authorization"))); + oauth2.setAccessTokenUrl(authServer.url(QLatin1String("accessToken"))); + + ReplyHandler replyHandler; + oauth2.setReplyHandler(&replyHandler); + + QSignalSpy requestFailedSpy(&oauth2, &QAbstractOAuth::requestFailed); + QSignalSpy grantedSpy(&oauth2, &QOAuth2AuthorizationCodeFlow::granted); + QSignalSpy statusSpy(&oauth2, &QAbstractOAuth2::statusChanged); + auto clearSpies = [&](){ + requestFailedSpy.clear(); + grantedSpy.clear(); + statusSpy.clear(); + }; + + connect(&oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, + &oauth2, [&](const QUrl &url) { + // Successful authorization stage, after which we can test token requests. + // For clarity: in these tests we omit browser interaction by directly triggering + // the emission of replyhandler::callbackReceived() signal + const QUrlQuery query(url.query()); + replyHandler.emitCallbackReceived(QVariantMap { + { QLatin1String("code"), QLatin1String("test") }, + { QLatin1String("state"), + query.queryItemValue(QLatin1String("state")) } + }); + }); + + // Check the initial state + QVERIFY(requestFailedSpy.isEmpty()); + QVERIFY(grantedSpy.isEmpty()); + QVERIFY(statusSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::NotAuthenticated); + + // Try to get an access token with an invalid response + accessTokenResponse = "an invalid response"_ba; + expectWarning(); + oauth2.grant(); + QTRY_COMPARE(requestFailedSpy.size(), 1); + QVERIFY(grantedSpy.isEmpty()); + QCOMPARE(statusSpy.size(), 1); // Authorization was successful so we get one signal + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived); + + // Try to get an access token, but replyhandler indicates an error + clearSpies(); + replyHandler.aTokenRequestError = QAbstractOAuth::Error::NetworkError; + expectWarning(); + oauth2.grant(); + QTRY_COMPARE(requestFailedSpy.size(), 1); + QVERIFY(grantedSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived); + + // Make a successful access & refresh token acquisition + replyHandler.aTokenRequestError = QAbstractOAuth::Error::NoError; + clearSpies(); + accessTokenResponse = + "HTTP/1.0 200 OK\r\n" + "Content-Type: application/x-www-form-urlencoded; charset=\"utf-8\"\r\n" + "\r\n" + "access_token=the_access_token&token_type=bearer&refresh_token=the_refresh_token"_ba; + oauth2.grant(); + QTRY_COMPARE(grantedSpy.size(), 1); + QCOMPARE(statusSpy.size(), 3); + // First status change is going from TempCred back to NotAuthenticated + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::NotAuthenticated); + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::TemporaryCredentialsReceived); + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::Granted); + QVERIFY(requestFailedSpy.isEmpty()); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::Granted); + QCOMPARE(oauth2.token(), u"the_access_token"_s); + QCOMPARE(oauth2.refreshToken(), u"the_refresh_token"_s); + + // Successfully refresh access token + clearSpies(); + oauth2.refreshAccessToken(); + QTRY_COMPARE(statusSpy.size(), 2); + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::RefreshingToken); + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::Granted); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::Granted); + QVERIFY(requestFailedSpy.isEmpty()); + + // Failed access token refresh + clearSpies(); + replyHandler.aTokenRequestError = QAbstractOAuth::Error::ServerError; + expectWarning(); + oauth2.refreshAccessToken(); + QTRY_COMPARE(statusSpy.size(), 2); + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::RefreshingToken); + QCOMPARE(statusSpy.takeFirst().at(0).value<QAbstractOAuth::Status>(), + QAbstractOAuth::Status::Granted); // back to granted since we have an access token + QCOMPARE(requestFailedSpy.size(), 1); + QCOMPARE(oauth2.status(), QAbstractOAuth::Status::Granted); +} + void tst_OAuth2::prepareRequest() { QOAuth2AuthorizationCodeFlow oauth2;