File indexing completed on 2024-11-24 04:43:51

0001 /*
0002     SPDX-FileCopyrightText: 2018 Krzysztof Nowicki <krissn@op.pl>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "auth/ewsoauth.h"
0008 #include <QTest>
0009 #include <QTimer>
0010 #include <functional>
0011 
0012 #include "ewsoauth_ut_mock.h"
0013 
0014 static const QString testEmail = QStringLiteral("joe.bloggs@unknown.com");
0015 static const QString testClientId = QStringLiteral("b43c59cd-dd1c-41fd-bb9a-b0a1d5696a93");
0016 static const QString testReturnUri = QStringLiteral("urn:ietf:wg:oauth:2.0:oob");
0017 // static const QString testReturnUriPercent = QUrl::toPercentEncoding(testReturnUri);
0018 static const QString testState = QStringLiteral("joidsiuhq");
0019 static const QString resource = QStringLiteral("https://outlook.office365.com/");
0020 // static const QString resourcePercent = QUrl::toPercentEncoding(resource);
0021 static const QString authUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize");
0022 static const QString tokenUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/token");
0023 
0024 static const QString accessToken1 = QStringLiteral("IERbOTo5NSdtY5HMntWTH1wgrRt98KmbF7nNloIdZ4SSYOU7pziJJakpHy8r6kxQi+7T9w36mWv9IWLrvEwTsA");
0025 static const QString refreshToken1 = QStringLiteral("YW7lJFWcEISynbraq4NiLLke3rOieFdvoJEDxpjCXorJblIGM56OJSu1PZXMCQL5W3KLxS9ydxqLHxRTSdw");
0026 static const QString idToken1 = QStringLiteral("gz7l0chu9xIi1MMgPkpHGQTmo3W7L1rQbmWAxEL5VSKHeqdIJ7E3K7vmMYTl/C1fWihB5XiLjD2GSVQoOzTfCw");
0027 
0028 class UtEwsOAuth : public QObject
0029 {
0030     Q_OBJECT
0031 private Q_SLOTS:
0032     void initialInteractiveSuccessful();
0033     void initialRefreshSuccessful();
0034     void refreshSuccessful();
0035 
0036 private:
0037     static QString formatJsonSorted(const QVariantMap &map);
0038     static int performAuthAction(EwsOAuth &oAuth, int timeout, std::function<bool(EwsOAuth *)> actionFn);
0039     static void setUpAccessFunction(const QString &refreshToken);
0040     static void setUpTokenFunction(const QString &accessToken,
0041                                    const QString &refreshToken,
0042                                    const QString &idToken,
0043                                    quint64 time,
0044                                    int tokenLifetime,
0045                                    int extTokenLifetime,
0046                                    QString &tokenReplyData);
0047     static void dumpEvents(const QStringList &events, const QStringList &expectedEvents);
0048 
0049     void setUpOAuth(EwsOAuth &oAuth, QStringList &events, const QString &password, const QMap<QString, QString> &map);
0050 };
0051 
0052 void UtEwsOAuth::initialInteractiveSuccessful()
0053 {
0054     EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri);
0055 
0056     QVERIFY(Mock::QWebEngineView::instance);
0057     QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance);
0058 
0059     QStringList events;
0060 
0061     setUpOAuth(oAuth, events, QString(), QMap<QString, QString>());
0062 
0063     Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri());
0064     auto time = QDateTime::currentSecsSinceEpoch();
0065 
0066     constexpr unsigned int tokenLifetime = 86399;
0067     constexpr unsigned int extTokenLifetime = 345599;
0068     QString tokenReplyData;
0069 
0070     setUpAccessFunction(refreshToken1);
0071     setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData);
0072     Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState);
0073 
0074     const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) {
0075         oAuth->init();
0076         return true;
0077     });
0078     QVERIFY(initStatus == 1);
0079 
0080     const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) {
0081         return oAuth->authenticate(true);
0082     });
0083     QVERIFY(authStatus == 0);
0084 
0085     const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState);
0086     const QStringList expectedEvents = {
0087         Mock::requestWalletMapString(),
0088         Mock::modifyParamsAuthString(testClientId, testReturnUri, testState),
0089         Mock::authorizeWithBrowserString(authUrlString),
0090         Mock::loadWebPageString(authUrlString),
0091         Mock::interceptRequestString(authUrlString),
0092         Mock::interceptRequestBlockedString(false),
0093         Mock::interceptRequestString(testReturnUri + QStringLiteral("?code=") + QString::fromLatin1(QUrl::toPercentEncoding(refreshToken1))),
0094         Mock::interceptRequestBlockedString(true),
0095         Mock::authorizationCallbackReceivedString(refreshToken1),
0096         Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1),
0097         Mock::networkReplyFinishedString(tokenReplyData),
0098         Mock::replyDataCallbackString(tokenReplyData),
0099         Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource)};
0100     dumpEvents(events, expectedEvents);
0101 
0102     QVERIFY(events == expectedEvents);
0103 }
0104 
0105 void UtEwsOAuth::initialRefreshSuccessful()
0106 {
0107     EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri);
0108 
0109     QVERIFY(Mock::QWebEngineView::instance);
0110     QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance);
0111 
0112     QStringList events;
0113 
0114     QMap<QString, QString> map = {{QStringLiteral("refresh-token"), refreshToken1}};
0115 
0116     setUpOAuth(oAuth, events, QString(), map);
0117 
0118     Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri());
0119     auto time = QDateTime::currentSecsSinceEpoch();
0120 
0121     constexpr unsigned int tokenLifetime = 86399;
0122     constexpr unsigned int extTokenLifetime = 345599;
0123     QString tokenReplyData;
0124 
0125     setUpAccessFunction(refreshToken1);
0126     setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData);
0127     Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState);
0128 
0129     const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) {
0130         oAuth->init();
0131         return true;
0132     });
0133     QVERIFY(initStatus == 1);
0134 
0135     const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) {
0136         return oAuth->authenticate(true);
0137     });
0138     QVERIFY(authStatus == 0);
0139 
0140     const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState);
0141     const QStringList expectedEvents = {Mock::requestWalletMapString(),
0142                                         Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1),
0143                                         Mock::networkReplyFinishedString(tokenReplyData),
0144                                         Mock::replyDataCallbackString(tokenReplyData),
0145                                         Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource)};
0146     dumpEvents(events, expectedEvents);
0147 
0148     QVERIFY(events == expectedEvents);
0149 }
0150 
0151 void UtEwsOAuth::refreshSuccessful()
0152 {
0153     EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri);
0154 
0155     QVERIFY(Mock::QWebEngineView::instance);
0156     QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance);
0157 
0158     QStringList events;
0159 
0160     setUpOAuth(oAuth, events, QString(), QMap<QString, QString>());
0161 
0162     Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri());
0163     auto time = QDateTime::currentSecsSinceEpoch();
0164 
0165     constexpr unsigned int tokenLifetime = 86399;
0166     constexpr unsigned int extTokenLifetime = 345599;
0167     QString tokenReplyData;
0168 
0169     setUpAccessFunction(refreshToken1);
0170     setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData);
0171     Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState);
0172 
0173     const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) {
0174         oAuth->init();
0175         return true;
0176     });
0177     QVERIFY(initStatus == 1);
0178 
0179     const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) {
0180         return oAuth->authenticate(true);
0181     });
0182     QVERIFY(authStatus == 0);
0183 
0184     const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState);
0185     const QStringList expectedEvents = {
0186         Mock::requestWalletMapString(),
0187         Mock::modifyParamsAuthString(testClientId, testReturnUri, testState),
0188         Mock::authorizeWithBrowserString(authUrlString),
0189         Mock::loadWebPageString(authUrlString),
0190         Mock::interceptRequestString(authUrlString),
0191         Mock::interceptRequestBlockedString(false),
0192         Mock::interceptRequestString(testReturnUri + QStringLiteral("?code=") + QString::fromLatin1(QUrl::toPercentEncoding(refreshToken1))),
0193         Mock::interceptRequestBlockedString(true),
0194         Mock::authorizationCallbackReceivedString(refreshToken1),
0195         Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1),
0196         Mock::networkReplyFinishedString(tokenReplyData),
0197         Mock::replyDataCallbackString(tokenReplyData),
0198         Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource)};
0199     dumpEvents(events, expectedEvents);
0200 
0201     QVERIFY(events == expectedEvents);
0202 
0203     events.clear();
0204 
0205     oAuth.notifyRequestAuthFailed();
0206 
0207     const auto reauthStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) {
0208         return oAuth->authenticate(false);
0209     });
0210     QVERIFY(reauthStatus == 0);
0211 
0212     const QStringList expectedEventsRefresh = {
0213         Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1),
0214         Mock::networkReplyFinishedString(tokenReplyData),
0215         Mock::replyDataCallbackString(tokenReplyData),
0216         Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource)};
0217     dumpEvents(events, expectedEvents);
0218 
0219     QVERIFY(events == expectedEventsRefresh);
0220 }
0221 
0222 QString UtEwsOAuth::formatJsonSorted(const QVariantMap &map)
0223 {
0224     QStringList keys = map.keys();
0225     keys.sort();
0226     QStringList elems;
0227     for (const auto &key : std::as_const(keys)) {
0228         QString val = map[key].toString();
0229         val.replace(QLatin1Char('"'), QStringLiteral("\\\""));
0230         elems.append(QStringLiteral("\"%1\":\"%2\"").arg(key, val));
0231     }
0232     return QStringLiteral("{") + elems.join(QLatin1Char(',')) + QStringLiteral("}");
0233 }
0234 
0235 int UtEwsOAuth::performAuthAction(EwsOAuth &oAuth, int timeout, std::function<bool(EwsOAuth *)> actionFn)
0236 {
0237     QEventLoop loop;
0238     int status = -1;
0239     QTimer timer;
0240     connect(&oAuth, &EwsOAuth::authSucceeded, &timer, [&]() {
0241         qDebug() << "succeeded";
0242         loop.exit(0);
0243         status = 0;
0244     });
0245     connect(&oAuth, &EwsOAuth::authFailed, &timer, [&](const QString &msg) {
0246         qDebug() << "failed" << msg;
0247         loop.exit(1);
0248         status = 1;
0249     });
0250     connect(&timer, &QTimer::timeout, &timer, [&]() {
0251         qDebug() << "timeout";
0252         loop.exit(1);
0253         status = 1;
0254     });
0255     timer.setSingleShot(true);
0256     timer.start(timeout);
0257 
0258     if (!actionFn(&oAuth)) {
0259         return -1;
0260     }
0261 
0262     if (status == -1) {
0263         status = loop.exec();
0264     }
0265 
0266     return status;
0267 }
0268 
0269 void UtEwsOAuth::setUpAccessFunction(const QString &refreshToken)
0270 {
0271     Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map) {
0272         map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken);
0273     });
0274 }
0275 
0276 void UtEwsOAuth::setUpTokenFunction(const QString &accessToken,
0277                                     const QString &refreshToken,
0278                                     const QString &idToken,
0279                                     quint64 time,
0280                                     int tokenLifetime,
0281                                     int extTokenLifetime,
0282                                     QString &tokenReplyData)
0283 {
0284     Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction(
0285         [=, &tokenReplyData](QString &data, QMap<Mock::QNetworkRequest::KnownHeaders, QVariant> &headers) {
0286             QVariantMap map;
0287             map[QStringLiteral("token_type")] = QStringLiteral("Bearer");
0288             map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All");
0289             map[QStringLiteral("expires_in")] = QString::number(tokenLifetime);
0290             map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime);
0291             map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime);
0292             map[QStringLiteral("not_before")] = QString::number(time);
0293             map[QStringLiteral("resource")] = resource;
0294             map[QStringLiteral("access_token")] = accessToken;
0295             map[QStringLiteral("refresh_token")] = refreshToken;
0296             map[QStringLiteral("foci")] = QStringLiteral("1");
0297             map[QStringLiteral("id_token")] = idToken;
0298             tokenReplyData = formatJsonSorted(map);
0299             data = tokenReplyData;
0300             headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8");
0301 
0302             return Mock::QNetworkReply::NoError;
0303         });
0304 }
0305 
0306 void UtEwsOAuth::dumpEvents(const QStringList &events, const QStringList &expectedEvents)
0307 {
0308     for (const auto &event : events) {
0309         qDebug() << "Got event:" << event;
0310     }
0311     if (events != expectedEvents) {
0312         for (const auto &event : expectedEvents) {
0313             qDebug() << "Expected event:" << event;
0314         }
0315     }
0316 }
0317 
0318 void UtEwsOAuth::setUpOAuth(EwsOAuth &oAuth, QStringList &events, const QString &password, const QMap<QString, QString> &map)
0319 {
0320     connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&events](const QString &event) {
0321         events.append(event);
0322     });
0323     connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, [&events](const QString &event) {
0324         events.append(event);
0325     });
0326     connect(&oAuth, &EwsOAuth::requestWalletPassword, this, [&oAuth, &events, password](bool) {
0327         events.append(QStringLiteral("RequestWalletPassword"));
0328         oAuth.walletPasswordRequestFinished(password);
0329     });
0330     connect(&oAuth, &EwsOAuth::requestWalletMap, this, [&oAuth, &events, map]() {
0331         events.append(QStringLiteral("RequestWalletMap"));
0332         oAuth.walletMapRequestFinished(map);
0333     });
0334 }
0335 
0336 QTEST_MAIN(UtEwsOAuth)
0337 
0338 #include "ewsoauth_ut.moc"