mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-22 04:55:48 +03:00
parent
800b9cf167
commit
0ceb806f1a
4 changed files with 288 additions and 4 deletions
|
@ -16,6 +16,7 @@
|
|||
#include <QPointer>
|
||||
#include <QTcpServer>
|
||||
#include <QUrl>
|
||||
#include "accountfwd.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
281
test/testoauth.cpp
Normal file
281
test/testoauth.cpp
Normal file
|
@ -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"
|
Loading…
Reference in a new issue