diff --git a/resources/ews/ewsclient/CMakeLists.txt b/resources/ews/ewsclient/CMakeLists.txt index 86457d9fc..860291809 100644 --- a/resources/ews/ewsclient/CMakeLists.txt +++ b/resources/ews/ewsclient/CMakeLists.txt @@ -1,106 +1,106 @@ # # Copyright (C) 2015-2018 Krzysztof Nowicki # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Library General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Library General Public License for more details. # # You should have received a copy of the GNU Library General Public License # along with this library; see the file COPYING.LIB. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. include_directories(${CMAKE_CURRENT_BINARY_DIR}/../) set(EWSCLIENT_SRCS auth/ewsabstractauth.cpp auth/ewspasswordauth.cpp ewsattachment.cpp ewsattendee.cpp ewsclient.cpp ewscreatefolderrequest.cpp ewscreateitemrequest.cpp ewsdeletefolderrequest.cpp ewsdeleteitemrequest.cpp ewseffectiverights.cpp ewseventrequestbase.cpp ewsfindfolderrequest.cpp ewsfinditemrequest.cpp ewsfolder.cpp ewsfoldershape.cpp ewsgeteventsrequest.cpp ewsgetstreamingeventsrequest.cpp ewsgetfolderrequest.cpp ewsgetitemrequest.cpp ewsid.cpp ewsitem.cpp ewsitembase.cpp ewsitemshape.cpp ewsjob.cpp ewsmailbox.cpp ewsmovefolderrequest.cpp ewsmoveitemrequest.cpp ewsoccurrence.cpp ewspoxautodiscoverrequest.cpp ewspropertyfield.cpp ewsrecurrence.cpp ewsrequest.cpp ewsserverversion.cpp ewssubscriberequest.cpp ewssyncfolderhierarchyrequest.cpp ewssyncfolderitemsrequest.cpp ewstypes.cpp ewsunsubscriberequest.cpp ewsupdatefolderrequest.cpp ewsupdateitemrequest.cpp ewsxml.cpp ewsclient_debug.cpp) if (Qt5NetworkAuth_FOUND) list(APPEND EWSCLIENT_SRCS - ewsoauth.cpp) + auth/ewsoauth.cpp) endif () ecm_qt_declare_logging_category(EWSCLIENT_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.kde.pim.ews.client) ecm_qt_declare_logging_category(EWSCLIENT_SRCS HEADER ewscli_proto_debug.h IDENTIFIER EWSCLI_PROTO_LOG CATEGORY_NAME org.kde.pim.ews.client.proto DEFAULT_SEVERITY Warning) ecm_qt_declare_logging_category(EWSCLIENT_SRCS HEADER ewscli_req_debug.h IDENTIFIER EWSCLI_REQUEST_LOG CATEGORY_NAME org.kde.pim.ews.client.request DEFAULT_SEVERITY Warning) ecm_qt_declare_logging_category(EWSCLIENT_SRCS HEADER ewscli_failedreq_debug.h IDENTIFIER EWSCLI_FAILEDREQUEST_LOG CATEGORY_NAME org.kde.pim.ews.client.failedrequest DEFAULT_SEVERITY Warning) add_library(ewsclient STATIC ${EWSCLIENT_SRCS}) target_link_libraries(ewsclient Qt5::Network KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets KF5::KIONTLM KF5::Codecs KF5::I18n KF5::Mime KF5::CalendarCore) if (Qt5NetworkAuth_FOUND) target_link_libraries(ewsclient Qt5::NetworkAuth Qt5::WebEngineWidgets) endif () diff --git a/resources/ews/ewsclient/ewsoauth.cpp b/resources/ews/ewsclient/auth/ewsoauth.cpp similarity index 82% rename from resources/ews/ewsclient/ewsoauth.cpp rename to resources/ews/ewsclient/auth/ewsoauth.cpp index 33e0fb33b..813ea6bd9 100644 --- a/resources/ews/ewsclient/ewsoauth.cpp +++ b/resources/ews/ewsclient/auth/ewsoauth.cpp @@ -1,435 +1,439 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ewsoauth.h" #ifdef EWSOAUTH_UNITTEST #include "ewsoauth_ut_mock.h" using namespace Mock; #else #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #endif #include "ewsclient_debug.h" static const auto o365AuthorizationUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize")); static const auto o365AccessTokenUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/token")); static const auto o365FakeUserAgent = QStringLiteral("Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36"); static const auto o365Resource = QStringLiteral("https%3A%2F%2Foutlook.office365.com%2F"); +static const QString accessTokenMapKey = QStringLiteral("access-token"); +static const QString refreshTokenMapKey = QStringLiteral("refresh-token"); + class EwsOAuthUrlSchemeHandler final : public QWebEngineUrlSchemeHandler { Q_OBJECT public: EwsOAuthUrlSchemeHandler(QObject *parent = Q_NULLPTR) : QWebEngineUrlSchemeHandler(parent) {}; ~EwsOAuthUrlSchemeHandler() override = default; void requestStarted(QWebEngineUrlRequestJob *request) override; signals: void returnUriReceived(QUrl url); }; class EwsOAuthReplyHandler final : public QAbstractOAuthReplyHandler { Q_OBJECT public: EwsOAuthReplyHandler(QObject *parent, const QString &returnUri) : QAbstractOAuthReplyHandler(parent), mReturnUri(returnUri) {}; ~EwsOAuthReplyHandler() override = default; QString callback() const override { return mReturnUri; }; void networkReplyFinished(QNetworkReply *reply) override; Q_SIGNALS: void replyError(const QString &error); private: const QString mReturnUri; }; class EwsOAuthRequestInterceptor final : public QWebEngineUrlRequestInterceptor { Q_OBJECT public: EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri) : QWebEngineUrlRequestInterceptor(parent), mRedirectUri(redirectUri) { }; ~EwsOAuthRequestInterceptor() override = default; void interceptRequest(QWebEngineUrlRequestInfo &info) override; Q_SIGNALS: void redirectUriIntercepted(const QUrl &url); private: const QString mRedirectUri; }; class EwsOAuthPrivate final : public QObject { Q_OBJECT public: EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri); ~EwsOAuthPrivate() override = default; - void authenticate(); + bool authenticate(bool interactive); void modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters); void authorizeWithBrowser(const QUrl &url); void redirectUriIntercepted(const QUrl &url); void granted(); void error(const QString &error, const QString &errorDescription, const QUrl &uri); QWebEngineView mWebView; QWebEngineProfile mWebProfile; QWebEnginePage mWebPage; QOAuth2AuthorizationCodeFlow mOAuth2; EwsOAuthReplyHandler mReplyHandler; EwsOAuthRequestInterceptor mRequestInterceptor; EwsOAuthUrlSchemeHandler mSchemeHandler; QString mToken; const QString mEmail; const QString mRedirectUri; - EwsOAuth::State mState; QWidget *mParentWindow; + bool mAuthenticated; QPointer mWebDialog; EwsOAuth *q_ptr = nullptr; Q_DECLARE_PUBLIC(EwsOAuth) }; void EwsOAuthUrlSchemeHandler::requestStarted(QWebEngineUrlRequestJob *request) { returnUriReceived(request->requestUrl()); } void EwsOAuthReplyHandler::networkReplyFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { Q_EMIT replyError(reply->errorString()); return; } else if (reply->header(QNetworkRequest::ContentTypeHeader).isNull()) { Q_EMIT replyError(QStringLiteral("Empty or no Content-type header")); return; } const auto cth = reply->header(QNetworkRequest::ContentTypeHeader); const auto ct = cth.isNull() ? QStringLiteral("text/html") : cth.toString(); const auto data = reply->readAll(); if (data.isEmpty()) { Q_EMIT replyError(QStringLiteral("No data received")); return; } Q_EMIT replyDataReceived(data); QVariantMap tokens; if (ct.startsWith(QStringLiteral("text/html")) || ct.startsWith(QStringLiteral("application/x-www-form-urlencoded"))) { QUrlQuery q(QString::fromUtf8(data)); const auto items = q.queryItems(QUrl::FullyDecoded); for (const auto it : items) { tokens.insert(it.first, it.second); } } else if (ct.startsWith(QStringLiteral("application/json")) || ct.startsWith(QStringLiteral("text/javascript"))) { const auto document = QJsonDocument::fromJson(data); if (!document.isObject()) { Q_EMIT replyError(QStringLiteral("Invalid JSON data received")); return; } const auto object = document.object(); if (object.isEmpty()) { Q_EMIT replyError(QStringLiteral("Empty JSON data received")); return; } tokens = object.toVariantMap(); } else { Q_EMIT replyError(QStringLiteral("Unknown content type")); return; } const auto error = tokens.value(QStringLiteral("error")); if (error.isValid()) { Q_EMIT replyError(QStringLiteral("Received error response: ") + error.toString()); return; } const auto accessToken = tokens.value(QStringLiteral("access_token")); if (!accessToken.isValid() || accessToken.toString().isEmpty()) { Q_EMIT replyError(QStringLiteral("Received empty or no access token")); return; } Q_EMIT tokensReceived(tokens); } void EwsOAuthRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { const auto url = info.requestUrl(); qCDebug(EWSCLI_LOG) << QStringLiteral("Intercepted browser navigation to ") << url; if (url.toString(QUrl::RemoveQuery) == mRedirectUri) { qCDebug(EWSCLI_LOG) << QStringLiteral("Found redirect URI - blocking request"); redirectUriIntercepted(url); info.block(true); } } EwsOAuthPrivate::EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri) : QObject(nullptr), mWebView(nullptr), mWebProfile(), mWebPage(&mWebProfile), mReplyHandler(this, redirectUri), - mRequestInterceptor(this, redirectUri), mEmail(email), mRedirectUri(redirectUri), mState(EwsOAuth::NotAuthenticated), - mParentWindow(nullptr), q_ptr(parent) + mRequestInterceptor(this, redirectUri), mEmail(email), mRedirectUri(redirectUri), mParentWindow(nullptr), + mAuthenticated(false), q_ptr(parent) { mOAuth2.setReplyHandler(&mReplyHandler); mOAuth2.setAuthorizationUrl(o365AuthorizationUrl); mOAuth2.setAccessTokenUrl(o365AccessTokenUrl); mOAuth2.setClientIdentifier(appId); /* Bad bad Microsoft... * When Conditional Access is enabled on the server the OAuth2 authentication server only supports Windows, * MacOSX, Android and iOS. No option to include Linux. Support (i.e. guarantee that it works) * is one thing, but blocking unsupported browsers completely is just wrong. * Fortunately enough this can be worked around by faking the user agent to something "supported". */ mWebProfile.setHttpUserAgent(o365FakeUserAgent); mWebProfile.setRequestInterceptor(&mRequestInterceptor); mWebProfile.installUrlSchemeHandler("urn", &mSchemeHandler); mWebView.setPage(&mWebPage); mOAuth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap *parameters) { modifyParametersFunction(stage, parameters); }); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &EwsOAuthPrivate::authorizeWithBrowser); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::granted, this, &EwsOAuthPrivate::granted); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::error, this, &EwsOAuthPrivate::error); connect(&mRequestInterceptor, &EwsOAuthRequestInterceptor::redirectUriIntercepted, this, &EwsOAuthPrivate::redirectUriIntercepted, Qt::QueuedConnection); connect(&mReplyHandler, &EwsOAuthReplyHandler::replyError, this, [this](const QString &err) { error(QStringLiteral("Network reply error"), err, QUrl()); }); } -void EwsOAuthPrivate::authenticate() +bool EwsOAuthPrivate::authenticate(bool interactive) { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Starting OAuth2 authentication"); - mState = EwsOAuth::Authenticating; - #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) if (!mOAuth2.refreshToken().isEmpty()) { #else if (mOAuth2.status() == QAbstractOAuth::Status::Granted) { #endif mOAuth2.refreshAccessToken(); + return true; + } else if (interactive) { + mOAuth2.grant(); + return true; } else { - Q_EMIT q->browserDisplayRequest(); + return false; } } void EwsOAuthPrivate::modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters) { switch (stage) { case QAbstractOAuth::Stage::RequestingAccessToken: parameters->insert(QStringLiteral("resource"), o365Resource); break; case QAbstractOAuth::Stage::RequestingAuthorization: parameters->insert(QStringLiteral("prompt"), QStringLiteral("login")); parameters->insert(QStringLiteral("login_hint"), mEmail); parameters->insert(QStringLiteral("resource"), o365Resource); break; default: break; } } void EwsOAuthPrivate::authorizeWithBrowser(const QUrl &url) { qCInfoNC(EWSCLI_LOG) << QStringLiteral("Launching browser for authentication"); mWebDialog = new QDialog(mParentWindow); mWebDialog->setObjectName(QStringLiteral("Akonadi EWS Resource - Authentication")); mWebDialog->setWindowIcon(QIcon("akonadi-ews")); mWebDialog->resize(400, 500); auto layout = new QHBoxLayout(mWebDialog); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(&mWebView); mWebView.show(); connect(mWebDialog.data(), &QDialog::rejected, this, [this]() { error(QStringLiteral("User cancellation"), QStringLiteral("The authentication browser was closed"), QUrl()); }); mWebView.load(url); mWebDialog->show(); } void EwsOAuthPrivate::redirectUriIntercepted(const QUrl &url) { qCDebug(EWSCLI_LOG) << QStringLiteral("Intercepted redirect URI from browser"); QUrlQuery query(url); QVariantMap varmap; for (const auto item : query.queryItems()) { varmap[item.first] = item.second; } mOAuth2.authorizationCallbackReceived(varmap); mWebView.stop(); mWebDialog->hide(); } void EwsOAuthPrivate::granted() { Q_Q(EwsOAuth); - mState = EwsOAuth::Authenticated; + qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication succeeded"); - // TODO: Store refreshUri + mAuthenticated = true; - qCInfo(EWSCLI_LOG) << QStringLiteral("Authentication succeeded"); + QMap map; + map[accessTokenMapKey] = mOAuth2.token(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + map[refreshTokenMapKey] = mOAuth2.refreshToken(); +#endif + Q_EMIT q->setWalletMap(map); - Q_EMIT(q->granted()); + Q_EMIT q->authSucceeded(); } void EwsOAuthPrivate::error(const QString &error, const QString &errorDescription, const QUrl &uri) { Q_Q(EwsOAuth); -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - if (!mOAuth2.refreshToken().isEmpty()) { - qCInfoNC(EWSCLI_LOG) << QStringLiteral("Refresh token failed. Falling back to full authentication: ") - << error << errorDescription; - mOAuth2.setRefreshToken(QString()); - authenticate(); - } -#else - if (mOAuth2.status() == QAbstractOAuth::Status::RefreshingToken) { - qCInfoNC(EWSCLI_LOG) << QStringLiteral("Refresh token failed. Falling back to full authentication: ") - << error << errorDescription; - authenticate(); - } -#endif - mState = EwsOAuth::AuthenticationFailed; + Q_UNUSED(uri); + + mAuthenticated = false; +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + mOAuth2.setRefreshToken(QString()); +#endif qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication failed: ") << error << errorDescription; - Q_EMIT(q->error(error, errorDescription, uri)); + Q_EMIT q->authFailed(error); } EwsOAuth::EwsOAuth(QObject *parent, const QString &email, const QString &appId, const QString &redirectUri) - : QObject(parent), d_ptr(new EwsOAuthPrivate(this, email, appId, redirectUri)) + : EwsAbstractAuth(parent), d_ptr(new EwsOAuthPrivate(this, email, appId, redirectUri)) { } EwsOAuth::~EwsOAuth() { } -void EwsOAuth::authenticate() +void EwsOAuth::setParentWindow(QWidget *window) { Q_D(EwsOAuth); - d->authenticate(); + d->mParentWindow = window; } -QString EwsOAuth::token() const +void EwsOAuth::init() { - Q_D(const EwsOAuth); - - return d->mOAuth2.token(); + requestWalletMap(); } -QString EwsOAuth::refreshToken() const +bool EwsOAuth::getAuthData(QString &username, QString &password, QStringList &customHeaders) { Q_D(const EwsOAuth); -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - return d->mOAuth2.refreshToken(); -#else - return QString(); -#endif -} - -EwsOAuth::State EwsOAuth::state() const -{ - Q_D(const EwsOAuth); + Q_UNUSED(username); + Q_UNUSED(password); - return d->mState; + if (d->mAuthenticated) { + customHeaders.append(QStringLiteral("Authorization: Bearer ") + d->mOAuth2.token()); + return true; + } else { + return false; + } } -void EwsOAuth::setParentWindow(QWidget *window) +void EwsOAuth::notifyRequestAuthFailed() { Q_D(EwsOAuth); - d->mParentWindow = window; + d->mOAuth2.setToken(QString()); + d->mAuthenticated = false; + + EwsAbstractAuth::notifyRequestAuthFailed(); } -void EwsOAuth::setAccessToken(const QString &accessToken) +bool EwsOAuth::authenticate(bool interactive) { Q_D(EwsOAuth); - d->mOAuth2.setToken(accessToken); - d->mState = Authenticated; + return d->authenticate(interactive); } -void EwsOAuth::setRefreshToken(const QString &refreshToken) +const QString &EwsOAuth::reauthPrompt() const { - Q_D(EwsOAuth); - -#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) - d->mOAuth2.setRefreshToken(refreshToken); -#else - Q_UNUSED(refreshToken); -#endif + static const QString prompt = i18nc("@info", "Microsoft Exchange credentials for the account %1 are no longer valid. You need to authenticate in order to continue using it."); + return prompt; } -void EwsOAuth::resetAccessToken() +const QString &EwsOAuth::authFailedPrompt() const { - Q_D(EwsOAuth); + static const QString prompt = i18nc("@info", "Failed to obtain credentials for Microsoft Exchange account %1. Please update it in the account settings page."); + return prompt; +} - d->mOAuth2.setToken(QString()); - d->mState = NotAuthenticated; +void EwsOAuth::walletPasswordRequestFinished(const QString &password) +{ + Q_UNUSED(password); } -void EwsOAuth::browserDisplayReply(bool display) +void EwsOAuth::walletMapRequestFinished(const QMap &map) { Q_D(EwsOAuth); - if (display) { - d->mOAuth2.grant(); +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + if (map.contains(refreshTokenMapKey)) { + d->mOAuth2.setRefreshToken(map[refreshTokenMapKey]); + } +#endif + if (map.contains(accessTokenMapKey)) { + d->mOAuth2.setToken(map[accessTokenMapKey]); + d->mAuthenticated = true; + Q_EMIT authSucceeded(); } else { - Q_EMIT error(QStringLiteral("User cancellation"), QStringLiteral("The authentication was cancelled by the user"), QUrl()); + Q_EMIT authFailed(QStringLiteral("Access token request failed")); } } #include "ewsoauth.moc" diff --git a/resources/ews/ewsclient/ewsoauth.h b/resources/ews/ewsclient/auth/ewsoauth.h similarity index 67% rename from resources/ews/ewsclient/ewsoauth.h rename to resources/ews/ewsclient/auth/ewsoauth.h index be3663fb1..e22ef6d68 100644 --- a/resources/ews/ewsclient/ewsoauth.h +++ b/resources/ews/ewsclient/auth/ewsoauth.h @@ -1,63 +1,55 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EWSOAUTH_H #define EWSOAUTH_H #include #include +#include "ewsabstractauth.h" + class QWidget; class EwsOAuthPrivate; -class EwsOAuth : public QObject +class EwsOAuth : public EwsAbstractAuth { Q_OBJECT public: - enum State { - NotAuthenticated, - Authenticating, - Authenticated, - AuthenticationFailed - }; - EwsOAuth(QObject *parent, const QString &email, const QString &appId, const QString &redirectUri); ~EwsOAuth() override; - void authenticate(); - QString token() const; - State state() const; - QString refreshToken() const; - void setParentWindow(QWidget *w); - void setAccessToken(const QString &accessToken); - void setRefreshToken(const QString &refreshToken); - void resetAccessToken(); - void browserDisplayReply(bool display); -Q_SIGNALS: - void browserDisplayRequest(); - void granted(); - void error(const QString &error, const QString &errorDescription, const QUrl &uri); + void init() override; + bool getAuthData(QString &username, QString &password, QStringList &customHeaders) override; + void notifyRequestAuthFailed() override; + bool authenticate(bool interactive) override; + const QString &reauthPrompt() const override; + const QString &authFailedPrompt() const override; + + void walletPasswordRequestFinished(const QString &password) override; + void walletMapRequestFinished(const QMap &map) override; + private: QScopedPointer d_ptr; Q_DECLARE_PRIVATE(EwsOAuth) }; #endif /* EWSOAUTH_H */ diff --git a/resources/ews/test/unittests/CMakeLists.txt b/resources/ews/test/unittests/CMakeLists.txt index 48864ee2e..b01e66b44 100644 --- a/resources/ews/test/unittests/CMakeLists.txt +++ b/resources/ews/test/unittests/CMakeLists.txt @@ -1,62 +1,64 @@ # # Copyright (C) 2015-2018 Krzysztof Nowicki # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Library General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Library General Public License for more details. # # You should have received a copy of the GNU Library General Public License # along with this library; see the file COPYING.LIB. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301, USA. kde_enable_exceptions() add_library(uttesthelpers STATIC faketransferjob.cpp) target_link_libraries(uttesthelpers Qt5::Core KF5::KIOCore) macro(akonadi_ews_add_ut_advanced utname extra_SRCS) add_executable(${utname} ${utname}.cpp ${extra_SRCS}) target_link_libraries(${utname} Qt5::Test uttesthelpers) add_test(NAME ${utname} COMMAND ${utname}) endmacro(akonadi_ews_add_ut_advanced utname) macro(akonadi_ews_add_ut utname) akonadi_ews_add_ut_advanced(${utname} "") target_link_libraries(${utname} ewsclient) endmacro(akonadi_ews_add_ut utname) akonadi_ews_add_ut(ewsmoveitemrequest_ut) akonadi_ews_add_ut(ewsdeleteitemrequest_ut) akonadi_ews_add_ut(ewsgetitemrequest_ut) akonadi_ews_add_ut(ewsunsubscriberequest_ut) akonadi_ews_add_ut(ewsattachment_ut) qt5_wrap_cpp(ewssettings_ut_SRCS ewssettings_ut_mock.h) akonadi_ews_add_ut_advanced(ewssettings_ut "${CMAKE_CURRENT_SOURCE_DIR}/../../ewssettings.cpp;${ewssettings_ut_SRCS}") target_link_libraries(ewssettings_ut KF5::AkonadiCore KF5::WidgetsAddons KF5::I18n KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons KF5::Wallet) target_compile_definitions(ewssettings_ut PUBLIC -DEWSSETTINGS_UNITTEST) if (Qt5NetworkAuth_FOUND) set(ewsoauth_ut_SRCS ewsoauth_ut_mock.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../ewsclient/ewsoauth.cpp) + ${CMAKE_CURRENT_SOURCE_DIR}/../../ewsclient/auth/ewsabstractauth.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../ewsclient/auth/ewsoauth.cpp) akonadi_ews_add_ut_advanced(ewsoauth_ut "${ewsoauth_ut_SRCS}") target_link_libraries(ewsoauth_ut Qt5::Widgets + KF5::I18n ) target_compile_definitions(ewsoauth_ut PUBLIC -DEWSOAUTH_UNITTEST) endif () diff --git a/resources/ews/test/unittests/ewsoauth_ut.cpp b/resources/ews/test/unittests/ewsoauth_ut.cpp index b912db617..832381627 100644 --- a/resources/ews/test/unittests/ewsoauth_ut.cpp +++ b/resources/ews/test/unittests/ewsoauth_ut.cpp @@ -1,408 +1,360 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include -#include "ewsoauth.h" +#include "auth/ewsoauth.h" #include "ewsoauth_ut_mock.h" static const QString testEmail = QStringLiteral("joe.bloggs@unknown.com"); static const QString testClientId = QStringLiteral("b43c59cd-dd1c-41fd-bb9a-b0a1d5696a93"); static const QString testReturnUri = QStringLiteral("urn:ietf:wg:oauth:2.0:oob"); static const QString testReturnUriPercent = QUrl::toPercentEncoding(testReturnUri); static const QString testState = QStringLiteral("joidsiuhq"); static const QString resource = QStringLiteral("https://outlook.office365.com/"); static const QString resourcePercent = QUrl::toPercentEncoding(resource); static const QString authUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize"); static const QString tokenUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/token"); static const QString accessToken1 = QStringLiteral("IERbOTo5NSdtY5HMntWTH1wgrRt98KmbF7nNloIdZ4SSYOU7pziJJakpHy8r6kxQi+7T9w36mWv9IWLrvEwTsA"); static const QString refreshToken1 = QStringLiteral("YW7lJFWcEISynbraq4NiLLke3rOieFdvoJEDxpjCXorJblIGM56OJSu1PZXMCQL5W3KLxS9ydxqLHxRTSdw"); static const QString idToken1 = QStringLiteral("gz7l0chu9xIi1MMgPkpHGQTmo3W7L1rQbmWAxEL5VSKHeqdIJ7E3K7vmMYTl/C1fWihB5XiLjD2GSVQoOzTfCw"); class UtEwsOAuth : public QObject { Q_OBJECT private Q_SLOTS: - void initialAuthSuccessful(); - void initialRefreshAuthSuccessful(); - void refreshAuthSuccessful(); + void initialInteractiveSuccessful(); + void initialRefreshSuccessful(); + void refreshSuccessful(); private: static QString formatJsonSorted(const QVariantMap &map); + static int performAuthAction(EwsOAuth &oAuth, int timeout, std::function actionFn); + static void setUpAccessFunction(const QString &refreshToken); + static void setUpTokenFunction(const QString &accessToken, const QString &refreshToken, const QString &idToken, + quint64 time, int tokenLifetime, int extTokenLifetime, QString &tokenReplyData); + static void dumpEvents(const QStringList &events, const QStringList &expectedEvents); + + void setUpOAuth(EwsOAuth &oAuth, QStringList &events, QString password, QMap map); }; -void UtEwsOAuth::initialAuthSuccessful() +void UtEwsOAuth::initialInteractiveSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); - QEventLoop loop; - bool browserRequest = false; QStringList events; - int status = -1; - - connect(&oAuth, &EwsOAuth::granted, this, [&]() { - qDebug() << "granted"; - loop.exit(0); - status = 0; - }); - connect(&oAuth, &EwsOAuth::error, this, [&](const QString &msg, const QString &descr, const QUrl &url) { - qDebug() << "error" << msg << descr << url; - loop.exit(1); - status = 1; - }); - QTimer timer; - connect(&timer, &QTimer::timeout, this, [&]() { - qDebug() << "timeout"; - loop.exit(1); - status = 1; - }); - connect(&oAuth, &EwsOAuth::browserDisplayRequest, this, [&]() { - events.append("BrowserDisplayRequest"); - browserRequest = true; - oAuth.browserDisplayReply(true); - }); - connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&](const QString &event) { - events.append(event); - }); - connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, - [&](const QString &event) { - events.append(event); - }); + setUpOAuth(oAuth, events, QString(), QMap()); + Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); - + constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; - Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map){ - map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken1); - }); - Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction( - [&](QString &data, QMap &headers) { - QVariantMap map; - map[QStringLiteral("token_type")] = QStringLiteral("Bearer"); - map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All"); - map[QStringLiteral("expires_in")] = QString::number(tokenLifetime); - map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime); - map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime); - map[QStringLiteral("not_before")] = QString::number(time); - map[QStringLiteral("resource")] = resource; - map[QStringLiteral("access_token")] = accessToken1; - map[QStringLiteral("refresh_token")] = refreshToken1; - map[QStringLiteral("foci")] = QStringLiteral("1"); - map[QStringLiteral("id_token")] = idToken1; - tokenReplyData = formatJsonSorted(map); - data = tokenReplyData; - headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8"); - - return Mock::QNetworkReply::NoError; - }); + setUpAccessFunction(refreshToken1); + setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); - timer.setSingleShot(true); - timer.start(1000); + const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { + oAuth->init(); + return true; + }); + QVERIFY(initStatus == 1); - oAuth.authenticate(); - if (status == -1) { - status = loop.exec(); - } + const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { + return oAuth->authenticate(true); + }); + QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { - Mock::browserDisplayRequestString(), + Mock::requestWalletMapString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + "?code=" + QUrl::toPercentEncoding(refreshToken1)), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; - for (const auto event : events) { - qDebug() << "Got event:" << event; - } + dumpEvents(events, expectedEvents); - QVERIFY(status == 0); QVERIFY(events == expectedEvents); } -void UtEwsOAuth::initialRefreshAuthSuccessful() +void UtEwsOAuth::initialRefreshSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); - QEventLoop loop; QStringList events; - int status = -1; - oAuth.setAccessToken(QString()); - oAuth.setRefreshToken(refreshToken1); - - connect(&oAuth, &EwsOAuth::granted, this, [&]() { - qDebug() << "granted"; - loop.exit(0); - status = 0; - }); - connect(&oAuth, &EwsOAuth::error, this, [&](const QString &msg, const QString &descr, const QUrl &url) { - qDebug() << "error" << msg << descr << url; - loop.exit(1); - status = 1; - }); - QTimer timer; - connect(&timer, &QTimer::timeout, this, [&]() { - qDebug() << "timeout"; - loop.exit(1); - status = 1; - }); - connect(&oAuth, &EwsOAuth::browserDisplayRequest, this, [&]() { - events.append("BrowserDisplayRequest"); - oAuth.browserDisplayReply(true); - }); - connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&](const QString &event) { - events.append(event); - }); - connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, - [&](const QString &event) { - events.append(event); - }); + QMap map = { + {QStringLiteral("refresh-token"), refreshToken1} + }; + + setUpOAuth(oAuth, events, QString(), map); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; - Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map){ - map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken1); - }); - Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction( - [&](QString &data, QMap &headers) { - QVariantMap map; - map[QStringLiteral("token_type")] = QStringLiteral("Bearer"); - map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All"); - map[QStringLiteral("expires_in")] = QString::number(tokenLifetime); - map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime); - map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime); - map[QStringLiteral("not_before")] = QString::number(time); - map[QStringLiteral("resource")] = resource; - map[QStringLiteral("access_token")] = accessToken1; - map[QStringLiteral("refresh_token")] = refreshToken1; - map[QStringLiteral("foci")] = QStringLiteral("1"); - map[QStringLiteral("id_token")] = idToken1; - tokenReplyData = formatJsonSorted(map); - data = tokenReplyData; - headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8"); - - return Mock::QNetworkReply::NoError; - }); + setUpAccessFunction(refreshToken1); + setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); - timer.setSingleShot(true); - timer.start(1000); + const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { + oAuth->init(); + return true; + }); + QVERIFY(initStatus == 1); - oAuth.authenticate(); - if (status == -1) { - status = loop.exec(); - }; + const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { + return oAuth->authenticate(true); + }); + QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { + Mock::requestWalletMapString(), #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) - Mock::browserDisplayRequestString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + "?code=" + QUrl::toPercentEncoding(refreshToken1)), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), #endif Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; - for (const auto event : events) { - qDebug() << "Got event:" << event; - } + dumpEvents(events, expectedEvents); - QVERIFY(status == 0); QVERIFY(events == expectedEvents); } -void UtEwsOAuth::refreshAuthSuccessful() +void UtEwsOAuth::refreshSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); - QEventLoop loop; QStringList events; - int status = -1; - - connect(&oAuth, &EwsOAuth::granted, this, [&]() { - qDebug() << "granted"; - loop.exit(0); - status = 0; - }); - connect(&oAuth, &EwsOAuth::error, this, [&](const QString &msg, const QString &descr, const QUrl &url) { - qDebug() << "error" << msg << descr << url; - loop.exit(1); - status = 1; - }); - QTimer timer; - connect(&timer, &QTimer::timeout, this, [&]() { - qDebug() << "timeout"; - loop.exit(1); - status = 1; - }); - connect(&oAuth, &EwsOAuth::browserDisplayRequest, this, [&]() { - events.append("BrowserDisplayRequest"); - oAuth.browserDisplayReply(true); - }); - connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&](const QString &event) { - events.append(event); - }); - connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, - [&](const QString &event) { - events.append(event); - }); + setUpOAuth(oAuth, events, QString(), QMap()); + Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; - Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map){ - map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken1); - }); - Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction( - [&](QString &data, QMap &headers) { - QVariantMap map; - map[QStringLiteral("token_type")] = QStringLiteral("Bearer"); - map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All"); - map[QStringLiteral("expires_in")] = QString::number(tokenLifetime); - map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime); - map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime); - map[QStringLiteral("not_before")] = QString::number(time); - map[QStringLiteral("resource")] = resource; - map[QStringLiteral("access_token")] = accessToken1; - map[QStringLiteral("refresh_token")] = refreshToken1; - map[QStringLiteral("foci")] = QStringLiteral("1"); - map[QStringLiteral("id_token")] = idToken1; - tokenReplyData = formatJsonSorted(map); - data = tokenReplyData; - headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8"); - - return Mock::QNetworkReply::NoError; - }); + setUpAccessFunction(refreshToken1); + setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); - timer.setSingleShot(true); - timer.start(1000); + const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { + oAuth->init(); + return true; + }); + QVERIFY(initStatus == 1); - oAuth.authenticate(); - if (status == -1) { - status = loop.exec(); - }; + const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { + return oAuth->authenticate(true); + }); + QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { - Mock::browserDisplayRequestString(), + Mock::requestWalletMapString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + "?code=" + QUrl::toPercentEncoding(refreshToken1)), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; - for (const auto event : events) { - qDebug() << "Got event:" << event; - } + dumpEvents(events, expectedEvents); - QVERIFY(status == 0); QVERIFY(events == expectedEvents); events.clear(); - oAuth.resetAccessToken(); - status = -1; + + oAuth.notifyRequestAuthFailed(); - timer.start(1000); - - oAuth.authenticate(); - if (status == -1) { - status = loop.exec(); - }; + const auto reauthStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { + return oAuth->authenticate(false); + }); + QVERIFY(reauthStatus == 0); const QStringList expectedEventsRefresh = { Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; - for (const auto event : events) { - qDebug() << "Got event:" << event; - } + dumpEvents(events, expectedEvents); - QVERIFY(status == 0); QVERIFY(events == expectedEventsRefresh); } QString UtEwsOAuth::formatJsonSorted(const QVariantMap &map) { QStringList keys = map.keys(); keys.sort(); QStringList elems; for (const auto key : keys) { QString val = map[key].toString(); val.replace('"', QStringLiteral("\\\"")); elems.append(QStringLiteral("\"%1\":\"%2\"").arg(key, val)); } return QStringLiteral("{") + elems.join(',') + QStringLiteral("}"); } +int UtEwsOAuth::performAuthAction(EwsOAuth &oAuth, int timeout, std::function actionFn) +{ + QEventLoop loop; + int status = -1; + QTimer timer; + connect(&oAuth, &EwsOAuth::authSucceeded, &timer, [&]() { + qDebug() << "succeeded"; + loop.exit(0); + status = 0; + }); + connect(&oAuth, &EwsOAuth::authFailed, &timer, [&](const QString &msg) { + qDebug() << "failed" << msg; + loop.exit(1); + status = 1; + }); + connect(&timer, &QTimer::timeout, &timer, [&]() { + qDebug() << "timeout"; + loop.exit(1); + status = 1; + }); + timer.setSingleShot(true); + timer.start(timeout); + + if (!actionFn(&oAuth)) + { + return -1; + } + + if (status == -1) { + status = loop.exec(); + } + + return status; +} + +void UtEwsOAuth::setUpAccessFunction(const QString &refreshToken) +{ + Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map){ + map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken); + }); +} + +void UtEwsOAuth::setUpTokenFunction(const QString &accessToken, const QString &refreshToken, const QString &idToken, + quint64 time, int tokenLifetime, int extTokenLifetime, QString &tokenReplyData) +{ + Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction( + [=, &tokenReplyData] (QString &data, QMap &headers) { + QVariantMap map; + map[QStringLiteral("token_type")] = QStringLiteral("Bearer"); + map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All"); + map[QStringLiteral("expires_in")] = QString::number(tokenLifetime); + map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime); + map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime); + map[QStringLiteral("not_before")] = QString::number(time); + map[QStringLiteral("resource")] = resource; + map[QStringLiteral("access_token")] = accessToken; + map[QStringLiteral("refresh_token")] = refreshToken; + map[QStringLiteral("foci")] = QStringLiteral("1"); + map[QStringLiteral("id_token")] = idToken; + tokenReplyData = formatJsonSorted(map); + data = tokenReplyData; + headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8"); + + return Mock::QNetworkReply::NoError; + }); +} + +void UtEwsOAuth::dumpEvents(const QStringList &events, const QStringList &expectedEvents) +{ + for (const auto event : events) { + qDebug() << "Got event:" << event; + } + if (events != expectedEvents) { + for (const auto event : expectedEvents) { + qDebug() << "Expected event:" << event; + } + } +} + +void UtEwsOAuth::setUpOAuth(EwsOAuth &oAuth, QStringList &events, QString password, QMap map) +{ + connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&events](const QString &event) { + events.append(event); + }); + connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, + [&events](const QString &event) { + events.append(event); + }); + connect(&oAuth, &EwsOAuth::requestWalletPassword, this, [&oAuth, &events, password](bool) { + events.append("RequestWalletPassword"); + oAuth.walletPasswordRequestFinished(password); + }); + connect(&oAuth, &EwsOAuth::requestWalletMap, this, [&oAuth, &events, map]() { + events.append("RequestWalletMap"); + oAuth.walletMapRequestFinished(map); + }); +} + QTEST_MAIN(UtEwsOAuth) #include "ewsoauth_ut.moc" diff --git a/resources/ews/test/unittests/ewsoauth_ut_mock.cpp b/resources/ews/test/unittests/ewsoauth_ut_mock.cpp index c7f91519e..be4bc7dd1 100644 --- a/resources/ews/test/unittests/ewsoauth_ut_mock.cpp +++ b/resources/ews/test/unittests/ewsoauth_ut_mock.cpp @@ -1,450 +1,455 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ewsoauth_ut_mock.h" Q_LOGGING_CATEGORY(EWSCLI_LOG, "org.kde.pim.ews.client", QtInfoMsg) namespace Mock { QPointer QWebEngineView::instance; QPointer QOAuth2AuthorizationCodeFlow::instance; QUrl QWebEngineUrlRequestJob::requestUrl() const { return mUrl; } QUrl QWebEngineUrlRequestInfo::requestUrl() const { return mUrl; } void QWebEngineUrlRequestInfo::block(bool) { mBlocked = true; } QWebEngineUrlRequestInterceptor::QWebEngineUrlRequestInterceptor(QObject *parent) : QObject(parent) { } QWebEngineUrlRequestInterceptor::~QWebEngineUrlRequestInterceptor() { } QWebEngineUrlSchemeHandler::QWebEngineUrlSchemeHandler(QObject *parent) : QObject(parent) { } QWebEngineUrlSchemeHandler::~QWebEngineUrlSchemeHandler() { } QWebEngineProfile::QWebEngineProfile(QObject *parent) : QObject(parent), mInterceptor(nullptr), mHandler(nullptr) { } QWebEngineProfile::~QWebEngineProfile() { } void QWebEngineProfile::setHttpUserAgent(const QString &ua) { mUserAgent = ua; } void QWebEngineProfile::setRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor) { mInterceptor = interceptor; } void QWebEngineProfile::installUrlSchemeHandler(QByteArray const &scheme, QWebEngineUrlSchemeHandler *handler) { mScheme = scheme; mHandler = handler; } QWebEnginePage::QWebEnginePage(QWebEngineProfile *profile, QObject *parent) : QObject(parent), mProfile(profile) { connect(profile, &QWebEngineProfile::logEvent, this, &QWebEnginePage::logEvent); } QWebEnginePage::~QWebEnginePage() { } QWebEngineView::QWebEngineView(QWidget *parent) : QWidget(parent), mPage(nullptr) { if (!instance) { instance = this; } else { qDebug() << "QWebEngineView instance already exists!"; } } QWebEngineView::~QWebEngineView() { } void QWebEngineView::load(const QUrl &url) { Q_EMIT logEvent(QStringLiteral("LoadWebPage:") + url.toString()); simulatePageLoad(url); QVariantMap params; if (mAuthFunction) { mAuthFunction(url, params); simulatePageLoad(QUrl(mRedirectUri + QStringLiteral("?") + QOAuth2AuthorizationCodeFlow::mapToSortedQuery(params).toString())); } else { qWarning() << "No authentication callback defined"; } } void QWebEngineView::simulatePageLoad(const QUrl &url) { if (mPage && mPage->mProfile && mPage->mProfile->mInterceptor) { QWebEngineUrlRequestInfo info(url, this); Q_EMIT logEvent(QStringLiteral("InterceptRequest:") + url.toString()); mPage->mProfile->mInterceptor->interceptRequest(info); Q_EMIT logEvent(QStringLiteral("InterceptRequestBlocked:%1").arg(info.mBlocked)); } else { qWarning() << "Cannot reach to request interceptor"; } } void QWebEngineView::setPage(QWebEnginePage *page) { mPage = page; connect(page, &QWebEnginePage::logEvent, this, &QWebEngineView::logEvent); } void QWebEngineView::stop() { } void QWebEngineView::setAuthFunction(const AuthFunc &func) { mAuthFunction = func; } void QWebEngineView::setRedirectUri(const QString &uri) { mRedirectUri = uri; } QNetworkReply::NetworkError QNetworkReply::error() const { return NoError; } QVariant QNetworkReply::header(QNetworkRequest::KnownHeaders header) const { return mHeaders[header]; } QAbstractOAuthReplyHandler::QAbstractOAuthReplyHandler(QObject *parent) : QObject(parent) { } QAbstractOAuthReplyHandler::~QAbstractOAuthReplyHandler() { } QAbstractOAuth::QAbstractOAuth(QObject *parent) : QObject(parent), mStatus(Status::NotAuthenticated) { } void QAbstractOAuth::setReplyHandler(QAbstractOAuthReplyHandler *handler) { mReplyHandler = handler; } void QAbstractOAuth::setAuthorizationUrl(const QUrl &url) { mAuthUrl = url; } void QAbstractOAuth::setClientIdentifier(const QString &identifier) { mClientId = identifier; } void QAbstractOAuth::setModifyParametersFunction(const std::function*)> &func) { mModifyParamsFunc = func; } QString QAbstractOAuth::token() const { return mToken; } void QAbstractOAuth::setToken(const QString &token) { mToken = token; } QAbstractOAuth::Status QAbstractOAuth::status() const { return mStatus; } QAbstractOAuth2::QAbstractOAuth2(QObject *parent) : QAbstractOAuth(parent) { } QString QAbstractOAuth2::refreshToken() const { return mRefreshToken; } void QAbstractOAuth2::setRefreshToken(const QString &token) { mRefreshToken = token; } QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QObject *parent) : QAbstractOAuth2(parent) { if (!instance) { instance = this; } else { qDebug() << "QOAuth2AuthorizationCodeFlow instance already exists!"; } } QOAuth2AuthorizationCodeFlow::~QOAuth2AuthorizationCodeFlow() { } void QOAuth2AuthorizationCodeFlow::setAccessTokenUrl(const QUrl &url) { mTokenUrl = url; } void QOAuth2AuthorizationCodeFlow::grant() { QMap map; map[QStringLiteral("response_type")] = QStringLiteral("code"); map[QStringLiteral("client_id")] = QUrl::toPercentEncoding(mClientId); map[QStringLiteral("redirect_uri")] = QUrl::toPercentEncoding(mReplyHandler->callback()); map[QStringLiteral("scope")] = QString(); map[QStringLiteral("state")] = mState; Q_EMIT logEvent(QStringLiteral("ModifyParams:RequestingAuthorization:") + mapToSortedQuery(map).toString()); if (mModifyParamsFunc) { mModifyParamsFunc(Stage::RequestingAuthorization, &map); } mResource = QUrl::fromPercentEncoding(map[QStringLiteral("resource")].toByteArray()); QUrl url(mAuthUrl); url.setQuery(mapToSortedQuery(map)); Q_EMIT logEvent(QStringLiteral("AuthorizeWithBrowser:") + url.toString()); connect(this, &QAbstractOAuth2::authorizationCallbackReceived, this, &QOAuth2AuthorizationCodeFlow::authCallbackReceived, Qt::UniqueConnection); Q_EMIT authorizeWithBrowser(url); } void QOAuth2AuthorizationCodeFlow::refreshAccessToken() { mStatus = Status::RefreshingToken; doRefreshAccessToken(); } void QOAuth2AuthorizationCodeFlow::doRefreshAccessToken() { QMap map; map[QStringLiteral("grant_type")] = QStringLiteral("authorization_code"); map[QStringLiteral("code")] = QUrl::toPercentEncoding(mRefreshToken); map[QStringLiteral("client_id")] = QUrl::toPercentEncoding(mClientId); map[QStringLiteral("redirect_uri")] = QUrl::toPercentEncoding(mReplyHandler->callback()); Q_EMIT logEvent(QStringLiteral("ModifyParams:RequestingAccessToken:") + mapToSortedQuery(map).toString()); if (mModifyParamsFunc) { mModifyParamsFunc(Stage::RequestingAccessToken, &map); } connect(mReplyHandler, &QAbstractOAuthReplyHandler::tokensReceived, this, &QOAuth2AuthorizationCodeFlow::tokenCallbackReceived, Qt::UniqueConnection); connect(mReplyHandler, &QAbstractOAuthReplyHandler::replyDataReceived, this, &QOAuth2AuthorizationCodeFlow::replyDataCallbackReceived, Qt::UniqueConnection); if (mTokenFunc) { QNetworkReply reply(this); QString data; reply.mError = mTokenFunc(data, reply.mHeaders); reply.setData(data.toUtf8()); reply.open(QIODevice::ReadOnly); Q_EMIT logEvent(QStringLiteral("NetworkReplyFinished:") + data); mReplyHandler->networkReplyFinished(&reply); } else { qWarning() << "No token function defined"; } } QUrlQuery QOAuth2AuthorizationCodeFlow::mapToSortedQuery(QMap const &map) { QUrlQuery query; QStringList keys = map.keys(); keys.sort(); for (const auto key : keys) { query.addQueryItem(key, map[key].toString()); } return query; } void QOAuth2AuthorizationCodeFlow::authCallbackReceived(QMap const ¶ms) { Q_EMIT logEvent(QStringLiteral("AuthorizatioCallbackReceived:") + mapToSortedQuery(params).toString()); mRefreshToken = params[QStringLiteral("code")].toString(); if (!mRefreshToken.isEmpty()) { mStatus = Status::TemporaryCredentialsReceived; doRefreshAccessToken(); } else { Q_EMIT error(QString(), QString(), QUrl()); } } void QOAuth2AuthorizationCodeFlow::tokenCallbackReceived(const QVariantMap &tokens) { Q_EMIT logEvent(QStringLiteral("TokenCallback:") + mapToSortedQuery(tokens).toString()); mToken = tokens["access_token"].toString(); mRefreshToken = tokens["refresh_token"].toString(); mStatus = Status::Granted; Q_EMIT granted(); } void QOAuth2AuthorizationCodeFlow::replyDataCallbackReceived(const QByteArray &data) { Q_EMIT logEvent(QStringLiteral("ReplyDataCallback:") + data); } QString QOAuth2AuthorizationCodeFlow::redirectUri() const { return mReplyHandler->callback(); } void QOAuth2AuthorizationCodeFlow::setTokenFunction(const TokenFunc &func) { mTokenFunc = func; } void QOAuth2AuthorizationCodeFlow::setState(const QString &state) { mState = state; } QString browserDisplayRequestString() { return QStringLiteral("BrowserDisplayRequest"); } QString modifyParamsAuthString(const QString &clientId, const QString &returnUri, const QString &state) { return QStringLiteral("ModifyParams:RequestingAuthorization:client_id=%1&redirect_uri=%2&response_type=code&scope&state=%3") .arg(QString::fromUtf8(QUrl::toPercentEncoding(clientId)), QUrl::toPercentEncoding(returnUri), QUrl::toPercentEncoding(state)); } QString authUrlString(const QString &authUrl, const QString &clientId, const QString &returnUri, const QString &email, const QString &resource, const QString &state) { return QStringLiteral("%1?client_id=%2&login_hint=%3&prompt=login&redirect_uri=%4&resource=%5&response_type=code&scope&state=%6") .arg(authUrl, QUrl::toPercentEncoding(clientId), email, QUrl::toPercentEncoding(returnUri), QUrl::toPercentEncoding(resource), QUrl::toPercentEncoding(state)); } QString authorizeWithBrowserString(const QString &url) { return QStringLiteral("AuthorizeWithBrowser:") + url; } QString loadWebPageString(const QString &url) { return QStringLiteral("LoadWebPage:") + url; } QString interceptRequestString(const QString &url) { return QStringLiteral("InterceptRequest:") + url; } QString interceptRequestBlockedString(bool blocked) { return QStringLiteral("InterceptRequestBlocked:%1").arg(blocked); } QString authorizationCallbackReceivedString(const QString &code) { return QStringLiteral("AuthorizatioCallbackReceived:code=%1").arg(code); } QString modifyParamsTokenString(const QString &clientId, const QString &returnUri, const QString &code) { return QStringLiteral("ModifyParams:RequestingAccessToken:client_id=%1&code=%2&grant_type=authorization_code&redirect_uri=%3") .arg(QString::fromUtf8(QUrl::toPercentEncoding(clientId)), QUrl::toPercentEncoding(code), QUrl::toPercentEncoding(returnUri)); } QString networkReplyFinishedString(const QString &data) { return QStringLiteral("NetworkReplyFinished:") + data; } QString replyDataCallbackString(const QString &data) { return QStringLiteral("ReplyDataCallback:") + data; } QString tokenCallbackString(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, unsigned int tokenLifetime, unsigned int extTokenLifetime, const QString &resource) { return QStringLiteral("TokenCallback:access_token=%1&expires_in=%2&expires_on=%3&ext_expires_in=%4&foci=1&id_token=%5¬_before=%6&refresh_token=%7&resource=%8&scope=ReadWrite.All&token_type=Bearer") .arg(accessToken).arg(tokenLifetime).arg(time + tokenLifetime).arg(extTokenLifetime).arg(idToken).arg(time) .arg(refreshToken).arg(resource); } +QString requestWalletMapString() +{ + return QStringLiteral("RequestWalletMap"); +} + } diff --git a/resources/ews/test/unittests/ewsoauth_ut_mock.h b/resources/ews/test/unittests/ewsoauth_ut_mock.h index c4a72270a..23419dad2 100644 --- a/resources/ews/test/unittests/ewsoauth_ut_mock.h +++ b/resources/ews/test/unittests/ewsoauth_ut_mock.h @@ -1,294 +1,297 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EWSOAUTH_UT_MOCK_H #define EWSOAUTH_UT_MOCK_H #include #include #include #include #include #include #include #include #include #include #include +#include + Q_DECLARE_LOGGING_CATEGORY(EWSCLI_LOG) namespace Mock { class QWebEngineUrlRequestJob : public QObject { Q_OBJECT public: QWebEngineUrlRequestJob(const QUrl &url, QObject *parent) : QObject(parent), mUrl(url) {}; ~QWebEngineUrlRequestJob() override = default; QUrl requestUrl() const; QUrl mUrl; }; class QWebEngineUrlRequestInfo : public QObject { Q_OBJECT public: QWebEngineUrlRequestInfo(const QUrl &url, QObject *parent) : QObject(parent), mBlocked(false), mUrl(url) {}; ~QWebEngineUrlRequestInfo() override = default; QUrl requestUrl() const; void block(bool shouldBlock); bool mBlocked; QUrl mUrl; }; class QWebEngineUrlRequestInterceptor : public QObject { Q_OBJECT public: QWebEngineUrlRequestInterceptor(QObject *parent); ~QWebEngineUrlRequestInterceptor() override; virtual void interceptRequest(QWebEngineUrlRequestInfo &info) = 0; }; class QWebEngineUrlSchemeHandler : public QObject { Q_OBJECT public: QWebEngineUrlSchemeHandler(QObject *parent); ~QWebEngineUrlSchemeHandler() override; virtual void requestStarted(QWebEngineUrlRequestJob *request) = 0; }; class QWebEngineProfile : public QObject { Q_OBJECT public: QWebEngineProfile(QObject *parent = nullptr); ~QWebEngineProfile() override; void setHttpUserAgent(const QString &ua); void setRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor); void installUrlSchemeHandler(QByteArray const &scheme, QWebEngineUrlSchemeHandler *handler); Q_SIGNALS: void logEvent(const QString &event); public: QString mUserAgent; QWebEngineUrlRequestInterceptor *mInterceptor; QString mScheme; QWebEngineUrlSchemeHandler *mHandler; }; class QWebEnginePage : public QObject { Q_OBJECT public: QWebEnginePage(QWebEngineProfile *profile, QObject *parent = nullptr); ~QWebEnginePage() override; Q_SIGNALS: void logEvent(const QString &event); public: QWebEngineProfile *mProfile; }; class QWebEngineView : public QWidget { Q_OBJECT public: typedef std::function AuthFunc; QWebEngineView(QWidget *parent); ~QWebEngineView() override; void load(const QUrl &url); void setPage(QWebEnginePage *page); void stop(); void setAuthFunction(const AuthFunc &func); void setRedirectUri(const QString &uri); static QPointer instance; Q_SIGNALS: void logEvent(const QString &event); protected: void simulatePageLoad(const QUrl &url); QWebEnginePage *mPage; QString mRedirectUri; AuthFunc mAuthFunction; }; class QNetworkRequest { public: enum KnownHeaders { ContentTypeHeader }; }; class QNetworkReply : public QBuffer { Q_OBJECT public: enum NetworkError { NoError = 0, }; Q_ENUM(NetworkError) QNetworkReply(QObject *parent) : QBuffer(parent) {}; ~QNetworkReply() override = default; NetworkError error() const; QVariant header(QNetworkRequest::KnownHeaders header) const; QMap mHeaders; NetworkError mError; }; class QAbstractOAuthReplyHandler : public QObject { Q_OBJECT public: QAbstractOAuthReplyHandler(QObject *parent); ~QAbstractOAuthReplyHandler() override; virtual QString callback() const = 0; virtual void networkReplyFinished(QNetworkReply *reply) = 0; Q_SIGNALS: void replyDataReceived(const QByteArray &data); void tokensReceived(const QVariantMap &tokens); }; class QAbstractOAuth : public QObject { Q_OBJECT public: Q_ENUMS(Stage) enum class Stage { RequestingTemporaryCredentials, RequestingAuthorization, RequestingAccessToken, RefreshingAccessToken }; Q_ENUMS(Status) enum class Status { NotAuthenticated, TemporaryCredentialsReceived, Granted, RefreshingToken }; QAbstractOAuth(QObject *parent); ~QAbstractOAuth() override = default; void setReplyHandler(QAbstractOAuthReplyHandler *handler); void setAuthorizationUrl(const QUrl &url); void setClientIdentifier(const QString &identifier); void setModifyParametersFunction(const std::function*)> &func); QString token() const; void setToken(const QString &token); Status status() const; Q_SIGNALS: void authorizeWithBrowser(const QUrl &url); void granted(); void logEvent(const QString &event); protected: QAbstractOAuthReplyHandler *mReplyHandler; QUrl mAuthUrl; QString mClientId; std::function*)> mModifyParamsFunc; QString mToken; QString mRefreshToken; QUrl mTokenUrl; QString mResource; Status mStatus; }; class QAbstractOAuth2 : public QAbstractOAuth { Q_OBJECT public: QAbstractOAuth2(QObject *parent); ~QAbstractOAuth2() override = default; QString refreshToken() const; void setRefreshToken(const QString &token); Q_SIGNALS: void authorizationCallbackReceived(QMap const ¶ms); void error(const QString &error, const QString &errorDescription, const QUrl &uri); }; class QOAuth2AuthorizationCodeFlow : public QAbstractOAuth2 { Q_OBJECT public: typedef std::function &)> TokenFunc; QOAuth2AuthorizationCodeFlow(QObject *parent = nullptr); ~QOAuth2AuthorizationCodeFlow() override; void setAccessTokenUrl(const QUrl &url); void grant(); void refreshAccessToken(); QString redirectUri() const; void setTokenFunction(const TokenFunc &func); void setState(const QString &state); static QUrlQuery mapToSortedQuery(QMap const &map); static QPointer instance; protected: void authCallbackReceived(QMap const ¶ms); void replyDataCallbackReceived(const QByteArray &data); void tokenCallbackReceived(const QVariantMap &tokens); void doRefreshAccessToken(); TokenFunc mTokenFunc; QString mState; }; QString browserDisplayRequestString(); QString modifyParamsAuthString(const QString &clientId, const QString &returnUri, const QString &state); QString authUrlString(const QString &authUrl, const QString &clientId, const QString &returnUri, const QString &email, const QString &resource, const QString &state); QString authorizeWithBrowserString(const QString &url); QString loadWebPageString(const QString &url); QString interceptRequestString(const QString &url); QString interceptRequestBlockedString(bool blocked); QString authorizationCallbackReceivedString(const QString &code); QString modifyParamsTokenString(const QString &clientId, const QString &returnUri, const QString &code); QString networkReplyFinishedString(const QString &data); QString replyDataCallbackString(const QString &data); QString tokenCallbackString(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, unsigned int tokenLifetime, unsigned int extTokenLifetime, const QString &resource); +QString requestWalletMapString(); } #endif /* EWSOAUTH_UT_MOCK_H */