diff --git a/src/gui/connectionvalidator.cpp b/src/gui/connectionvalidator.cpp index 9468f2ad0f5..e6e2b9ac6c8 100644 --- a/src/gui/connectionvalidator.cpp +++ b/src/gui/connectionvalidator.cpp @@ -40,7 +40,10 @@ Q_LOGGING_CATEGORY(lcConnectionValidator, "sync.connectionvalidator", QtInfoMsg) // Make sure the timeout for this job is less than how often we get called // This makes sure we get tried often enough without "ConnectionValidator already running" namespace { - const auto timeoutToUse = ConnectionValidator::DefaultCallingInterval - 5s; + auto timeoutToUse() + { + return std::min(ConnectionValidator::DefaultCallingInterval - 5s, AbstractNetworkJob::httpTimeout); + }; } ConnectionValidator::ConnectionValidator(AccountPtr account, QObject *parent) @@ -194,7 +197,7 @@ void ConnectionValidator::checkAuthentication() // we explicitly use a legacy dav path here auto *job = new PropfindJob(_account, _account->url(), Theme::instance()->webDavPath(), PropfindJob::Depth::Zero, this); job->setAuthenticationJob(true); // don't retry - job->setTimeout(timeoutToUse); + job->setTimeout(timeoutToUse()); job->setProperties({ QByteArrayLiteral("getlastmodified") }); connect(job, &PropfindJob::finishedWithoutError, this, &ConnectionValidator::slotAuthSuccess); connect(job, &PropfindJob::finishedWithError, this, &ConnectionValidator::slotAuthFailed); diff --git a/src/gui/fetchserversettings.cpp b/src/gui/fetchserversettings.cpp index b7daa465f87..6715cd67acc 100644 --- a/src/gui/fetchserversettings.cpp +++ b/src/gui/fetchserversettings.cpp @@ -27,7 +27,10 @@ using namespace OCC; Q_LOGGING_CATEGORY(lcfetchserversettings, "sync.fetchserversettings", QtInfoMsg) namespace { -constexpr auto timeoutC = 20s; +auto fetchSettingsTimeout() +{ + return std::min(20s, AbstractNetworkJob::httpTimeout); +} } // TODO: move to libsync? @@ -43,7 +46,7 @@ void FetchServerSettingsJob::start() // The main flow now needs the capabilities auto *job = new JsonApiJob(_account, QStringLiteral("ocs/v2.php/cloud/capabilities"), {}, {}, this); job->setAuthenticationJob(isAuthJob()); - job->setTimeout(timeoutC); + job->setTimeout(fetchSettingsTimeout()); connect(job, &JsonApiJob::finishedSignal, this, [job, this] { auto caps = @@ -69,7 +72,7 @@ void FetchServerSettingsJob::start() } auto *userJob = new JsonApiJob(_account, QStringLiteral("ocs/v2.php/cloud/user"), SimpleNetworkJob::UrlQuery{}, QNetworkRequest{}, this); userJob->setAuthenticationJob(isAuthJob()); - userJob->setTimeout(timeoutC); + userJob->setTimeout(fetchSettingsTimeout()); connect(userJob, &JsonApiJob::finishedSignal, this, [userJob, this] { if (userJob->timedOut()) { Q_EMIT finishedSignal(Result::TimeOut); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5203167863f..c94d91ef229 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,6 +11,7 @@ owncloud_add_test(SyncFileItem) owncloud_add_test(ConcatUrl) owncloud_add_test(XmlParse) owncloud_add_test(ChecksumValidator) +owncloud_add_test(ConnectionValidator) # TODO: we need keychain access for this test diff --git a/test/testconnectionvalidator/capabilities.json.in b/test/testconnectionvalidator/capabilities.json.in new file mode 100644 index 00000000000..9f056b19278 --- /dev/null +++ b/test/testconnectionvalidator/capabilities.json.in @@ -0,0 +1,179 @@ +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 100, + "message": "OK" + }, + "data": { + "capabilities": { + "core": { + "pollinterval": 60, + "webdav-root": "remote.php/webdav", + "status": { + "installed": true, + "maintenance": false, + "needsDbUpgrade": false, + "version": "@{version}", + "versionstring": "10.11.0", + "edition": "Community", + "productname": "Infinite Scale", + "product": "Infinite Scale", + "productversion": "@{productversion}" + }, + "support-url-signing": true, + "support-sse": true + }, + "checksums": { + "supportedTypes": [ + "sha1", + "md5", + "adler32" + ], + "preferredUploadType": "sha1" + }, + "files": { + "privateLinks": true, + "bigfilechunking": false, + "undelete": true, + "versioning": true, + "favorites": false, + "full_text_search": false, + "tags": true, + "blacklisted_files": [], + "tus_support": { + "version": "1.0.0", + "resumable": "1.0.0", + "extension": "creation,creation-with-upload", + "max_chunk_size": 100000000, + "http_method_override": "" + }, + "archivers": [ + { + "enabled": true, + "version": "2.0.0", + "formats": [ + "tar", + "zip" + ], + "archiver_url": "/archiver", + "max_num_files": "10000", + "max_size": "1073741824" + } + ], + "app_providers": [ + { + "enabled": true, + "version": "1.1.0", + "apps_url": "/app/list", + "open_url": "/app/open", + "open_web_url": "/app/open-with-web", + "new_url": "/app/new" + } + ] + }, + "dav": { + "chunking": "", + "trashbin": "1.0", + "reports": [ + "search-files" + ], + "chunkingParallelUploadDisabled": false + }, + "files_sharing": { + "api_enabled": true, + "resharing": true, + "group_sharing": true, + "sharing_roles": true, + "deny_access": false, + "auto_accept_share": true, + "share_with_group_members_only": true, + "share_with_membership_groups_only": true, + "search_min_length": 3, + "default_permissions": 22, + "user_enumeration": { + "enabled": true, + "group_members_only": true + }, + "federation": { + "outgoing": false, + "incoming": false + }, + "public": { + "enabled": true, + "send_mail": true, + "social_share": true, + "upload": true, + "multiple": true, + "supports_upload_only": true, + "password": { + "enforced_for": { + "read_only": false, + "read_write": false, + "read_write_delete": false, + "upload_only": false + }, + "enforced": false + }, + "expire_date": { + "enabled": false + }, + "can_edit": true, + "alias": true + }, + "user": { + "send_mail": true, + "profile_picture": false, + "settings": [ + { + "enabled": true, + "version": "1.0.0" + } + ], + "expire_date": { + "enabled": true + } + } + }, + "spaces": { + "version": "1.0.0", + "enabled": true, + "projects": true, + "share_jail": true, + "max_quota": 0 + }, + "graph": { + "personal-data-export": true, + "users": { + "read_only_attributes": [ + "user.onPremisesSamAccountName", + "user.displayName", + "user.mail", + "user.passwordProfile", + "user.appRoleAssignments" + ], + "create_disabled": true, + "delete_disabled": true, + "change_password_self_disabled": true + } + }, + "notifications": { + "ocs-endpoints": [ + "list", + "get", + "delete" + ] + } + }, + "version": { + "major": 10, + "minor": 11, + "micro": 0, + "string": "10.11.0", + "edition": "Community", + "product": "Infinite Scale", + "productversion": "4.0.5" + } + } + } +} diff --git a/test/testconnectionvalidator/status.php.json.in b/test/testconnectionvalidator/status.php.json.in new file mode 100644 index 00000000000..060ca6b4e5c --- /dev/null +++ b/test/testconnectionvalidator/status.php.json.in @@ -0,0 +1,11 @@ +{ + "installed": true, + "maintenance": @{maintenance}, + "needsDbUpgrade": false, + "version": "10.11.0.0", + "versionstring": "10.11.0", + "edition": "Community", + "productname": "Infinite Scale", + "product": "Infinite Scale", + "productversion": "4.0.5" +} diff --git a/test/testconnectionvalidator/testconnectionvalidator.cpp b/test/testconnectionvalidator/testconnectionvalidator.cpp new file mode 100644 index 00000000000..a62d4f8b38b --- /dev/null +++ b/test/testconnectionvalidator/testconnectionvalidator.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) by Hannah von Reth + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include + +#include "gui/connectionvalidator.h" +#include "libsync/abstractnetworkjob.h" +#include "libsync/httplogger.h" + +#include "testutils/syncenginetestutils.h" +#include "testutils/testutils.h" + +using namespace std::chrono_literals; + +using namespace OCC; + +class TestConnectionValidator : public QObject +{ + Q_OBJECT + + enum class FailStage { Invalid, StatusPhp, AuthValidation, Capabilities, UserInfo }; + + // we can't use QMap direclty with QFETCH + using Values = QMap; + auto getPayload(const QString &payloadName) + { + QFile f(QStringLiteral(SOURCEDIR "/test/testconnectionvalidator/%1").arg(payloadName)); + Q_ASSERT(f.open(QIODevice::ReadOnly)); + return f.readAll(); + } + + auto getPayloadTemplated(const QString &payloadName, const QMap &values) + { + return Utility::renderTemplate(QString::fromUtf8(getPayload(payloadName)), values).toUtf8(); + } + +private Q_SLOTS: + + + void initTestCase() { AbstractNetworkJob::httpTimeout = 1s; } + + void testStatusPhp_data() + { + QTest::addColumn("failStage"); + QTest::addColumn("values"); + QTest::addColumn("status"); + + const auto defaultValue = Values{{QStringLiteral("maintenance"), QStringLiteral("false")}, {QStringLiteral("version"), QStringLiteral("10.11.0.0")}, + {QStringLiteral("productversion"), QStringLiteral("4.0.5")}}; + + QTest::newRow("stauts.php maintenance") << FailStage::StatusPhp << [value = defaultValue]() mutable { + value[QStringLiteral("maintenance")] = QStringLiteral("true"); + return value; + }() << ConnectionValidator::MaintenanceMode; + QTest::newRow("stauts.php ServiceUnavailable") << FailStage::StatusPhp << defaultValue << ConnectionValidator::StatusNotFound; + + QTest::newRow("auth 401") << FailStage::AuthValidation << defaultValue << ConnectionValidator::CredentialsWrong; + QTest::newRow("auth timeout") << FailStage::AuthValidation << defaultValue << ConnectionValidator::Timeout; + QTest::newRow("auth ServiceUnavailable") << FailStage::AuthValidation << defaultValue << ConnectionValidator::ServiceUnavailable; + + QTest::newRow("capabilites timeout") << FailStage::Capabilities << defaultValue << ConnectionValidator::CredentialsWrong; + QTest::newRow("capabilites 401") << FailStage::Capabilities << defaultValue << ConnectionValidator::Timeout; + QTest::newRow("capabilites unsupported server") << FailStage::Capabilities << [value = defaultValue]() mutable { + value[QStringLiteral("version")] = QStringLiteral("7.0"); + value[QStringLiteral("productversion")] = QString(); + return value; + }() << ConnectionValidator::ServerVersionMismatch; + + + QTest::newRow("user info timeout") << FailStage::UserInfo << defaultValue << ConnectionValidator::Timeout; + QTest::newRow("user info 401") << FailStage::UserInfo << defaultValue << ConnectionValidator::CredentialsWrong; + QTest::newRow("success") << FailStage::UserInfo << defaultValue << ConnectionValidator::Connected; + } + + void testStatusPhp() + { + QFETCH(FailStage, failStage); + QFETCH(Values, values); + QFETCH(ConnectionValidator::Status, status); + + auto reachedStage = FailStage::Invalid; + FakeFolder fakeFolder({}); + + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + const auto path = request.url().path(); + const auto verb = HttpLogger::requestVerb(op, request); + if (op == QNetworkAccessManager::GetOperation) { + if (path.endsWith(QLatin1String("status.php"))) { + reachedStage = FailStage::StatusPhp; + if (failStage == FailStage::StatusPhp) { + if (status == ConnectionValidator::Timeout) { + return new FakeHangingReply(op, request, this); + } else if (status == ConnectionValidator::StatusNotFound) { + return new FakeErrorReply(op, request, this, 500); + } + } + return new FakePayloadReply(op, request, getPayloadTemplated(QStringLiteral("status.php.json.in"), values), this); + } else if (path.endsWith(QLatin1String("capabilities"))) { + reachedStage = FailStage::Capabilities; + if (failStage == FailStage::Capabilities) { + if (status == ConnectionValidator::CredentialsWrong) { + return new FakeErrorReply(op, request, this, 401); + } else if (status == ConnectionValidator::Timeout) { + return new FakeHangingReply(op, request, this); + } + } + return new FakePayloadReply(op, request, getPayloadTemplated(QStringLiteral("capabilities.json.in"), values), this); + } else if (path.endsWith(QLatin1String("user"))) { + reachedStage = FailStage::UserInfo; + if (failStage == FailStage::UserInfo) { + if (status == ConnectionValidator::CredentialsWrong) { + return new FakeErrorReply(op, request, this, 401); + } else if (status == ConnectionValidator::Timeout) { + return new FakeHangingReply(op, request, this); + } + } + return new FakePayloadReply(op, request, getPayload(QStringLiteral("user.json")), this); + } + } else if (failStage == FailStage::AuthValidation && verb == "PROPFIND") { + reachedStage = FailStage::AuthValidation; + if (status == ConnectionValidator::CredentialsWrong) { + return new FakeErrorReply(op, request, this, 401); + } else if (status == ConnectionValidator::Timeout) { + return new FakeHangingReply(op, request, this); + } else if (status == ConnectionValidator::ServiceUnavailable) { + return new FakeErrorReply(op, request, this, 503); + } + } + return nullptr; + }); + + ConnectionValidator val(fakeFolder.account()); + val.checkServer(ConnectionValidator::ValidationMode::ValidateAuthAndUpdate); + + QSignalSpy spy(&val, &ConnectionValidator::connectionResult); + QVERIFY(spy.wait(30s)); + QCOMPARE(spy.first().first().value(), status); + QCOMPARE(reachedStage, failStage); + } +}; + +QTEST_MAIN(TestConnectionValidator) +#include "testconnectionvalidator.moc" diff --git a/test/testconnectionvalidator/user.json b/test/testconnectionvalidator/user.json new file mode 100644 index 00000000000..62c8f2b1496 --- /dev/null +++ b/test/testconnectionvalidator/user.json @@ -0,0 +1,17 @@ +{ + "ocs": { + "meta": { + "status": "ok", + "statuscode": 200, + "message": "OK", + "totalitems": "", + "itemsperpage": "" + }, + "data": { + "id": "test", + "display-name": "test", + "email": "test@test.com", + "language": "en" + } + } +}