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"