Procházet zdrojové kódy

add automated tests for LockFileJob to validate proper behavior

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
Matthieu Gallien před 3 roky
rodič
revize
2ea68d75bd

+ 11 - 11
src/libsync/lockfilejobs.cpp

@@ -65,14 +65,14 @@ bool LockFileJob::finished()
         const auto httpErrorCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
         if (httpErrorCode == LOCKED_HTTP_ERROR_CODE) {
             const auto record = handleReply();
-            if (static_cast<SyncFileItem::LockOwnerType>(record._lockOwnerType) == SyncFileItem::LockOwnerType::UserLock) {
-                Q_EMIT finishedWithError(httpErrorCode, {}, record._lockOwnerDisplayName);
+            if (static_cast<SyncFileItem::LockOwnerType>(record._lockstate._lockOwnerType) == SyncFileItem::LockOwnerType::UserLock) {
+                Q_EMIT finishedWithError(httpErrorCode, {}, record._lockstate._lockOwnerDisplayName);
             } else {
-                Q_EMIT finishedWithError(httpErrorCode, {}, record._lockEditorApp);
+                Q_EMIT finishedWithError(httpErrorCode, {}, record._lockstate._lockEditorApp);
             }
         } else if (httpErrorCode == PRECONDITION_FAILED_ERROR_CODE) {
             const auto record = handleReply();
-            if (_requestedLockState == SyncFileItem::LockStatus::UnlockedItem && !record._locked) {
+            if (_requestedLockState == SyncFileItem::LockStatus::UnlockedItem && !record._lockstate._locked) {
                 Q_EMIT finishedWithoutError();
             } else {
                 Q_EMIT finishedWithError(httpErrorCode, reply()->errorString(), {});
@@ -90,13 +90,13 @@ bool LockFileJob::finished()
 
 void LockFileJob::setFileRecordLocked(SyncJournalFileRecord &record) const
 {
-    record._locked = (_lockStatus == SyncFileItem::LockStatus::LockedItem);
-    record._lockOwnerType = static_cast<int>(_lockOwnerType);
-    record._lockOwnerDisplayName = _userDisplayName;
-    record._lockOwnerId = _userId;
-    record._lockEditorApp = _editorName;
-    record._lockTime = _lockTime;
-    record._lockTimeout = _lockTimeout;
+    record._lockstate._locked = (_lockStatus == SyncFileItem::LockStatus::LockedItem);
+    record._lockstate._lockOwnerType = static_cast<int>(_lockOwnerType);
+    record._lockstate._lockOwnerDisplayName = _userDisplayName;
+    record._lockstate._lockOwnerId = _userId;
+    record._lockstate._lockEditorApp = _editorName;
+    record._lockstate._lockTime = _lockTime;
+    record._lockstate._lockTimeout = _lockTimeout;
 }
 
 void LockFileJob::resetState()

+ 1 - 0
test/CMakeLists.txt

@@ -64,6 +64,7 @@ nextcloud_add_test(UnifiedSearchListmodel)
 nextcloud_add_test(ActivityListModel)
 nextcloud_add_test(ActivityData)
 nextcloud_add_test(TalkReply)
+nextcloud_add_test(LockFile)
 
 if( UNIX AND NOT APPLE )
     nextcloud_add_test(InotifyWatcher)

+ 49 - 0
test/syncenginetestutils.cpp

@@ -1000,6 +1000,8 @@ QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, cons
             if (contentType.startsWith(QStringLiteral("multipart/related; boundary="))) {
                 reply = new FakePutMultiFileReply { info, op, newRequest, contentType, outgoingData->readAll(), this };
             }
+        } else if (verb == QLatin1String("LOCK") || verb == QLatin1String("UNLOCK")) {
+            reply = new FakeFileLockReply{info, op, newRequest, this};
         } else {
             qDebug() << verb << outgoingData;
             Q_UNREACHABLE();
@@ -1251,3 +1253,50 @@ FakeJsonErrorReply::FakeJsonErrorReply(QNetworkAccessManager::Operation op,
     : FakeErrorReply{ op, request, parent, httpErrorCode, reply.toJson() }
 {
 }
+
+FakeFileLockReply::FakeFileLockReply(FileInfo &remoteRootFileInfo,
+                                     QNetworkAccessManager::Operation op,
+                                     const QNetworkRequest &request,
+                                     QObject *parent)
+    : FakePropfindReply(remoteRootFileInfo, op, request, parent)
+{
+    const auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
+
+    setRequest(request);
+    setUrl(request.url());
+    setOperation(op);
+    open(QIODevice::ReadOnly);
+
+    QString fileName = getFilePathFromUrl(request.url());
+    Q_ASSERT(!fileName.isNull()); // for root, it should be empty
+    FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
+    if (!fileInfo) {
+        QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection);
+        return;
+    }
+
+    const QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
+
+    // Don't care about the request and just return a full propfind
+    const QString davUri { QStringLiteral("DAV:") };
+    const QString ocUri { QStringLiteral("http://owncloud.org/ns") };
+    const QString ncUri { QStringLiteral("http://nextcloud.org/ns") };
+    payload.clear();
+    QBuffer buffer { &payload };
+    buffer.open(QIODevice::WriteOnly);
+    QXmlStreamWriter xml(&buffer);
+    xml.writeNamespace(davUri, QStringLiteral("d"));
+    xml.writeNamespace(ocUri, QStringLiteral("oc"));
+    xml.writeNamespace(ncUri, QStringLiteral("nc"));
+    xml.writeStartDocument();
+    xml.writeStartElement(davUri, QStringLiteral("prop"));
+    xml.writeTextElement(ncUri, QStringLiteral("lock"), verb == QStringLiteral("LOCK") ? "1" : "0");
+    xml.writeTextElement(ncUri, QStringLiteral("lock-owner-type"), QString::number(0));
+    xml.writeTextElement(ncUri, QStringLiteral("lock-owner"), QStringLiteral("admin"));
+    xml.writeTextElement(ncUri, QStringLiteral("lock-owner-displayname"), QStringLiteral("John Doe"));
+    xml.writeTextElement(ncUri, QStringLiteral("lock-owner-editor"), {});
+    xml.writeTextElement(ncUri, QStringLiteral("lock-time"), QString::number(1234560));
+    xml.writeTextElement(ncUri, QStringLiteral("lock-timeout"), QString::number(1800));
+    xml.writeEndElement(); // prop
+    xml.writeEndDocument();
+}

+ 10 - 0
test/syncenginetestutils.h

@@ -402,6 +402,16 @@ public:
     qint64 readData(char *, qint64) override { return 0; }
 };
 
+class FakeFileLockReply : public FakePropfindReply
+{
+    Q_OBJECT
+public:
+    FakeFileLockReply(FileInfo &remoteRootFileInfo,
+                      QNetworkAccessManager::Operation op,
+                      const QNetworkRequest &request,
+                      QObject *parent);
+};
+
 // A delayed reply
 template <class OriginalReply>
 class DelayedReply : public OriginalReply

+ 1 - 3
test/testlocaldiscovery.cpp

@@ -597,8 +597,6 @@ private slots:
 
     void testDiscoverLockChanges()
     {
-//        Logger::instance()->setLogDebug(true);
-
         FakeFolder fakeFolder{FileInfo{}};
         fakeFolder.syncEngine().account()->setCapabilities({{"activity", QVariantMap{{"apiv2", QVariantList{"filters", "filters-api", "previews", "rich-strings"}}}},
                                                             {"bruteforce", QVariantMap{{"delay", 0}}},
@@ -626,7 +624,7 @@ private slots:
                                                                       "<nc:lock-owner>user1</nc:lock-owner>"
                                                                       "<nc:lock-owner-displayname>user1</nc:lock-owner-displayname>"
                                                                       "<nc:lock-owner-editor>user1</nc:lock-owner-editor>"
-                                                                      "<nc:lock-time>1648046707</nc:lock-time><oc:size>20020</oc:size>";
+                                                                      "<nc:lock-time>1648046707</nc:lock-time>";
 
         fakeFolder.remoteModifier().mkdir(QStringLiteral("subfolder"));
         fakeFolder.remoteModifier().insert(fooFileSubFolder);

+ 381 - 0
test/testlockfile.cpp

@@ -0,0 +1,381 @@
+#include "lockfilejobs.h"
+
+#include "account.h"
+#include "accountstate.h"
+#include "common/syncjournaldb.h"
+#include "common/syncjournalfilerecord.h"
+#include "syncenginetestutils.h"
+
+#include <QTest>
+#include <QSignalSpy>
+
+class TestLockFile : public QObject
+{
+    Q_OBJECT
+
+public:
+    TestLockFile() = default;
+
+private slots:
+    void initTestCase()
+    {
+    }
+
+    void testLockFile_lockFile_jobSuccess()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobSuccess.wait());
+        QCOMPARE(jobFailure.count(), 0);
+
+        auto fileRecord = OCC::SyncJournalFileRecord{};
+        QVERIFY(fakeFolder.syncJournal().getFileRecord(testFileName, &fileRecord));
+        QCOMPARE(fileRecord._lockstate._locked, true);
+        QCOMPARE(fileRecord._lockstate._lockEditorApp, QString{});
+        QCOMPARE(fileRecord._lockstate._lockOwnerDisplayName, QStringLiteral("John Doe"));
+        QCOMPARE(fileRecord._lockstate._lockOwnerId, QStringLiteral("admin"));
+        QCOMPARE(fileRecord._lockstate._lockOwnerType, static_cast<qint64>(OCC::SyncFileItem::LockOwnerType::UserLock));
+        QCOMPARE(fileRecord._lockstate._lockTime, 1234560);
+        QCOMPARE(fileRecord._lockstate._lockTimeout, 1800);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
+
+    void testLockFile_lockFile_unlockFile_jobSuccess()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto lockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy lockFileJobSuccess(lockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy lockFileJobFailure(lockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        lockFileJob->start();
+
+        QVERIFY(lockFileJobSuccess.wait());
+        QCOMPARE(lockFileJobFailure.count(), 0);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto unlockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy unlockFileJobSuccess(unlockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy unlockFileJobFailure(unlockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        unlockFileJob->start();
+
+        QVERIFY(unlockFileJobSuccess.wait());
+        QCOMPARE(unlockFileJobFailure.count(), 0);
+
+        auto fileRecord = OCC::SyncJournalFileRecord{};
+        QVERIFY(fakeFolder.syncJournal().getFileRecord(testFileName, &fileRecord));
+        QCOMPARE(fileRecord._lockstate._locked, false);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
+
+    void testLockFile_lockFile_alreadyLockedByUser()
+    {
+        static constexpr auto LockedHttpErrorCode = 423;
+        static constexpr auto PreconditionFailedHttpErrorCode = 412;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock>1</nc:lock>\n"
+                                          " <nc:lock-owner-type>0</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>john</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
+            } else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobFailure.wait());
+        QCOMPARE(jobSuccess.count(), 0);
+    }
+
+    void testLockFile_lockFile_alreadyLockedByApp()
+    {
+        static constexpr auto LockedHttpErrorCode = 423;
+        static constexpr auto PreconditionFailedHttpErrorCode = 412;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock>1</nc:lock>\n"
+                                          " <nc:lock-owner-type>1</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>john</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>Text</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
+            } else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobFailure.wait());
+        QCOMPARE(jobSuccess.count(), 0);
+    }
+
+    void testLockFile_unlockFile_alreadyUnlocked()
+    {
+        static constexpr auto LockedHttpErrorCode = 423;
+        static constexpr auto PreconditionFailedHttpErrorCode = 412;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock/>\n"
+                                          " <nc:lock-owner-type>0</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>john</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
+            } else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobSuccess.wait());
+        QCOMPARE(jobFailure.count(), 0);
+    }
+
+    void testLockFile_unlockFile_lockedBySomeoneElse()
+    {
+        static constexpr auto LockedHttpErrorCode = 423;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock>1</nc:lock>\n"
+                                          " <nc:lock-owner-type>0</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>alice</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>Alice Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>Text</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK") ||
+                                                                 request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK"))) {
+                reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobFailure.wait());
+        QCOMPARE(jobSuccess.count(), 0);
+    }
+
+    void testLockFile_lockFile_jobError()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        static constexpr auto InternalServerErrorHttpErrorCode = 500;
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK") ||
+                                                                 request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK"))) {
+                reply = new FakeErrorReply(op, request, nullptr, InternalServerErrorHttpErrorCode, {});
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(QStringLiteral("file.txt"));
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto lockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy lockFileJobSuccess(lockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy lockFileJobFailure(lockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        lockFileJob->start();
+
+        QVERIFY(lockFileJobFailure.wait());
+        QCOMPARE(lockFileJobSuccess.count(), 0);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto unlockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy unlockFileJobSuccess(unlockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy unlockFileJobFailure(unlockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        unlockFileJob->start();
+
+        QVERIFY(unlockFileJobFailure.wait());
+        QCOMPARE(unlockFileJobSuccess.count(), 0);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
+
+    void testLockFile_lockFile_preconditionFailedError()
+    {
+        static constexpr auto PreconditionFailedHttpErrorCode = 412;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock>1</nc:lock>\n"
+                                          " <nc:lock-owner-type>0</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>alice</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>Alice Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>Text</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && (request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK") ||
+                                                                 request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK"))) {
+                reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobFailure.wait());
+        QCOMPARE(jobSuccess.count(), 0);
+    }
+};
+
+QTEST_GUILESS_MAIN(TestLockFile)
+#include "testlockfile.moc"

+ 242 - 0
test/testlockfilejobs.cpp

@@ -0,0 +1,242 @@
+#include "lockfilejobs.h"
+
+#include "account.h"
+#include "accountstate.h"
+#include "common/syncjournaldb.h"
+#include "common/syncjournalfilerecord.h"
+#include "syncenginetestutils.h"
+
+#include <QTest>
+#include <QSignalSpy>
+
+class TestLockFileJobs : public QObject
+{
+    Q_OBJECT
+
+public:
+    TestLockFileJobs() = default;
+
+private slots:
+    void initTestCase()
+    {
+    }
+
+    void testLockFileJob_lockFile_jobSuccess()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobSuccess.wait());
+        QCOMPARE(jobFailure.count(), 0);
+
+        auto fileRecord = OCC::SyncJournalFileRecord{};
+        QVERIFY(fakeFolder.syncJournal().getFileRecord(testFileName, &fileRecord));
+        QCOMPARE(fileRecord._locked, true);
+        QCOMPARE(fileRecord._lockEditorApp, QString{});
+        QCOMPARE(fileRecord._lockOwnerDisplayName, QStringLiteral("John Doe"));
+        QCOMPARE(fileRecord._lockOwnerId, QStringLiteral("john"));
+        QCOMPARE(fileRecord._lockOwnerType, static_cast<qint64>(OCC::SyncFileItem::LockOwnerType::UserLock));
+        QCOMPARE(fileRecord._lockTime, 1234560);
+        QCOMPARE(fileRecord._lockTimeout, 1800);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
+
+    void testLockFileJob_lockFile_unlockFile_jobSuccess()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto lockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy lockFileJobSuccess(lockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy lockFileJobFailure(lockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        lockFileJob->start();
+
+        QVERIFY(lockFileJobSuccess.wait());
+        QCOMPARE(lockFileJobFailure.count(), 0);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto unlockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy unlockFileJobSuccess(unlockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy unlockFileJobFailure(unlockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        unlockFileJob->start();
+
+        QVERIFY(unlockFileJobSuccess.wait());
+        QCOMPARE(unlockFileJobFailure.count(), 0);
+
+        auto fileRecord = OCC::SyncJournalFileRecord{};
+        QVERIFY(fakeFolder.syncJournal().getFileRecord(testFileName, &fileRecord));
+        QCOMPARE(fileRecord._locked, false);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
+
+    void testLockFileJob_lockFile_alreadyLocked()
+    {
+        static constexpr auto LockedHttpErrorCode = 423;
+        static constexpr auto PreconditionFailedHttpErrorCode = 412;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock>1</nc:lock>\n"
+                                          " <nc:lock-owner-type>0</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>john</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
+            } else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobFailure.wait());
+        QCOMPARE(jobSuccess.count(), 0);
+    }
+
+    void testLockFileJob_unlockFile_alreadyUnlocked()
+    {
+        static constexpr auto LockedHttpErrorCode = 423;
+        static constexpr auto PreconditionFailedHttpErrorCode = 412;
+
+        const auto testFileName = QStringLiteral("file.txt");
+
+        const auto replyData = QByteArray("<?xml version=\"1.0\"?>\n"
+                                          "<d:prop xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\" xmlns:oc=\"http://owncloud.org/ns\" xmlns:nc=\"http://nextcloud.org/ns\">\n"
+                                          " <nc:lock/>\n"
+                                          " <nc:lock-owner-type>0</nc:lock-owner-type>\n"
+                                          " <nc:lock-owner>john</nc:lock-owner>\n"
+                                          " <nc:lock-owner-displayname>John Doe</nc:lock-owner-displayname>\n"
+                                          " <nc:lock-owner-editor>john</nc:lock-owner-editor>\n"
+                                          " <nc:lock-time>1650619678</nc:lock-time>\n"
+                                          " <nc:lock-timeout>300</nc:lock-timeout>\n"
+                                          " <nc:lock-token>files_lock/310997d7-0aae-4e48-97e1-eeb6be6e2202</nc:lock-token>\n"
+                                          "</d:prop>\n");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([replyData] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, LockedHttpErrorCode, replyData);
+            } else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, PreconditionFailedHttpErrorCode, replyData);
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto job = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy jobSuccess(job, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy jobFailure(job, &OCC::LockFileJob::finishedWithError);
+
+        job->start();
+
+        QVERIFY(jobFailure.wait());
+        QCOMPARE(jobSuccess.count(), 0);
+    }
+
+    void testLockFileJob_lockFile_jobError()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        static constexpr auto InternalServerErrorHttpErrorCode = 500;
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        fakeFolder.setServerOverride([] (FakeQNAM::Operation op, const QNetworkRequest &request, QIODevice *) {
+            QNetworkReply *reply = nullptr;
+            if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("LOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, InternalServerErrorHttpErrorCode, {});
+            } else if (op == QNetworkAccessManager::CustomOperation && request.attribute(QNetworkRequest::CustomVerbAttribute).toString() == QStringLiteral("UNLOCK")) {
+                reply = new FakeErrorReply(op, request, nullptr, InternalServerErrorHttpErrorCode, {});
+            }
+
+            return reply;
+        });
+
+        fakeFolder.localModifier().insert(QStringLiteral("file.txt"));
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto lockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QSignalSpy lockFileJobSuccess(lockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy lockFileJobFailure(lockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        lockFileJob->start();
+
+        QVERIFY(lockFileJobFailure.wait());
+        QCOMPARE(lockFileJobSuccess.count(), 0);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto unlockFileJob = new OCC::LockFileJob(fakeFolder.account(), &fakeFolder.syncJournal(), QStringLiteral("/") + testFileName, OCC::SyncFileItem::LockStatus::UnlockedItem);
+
+        QSignalSpy unlockFileJobSuccess(unlockFileJob, &OCC::LockFileJob::finishedWithoutError);
+        QSignalSpy unlockFileJobFailure(unlockFileJob, &OCC::LockFileJob::finishedWithError);
+
+        unlockFileJob->start();
+
+        QVERIFY(unlockFileJobFailure.wait());
+        QCOMPARE(unlockFileJobSuccess.count(), 0);
+
+        QVERIFY(fakeFolder.syncOnce());
+    }
+};
+
+QTEST_MAIN(TestLockFileJobs)
+#include "testlockfilejobs.moc"