1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QtCore> |
30 | #include <QtTest> |
31 | #include <QtNetwork> |
32 | #include <QHostInfo> |
33 | |
34 | #include <QtNetworkAuth/qoauth1.h> |
35 | #include <QtNetworkAuth/qoauth1signature.h> |
36 | |
37 | #include <private/qoauth1_p.h> |
38 | |
39 | #include "webserver.h" |
40 | |
41 | Q_DECLARE_METATYPE(QNetworkAccessManager::Operation) |
42 | Q_DECLARE_METATYPE(QAbstractOAuth::Error) |
43 | |
44 | // TODO: Test PUT and DELETE operations. |
45 | // TODO: Write tests to test errors. |
46 | // TODO: Remove common event loop |
47 | |
48 | typedef QSharedPointer<QNetworkReply> QNetworkReplyPtr; |
49 | |
50 | class tst_OAuth1 : public QObject |
51 | { |
52 | Q_OBJECT |
53 | |
54 | using StringPair = QPair<QString, QString>; |
55 | |
56 | QEventLoop *loop = nullptr; |
57 | enum RunSimpleRequestReturn { Timeout = 0, Success, Failure }; |
58 | int returnCode; |
59 | |
60 | using QObject::connect; |
61 | static bool connect(const QNetworkReplyPtr &ptr, |
62 | const char *signal, |
63 | const QObject *receiver, |
64 | const char *slot, |
65 | Qt::ConnectionType ct = Qt::AutoConnection) |
66 | { |
67 | return connect(sender: ptr.data(), signal, receiver, member: slot, ct); |
68 | } |
69 | bool connect(const QNetworkReplyPtr &ptr, |
70 | const char *signal, |
71 | const char *slot, |
72 | Qt::ConnectionType ct = Qt::AutoConnection) |
73 | { |
74 | return connect(asender: ptr.data(), asignal: signal, amember: slot, atype: ct); |
75 | } |
76 | |
77 | public: |
78 | int waitForFinish(QNetworkReplyPtr &reply); |
79 | void fillParameters(QVariantMap *parameters, const QUrlQuery &query); |
80 | |
81 | template<class Type> |
82 | struct PropertyTester |
83 | { |
84 | typedef Type InnerType; |
85 | typedef void(QOAuth1::*ConstSignalType)(const Type &); |
86 | typedef void(QOAuth1::*SignalType)(Type); |
87 | typedef QVector<std::function<void(Type *, QOAuth1 *object)>> SetterFunctions; |
88 | |
89 | private: |
90 | // Each entry in setters should set its first parameter to an expected value |
91 | // and act on its second, a QOAuth1 object, to trigger signal; this |
92 | // function shall check that signal is passed the value the setter previously |
93 | // told us to expect. |
94 | template<class SignalType> |
95 | static void runImpl(SignalType signal, const SetterFunctions &setters) |
96 | { |
97 | QOAuth1 obj; |
98 | Type expectedValue; |
99 | QSignalSpy spy(&obj, signal); |
100 | connect(&obj, signal, [&](const Type &value) { |
101 | QCOMPARE(expectedValue, value); |
102 | }); |
103 | for (const auto &setter : setters) { |
104 | const auto previous = expectedValue; |
105 | setter(&expectedValue, &obj); |
106 | QVERIFY(previous != expectedValue); // To check if the value was modified |
107 | } |
108 | QCOMPARE(spy.count(), setters.size()); // The signal should be emitted |
109 | } |
110 | |
111 | public: |
112 | |
113 | static void run(ConstSignalType signal, const SetterFunctions &setters) |
114 | { |
115 | runImpl(signal, setters); |
116 | } |
117 | |
118 | static void run(SignalType signal, const SetterFunctions &setters) |
119 | { |
120 | runImpl(signal, setters); |
121 | } |
122 | }; |
123 | |
124 | QVariantMap parseAuthorizationString(const QString &string) |
125 | { |
126 | const QString prefix = QStringLiteral("OAuth " ); |
127 | QVariantMap ret; |
128 | Q_ASSERT(string.startsWith(prefix)); |
129 | QRegularExpression rx("(?<key>.[^=]*)=\"(?<value>.[^\"]*)\",?" ); |
130 | auto globalMatch = rx.globalMatch(subject: string, offset: prefix.size()); |
131 | while (globalMatch.hasNext()) { |
132 | const auto match = globalMatch.next(); |
133 | auto key = match.captured(name: "key" ); |
134 | QString value = match.captured(name: "value" ); |
135 | value = QString::fromUtf8(str: QByteArray::fromPercentEncoding(pctEncoded: value.toUtf8())); |
136 | ret.insert(key, value); |
137 | } |
138 | return ret; |
139 | } |
140 | |
141 | public Q_SLOTS: |
142 | void finished(); |
143 | void gotError(); |
144 | |
145 | private Q_SLOTS: |
146 | void clientIdentifierSignal(); |
147 | void clientSharedSecretSignal(); |
148 | void tokenSignal(); |
149 | void tokenSecretSignal(); |
150 | void temporaryCredentialsUrlSignal(); |
151 | void temporaryTokenCredentialsUrlSignal(); |
152 | void tokenCredentialsUrlSignal(); |
153 | void signatureMethodSignal(); |
154 | |
155 | void getToken_data(); |
156 | void getToken(); |
157 | |
158 | void prepareRequestSignature_data(); |
159 | void prepareRequestSignature(); |
160 | |
161 | void grant_data(); |
162 | void grant(); |
163 | |
164 | void authenticatedCalls_data(); |
165 | void authenticatedCalls(); |
166 | |
167 | void prepareRequestCalls_data(); |
168 | void prepareRequestCalls(); |
169 | |
170 | void secondTemporaryToken(); |
171 | }; |
172 | |
173 | const auto oauthVersion = QStringLiteral("oauth_version" ); |
174 | const auto oauthConsumerKey = QStringLiteral("oauth_consumer_key" ); |
175 | const auto oauthNonce = QStringLiteral("oauth_nonce" ); |
176 | const auto oauthSignatureMethod = QStringLiteral("oauth_signature_method" ); |
177 | const auto oauthTimestamp = QStringLiteral("oauth_timestamp" ); |
178 | const auto oauthToken = QStringLiteral("oauth_token" ); |
179 | const auto oauthSignature = QStringLiteral("oauth_signature" ); |
180 | |
181 | bool hostReachable(const QLatin1String &host) |
182 | { |
183 | // check host exists |
184 | QHostInfo hostInfo = QHostInfo::fromName(name: host); |
185 | if (hostInfo.error() != QHostInfo::NoError) |
186 | return false; |
187 | |
188 | // try to connect to host |
189 | QTcpSocket socket; |
190 | socket.connectToHost(hostName: host, port: 80); |
191 | if (!socket.waitForConnected(msecs: 1000)) |
192 | return false; |
193 | |
194 | return true; |
195 | } |
196 | |
197 | int tst_OAuth1::waitForFinish(QNetworkReplyPtr &reply) |
198 | { |
199 | int count = 0; |
200 | |
201 | connect(ptr: reply, SIGNAL(finished()), SLOT(finished())); |
202 | connect(ptr: reply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(gotError())); |
203 | returnCode = Success; |
204 | loop = new QEventLoop; |
205 | QSignalSpy spy(reply.data(), SIGNAL(downloadProgress(qint64,qint64))); |
206 | while (!reply->isFinished()) { |
207 | QTimer::singleShot(msec: 5000, receiver: loop, SLOT(quit())); |
208 | if (loop->exec() == Timeout && count == spy.count() && !reply->isFinished()) { |
209 | returnCode = Timeout; |
210 | break; |
211 | } |
212 | count = spy.count(); |
213 | } |
214 | delete loop; |
215 | loop = nullptr; |
216 | |
217 | return returnCode; |
218 | } |
219 | |
220 | void tst_OAuth1::fillParameters(QVariantMap *parameters, const QUrlQuery &query) |
221 | { |
222 | const auto list = query.queryItems(); |
223 | for (auto it = list.begin(), end = list.end(); it != end; ++it) |
224 | parameters->insert(key: it->first, value: it->second); |
225 | } |
226 | |
227 | void tst_OAuth1::finished() |
228 | { |
229 | if (loop) |
230 | loop->exit(returnCode: returnCode = Success); |
231 | } |
232 | |
233 | void tst_OAuth1::gotError() |
234 | { |
235 | if (loop) |
236 | loop->exit(returnCode: returnCode = Failure); |
237 | disconnect(sender: QObject::sender(), SIGNAL(finished()), receiver: this, member: nullptr); |
238 | } |
239 | |
240 | void tst_OAuth1::clientIdentifierSignal() |
241 | { |
242 | using PropertyTester = PropertyTester<QString>; |
243 | PropertyTester::SetterFunctions setters { |
244 | [](QString *expectedValue, QOAuth1 *object) { |
245 | *expectedValue = "setClientIdentifier" ; |
246 | object->setClientIdentifier(*expectedValue); |
247 | }, |
248 | [](QString *expectedValue, QOAuth1 *object) { |
249 | *expectedValue = "setClientCredentials" ; |
250 | object->setClientCredentials(qMakePair(x: *expectedValue, y: QString())); |
251 | } |
252 | }; |
253 | PropertyTester::run(signal: &QOAuth1::clientIdentifierChanged, setters); |
254 | } |
255 | |
256 | void tst_OAuth1::clientSharedSecretSignal() |
257 | { |
258 | using PropertyTester = PropertyTester<QString>; |
259 | PropertyTester::SetterFunctions setters { |
260 | [](QString *expectedValue, QOAuth1 *object) { |
261 | *expectedValue = "setClientSharedSecret" ; |
262 | object->setClientSharedSecret(*expectedValue); |
263 | }, |
264 | [](QString *expectedValue, QOAuth1 *object) { |
265 | *expectedValue = "setClientCredentials" ; |
266 | object->setClientCredentials(qMakePair(x: QString(), y: *expectedValue)); |
267 | } |
268 | }; |
269 | PropertyTester::run(signal: &QOAuth1::clientSharedSecretChanged, setters); |
270 | } |
271 | |
272 | void tst_OAuth1::tokenSignal() |
273 | { |
274 | using PropertyTester = PropertyTester<QString>; |
275 | PropertyTester::SetterFunctions setters { |
276 | [](QString *expectedValue, QOAuth1 *object) { |
277 | *expectedValue = "setToken" ; |
278 | object->setToken(*expectedValue); |
279 | }, |
280 | [](QString *expectedValue, QOAuth1 *object) { |
281 | *expectedValue = "setTokenCredentials" ; |
282 | object->setTokenCredentials(qMakePair(x: *expectedValue, y: QString())); |
283 | } |
284 | }; |
285 | PropertyTester::run(signal: &QOAuth1::tokenChanged, setters); |
286 | } |
287 | |
288 | void tst_OAuth1::tokenSecretSignal() |
289 | { |
290 | using PropertyTester = PropertyTester<QString>; |
291 | PropertyTester::SetterFunctions setters { |
292 | [](QString *expectedValue, QOAuth1 *object) { |
293 | *expectedValue = "setTokenSecret" ; |
294 | object->setTokenSecret(*expectedValue); |
295 | }, |
296 | [](QString *expectedValue, QOAuth1 *object) { |
297 | *expectedValue = "setTokenCredentials" ; |
298 | object->setTokenCredentials(qMakePair(x: QString(), y: *expectedValue)); |
299 | } |
300 | }; |
301 | PropertyTester::run(signal: &QOAuth1::tokenSecretChanged, setters); |
302 | } |
303 | |
304 | void tst_OAuth1::temporaryCredentialsUrlSignal() |
305 | { |
306 | using PropertyTester = PropertyTester<QUrl>; |
307 | PropertyTester::SetterFunctions setters { |
308 | [](QUrl *expectedValue, QOAuth1 *object) { |
309 | *expectedValue = QUrl("http://example.net/" ); |
310 | object->setTemporaryCredentialsUrl(*expectedValue); |
311 | } |
312 | }; |
313 | PropertyTester::run(signal: &QOAuth1::temporaryCredentialsUrlChanged, setters); |
314 | } |
315 | |
316 | void tst_OAuth1::temporaryTokenCredentialsUrlSignal() |
317 | { |
318 | using PropertyTester = PropertyTester<QUrl>; |
319 | PropertyTester::SetterFunctions setters { |
320 | [](QUrl *expectedValue, QOAuth1 *object) { |
321 | *expectedValue = QUrl("http://example.net/" ); |
322 | object->setTemporaryCredentialsUrl(*expectedValue); |
323 | } |
324 | }; |
325 | PropertyTester::run(signal: &QOAuth1::temporaryCredentialsUrlChanged, setters); |
326 | } |
327 | |
328 | void tst_OAuth1::tokenCredentialsUrlSignal() |
329 | { |
330 | using PropertyTester = PropertyTester<QUrl>; |
331 | PropertyTester::SetterFunctions setters { |
332 | [](QUrl *expectedValue, QOAuth1 *object) { |
333 | *expectedValue = QUrl("http://example.net/" ); |
334 | object->setTokenCredentialsUrl(*expectedValue); |
335 | } |
336 | }; |
337 | PropertyTester::run(signal: &QOAuth1::tokenCredentialsUrlChanged, setters); |
338 | } |
339 | |
340 | void tst_OAuth1::signatureMethodSignal() |
341 | { |
342 | using PropertyTester = PropertyTester<QOAuth1::SignatureMethod>; |
343 | PropertyTester::SetterFunctions setters { |
344 | [](PropertyTester::InnerType *expectedValue, QOAuth1 *object) { |
345 | QVERIFY(object->signatureMethod() != QOAuth1::SignatureMethod::PlainText); |
346 | *expectedValue = QOAuth1::SignatureMethod::PlainText; |
347 | object->setSignatureMethod(*expectedValue); |
348 | } |
349 | }; |
350 | PropertyTester::run(signal: &QOAuth1::signatureMethodChanged, setters); |
351 | } |
352 | |
353 | void tst_OAuth1::getToken_data() |
354 | { |
355 | QTest::addColumn<StringPair>(name: "clientCredentials" ); |
356 | QTest::addColumn<StringPair>(name: "token" ); |
357 | QTest::addColumn<StringPair>(name: "expectedToken" ); |
358 | QTest::addColumn<QOAuth1::SignatureMethod>(name: "signatureMethod" ); |
359 | QTest::addColumn<QNetworkAccessManager::Operation>(name: "requestType" ); |
360 | |
361 | const StringPair emptyCredentials; |
362 | QTest::newRow(dataTag: "temporary_get_plainText" ) |
363 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
364 | << emptyCredentials |
365 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
366 | << QOAuth1::SignatureMethod::PlainText |
367 | << QNetworkAccessManager::GetOperation; |
368 | |
369 | QTest::newRow(dataTag: "temporary_get_hmacSha1" ) |
370 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
371 | << emptyCredentials |
372 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
373 | << QOAuth1::SignatureMethod::Hmac_Sha1 |
374 | << QNetworkAccessManager::GetOperation; |
375 | |
376 | QTest::newRow(dataTag: "temporary_post_plainText" ) |
377 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
378 | << emptyCredentials |
379 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
380 | << QOAuth1::SignatureMethod::PlainText |
381 | << QNetworkAccessManager::PostOperation; |
382 | |
383 | QTest::newRow(dataTag: "temporary_post_hmacSha1" ) |
384 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
385 | << emptyCredentials |
386 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
387 | << QOAuth1::SignatureMethod::Hmac_Sha1 |
388 | << QNetworkAccessManager::PostOperation; |
389 | |
390 | QTest::newRow(dataTag: "token_get_plainText" ) |
391 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
392 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
393 | << qMakePair(QStringLiteral("accesskey" ), QStringLiteral("accesssecret" )) |
394 | << QOAuth1::SignatureMethod::PlainText |
395 | << QNetworkAccessManager::GetOperation; |
396 | |
397 | QTest::newRow(dataTag: "token_get_hmacSha1" ) |
398 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
399 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
400 | << qMakePair(QStringLiteral("accesskey" ), QStringLiteral("accesssecret" )) |
401 | << QOAuth1::SignatureMethod::Hmac_Sha1 |
402 | << QNetworkAccessManager::GetOperation; |
403 | |
404 | QTest::newRow(dataTag: "token_post_plainText" ) |
405 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
406 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
407 | << qMakePair(QStringLiteral("accesskey" ), QStringLiteral("accesssecret" )) |
408 | << QOAuth1::SignatureMethod::PlainText |
409 | << QNetworkAccessManager::PostOperation; |
410 | |
411 | QTest::newRow(dataTag: "token_post_hmacSha1" ) |
412 | << qMakePair(QStringLiteral("key" ), QStringLiteral("secret" )) |
413 | << qMakePair(QStringLiteral("requestkey" ), QStringLiteral("requestsecret" )) |
414 | << qMakePair(QStringLiteral("accesskey" ), QStringLiteral("accesssecret" )) |
415 | << QOAuth1::SignatureMethod::Hmac_Sha1 |
416 | << QNetworkAccessManager::PostOperation; |
417 | } |
418 | |
419 | void tst_OAuth1::getToken() |
420 | { |
421 | QFETCH(StringPair, clientCredentials); |
422 | QFETCH(StringPair, token); |
423 | QFETCH(StringPair, expectedToken); |
424 | QFETCH(QOAuth1::SignatureMethod, signatureMethod); |
425 | QFETCH(QNetworkAccessManager::Operation, requestType); |
426 | |
427 | StringPair tokenReceived; |
428 | QNetworkAccessManager networkAccessManager; |
429 | QNetworkReplyPtr reply; |
430 | QVariantMap ; |
431 | |
432 | WebServer webServer([&](const WebServer::HttpRequest &request, QTcpSocket *socket) { |
433 | oauthHeaders = parseAuthorizationString(string: request.headers["Authorization" ]); |
434 | const QString format = "oauth_token=%1&oauth_token_secret=%2" ; |
435 | const QByteArray text = format.arg(args&: expectedToken.first, args&: expectedToken.second).toUtf8(); |
436 | const QByteArray replyMessage { |
437 | "HTTP/1.0 200 OK\r\n" |
438 | "Content-Type: application/x-www-form-urlencoded; charset=\"utf-8\"\r\n" |
439 | "Content-Length: " + QByteArray::number(text.size()) + "\r\n\r\n" |
440 | + text |
441 | }; |
442 | socket->write(data: replyMessage); |
443 | }); |
444 | |
445 | struct OAuth1 : QOAuth1 |
446 | { |
447 | OAuth1(QNetworkAccessManager *manager) : QOAuth1(manager) {} |
448 | using QOAuth1::requestTokenCredentials; |
449 | } o1(&networkAccessManager); |
450 | const auto url = webServer.url(QStringLiteral("token" )); |
451 | |
452 | o1.setSignatureMethod(signatureMethod); |
453 | o1.setClientCredentials(clientIdentifier: clientCredentials.first, clientSharedSecret: clientCredentials.second); |
454 | o1.setTokenCredentials(token); |
455 | o1.setTemporaryCredentialsUrl(url); |
456 | QVariantMap parameters {{ "c2&a3" , "c2=a3" }}; |
457 | reply.reset(t: o1.requestTokenCredentials(operation: requestType, url, temporaryToken: token, parameters)); |
458 | QVERIFY(!reply.isNull()); |
459 | connect(sender: &o1, signal: &QOAuth1::tokenChanged, slot: [&tokenReceived](const QString &token){ |
460 | tokenReceived.first = token; |
461 | }); |
462 | connect(sender: &o1, signal: &QOAuth1::tokenSecretChanged, slot: [&tokenReceived](const QString &tokenSecret) { |
463 | tokenReceived.second = tokenSecret; |
464 | }); |
465 | QVERIFY(waitForFinish(reply) == Success); |
466 | QCOMPARE(tokenReceived, expectedToken); |
467 | QCOMPARE(oauthHeaders["oauth_consumer_key" ], clientCredentials.first); |
468 | QCOMPARE(oauthHeaders["oauth_version" ], "1.0" ); |
469 | QString expectedSignature; |
470 | { |
471 | QVariantMap = oauthHeaders; |
472 | modifiedHeaders.insert(map: parameters); |
473 | modifiedHeaders.remove(key: "oauth_signature" ); |
474 | QOAuth1Signature signature(url, |
475 | clientCredentials.second, |
476 | token.second, |
477 | static_cast<QOAuth1Signature::HttpRequestMethod>(requestType), |
478 | modifiedHeaders); |
479 | switch (signatureMethod) { |
480 | case QOAuth1::SignatureMethod::PlainText: |
481 | expectedSignature = signature.plainText(); |
482 | break; |
483 | case QOAuth1::SignatureMethod::Hmac_Sha1: |
484 | expectedSignature = signature.hmacSha1().toBase64(); |
485 | break; |
486 | case QOAuth1::SignatureMethod::Rsa_Sha1: |
487 | expectedSignature = signature.rsaSha1(); |
488 | break; |
489 | } |
490 | } |
491 | QCOMPARE(oauthHeaders["oauth_signature" ], expectedSignature); |
492 | } |
493 | |
494 | void tst_OAuth1::prepareRequestSignature_data() |
495 | { |
496 | QTest::addColumn<QString>(name: "consumerKey" ); |
497 | QTest::addColumn<QString>(name: "consumerSecret" ); |
498 | QTest::addColumn<QString>(name: "accessKey" ); |
499 | QTest::addColumn<QString>(name: "accessKeySecret" ); |
500 | QTest::addColumn<QNetworkRequest>(name: "request" ); |
501 | QTest::addColumn<QByteArray>(name: "operation" ); |
502 | QTest::addColumn<QByteArray>(name: "body" ); |
503 | QTest::addColumn<QVariantMap>(name: "extraParams" ); |
504 | |
505 | QTest::newRow(dataTag: "get_simple" ) |
506 | << "key" |
507 | << "secret" |
508 | << "accesskey" |
509 | << "accesssecret" |
510 | << QNetworkRequest(QUrl("http://term.ie/oauth/example/echo_api.php" )) |
511 | << QByteArray("GET" ) |
512 | << QByteArray() |
513 | << QVariantMap(); |
514 | |
515 | QTest::newRow(dataTag: "get_params" ) |
516 | << "key" |
517 | << "secret" |
518 | << "accesskey" |
519 | << "accesssecret" |
520 | << QNetworkRequest(QUrl("http://term.ie/oauth/example/echo_api.php?" |
521 | "first=first&second=second" )) |
522 | << QByteArray("GET" ) |
523 | << QByteArray() |
524 | << QVariantMap(); |
525 | |
526 | QNetworkRequest postRequest(QUrl("http://term.ie/oauth/example/echo_api.php" )); |
527 | postRequest.setHeader(header: QNetworkRequest::ContentTypeHeader, |
528 | value: QByteArray("application/x-www-form-urlencoded" )); |
529 | QTest::newRow(dataTag: "post_params" ) |
530 | << "key" |
531 | << "secret" |
532 | << "accesskey" |
533 | << "accesssecret" |
534 | << postRequest |
535 | << QByteArray("POST" ) |
536 | << QByteArray("first=first&second=second" ) |
537 | << QVariantMap({ |
538 | {"first" , "first" }, |
539 | {"second" , "second" } |
540 | }); |
541 | |
542 | QTest::newRow(dataTag: "patch_param" ) |
543 | << "key" |
544 | << "secret" |
545 | << "accesskey" |
546 | << "accesssecret" |
547 | << QNetworkRequest(QUrl("http://term.ie/oauth/example/echo_api.php?" |
548 | "first=first&second=second" )) |
549 | << QByteArray("PATCH" ) |
550 | << QByteArray() |
551 | << QVariantMap(); |
552 | } |
553 | |
554 | void tst_OAuth1::prepareRequestSignature() |
555 | { |
556 | QFETCH(QString, consumerKey); |
557 | QFETCH(QString, consumerSecret); |
558 | QFETCH(QString, accessKey); |
559 | QFETCH(QString, accessKeySecret); |
560 | QFETCH(QNetworkRequest, request); |
561 | QFETCH(QByteArray, operation); |
562 | QFETCH(QByteArray, body); |
563 | QFETCH(QVariantMap, ); |
564 | |
565 | QOAuth1 o1; |
566 | o1.setClientCredentials(clientIdentifier: consumerKey, clientSharedSecret: consumerSecret); |
567 | o1.setTokenCredentials(token: accessKey, tokenSecret: accessKeySecret); |
568 | |
569 | o1.prepareRequest(request: &request, verb: operation, body); |
570 | |
571 | // extract oauth parameters from the headers |
572 | QVariantMap authArgs; |
573 | const auto = request.rawHeader(headerName: "Authorization" ); |
574 | QCOMPARE(authHeader.mid(0, 6), "OAuth " ); |
575 | const auto values = authHeader.mid(index: 6).split(sep: ','); |
576 | for (const auto &pair : values) { |
577 | const auto argPair = pair.split(sep: '='); |
578 | QCOMPARE(argPair.size(), 2); |
579 | QCOMPARE(argPair[1].front(), '\"'); |
580 | QCOMPARE(argPair[1].back(), '\"'); |
581 | authArgs.insert(key: argPair[0], value: argPair[1].mid(index: 1, len: argPair[1].size() - 2)); |
582 | } |
583 | |
584 | //compare known parameters |
585 | QCOMPARE(authArgs.value(oauthConsumerKey).toByteArray(), consumerKey); |
586 | QCOMPARE(authArgs.value(oauthToken).toByteArray(), accessKey); |
587 | QCOMPARE(authArgs.value(oauthSignatureMethod).toByteArray(), QByteArray("HMAC-SHA1" )); |
588 | QCOMPARE(authArgs.value(oauthVersion).toByteArray(), QByteArray("1.0" )); |
589 | QVERIFY(authArgs.contains(oauthNonce)); |
590 | QVERIFY(authArgs.contains(oauthTimestamp)); |
591 | QVERIFY(authArgs.contains(oauthSignature)); |
592 | |
593 | // verify the signature |
594 | const auto sigString = QUrl::fromPercentEncoding(authArgs.take(key: oauthSignature) |
595 | .toByteArray()).toUtf8(); |
596 | |
597 | authArgs.insert(map: extraParams); |
598 | QOAuth1Signature signature(request.url(), |
599 | consumerSecret, |
600 | accessKeySecret, |
601 | QOAuth1Signature::HttpRequestMethod::Custom, |
602 | authArgs); |
603 | signature.setCustomMethodString(operation); |
604 | const auto signatureData = signature.hmacSha1(); |
605 | QCOMPARE(signatureData.toBase64(), sigString); |
606 | } |
607 | |
608 | void tst_OAuth1::grant_data() |
609 | { |
610 | QTest::addColumn<QString>(name: "consumerKey" ); |
611 | QTest::addColumn<QString>(name: "consumerSecret" ); |
612 | QTest::addColumn<QString>(name: "requestToken" ); |
613 | QTest::addColumn<QString>(name: "requestTokenSecret" ); |
614 | QTest::addColumn<QString>(name: "accessToken" ); |
615 | QTest::addColumn<QString>(name: "accessTokenSecret" ); |
616 | QTest::addColumn<QUrl>(name: "requestTokenUrl" ); |
617 | QTest::addColumn<QUrl>(name: "accessTokenUrl" ); |
618 | QTest::addColumn<QUrl>(name: "authenticatedCallUrl" ); |
619 | QTest::addColumn<QNetworkAccessManager::Operation>(name: "requestType" ); |
620 | |
621 | if (hostReachable(host: QLatin1String("term.ie" ))) { |
622 | QTest::newRow(dataTag: "term.ie_get" ) << "key" |
623 | << "secret" |
624 | << "requestkey" |
625 | << "requestsecret" |
626 | << "accesskey" |
627 | << "accesssecret" |
628 | << QUrl("http://term.ie/oauth/example/request_token.php" ) |
629 | << QUrl("http://term.ie/oauth/example/access_token.php" ) |
630 | << QUrl("http://term.ie/oauth/example/echo_api.php" ) |
631 | << QNetworkAccessManager::GetOperation; |
632 | QTest::newRow(dataTag: "term.ie_post" ) << "key" |
633 | << "secret" |
634 | << "requestkey" |
635 | << "requestsecret" |
636 | << "accesskey" |
637 | << "accesssecret" |
638 | << QUrl("http://term.ie/oauth/example/request_token.php" ) |
639 | << QUrl("http://term.ie/oauth/example/access_token.php" ) |
640 | << QUrl("http://term.ie/oauth/example/echo_api.php" ) |
641 | << QNetworkAccessManager::PostOperation; |
642 | } else { |
643 | QSKIP("Skipping test due to unreachable term.ie host" ); |
644 | } |
645 | } |
646 | |
647 | void tst_OAuth1::grant() |
648 | { |
649 | QFETCH(QString, consumerKey); |
650 | QFETCH(QString, consumerSecret); |
651 | QFETCH(QString, requestToken); |
652 | QFETCH(QString, requestTokenSecret); |
653 | QFETCH(QString, accessToken); |
654 | QFETCH(QString, accessTokenSecret); |
655 | QFETCH(QUrl, requestTokenUrl); |
656 | QFETCH(QUrl, accessTokenUrl); |
657 | |
658 | bool tokenReceived = false; |
659 | QNetworkAccessManager networkAccessManager; |
660 | |
661 | QOAuth1 o1(&networkAccessManager); |
662 | |
663 | { |
664 | QSignalSpy clientIdentifierSpy(&o1, &QOAuth1::clientIdentifierChanged); |
665 | QSignalSpy clientSharedSecretSpy(&o1, &QOAuth1::clientSharedSecretChanged); |
666 | o1.setClientCredentials(clientIdentifier: consumerKey, clientSharedSecret: consumerSecret); |
667 | QCOMPARE(clientIdentifierSpy.count(), 1); |
668 | QCOMPARE(clientSharedSecretSpy.count(), 1); |
669 | } |
670 | { |
671 | QSignalSpy spy(&o1, &QOAuth1::temporaryCredentialsUrlChanged); |
672 | o1.setTemporaryCredentialsUrl(requestTokenUrl); |
673 | QCOMPARE(spy.count(), 1); |
674 | } |
675 | { |
676 | QSignalSpy spy(&o1, &QOAuth1::tokenCredentialsUrlChanged); |
677 | o1.setTokenCredentialsUrl(accessTokenUrl); |
678 | QCOMPARE(spy.count(), 1); |
679 | } |
680 | connect(sender: &o1, signal: &QAbstractOAuth::statusChanged, slot: [&](QAbstractOAuth::Status status) { |
681 | if (status == QAbstractOAuth::Status::TemporaryCredentialsReceived) { |
682 | if (!requestToken.isEmpty()) |
683 | QCOMPARE(requestToken, o1.tokenCredentials().first); |
684 | if (!requestTokenSecret.isEmpty()) |
685 | QCOMPARE(requestTokenSecret, o1.tokenCredentials().second); |
686 | tokenReceived = true; |
687 | } else if (status == QAbstractOAuth::Status::Granted) { |
688 | if (!accessToken.isEmpty()) |
689 | QCOMPARE(accessToken, o1.tokenCredentials().first); |
690 | if (!accessTokenSecret.isEmpty()) |
691 | QCOMPARE(accessTokenSecret, o1.tokenCredentials().second); |
692 | tokenReceived = true; |
693 | } |
694 | }); |
695 | |
696 | QEventLoop eventLoop; |
697 | |
698 | QSignalSpy grantSignalSpy(&o1, &QOAuth1::granted); |
699 | QTimer::singleShot(interval: 10000, receiver: &eventLoop, slot: &QEventLoop::quit); |
700 | connect(sender: &o1, signal: &QOAuth1::granted, receiver: &eventLoop, slot: &QEventLoop::quit); |
701 | o1.grant(); |
702 | eventLoop.exec(); |
703 | QVERIFY(tokenReceived); |
704 | QCOMPARE(grantSignalSpy.count(), 1); |
705 | QCOMPARE(o1.status(), QAbstractOAuth::Status::Granted); |
706 | } |
707 | |
708 | void tst_OAuth1::authenticatedCalls_data() |
709 | { |
710 | QTest::addColumn<QString>(name: "consumerKey" ); |
711 | QTest::addColumn<QString>(name: "consumerSecret" ); |
712 | QTest::addColumn<QString>(name: "accessKey" ); |
713 | QTest::addColumn<QString>(name: "accessKeySecret" ); |
714 | QTest::addColumn<QUrl>(name: "url" ); |
715 | QTest::addColumn<QVariantMap>(name: "parameters" ); |
716 | QTest::addColumn<QNetworkAccessManager::Operation>(name: "operation" ); |
717 | |
718 | const QVariantMap parameters { { QStringLiteral("first" ), QStringLiteral("first" ) }, |
719 | { QStringLiteral("second" ), QStringLiteral("second" ) }, |
720 | { QStringLiteral("third" ), QStringLiteral("third" ) }, |
721 | { QStringLiteral("c2&a3" ), QStringLiteral("2=%$&@q" ) } |
722 | }; |
723 | |
724 | if (hostReachable(host: QLatin1String("term.ie" ))) { |
725 | QTest::newRow(dataTag: "term.ie_get" ) << "key" |
726 | << "secret" |
727 | << "accesskey" |
728 | << "accesssecret" |
729 | << QUrl("http://term.ie/oauth/example/echo_api.php" ) |
730 | << parameters |
731 | << QNetworkAccessManager::GetOperation; |
732 | QTest::newRow(dataTag: "term.ie_post" ) << "key" |
733 | << "secret" |
734 | << "accesskey" |
735 | << "accesssecret" |
736 | << QUrl("http://term.ie/oauth/example/echo_api.php" ) |
737 | << parameters |
738 | << QNetworkAccessManager::PostOperation; |
739 | QTest::newRow(dataTag: "term.ie_percent_encoded_query" ) |
740 | << "key" |
741 | << "secret" |
742 | << "accesskey" |
743 | << "accesssecret" |
744 | << QUrl("http://term.ie/oauth/example/echo_api.php?key=%40value+1%2B2=3" ) |
745 | << parameters |
746 | << QNetworkAccessManager::GetOperation; |
747 | } else { |
748 | QSKIP("Skipping test due to unreachable term.ie host" ); |
749 | } |
750 | } |
751 | |
752 | void tst_OAuth1::authenticatedCalls() |
753 | { |
754 | QFETCH(QString, consumerKey); |
755 | QFETCH(QString, consumerSecret); |
756 | QFETCH(QString, accessKey); |
757 | QFETCH(QString, accessKeySecret); |
758 | QFETCH(QUrl, url); |
759 | QFETCH(QVariantMap, parameters); |
760 | QFETCH(QNetworkAccessManager::Operation, operation); |
761 | |
762 | QNetworkAccessManager networkAccessManager; |
763 | QNetworkReplyPtr reply; |
764 | QString receivedData; |
765 | QString ; |
766 | { |
767 | if (url.hasQuery()) { |
768 | parametersString = url.query(QUrl::FullyDecoded); |
769 | if (!parameters.empty()) |
770 | parametersString.append(c: QLatin1Char('&')); |
771 | } |
772 | bool first = true; |
773 | for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it) { |
774 | if (first) |
775 | first = false; |
776 | else |
777 | parametersString += QLatin1Char('&'); |
778 | parametersString += it.key() + QLatin1Char('=') + it.value().toString(); |
779 | } |
780 | } |
781 | |
782 | QOAuth1 o1(&networkAccessManager); |
783 | o1.setClientCredentials(clientIdentifier: consumerKey, clientSharedSecret: consumerSecret); |
784 | o1.setTokenCredentials(token: accessKey, tokenSecret: accessKeySecret); |
785 | if (operation == QNetworkAccessManager::GetOperation) |
786 | reply.reset(t: o1.get(url, parameters)); |
787 | else if (operation == QNetworkAccessManager::PostOperation) |
788 | reply.reset(t: o1.post(url, parameters)); |
789 | QVERIFY(!reply.isNull()); |
790 | QVERIFY(!reply->isFinished()); |
791 | |
792 | connect(sender: &networkAccessManager, signal: &QNetworkAccessManager::finished, |
793 | slot: [&](QNetworkReply *reply) { |
794 | QByteArray data = reply->readAll(); |
795 | QUrlQuery query(QString::fromUtf8(str: data)); |
796 | receivedData = query.toString(encoding: QUrl::FullyDecoded); |
797 | }); |
798 | QVERIFY(waitForFinish(reply) == Success); |
799 | QCOMPARE(receivedData, parametersString); |
800 | reply.clear(); |
801 | } |
802 | |
803 | void tst_OAuth1::prepareRequestCalls_data() |
804 | { |
805 | QTest::addColumn<QString>(name: "consumerKey" ); |
806 | QTest::addColumn<QString>(name: "consumerSecret" ); |
807 | QTest::addColumn<QString>(name: "accessKey" ); |
808 | QTest::addColumn<QString>(name: "accessKeySecret" ); |
809 | QTest::addColumn<QUrl>(name: "url" ); |
810 | QTest::addColumn<QVariantMap>(name: "parameters" ); |
811 | QTest::addColumn<QByteArray>(name: "operation" ); |
812 | |
813 | const QVariantMap parameters { { QStringLiteral("first" ), QStringLiteral("first" ) }, |
814 | { QStringLiteral("second" ), QStringLiteral("second" ) }, |
815 | { QStringLiteral("third" ), QStringLiteral("third" ) }, |
816 | { QStringLiteral("c2&a3" ), QStringLiteral("2=%$&@q" ) } |
817 | }; |
818 | |
819 | if (hostReachable(host: QLatin1String("term.ie" ))) { |
820 | QTest::newRow(dataTag: "term.ie_get" ) << "key" |
821 | << "secret" |
822 | << "accesskey" |
823 | << "accesssecret" |
824 | << QUrl("http://term.ie/oauth/example/echo_api.php" ) |
825 | << parameters |
826 | << QByteArray("GET" ); |
827 | QTest::newRow(dataTag: "term.ie_post" ) << "key" |
828 | << "secret" |
829 | << "accesskey" |
830 | << "accesssecret" |
831 | << QUrl("http://term.ie/oauth/example/echo_api.php" ) |
832 | << parameters |
833 | << QByteArray("POST" ); |
834 | QTest::newRow(dataTag: "term.ie_percent_encoded_query" ) |
835 | << "key" |
836 | << "secret" |
837 | << "accesskey" |
838 | << "accesssecret" |
839 | << QUrl("http://term.ie/oauth/example/echo_api.php?key=%40value+1%2B2=3" ) |
840 | << parameters |
841 | << QByteArray("GET" ); |
842 | } else { |
843 | QSKIP("Skipping test due to unreachable term.ie host" ); |
844 | } |
845 | } |
846 | |
847 | void tst_OAuth1::prepareRequestCalls() |
848 | { |
849 | QFETCH(QString, consumerKey); |
850 | QFETCH(QString, consumerSecret); |
851 | QFETCH(QString, accessKey); |
852 | QFETCH(QString, accessKeySecret); |
853 | QFETCH(QUrl, url); |
854 | QFETCH(QVariantMap, parameters); |
855 | QFETCH(QByteArray, operation); |
856 | |
857 | QNetworkAccessManager networkAccessManager; |
858 | QNetworkReplyPtr reply; |
859 | QString receivedData; |
860 | QString ; |
861 | { |
862 | if (url.hasQuery()) { |
863 | parametersString = url.query(QUrl::FullyDecoded); |
864 | if (!parameters.empty()) |
865 | parametersString.append(c: QLatin1Char('&')); |
866 | } |
867 | bool first = true; |
868 | for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it) { |
869 | if (first) |
870 | first = false; |
871 | else |
872 | parametersString += QLatin1Char('&'); |
873 | parametersString += it.key() + QLatin1Char('=') + it.value().toString(); |
874 | } |
875 | } |
876 | |
877 | QOAuth1 o1(&networkAccessManager); |
878 | o1.setClientCredentials(clientIdentifier: consumerKey, clientSharedSecret: consumerSecret); |
879 | o1.setTokenCredentials(token: accessKey, tokenSecret: accessKeySecret); |
880 | |
881 | if (operation != "POST" ) { |
882 | QUrlQuery query(url.query()); |
883 | for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it) |
884 | query.addQueryItem(key: it.key(), value: it.value().toString()); |
885 | url.setQuery(query); |
886 | } |
887 | QNetworkRequest request(url); |
888 | QByteArray body; |
889 | if (operation == "POST" ) { |
890 | QUrlQuery query(url.query()); |
891 | for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it) |
892 | query.addQueryItem(key: it.key(), value: it.value().toString()); |
893 | body = query.toString().toUtf8(); |
894 | request.setHeader(header: QNetworkRequest::ContentTypeHeader, |
895 | value: QByteArray("application/x-www-form-urlencoded" )); |
896 | } |
897 | |
898 | o1.prepareRequest(request: &request, verb: operation, body); |
899 | if (body.isEmpty()) |
900 | reply.reset(t: o1.networkAccessManager()->sendCustomRequest(request, verb: operation)); |
901 | else |
902 | reply.reset(t: o1.networkAccessManager()->sendCustomRequest(request, verb: operation, data: body)); |
903 | QVERIFY(!reply.isNull()); |
904 | QVERIFY(!reply->isFinished()); |
905 | |
906 | connect(sender: &networkAccessManager, signal: &QNetworkAccessManager::finished, |
907 | slot: [&](QNetworkReply *reply) { |
908 | QByteArray data = reply->readAll(); |
909 | QUrlQuery query(QString::fromUtf8(str: data)); |
910 | receivedData = query.toString(encoding: QUrl::FullyDecoded); |
911 | }); |
912 | QVERIFY(waitForFinish(reply) == Success); |
913 | QCOMPARE(receivedData, parametersString); |
914 | reply.clear(); |
915 | } |
916 | |
917 | void tst_OAuth1::secondTemporaryToken() |
918 | { |
919 | QNetworkAccessManager networkAccessManager; |
920 | |
921 | const StringPair expectedToken(qMakePair(QStringLiteral("temporaryKey" ), QStringLiteral("temporaryToken" ))); |
922 | WebServer webServer([&](const WebServer::HttpRequest &request, QTcpSocket *socket) { |
923 | Q_UNUSED(request); |
924 | const QString format = "oauth_token=%1&oauth_token_secret=%2&oauth_callback_confirmed=true" ; |
925 | const QByteArray text = format.arg(a1: expectedToken.first, a2: expectedToken.second).toUtf8(); |
926 | const QByteArray replyMessage { |
927 | "HTTP/1.0 200 OK\r\n" |
928 | "Content-Type: application/x-www-form-urlencoded; charset=\"utf-8\"\r\n" |
929 | "Content-Length: " + QByteArray::number(text.size()) + "\r\n\r\n" |
930 | + text |
931 | }; |
932 | socket->write(data: replyMessage); |
933 | }); |
934 | |
935 | QOAuth1 o1(&networkAccessManager); |
936 | |
937 | StringPair clientCredentials = qMakePair(QStringLiteral("user" ), QStringLiteral("passwd" )); |
938 | o1.setClientCredentials(clientCredentials); |
939 | o1.setTemporaryCredentialsUrl(webServer.url(QStringLiteral("temporary" ))); |
940 | o1.setAuthorizationUrl(webServer.url(QStringLiteral("authorization" ))); |
941 | o1.setTokenCredentialsUrl(webServer.url(QStringLiteral("token" ))); |
942 | |
943 | StringPair tokenReceived; |
944 | connect(sender: &o1, signal: &QOAuth1::tokenChanged, slot: [&tokenReceived](const QString &token) { |
945 | tokenReceived.first = token; |
946 | }); |
947 | bool replyReceived = false; |
948 | connect(sender: &o1, signal: &QOAuth1::tokenSecretChanged, slot: [&tokenReceived, &replyReceived](const QString &tokenSecret) { |
949 | tokenReceived.second = tokenSecret; |
950 | replyReceived = true; |
951 | }); |
952 | |
953 | o1.grant(); |
954 | QTRY_VERIFY(replyReceived); |
955 | |
956 | QVERIFY(!tokenReceived.first.isEmpty()); |
957 | QVERIFY(!tokenReceived.second.isEmpty()); |
958 | QCOMPARE(o1.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived); |
959 | QCOMPARE(tokenReceived, expectedToken); |
960 | |
961 | replyReceived = false; // reset this so we can 'synchronize' on it again |
962 | // Do the same request again, should end up in the same state!! |
963 | o1.grant(); |
964 | QTRY_VERIFY(replyReceived); |
965 | |
966 | QVERIFY(!tokenReceived.first.isEmpty()); |
967 | QVERIFY(!tokenReceived.second.isEmpty()); |
968 | QCOMPARE(o1.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived); |
969 | QCOMPARE(tokenReceived, expectedToken); |
970 | } |
971 | |
972 | QTEST_MAIN(tst_OAuth1) |
973 | #include "tst_oauth1.moc" |
974 | |