allow lock/unlock of files from files explorer integration
authorMatthieu Gallien <matthieu.gallien@nextcloud.com>
Wed, 13 Apr 2022 08:47:23 +0000 (10:47 +0200)
committerMatthieu Gallien <matthieu_gallien@yahoo.fr>
Mon, 2 May 2022 11:52:05 +0000 (13:52 +0200)
add new commands to the contextual menu provided by our files explorer
plugins to allow locking/unlocking a file

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
src/gui/socketapi/socketapi.cpp
src/gui/socketapi/socketapi.h
src/libsync/account.cpp
src/libsync/account.h
test/testlockfile.cpp
test/testlockfilejobs.cpp [deleted file]

index d05c75006d43566fcb1da0c8e4e4648213863821..44f9b4e607897992c3e20d6640b6217b171ff8e5 100644 (file)
@@ -958,6 +958,32 @@ void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *)
     solver.setRemoteVersionFilename(target);
 }
 
+void SocketApi::command_LOCK_FILE(const QString &localFile, SocketListener *listener)
+{
+    Q_UNUSED(listener)
+
+    setFileLock(localFile, SyncFileItem::LockStatus::LockedItem);
+}
+
+void SocketApi::command_UNLOCK_FILE(const QString &localFile, SocketListener *listener)
+{
+    Q_UNUSED(listener)
+
+    setFileLock(localFile, SyncFileItem::LockStatus::UnlockedItem);
+}
+
+void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const
+{
+    const auto fileData = FileData::get(localFile);
+
+    const auto shareFolder = fileData.folder;
+    if (!shareFolder || !shareFolder->accountState()->isConnected()) {
+        return;
+    }
+
+    shareFolder->accountState()->account()->setLockFileState(fileData.serverRelativePath, shareFolder->journalDb(), lockState);
+}
+
 void SocketApi::command_V2_LIST_ACCOUNTS(const QSharedPointer<SocketApiJobV2> &job) const
 {
     QJsonArray out;
@@ -1047,6 +1073,39 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi
     //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email …"));
 }
 
+void SocketApi::sendLockFileCommandMenuEntries(const QFileInfo &fileInfo,
+                                               Folder* const syncFolder,
+                                               const FileData &fileData,
+                                               const OCC::SocketListener* const listener) const
+{
+    if (!fileInfo.isDir() && syncFolder->accountState()->account()->capabilities().filesLockAvailable()) {
+        if (syncFolder->accountState()->account()->fileLockStatus(syncFolder->journalDb(), fileData.folderRelativePath) == SyncFileItem::LockStatus::UnlockedItem) {
+            listener->sendMessage(QLatin1String("MENU_ITEM:LOCK_FILE::") + tr("Lock file"));
+        } else {
+            if (syncFolder->accountState()->account()->fileCanBeUnlocked(syncFolder->journalDb(), fileData.folderRelativePath)) {
+                listener->sendMessage(QLatin1String("MENU_ITEM:UNLOCK_FILE::") + tr("Unlock file"));
+            }
+        }
+    }
+}
+
+void SocketApi::sendLockFileInfoMenuEntries(const QFileInfo &fileInfo,
+                                            Folder * const syncFolder,
+                                            const FileData &fileData,
+                                            const SocketListener * const listener,
+                                            const SyncJournalFileRecord &record) const
+{
+    static constexpr auto SECONDS_PER_MINUTE = 60;
+    if (!fileInfo.isDir() && syncFolder->accountState()->account()->capabilities().filesLockAvailable() &&
+            syncFolder->accountState()->account()->fileLockStatus(syncFolder->journalDb(), fileData.folderRelativePath) == SyncFileItem::LockStatus::LockedItem) {
+        listener->sendMessage(QLatin1String("MENU_ITEM:LOCKED_FILE_OWNER:d:") + tr("Locked by %1").arg(record._lockstate._lockOwnerDisplayName));
+        const auto lockExpirationTime = record._lockstate._lockTime + record._lockstate._lockTimeout;
+        const auto remainingTime = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime));
+        const auto remainingTimeInMinute = static_cast<int>(remainingTime > 0 ? remainingTime / SECONDS_PER_MINUTE : 0);
+        listener->sendMessage(QLatin1String("MENU_ITEM:LOCKED_FILE_DATE:d:") + tr("Expire in %1 minutes", "remaining time before lock expire", remainingTimeInMinute).arg(remainingTimeInMinute));
+    }
+}
+
 SocketApi::FileData SocketApi::FileData::get(const QString &localFile)
 {
     FileData data;
@@ -1133,6 +1192,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
         auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
 
         const QFileInfo fileInfo(fileData.localPath);
+        sendLockFileInfoMenuEntries(fileInfo, syncFolder, fileData, listener, record);
         if (!fileInfo.isDir()) {
             listener->sendMessage(QLatin1String("MENU_ITEM:ACTIVITY") + flagString + tr("Activity"));
         }
@@ -1145,6 +1205,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
             listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser"));
         }
 
