Test OAuth2
authorOlivier Goffart <ogoffart@woboq.com>
Mon, 25 Sep 2017 16:23:39 +0000 (18:23 +0200)
committerRoeland Jago Douma <roeland@famdouma.nl>
Thu, 5 Oct 2017 20:01:38 +0000 (22:01 +0200)
Include a test for PR #6057

src/gui/creds/oauth.h
test/CMakeLists.txt
test/syncenginetestutils.h
test/testoauth.cpp [new file with mode: 0644]

index 70243964731dec26a469e0ac4420d680634224ec..1c6b519e13315bfeaf26a252b5c51e77a9170d73 100644 (file)
@@ -16,6 +16,7 @@
 #include <QPointer>
 #include <QTcpServer>
 #include <QUrl>
+#include "accountfwd.h"
 
 namespace OCC {
 
index d5cc8615fb908ee488a2cf285cdb90560071aef1..14235a49d216725ecc843c20706c6ce824819645 100644 (file)
@@ -69,6 +69,8 @@ list(APPEND FolderMan_SRC ${FolderWatcher_SRC})
 list(APPEND FolderMan_SRC stub.cpp )
 owncloud_add_test(FolderMan "${FolderMan_SRC}")
 
+owncloud_add_test(OAuth "syncenginetestutils.h;../src/gui/creds/oauth.cpp")
+
 configure_file(test_journal.db "${PROJECT_BINARY_DIR}/bin/test_journal.db" COPYONLY)
 
 find_package(CMocka)
index 9253b6285a78f691d5485175ccfa596fcd36bef2..498ebcb4de2d77c9e3059131fe5988d8f7182c79 100644 (file)
@@ -738,6 +738,10 @@ public:
 protected:
     QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
                                          QIODevice *outgoingData = 0) {
+        if (_override) {
+            if (auto reply = _override(op, request))
+                return reply;
+        }
         const QString fileName = getFilePathFromUrl(request.url());
         Q_ASSERT(!fileName.isNull());
         if (_errorPaths.contains(fileName))
@@ -746,10 +750,6 @@ protected:
         bool isUpload = request.url().path().startsWith(sUploadUrl.path());
         FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
 
-        if (_override) {
-            if (auto reply = _override(op, request))
-                return reply;
-        }
 
         auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
         if (verb == "PROPFIND")
diff --git a/test/testoauth.cpp b/test/testoauth.cpp
new file mode 100644 (file)
index 0000000..76dbb3b
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ *    This software is in the public domain, furnished "as is", without technical
+ *    support, and with no warranty, express or implied, as to its usefulness for
+ *    any purpose.
+ *
+ */
+
+#include <QtTest/QtTest>
+#include <QDesktopServices>
+
+#include "gui/creds/oauth.h"
+#include "syncenginetestutils.h"
+#include "theme.h"
+#include "common/asserts.h"
+
+using namespace OCC;
+
+class DesktopServiceHook : public QObject
+{
+    Q_OBJECT
+signals:
+    void hooked(const QUrl &);
+public:
+    DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); }
+} desktopServiceHook;
+
+static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud");
+
+
+class FakePostReply : public QNetworkReply
+{
+    Q_OBJECT
+public:
+    std::unique_ptr<QIODevice> payload;
+    bool aborted = false;
+
+    FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
+                  std::unique_ptr<QIODevice> payload_, QObject *parent)
+        : QNetworkReply{parent}, payload{std::move(payload_)}
+    {
+        setRequest(request);
+        setUrl(request.url());
+        setOperation(op);
+        open(QIODevice::ReadOnly);
+        payload->open(QIODevice::ReadOnly);
+        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+    }
+
+    Q_INVOKABLE virtual void respond() {
+        if (aborted) {
+            setError(OperationCanceledError, "Operation Canceled");
+            emit metaDataChanged();
+            emit finished();
+            return;
+        }
+        setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
+        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+        emit metaDataChanged();
+        if (bytesAvailable())
+            emit readyRead();
+        emit finished();
+    }
+
+    void abort() override {
+        aborted = true;
+    }
+    qint64 bytesAvailable() const override {
+        if (aborted)
+            return 0;
+        return payload->bytesAvailable();
+    }
+
+    qint64 readData(char *data, qint64 maxlen) override {
+        return payload->read(data, maxlen);
+    }
+};
+
+// Reply with a small delay
+class SlowFakePostReply : public FakePostReply {
+    Q_OBJECT
+public:
+    using FakePostReply::FakePostReply;
+    void respond() override {
+        // override of FakePostReply::respond, will call the real one with a delay.
+        QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
+    }
+};
+
+
+class OAuthTestCase : public QObject
+{
+    Q_OBJECT
+public:
+    enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
+    Q_ENUM(State);
+    bool replyToBrowserOk = false;
+    bool gotAuthOk = false;
+    virtual bool done() const { return replyToBrowserOk && gotAuthOk; }
+
+    FakeQNAM *fakeQnam = nullptr;
+    QNetworkAccessManager realQNAM;
+    QPointer<QNetworkReply> browserReply = nullptr;
+    QString code = generateEtag();
+    OCC::AccountPtr account;
+
+    QScopedPointer<OAuth> oauth;
+
+    virtual void test() {
+        fakeQnam = new FakeQNAM({});
+        account = OCC::Account::create();
+        account->setUrl(sOAuthTestServer);
+        account->setCredentials(new FakeCredentials{fakeQnam});
+        fakeQnam->setParent(this);
+        fakeQnam->setOverride([this] (QNetworkAccessManager::Operation op, const QNetworkRequest &req) {
+            return this->tokenReply(op, req);
+        });
+
+        QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
+                         this, &OAuthTestCase::openBrowserHook);
+
+        oauth.reset(new OAuth(account.data(), nullptr));
+        QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
+        oauth->start();
+        QTRY_VERIFY(done());
+    }
+
+    virtual void openBrowserHook(const QUrl &url) {
+        QCOMPARE(state, StartState);
+        state = BrowserOpened;
+        QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
+        QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
+        QUrlQuery query(url);
+        QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
+        QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
+        QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
+        QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
+        redirectUri.setQuery("code=" + code);
+        createBrowserReply(QNetworkRequest(redirectUri));
+    }
+
+    virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
+        browserReply = realQNAM.get(request);
+        QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
+        return browserReply;
+    }
+
+    virtual void browserReplyFinished() {
+        QCOMPARE(sender(), browserReply.data());
+        QCOMPARE(state, TokenAsked);
+        browserReply->deleteLater();
+        QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
+        replyToBrowserOk = true;
+    };
+
+    virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
+    {
+        ASSERT(state == BrowserOpened);
+        state = TokenAsked;
+        ASSERT(op == QNetworkAccessManager::PostOperation);
+        ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
+        ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
+        std::unique_ptr<QBuffer> payload(new QBuffer());
+        payload->setData(tokenReplyPayload());
+        return new FakePostReply(op, req, std::move(payload), fakeQnam);
+    }
+
+    virtual QByteArray tokenReplyPayload() const {
+        QJsonDocument jsondata(QJsonObject{
+                { "access_token", "123" },
+                { "refresh_token" , "456" },
+                { "message_url",  "owncloud://success"},
+                { "user_id", "789" },
+                { "token_type", "Bearer" }
+        });
+        return jsondata.toJson();
+    }
+
+    virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
+        QCOMPARE(state, TokenAsked);
+        QCOMPARE(result, OAuth::LoggedIn);
+        QCOMPARE(user, QString("789"));
+        QCOMPARE(token, QString("123"));
+        QCOMPARE(refreshToken, QString("456"));
+        gotAuthOk = true;
+    }
+};
+
+class TestOAuth: public QObject
+{
+    Q_OBJECT
+
+private slots:
+    void testBasic()
+    {
+        OAuthTestCase test;
+        test.test();
+    }
+
+    // Test for https://github.com/owncloud/client/pull/6057
+    void testCloseBrowserDontCrash()
+    {
+        struct Test : OAuthTestCase {
+            QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
+            {
+                ASSERT(browserReply);
+                // simulate the fact that the browser is closing the connection
+                browserReply->abort();
+                QCoreApplication::processEvents();
+
+                ASSERT(state == BrowserOpened);
+                state = TokenAsked;
+
+                std::unique_ptr<QBuffer> payload(new QBuffer);
+                payload->setData(tokenReplyPayload());
+                return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
+            }
+
+            void browserReplyFinished() override
+            {
+                QCOMPARE(sender(), browserReply.data());
+                QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
+                replyToBrowserOk = true;
+            }
+        } test;
+        test.test();
+    }
+
+    void testRandomConnections()
+    {
+        // Test that we can send random garbage to the litening socket and it does not prevent the connection
+        struct Test : OAuthTestCase {
+            virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
+                QTimer::singleShot(0, this, [this, request] {
+                    auto port = request.url().port();
+                    state = CustomState;
+                    QVector<QByteArray> payloads = {
+                        "GET FOFOFO HTTP 1/1\n\n",
+                        "GET /?code=invalie HTTP 1/1\n\n",
+                        "GET /?code=xxxxx&bar=fff",
+                        QByteArray("\0\0\0", 3),
+                        QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
+                        QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
+                        QByteArray("\n\n\n\n"),
+                    };
+                    foreach (const auto &x, payloads) {
+                        auto socket = new QTcpSocket(this);
+                        socket->connectToHost("localhost", port);
+                        QVERIFY(socket->waitForConnected());
+                        socket->write(x);
+                    }
+
+                    // Do the actual request a bit later
+                    QTimer::singleShot(100, this, [this, request] {
+                        QCOMPARE(state, CustomState);
+                        state = BrowserOpened;
+                        this->OAuthTestCase::createBrowserReply(request);
+                    });
+               });
+               return nullptr;
+            }
+
+            QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
+            {
+                if (state == CustomState)
+                    return new FakeErrorReply{op, req, this, 500};
+                return OAuthTestCase::tokenReply(op, req);
+            }
+
+            void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
+                             const QString &refreshToken) override {
+                if (state != CustomState)
+                    return OAuthTestCase::oauthResult(result, user, token, refreshToken);
+                QCOMPARE(result, OAuth::Error);
+            }
+        } test;
+        test.test();
+    }
+};
+
+QTEST_GUILESS_MAIN(TestOAuth)
+#include "testoauth.moc"