/* * 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 #include "testutils/syncenginetestutils.h" #include #include using namespace std::chrono_literals; using namespace Qt::Literals::StringLiterals; using namespace OCC; namespace { // keep in sync with SyncEngine::abort QString formatAbortError(const QString &error) { return u"Aborted due to: %1"_s.arg(error); } class FakeBrokenXmlPropfindReply : public FakePropfindReply { Q_OBJECT public: FakeBrokenXmlPropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakePropfindReply(remoteRootFileInfo, op, request, parent) { QVERIFY(payload.size() > 50); // turncate the XML payload.chop(20); } }; class MissingPermissionsPropfindReply : public FakePropfindReply { Q_OBJECT public: MissingPermissionsPropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) : FakePropfindReply(remoteRootFileInfo, op, request, parent) { // If the propfind contains a single file without permissions, this is a server error const char toRemove[] = "RDNVCKW"; auto pos = payload.indexOf(toRemove, payload.size()/2); QVERIFY(pos > 0); payload.remove(pos, sizeof(toRemove) - 1); } }; enum ErrorKind : int { // Lower code are corresponding to HTML error code InvalidXML = 1000, Timeout, }; } Q_DECLARE_METATYPE(ErrorCategory) class TestRemoteDiscovery : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase_data() { QTest::addColumn("vfsMode"); QTest::addColumn("filesAreDehydrated"); QTest::newRow("Vfs::Mode::Off") << Vfs::Mode::Off << false; if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::WindowsCfApi)) { QTest::newRow("Vfs::Mode::WindowsCfApi dehydrated") << Vfs::Mode::WindowsCfApi << true; // TODO: the hydrated version will fail due to an issue in the winvfs plugin, so leave it disabled for now. // QTest::newRow("Vfs::Mode::WindowsCfApi hydrated") << Vfs::Mode::WindowsCfApi << false; } else if (Utility::isWindows()) { qWarning("Skipping Vfs::Mode::WindowsCfApi"); } } void testRemoteDiscoveryError_data() { qRegisterMetaType(); QTest::addColumn("errorKind"); QTest::addColumn("expectedErrorString"); QTest::addColumn("syncSucceeds"); QTest::addColumn("syncRootError"); QString itemErrorMessage = QStringLiteral("Internal Server Fake Error"); QTest::newRow("400") << 400 << itemErrorMessage << false << QString{}; QTest::newRow("401") << 401 << QStringLiteral("Fake credentials error") << false << QString{}; QTest::newRow("403") << 403 << QStringLiteral("Fake access denied error") << true << QString{}; QTest::newRow("404") << 404 << QString{} << true << QStringLiteral("This Space is currently unavailable"); QTest::newRow("500") << 500 << itemErrorMessage << true << QString{}; QTest::newRow("503") << 503 << itemErrorMessage << true << QString{}; // 200 should be an error since propfind should return 207 QTest::newRow("200") << 200 << itemErrorMessage << false << QString{}; QTest::newRow("InvalidXML") << +InvalidXML << "Unknown error" << false << QString{}; QTest::newRow("Timeout") << +Timeout << "Connection timed out" << false << QString{}; } // Check what happens when there is an error. void testRemoteDiscoveryError() { QFETCH_GLOBAL(Vfs::Mode, vfsMode); QFETCH_GLOBAL(bool, filesAreDehydrated); QFETCH(int, errorKind); QFETCH(QString, expectedErrorString); QFETCH(bool, syncSucceeds); QFETCH(QString, syncRootError); FakeFolder fakeFolder(FileInfo::A12_B12_C12_S12(), vfsMode, filesAreDehydrated); // Do Some change as well fakeFolder.localModifier().insert(QStringLiteral("A/z1")); fakeFolder.localModifier().insert(QStringLiteral("B/z1")); fakeFolder.localModifier().insert(QStringLiteral("C/z1")); fakeFolder.remoteModifier().insert(QStringLiteral("A/z2")); fakeFolder.remoteModifier().insert(QStringLiteral("B/z2")); fakeFolder.remoteModifier().insert(QStringLiteral("C/z2")); QVERIFY(fakeFolder.applyLocalModificationsWithoutSync()); auto oldLocalState = fakeFolder.currentLocalState(); auto oldRemoteState = fakeFolder.currentRemoteState(); QString errorFolder = Utility::concatUrlPath(OCC::TestUtils::dummyDavUrl(), QStringLiteral("B")).path(); QString fatalErrorPrefix = QStringLiteral("Server replied with an error while reading directory »B«: "); fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) -> QNetworkReply * { if (req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray() == "PROPFIND" && req.url().path().endsWith(errorFolder)) { if (errorKind == InvalidXML) { return new FakeBrokenXmlPropfindReply(fakeFolder.remoteModifier(), op, req, this); } else if (errorKind == Timeout) { return new FakeHangingReply(op, req, this); } else if (errorKind < 1000) { return new FakeErrorReply(op, req, this, errorKind); } } return nullptr; }); // So the test that test timeout finishes fast QScopedValueRollback setHttpTimeout(AbstractNetworkJob::httpTimeout, errorKind == Timeout ? 50ms : 10000s); ItemCompletedSpy completeSpy(fakeFolder); QSignalSpy errorSpy(&fakeFolder.syncEngine(), &SyncEngine::syncError); QCOMPARE(fakeFolder.applyLocalModificationsAndSync(), syncSucceeds); // The folder B should not have been sync'ed (and in particular not removed) QCOMPARE(oldLocalState.children[QStringLiteral("B")], fakeFolder.currentLocalState().children[QStringLiteral("B")]); QCOMPARE(oldRemoteState.children[QStringLiteral("B")], fakeFolder.currentRemoteState().children[QStringLiteral("B")]); if (!syncSucceeds) { QCOMPARE(errorSpy.size(), 1); QCOMPARE(errorSpy[0][0].toString(), formatAbortError(fatalErrorPrefix + expectedErrorString)); } else { QCOMPARE(completeSpy.findItem(QStringLiteral("B"))->instruction(), CSYNC_INSTRUCTION_IGNORE); QVERIFY(completeSpy.findItem(QStringLiteral("B"))->_errorString.contains(expectedErrorString)); // The other folder should have been sync'ed as the sync just ignored the faulty dir QCOMPARE(fakeFolder.currentRemoteState().children[QStringLiteral("A")], fakeFolder.currentLocalState().children[QStringLiteral("A")]); QCOMPARE(fakeFolder.currentRemoteState().children[QStringLiteral("C")], fakeFolder.currentLocalState().children[QStringLiteral("C")]); QCOMPARE(completeSpy.findItem(QStringLiteral("A/z1"))->instruction(), CSYNC_INSTRUCTION_NEW); } // // Check the same discovery error on the sync root // errorFolder = Utility::ensureTrailingSlash(OCC::TestUtils::dummyDavUrl().path()); fatalErrorPrefix = QStringLiteral("Server replied with an error while reading directory »/«: "); errorSpy.clear(); QVERIFY(!fakeFolder.applyLocalModificationsAndSync()); QCOMPARE(errorSpy.size(), 1); QCOMPARE(errorSpy[0][0].toString(), formatAbortError(syncRootError.isEmpty() ? QString(fatalErrorPrefix + expectedErrorString) : syncRootError)); } void testMissingData() { QFETCH_GLOBAL(Vfs::Mode, vfsMode); QFETCH_GLOBAL(bool, filesAreDehydrated); FakeFolder fakeFolder(FileInfo(), vfsMode, filesAreDehydrated); fakeFolder.remoteModifier().insert(QStringLiteral("good")); fakeFolder.remoteModifier().insert(QStringLiteral("noetag")); fakeFolder.remoteModifier().find(QStringLiteral("noetag"))->etag.clear(); fakeFolder.remoteModifier().insert(QStringLiteral("nofileid")); fakeFolder.remoteModifier().find(QStringLiteral("nofileid"))->fileId.clear(); fakeFolder.remoteModifier().mkdir(QStringLiteral("nopermissions")); fakeFolder.remoteModifier().insert(QStringLiteral("nopermissions/A")); fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) -> QNetworkReply * { if (req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray() == "PROPFIND" && req.url().path().endsWith(QLatin1String("nopermissions"))) return new MissingPermissionsPropfindReply(fakeFolder.remoteModifier(), op, req, this); return nullptr; }); ItemCompletedSpy completeSpy(fakeFolder); QVERIFY(!fakeFolder.applyLocalModificationsAndSync()); QCOMPARE(completeSpy.findItem(QStringLiteral("good"))->instruction(), CSYNC_INSTRUCTION_NEW); QCOMPARE(completeSpy.findItem(QStringLiteral("noetag"))->instruction(), CSYNC_INSTRUCTION_ERROR); QCOMPARE(completeSpy.findItem(QStringLiteral("nofileid"))->instruction(), CSYNC_INSTRUCTION_ERROR); QCOMPARE(completeSpy.findItem(QStringLiteral("nopermissions"))->instruction(), CSYNC_INSTRUCTION_NEW); QCOMPARE(completeSpy.findItem(QStringLiteral("nopermissions/A"))->instruction(), CSYNC_INSTRUCTION_ERROR); QVERIFY(completeSpy.findItem(QStringLiteral("noetag"))->_errorString.contains(QStringLiteral("etag"))); QVERIFY(completeSpy.findItem(QStringLiteral("nofileid"))->_errorString.contains(QStringLiteral("id"))); QVERIFY(completeSpy.findItem(QStringLiteral("nopermissions/A"))->_errorString.contains(QStringLiteral("permissions"))); } }; QTEST_GUILESS_MAIN(TestRemoteDiscovery) #include "testremotediscovery.moc"