+        sendLockFileCommandMenuEntries(fileInfo, syncFolder, fileData, listener);
         sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
 
         // Conflict files get conflict resolution actions
index 11f8836f46108c7d21cfa6b6b8b8f11bdd46c46d..1f36d6cef08fb8a6d7d1c064e91f8001a4d25819 100644 (file)
@@ -27,6 +27,7 @@
 class QUrl;
 class QLocalSocket;
 class QStringList;
+class QFileInfo;
 
 namespace OCC {
 
@@ -124,6 +125,10 @@ private:
     Q_INVOKABLE void command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *listener);
     Q_INVOKABLE void command_DELETE_ITEM(const QString &localFile, SocketListener *listener);
     Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_LOCK_FILE(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_UNLOCK_FILE(const QString &localFile, SocketListener *listener);
+
+    void setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const;
 
     // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
 #ifdef Q_OS_WIN
@@ -145,6 +150,17 @@ private:
     // Sends the context menu options relating to sharing to listener
     void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled);
 
+    void sendLockFileCommandMenuEntries(const QFileInfo &fileInfo,
+                                        Folder * const syncFolder,
+                                        const FileData &fileData,
+                                        const SocketListener * const listener) const;
+
+    void sendLockFileInfoMenuEntries(const QFileInfo &fileInfo,
+                                     Folder * const syncFolder,
+                                     const FileData &fileData,
+                                     const SocketListener * const listener,
+                                     const SyncJournalFileRecord &record) const;
+
     /** Send the list of menu item. (added in version 1.1)
      * argument is a list of files for which the menu should be shown, separated by '\x1e'
      * Reply with  GET_MENU_ITEMS:BEGIN
index 27e4d997dace13c177d51594de77d0aa2a9029c1..bdc08c9ba5f654b8e5cb4bbcc6166b75ccf86ec4 100644 (file)
 #include "pushnotifications.h"
 #include "version.h"
 
-#include <deletejob.h>
+#include "deletejob.h"
+#include "lockfilejobs.h"
 
+#include "common/syncjournaldb.h"
 #include "common/asserts.h"
 #include "clientsideencryption.h"
 #include "ocsuserstatusconnector.h"
@@ -113,6 +115,11 @@ AccountPtr Account::sharedFromThis()
     return _sharedThis.toStrongRef();
 }
 
+AccountPtr Account::sharedFromThis() const
+{
+    return _sharedThis.toStrongRef();
+}
+
 QString Account::davUser() const
 {
     return _davUser.isEmpty() && _credentials ? _credentials->user() : _davUser;
@@ -850,4 +857,58 @@ std::shared_ptr<UserStatusConnector> Account::userStatusConnector() const
     return _userStatusConnector;
 }
 
+void Account::setLockFileState(const QString &serverRelativePath,
+                               SyncJournalDb * const journal,
+                               const SyncFileItem::LockStatus lockStatus)
+{
+    auto job = std::make_unique<LockFileJob>(sharedFromThis(), journal, serverRelativePath, lockStatus);
+    connect(job.get(), &LockFileJob::finishedWithoutError, this, [this]() {
+        Q_EMIT lockFileSuccess();
+    });
+    connect(job.get(), &LockFileJob::finishedWithError, this, [lockStatus, serverRelativePath, this](const int httpErrorCode, const QString &errorString, const QString &lockOwnerName) {
+        auto errorMessage = QString{};
+        const auto filePath = serverRelativePath.mid(1);
+
+        if (httpErrorCode == LockFileJob::LOCKED_HTTP_ERROR_CODE) {
+            errorMessage = tr("File %1 is already locked by %2.").arg(filePath, lockOwnerName);
+        } else if (lockStatus == SyncFileItem::LockStatus::LockedItem) {
+             errorMessage = tr("Lock operation on %1 failed with error %2").arg(filePath, errorString);
+        } else if (lockStatus == SyncFileItem::LockStatus::UnlockedItem) {
+             errorMessage = tr("Unlock operation on %1 failed with error %2").arg(filePath, errorString);
+        }
+        Q_EMIT lockFileError(errorMessage);
+    });
+    job->start();
+    static_cast<void>(job.release());
+}
+
+SyncFileItem::LockStatus Account::fileLockStatus(SyncJournalDb * const journal,
+                                                 const QString &folderRelativePath) const
+{
+    SyncJournalFileRecord record;
+    if (journal->getFileRecord(folderRelativePath, &record)) {
+        return record._lockstate._locked ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem;
+    }
+
+    return SyncFileItem::LockStatus::UnlockedItem;
+}
+
+bool Account::fileCanBeUnlocked(SyncJournalDb * const journal,
+                                const QString &folderRelativePath) const
+{
+    SyncJournalFileRecord record;
+    if (journal->getFileRecord(folderRelativePath, &record)) {
+        if (record._lockstate._lockOwnerType != static_cast<int>(SyncFileItem::LockOwnerType::UserLock)) {
+            return false;
+        }
+
+        if (record._lockstate._lockOwnerId != sharedFromThis()->davUser()) {
+            return false;
+        }
+
+        return true;
+    }
+    return false;
+}
+
 } // namespace OCC
index c9d1ba551f541c11a635e3999e0a9e75886c1ed7..d211d77d014a36a76c8b11de64e7303235df47f3 100644 (file)
@@ -35,6 +35,7 @@
 #include <memory>
 #include "capabilities.h"
 #include "clientsideencryption.h"
+#include "syncfileitem.h"
 
 class QSettings;
 class QNetworkReply;
@@ -56,6 +57,7 @@ class AccessManager;
 class SimpleNetworkJob;
 class PushNotifications;
 class UserStatusConnector;
+class SyncJournalDb;
 
 /**
  * @brief Reimplement this to handle SSL errors from libsync
@@ -89,6 +91,8 @@ public:
 
     AccountPtr sharedFromThis();
 
+    AccountPtr sharedFromThis() const;
+
     /**
      * The user that can be used in dav url.
      *
@@ -275,6 +279,15 @@ public:
 
     std::shared_ptr<UserStatusConnector> userStatusConnector() const;
 
+    void setLockFileState(const QString &serverRelativePath,
+                          SyncJournalDb * const journal,
+                          const SyncFileItem::LockStatus lockStatus);
+
+    SyncFileItem::LockStatus fileLockStatus(SyncJournalDb * const journal,
+                                            const QString &folderRelativePath) const;
+
+    bool fileCanBeUnlocked(SyncJournalDb * const journal, const QString &folderRelativePath) const;
+
 public slots:
     /// Used when forgetting credentials
     void clearQNAMCache();
@@ -311,6 +324,9 @@ signals:
 
     void capabilitiesChanged();
 
+    void lockFileSuccess();
+    void lockFileError(const QString&);
+
 protected Q_SLOTS:
     void slotCredentialsFetched();
     void slotCredentialsAsked();
index 78e76e6a83aa30ca7860c13464e629c6789ecd6c..97538295e5b4ff57c44a9024511ff132e07f529c 100644 (file)
@@ -21,6 +21,113 @@ private slots:
     {
     }
 
+    void testLockFile_lockFile_lockSuccess()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
+        QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QVERIFY(lockFileSuccessSpy.wait());
+        QCOMPARE(lockFileErrorSpy.count(), 0);
+    }
+
+    void testLockFile_lockFile_lockError()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+        static constexpr auto LockedHttpErrorCode = 423;
+        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);
+            }
+
+            return reply;
+        });
+
+        QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
+        QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QVERIFY(lockFileErrorSpy.wait());
+        QCOMPARE(lockFileSuccessSpy.count(), 0);
+    }
+
+    void testLockFile_fileLockStatus_queryLockStatus()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
+        QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QVERIFY(lockFileSuccessSpy.wait());
+        QCOMPARE(lockFileErrorSpy.count(), 0);
+
+        auto lockStatus = fakeFolder.account()->fileLockStatus(&fakeFolder.syncJournal(), testFileName);
+        QCOMPARE(lockStatus, OCC::SyncFileItem::LockStatus::LockedItem);
+    }
+
+    void testLockFile_fileCanBeUnlocked_canUnlock()
+    {
+        const auto testFileName = QStringLiteral("file.txt");
+
+        FakeFolder fakeFolder{FileInfo{}};
+        QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+        QSignalSpy lockFileSuccessSpy(fakeFolder.account().data(), &OCC::Account::lockFileSuccess);
+        QSignalSpy lockFileErrorSpy(fakeFolder.account().data(), &OCC::Account::lockFileError);
+
+        fakeFolder.localModifier().insert(testFileName);
+
+        QVERIFY(fakeFolder.syncOnce());
+
+        fakeFolder.account()->setLockFileState(QStringLiteral("/") + testFileName, &fakeFolder.syncJournal(), OCC::SyncFileItem::LockStatus::LockedItem);
+
+        QVERIFY(lockFileSuccessSpy.wait());
+        QCOMPARE(lockFileErrorSpy.count(), 0);
+
+        auto lockStatus = fakeFolder.account()->fileCanBeUnlocked(&fakeFolder.syncJournal(), testFileName);
+        QCOMPARE(lockStatus, true);
+    }
+
     void testLockFile_lockFile_jobSuccess()
     {
         const auto testFileName = QStringLiteral("file.txt");
diff --git a/test/testlockfilejobs.cpp b/test/testlockfilejobs.cpp
deleted file mode 100644 (file)
index 6e0b5f7..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-#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"