"Fossies" - the Fresh Open Source Software Archive

Member "ownCloud-2.7.6.3261/test/testsyncengine.cpp" (5 Feb 2021, 33127 Bytes) of package /linux/misc/ownCloud-2.7.6.3261.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "testsyncengine.cpp": 2.7.5.3180_vs_2.7.6.3261.

    1 /*
    2  *    This software is in the public domain, furnished "as is", without technical
    3  *    support, and with no warranty, express or implied, as to its usefulness for
    4  *    any purpose.
    5  *
    6  */
    7 
    8 #include <QtTest>
    9 #include "syncenginetestutils.h"
   10 #include <syncengine.h>
   11 
   12 using namespace OCC;
   13 
   14 bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
   15 {
   16     if (auto item = spy.findItem(path)) {
   17         return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA;
   18     }
   19     return false;
   20 }
   21 
   22 bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
   23 {
   24     auto item = spy.findItem(path);
   25     return item->_instruction == instr;
   26 }
   27 
   28 bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path)
   29 {
   30     if (auto item = spy.findItem(path)) {
   31         return item->_status == SyncFileItem::Success;
   32     }
   33     return false;
   34 }
   35 
   36 class TestSyncEngine : public QObject
   37 {
   38     Q_OBJECT
   39 
   40 private slots:
   41     void testFileDownload() {
   42         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
   43         ItemCompletedSpy completeSpy(fakeFolder);
   44         fakeFolder.remoteModifier().insert("A/a0");
   45         fakeFolder.syncOnce();
   46         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
   47         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
   48     }
   49 
   50     void testFileUpload() {
   51         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
   52         ItemCompletedSpy completeSpy(fakeFolder);
   53         fakeFolder.localModifier().insert("A/a0");
   54         fakeFolder.syncOnce();
   55         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a0"));
   56         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
   57     }
   58 
   59     void testDirDownload() {
   60         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
   61         ItemCompletedSpy completeSpy(fakeFolder);
   62         fakeFolder.remoteModifier().mkdir("Y");
   63         fakeFolder.remoteModifier().mkdir("Z");
   64         fakeFolder.remoteModifier().insert("Z/d0");
   65         fakeFolder.syncOnce();
   66         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y"));
   67         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z"));
   68         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
   69         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
   70     }
   71 
   72     void testDirUpload() {
   73         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
   74         ItemCompletedSpy completeSpy(fakeFolder);
   75         fakeFolder.localModifier().mkdir("Y");
   76         fakeFolder.localModifier().mkdir("Z");
   77         fakeFolder.localModifier().insert("Z/d0");
   78         fakeFolder.syncOnce();
   79         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Y"));
   80         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z"));
   81         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "Z/d0"));
   82         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
   83     }
   84 
   85     void testLocalDelete() {
   86         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
   87         ItemCompletedSpy completeSpy(fakeFolder);
   88         fakeFolder.remoteModifier().remove("A/a1");
   89         fakeFolder.syncOnce();
   90         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1"));
   91         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
   92     }
   93 
   94     void testRemoteDelete() {
   95         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
   96         ItemCompletedSpy completeSpy(fakeFolder);
   97         fakeFolder.localModifier().remove("A/a1");
   98         fakeFolder.syncOnce();
   99         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a1"));
  100         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  101     }
  102 
  103     void testEmlLocalChecksum() {
  104         FakeFolder fakeFolder{FileInfo{}};
  105         fakeFolder.localModifier().insert("a1.eml", 64, 'A');
  106         fakeFolder.localModifier().insert("a2.eml", 64, 'A');
  107         fakeFolder.localModifier().insert("a3.eml", 64, 'A');
  108         fakeFolder.localModifier().insert("b3.txt", 64, 'A');
  109         // Upload and calculate the checksums
  110         // fakeFolder.syncOnce();
  111         fakeFolder.syncOnce();
  112 
  113         auto getDbChecksum = [&](QString path) {
  114             SyncJournalFileRecord record;
  115             fakeFolder.syncJournal().getFileRecord(path, &record);
  116             return record._checksumHeader;
  117         };
  118 
  119         // printf 'A%.0s' {1..64} | sha1sum -
  120         QByteArray referenceChecksum("SHA1:30b86e44e6001403827a62c58b08893e77cf121f");
  121         QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum);
  122         QCOMPARE(getDbChecksum("a2.eml"), referenceChecksum);
  123         QCOMPARE(getDbChecksum("a3.eml"), referenceChecksum);
  124         QCOMPARE(getDbChecksum("b3.txt"), referenceChecksum);
  125 
  126         ItemCompletedSpy completeSpy(fakeFolder);
  127         // Touch the file without changing the content, shouldn't upload
  128         fakeFolder.localModifier().setContents("a1.eml", 'A');
  129         // Change the content/size
  130         fakeFolder.localModifier().setContents("a2.eml", 'B');
  131         fakeFolder.localModifier().appendByte("a3.eml");
  132         fakeFolder.localModifier().appendByte("b3.txt");
  133         fakeFolder.syncOnce();
  134 
  135         QCOMPARE(getDbChecksum("a1.eml"), referenceChecksum);
  136         QCOMPARE(getDbChecksum("a2.eml"), QByteArray("SHA1:84951fc23a4dafd10020ac349da1f5530fa65949"));
  137         QCOMPARE(getDbChecksum("a3.eml"), QByteArray("SHA1:826b7e7a7af8a529ae1c7443c23bf185c0ad440c"));
  138         QCOMPARE(getDbChecksum("b3.eml"), getDbChecksum("a3.txt"));
  139 
  140         QVERIFY(!itemDidComplete(completeSpy, "a1.eml"));
  141         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a2.eml"));
  142         QVERIFY(itemDidCompleteSuccessfully(completeSpy, "a3.eml"));
  143         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  144     }
  145 
  146     void testSelectiveSyncBug() {
  147         // issue owncloud/enterprise#1965: files from selective-sync ignored
  148         // folders are uploaded anyway is some circumstances.
  149         FakeFolder fakeFolder{FileInfo{ QString(), {
  150             FileInfo { QStringLiteral("parentFolder"), {
  151                 FileInfo{ QStringLiteral("subFolderA"), {
  152                     { QStringLiteral("fileA.txt"), 400 },
  153                     { QStringLiteral("fileB.txt"), 400, 'o' },
  154                     FileInfo { QStringLiteral("subsubFolder"), {
  155                         { QStringLiteral("fileC.txt"), 400 },
  156                         { QStringLiteral("fileD.txt"), 400, 'o' }
  157                     }},
  158                     FileInfo{ QStringLiteral("anotherFolder"), {
  159                         FileInfo { QStringLiteral("emptyFolder"), { } },
  160                         FileInfo { QStringLiteral("subsubFolder"), {
  161                             { QStringLiteral("fileE.txt"), 400 },
  162                             { QStringLiteral("fileF.txt"), 400, 'o' }
  163                         }}
  164                     }}
  165                 }},
  166                 FileInfo{ QStringLiteral("subFolderB"), {} }
  167             }}
  168         }}};
  169 
  170         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  171         auto expectedServerState = fakeFolder.currentRemoteState();
  172 
  173         // Remove subFolderA with selectiveSync:
  174         fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
  175                                                                 {"parentFolder/subFolderA/"});
  176         fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/"));
  177         auto getEtag = [&](const QByteArray &file) {
  178             SyncJournalFileRecord rec;
  179             fakeFolder.syncJournal().getFileRecord(file, &rec);
  180             return rec._etag;
  181         };
  182         QVERIFY(getEtag("parentFolder") == "_invalid_");
  183         QVERIFY(getEtag("parentFolder/subFolderA") == "_invalid_");
  184         QVERIFY(getEtag("parentFolder/subFolderA/subsubFolder") != "_invalid_");
  185 
  186         // But touch local file before the next sync, such that the local folder
  187         // can't be removed
  188         fakeFolder.localModifier().setContents("parentFolder/subFolderA/fileB.txt", 'n');
  189         fakeFolder.localModifier().setContents("parentFolder/subFolderA/subsubFolder/fileD.txt", 'n');
  190         fakeFolder.localModifier().setContents("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt", 'n');
  191 
  192         // Several follow-up syncs don't change the state at all,
  193         // in particular the remote state doesn't change and fileB.txt
  194         // isn't uploaded.
  195 
  196         for (int i = 0; i < 3; ++i) {
  197             fakeFolder.syncOnce();
  198 
  199             {
  200                 // Nothing changed on the server
  201                 QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  202                 // The local state should still have subFolderA
  203                 auto local = fakeFolder.currentLocalState();
  204                 QVERIFY(local.find("parentFolder/subFolderA"));
  205                 QVERIFY(!local.find("parentFolder/subFolderA/fileA.txt"));
  206                 QVERIFY(local.find("parentFolder/subFolderA/fileB.txt"));
  207                 QVERIFY(!local.find("parentFolder/subFolderA/subsubFolder/fileC.txt"));
  208                 QVERIFY(local.find("parentFolder/subFolderA/subsubFolder/fileD.txt"));
  209                 QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileE.txt"));
  210                 QVERIFY(local.find("parentFolder/subFolderA/anotherFolder/subsubFolder/fileF.txt"));
  211                 QVERIFY(!local.find("parentFolder/subFolderA/anotherFolder/emptyFolder"));
  212                 QVERIFY(local.find("parentFolder/subFolderB"));
  213             }
  214         }
  215     }
  216 
  217     void abortAfterFailedMkdir() {
  218         FakeFolder fakeFolder{FileInfo{}};
  219         QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool)));
  220         fakeFolder.serverErrorPaths().append("NewFolder");
  221         fakeFolder.localModifier().mkdir("NewFolder");
  222         // This should be aborted and would otherwise fail in FileInfo::create.
  223         fakeFolder.localModifier().insert("NewFolder/NewFile");
  224         fakeFolder.syncOnce();
  225         QCOMPARE(finishedSpy.size(), 1);
  226         QCOMPARE(finishedSpy.first().first().toBool(), false);
  227     }
  228 
  229     /** Verify that an incompletely propagated directory doesn't have the server's
  230      * etag stored in the database yet. */
  231     void testDirEtagAfterIncompleteSync() {
  232         FakeFolder fakeFolder{FileInfo{}};
  233         QSignalSpy finishedSpy(&fakeFolder.syncEngine(), SIGNAL(finished(bool)));
  234         fakeFolder.serverErrorPaths().append("NewFolder/foo");
  235         fakeFolder.remoteModifier().mkdir("NewFolder");
  236         fakeFolder.remoteModifier().insert("NewFolder/foo");
  237         QVERIFY(!fakeFolder.syncOnce());
  238 
  239         SyncJournalFileRecord rec;
  240         fakeFolder.syncJournal().getFileRecord(QByteArrayLiteral("NewFolder"), &rec);
  241         QVERIFY(rec.isValid());
  242         QCOMPARE(rec._etag, QByteArrayLiteral("_invalid_"));
  243         QVERIFY(!rec._fileId.isEmpty());
  244     }
  245 
  246     void testDirDownloadWithError() {
  247         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  248         ItemCompletedSpy completeSpy(fakeFolder);
  249         fakeFolder.remoteModifier().mkdir("Y");
  250         fakeFolder.remoteModifier().mkdir("Y/Z");
  251         fakeFolder.remoteModifier().insert("Y/Z/d0");
  252         fakeFolder.remoteModifier().insert("Y/Z/d1");
  253         fakeFolder.remoteModifier().insert("Y/Z/d2");
  254         fakeFolder.remoteModifier().insert("Y/Z/d3");
  255         fakeFolder.remoteModifier().insert("Y/Z/d4");
  256         fakeFolder.remoteModifier().insert("Y/Z/d5");
  257         fakeFolder.remoteModifier().insert("Y/Z/d6");
  258         fakeFolder.remoteModifier().insert("Y/Z/d7");
  259         fakeFolder.remoteModifier().insert("Y/Z/d8");
  260         fakeFolder.remoteModifier().insert("Y/Z/d9");
  261         fakeFolder.serverErrorPaths().append("Y/Z/d2", 503);
  262         fakeFolder.serverErrorPaths().append("Y/Z/d3", 503);
  263         QVERIFY(!fakeFolder.syncOnce());
  264         QCoreApplication::processEvents(); // should not crash
  265 
  266         QSet<QString> seen;
  267         for(const QList<QVariant> &args : completeSpy) {
  268             auto item = args[0].value<SyncFileItemPtr>();
  269             qDebug() << item->_file << item->isDirectory() << item->_status;
  270             QVERIFY(!seen.contains(item->_file)); // signal only sent once per item
  271             seen.insert(item->_file);
  272             if (item->_file == "Y/Z/d2") {
  273                 QVERIFY(item->_status == SyncFileItem::NormalError);
  274             } else if (item->_file == "Y/Z/d3") {
  275                 QVERIFY(item->_status != SyncFileItem::Success);
  276             } else if (!item->isDirectory()) {
  277                 QVERIFY(item->_status == SyncFileItem::Success);
  278             }
  279         }
  280     }
  281 
  282     void testFakeConflict_data()
  283     {
  284         QTest::addColumn<bool>("sameMtime");
  285         QTest::addColumn<QByteArray>("checksums");
  286 
  287         QTest::addColumn<int>("expectedGET");
  288 
  289         QTest::newRow("Same mtime, but no server checksum -> ignored in reconcile")
  290             << true << QByteArray()
  291             << 0;
  292 
  293         QTest::newRow("Same mtime, weak server checksum differ -> downloaded")
  294             << true << QByteArray("Adler32:bad")
  295             << 1;
  296 
  297         QTest::newRow("Same mtime, matching weak checksum -> skipped")
  298             << true << QByteArray("Adler32:2a2010d")
  299             << 0;
  300 
  301         QTest::newRow("Same mtime, strong server checksum differ -> downloaded")
  302             << true << QByteArray("SHA1:bad")
  303             << 1;
  304 
  305         QTest::newRow("Same mtime, matching strong checksum -> skipped")
  306             << true << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  307             << 0;
  308 
  309 
  310         QTest::newRow("mtime changed, but no server checksum -> download")
  311             << false << QByteArray()
  312             << 1;
  313 
  314         QTest::newRow("mtime changed, weak checksum match -> download anyway")
  315             << false << QByteArray("Adler32:2a2010d")
  316             << 1;
  317 
  318         QTest::newRow("mtime changed, strong checksum match -> skip")
  319             << false << QByteArray("SHA1:56900fb1d337cf7237ff766276b9c1e8ce507427")
  320             << 0;
  321     }
  322 
  323     void testFakeConflict()
  324     {
  325         QFETCH(bool, sameMtime);
  326         QFETCH(QByteArray, checksums);
  327         QFETCH(int, expectedGET);
  328 
  329         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  330 
  331         int nGET = 0;
  332         fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  333             if (op == QNetworkAccessManager::GetOperation)
  334                 ++nGET;
  335             return nullptr;
  336         });
  337 
  338         // For directly editing the remote checksum
  339         FileInfo &remoteInfo = fakeFolder.remoteModifier();
  340 
  341         // Base mtime with no ms content (filesystem is seconds only)
  342         auto mtime = QDateTime::currentDateTimeUtc().addDays(-4);
  343         mtime.setMSecsSinceEpoch(mtime.toMSecsSinceEpoch() / 1000 * 1000);
  344 
  345         fakeFolder.localModifier().setContents("A/a1", 'C');
  346         fakeFolder.localModifier().setModTime("A/a1", mtime);
  347         fakeFolder.remoteModifier().setContents("A/a1", 'C');
  348         if (!sameMtime)
  349             mtime = mtime.addDays(1);
  350         fakeFolder.remoteModifier().setModTime("A/a1", mtime);
  351         remoteInfo.find("A/a1")->checksums = checksums;
  352         QVERIFY(fakeFolder.syncOnce());
  353         QCOMPARE(nGET, expectedGET);
  354 
  355         // check that mtime in journal and filesystem agree
  356         QString a1path = fakeFolder.localPath() + "A/a1";
  357         SyncJournalFileRecord a1record;
  358         fakeFolder.syncJournal().getFileRecord(QByteArray("A/a1"), &a1record);
  359         QCOMPARE(a1record._modtime, (qint64)FileSystem::getModTime(a1path));
  360 
  361         // Extra sync reads from db, no difference
  362         QVERIFY(fakeFolder.syncOnce());
  363         QCOMPARE(nGET, expectedGET);
  364     }
  365 
  366     /**
  367      * Checks whether SyncFileItems have the expected properties before start
  368      * of propagation.
  369      */
  370     void testSyncFileItemProperties()
  371     {
  372         auto initialMtime = QDateTime::currentDateTimeUtc().addDays(-7);
  373         auto changedMtime = QDateTime::currentDateTimeUtc().addDays(-4);
  374         auto changedMtime2 = QDateTime::currentDateTimeUtc().addDays(-3);
  375 
  376         // Base mtime with no ms content (filesystem is seconds only)
  377         initialMtime.setMSecsSinceEpoch(initialMtime.toMSecsSinceEpoch() / 1000 * 1000);
  378         changedMtime.setMSecsSinceEpoch(changedMtime.toMSecsSinceEpoch() / 1000 * 1000);
  379         changedMtime2.setMSecsSinceEpoch(changedMtime2.toMSecsSinceEpoch() / 1000 * 1000);
  380 
  381         // Ensure the initial mtimes are as expected
  382         auto initialFileInfo = FileInfo::A12_B12_C12_S12();
  383         initialFileInfo.setModTime("A/a1", initialMtime);
  384         initialFileInfo.setModTime("B/b1", initialMtime);
  385         initialFileInfo.setModTime("C/c1", initialMtime);
  386 
  387         FakeFolder fakeFolder{ initialFileInfo };
  388 
  389 
  390         // upload a
  391         fakeFolder.localModifier().appendByte("A/a1");
  392         fakeFolder.localModifier().setModTime("A/a1", changedMtime);
  393         // download b
  394         fakeFolder.remoteModifier().appendByte("B/b1");
  395         fakeFolder.remoteModifier().setModTime("B/b1", changedMtime);
  396         // conflict c
  397         fakeFolder.localModifier().appendByte("C/c1");
  398         fakeFolder.localModifier().appendByte("C/c1");
  399         fakeFolder.localModifier().setModTime("C/c1", changedMtime);
  400         fakeFolder.remoteModifier().appendByte("C/c1");
  401         fakeFolder.remoteModifier().setModTime("C/c1", changedMtime2);
  402 
  403         connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) {
  404             SyncFileItemPtr a1, b1, c1;
  405             for (auto &item : items) {
  406                 if (item->_file == "A/a1")
  407                     a1 = item;
  408                 if (item->_file == "B/b1")
  409                     b1 = item;
  410                 if (item->_file == "C/c1")
  411                     c1 = item;
  412             }
  413 
  414             // a1: should have local size and modtime
  415             QVERIFY(a1);
  416             QCOMPARE(a1->_instruction, CSYNC_INSTRUCTION_SYNC);
  417             QCOMPARE(a1->_direction, SyncFileItem::Up);
  418             QCOMPARE(a1->_size, qint64(5));
  419 
  420             QCOMPARE(Utility::qDateTimeFromTime_t(a1->_modtime), changedMtime);
  421             QCOMPARE(a1->_previousSize, qint64(4));
  422             QCOMPARE(Utility::qDateTimeFromTime_t(a1->_previousModtime), initialMtime);
  423 
  424             // b2: should have remote size and modtime
  425             QVERIFY(b1);
  426             QCOMPARE(b1->_instruction, CSYNC_INSTRUCTION_SYNC);
  427             QCOMPARE(b1->_direction, SyncFileItem::Down);
  428             QCOMPARE(b1->_size, qint64(17));
  429             QCOMPARE(Utility::qDateTimeFromTime_t(b1->_modtime), changedMtime);
  430             QCOMPARE(b1->_previousSize, qint64(16));
  431             QCOMPARE(Utility::qDateTimeFromTime_t(b1->_previousModtime), initialMtime);
  432 
  433             // c1: conflicts are downloads, so remote size and modtime
  434             QVERIFY(c1);
  435             QCOMPARE(c1->_instruction, CSYNC_INSTRUCTION_CONFLICT);
  436             QCOMPARE(c1->_direction, SyncFileItem::None);
  437             QCOMPARE(c1->_size, qint64(25));
  438             QCOMPARE(Utility::qDateTimeFromTime_t(c1->_modtime), changedMtime2);
  439             QCOMPARE(c1->_previousSize, qint64(26));
  440             QCOMPARE(Utility::qDateTimeFromTime_t(c1->_previousModtime), changedMtime);
  441         });
  442 
  443         QVERIFY(fakeFolder.syncOnce());
  444     }
  445 
  446     /**
  447      * Checks whether subsequent large uploads are skipped after a 507 error
  448      */
  449     void testInsufficientRemoteStorage()
  450     {
  451         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  452 
  453         // Disable parallel uploads
  454         SyncOptions syncOptions;
  455         syncOptions._parallelNetworkJobs = 0;
  456         fakeFolder.syncEngine().setSyncOptions(syncOptions);
  457 
  458         // Produce an error based on upload size
  459         int remoteQuota = 1000;
  460         int n507 = 0, nPUT = 0;
  461         QObject parent;
  462         fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  463             if (op == QNetworkAccessManager::PutOperation) {
  464                 nPUT++;
  465                 if (request.rawHeader("OC-Total-Length").toInt() > remoteQuota) {
  466                     n507++;
  467                     return new FakeErrorReply(op, request, &parent, 507);
  468                 }
  469             }
  470             return nullptr;
  471         });
  472 
  473         fakeFolder.localModifier().insert("A/big", 800);
  474         QVERIFY(fakeFolder.syncOnce());
  475         QCOMPARE(nPUT, 1);
  476         QCOMPARE(n507, 0);
  477 
  478         nPUT = 0;
  479         fakeFolder.localModifier().insert("A/big1", 500); // ok
  480         fakeFolder.localModifier().insert("A/big2", 1200); // 507 (quota guess now 1199)
  481         fakeFolder.localModifier().insert("A/big3", 1200); // skipped
  482         fakeFolder.localModifier().insert("A/big4", 1500); // skipped
  483         fakeFolder.localModifier().insert("A/big5", 1100); // 507 (quota guess now 1099)
  484         fakeFolder.localModifier().insert("A/big6", 900); // ok (quota guess now 199)
  485         fakeFolder.localModifier().insert("A/big7", 200); // skipped
  486         fakeFolder.localModifier().insert("A/big8", 199); // ok (quota guess now 0)
  487 
  488         fakeFolder.localModifier().insert("B/big8", 1150); // 507
  489         QVERIFY(!fakeFolder.syncOnce());
  490         QCOMPARE(nPUT, 6);
  491         QCOMPARE(n507, 3);
  492     }
  493 
  494     // Checks whether downloads with bad checksums are accepted
  495     void testChecksumValidation()
  496     {
  497         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  498         QObject parent;
  499 
  500         QByteArray checksumValue;
  501         QByteArray contentMd5Value;
  502 
  503         fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  504             if (op == QNetworkAccessManager::GetOperation) {
  505                 auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
  506                 if (!checksumValue.isNull())
  507                     reply->setRawHeader("OC-Checksum", checksumValue);
  508                 if (!contentMd5Value.isNull())
  509                     reply->setRawHeader("Content-MD5", contentMd5Value);
  510                 return reply;
  511             }
  512             return nullptr;
  513         });
  514 
  515         // Basic case
  516         fakeFolder.remoteModifier().create("A/a3", 16, 'A');
  517         QVERIFY(fakeFolder.syncOnce());
  518         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  519 
  520         // Bad OC-Checksum
  521         checksumValue = "SHA1:bad";
  522         fakeFolder.remoteModifier().create("A/a4", 16, 'A');
  523         QVERIFY(!fakeFolder.syncOnce());
  524 
  525         // Good OC-Checksum
  526         checksumValue = "SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"; // printf 'A%.0s' {1..16} | sha1sum -
  527         QVERIFY(fakeFolder.syncOnce());
  528         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  529         checksumValue = QByteArray();
  530 
  531         // Bad Content-MD5
  532         contentMd5Value = "bad";
  533         fakeFolder.remoteModifier().create("A/a5", 16, 'A');
  534         QVERIFY(!fakeFolder.syncOnce());
  535 
  536         // Good Content-MD5
  537         contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum -
  538         QVERIFY(fakeFolder.syncOnce());
  539         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  540 
  541         // Invalid OC-Checksum is ignored
  542         checksumValue = "garbage";
  543         // contentMd5Value is still good
  544         fakeFolder.remoteModifier().create("A/a6", 16, 'A');
  545         QVERIFY(fakeFolder.syncOnce());
  546         contentMd5Value = "bad";
  547         fakeFolder.remoteModifier().create("A/a7", 16, 'A');
  548         QVERIFY(!fakeFolder.syncOnce());
  549         contentMd5Value.clear();
  550         QVERIFY(fakeFolder.syncOnce());
  551         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  552 
  553         // OC-Checksum contains Unsupported checksums
  554         checksumValue = "Unsupported:XXXX SHA1:invalid Invalid:XxX";
  555         fakeFolder.remoteModifier().create("A/a8", 16, 'A');
  556         QVERIFY(!fakeFolder.syncOnce()); // Since the supported SHA1 checksum is invalid, no download
  557         checksumValue =  "Unsupported:XXXX SHA1:19b1928d58a2030d08023f3d7054516dbc186f20 Invalid:XxX";
  558         QVERIFY(fakeFolder.syncOnce()); // The supported SHA1 checksum is valid now, so the file are downloaded
  559         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  560     }
  561 
  562     // Tests the behavior of invalid filename detection
  563     void testInvalidFilenameRegex()
  564     {
  565         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  566 
  567 #ifndef Q_OS_WIN  // We can't have local file with these character
  568         // For current servers, no characters are forbidden
  569         fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  570         fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt");
  571         QVERIFY(fakeFolder.syncOnce());
  572         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  573 
  574         // For legacy servers, some characters were forbidden by the client
  575         fakeFolder.syncEngine().account()->setServerVersion("8.0.0");
  576         fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt");
  577         QVERIFY(fakeFolder.syncOnce());
  578         QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt"));
  579 #endif
  580 
  581         // We can override that by setting the capability
  582         fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } });
  583         QVERIFY(fakeFolder.syncOnce());
  584         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  585 
  586         // Check that new servers also accept the capability
  587         fakeFolder.syncEngine().account()->setServerVersion("10.0.0");
  588         fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } });
  589         fakeFolder.localModifier().insert("C/myfile.txt");
  590         QVERIFY(fakeFolder.syncOnce());
  591         QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt"));
  592     }
  593 
  594     void testDiscoveryHiddenFile()
  595     {
  596         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  597         QVERIFY(fakeFolder.syncOnce());
  598         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  599 
  600         // We can't depend on currentLocalState for hidden files since
  601         // it should rightfully skip things like download temporaries
  602         auto localFileExists = [&](QString name) {
  603             return QFileInfo(fakeFolder.localPath() + name).exists();
  604         };
  605 
  606         fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  607         fakeFolder.remoteModifier().insert("A/.hidden");
  608         fakeFolder.localModifier().insert("B/.hidden");
  609         QVERIFY(fakeFolder.syncOnce());
  610         QVERIFY(!localFileExists("A/.hidden"));
  611         QVERIFY(!fakeFolder.currentRemoteState().find("B/.hidden"));
  612 
  613         fakeFolder.syncEngine().setIgnoreHiddenFiles(false);
  614         fakeFolder.syncJournal().forceRemoteDiscoveryNextSync();
  615         QVERIFY(fakeFolder.syncOnce());
  616         QVERIFY(localFileExists("A/.hidden"));
  617         QVERIFY(fakeFolder.currentRemoteState().find("B/.hidden"));
  618     }
  619 
  620     void testNoLocalEncoding()
  621     {
  622         const auto utf8Locale = QTextCodec::codecForLocale();
  623         if (utf8Locale->mibEnum() != 106) {
  624             QSKIP(qPrintable(QStringLiteral("Test only works for UTF8 locale, but current locale is %1").arg(QString::fromUtf8(utf8Locale->name()))));
  625         }
  626 
  627         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  628         QVERIFY(fakeFolder.syncOnce());
  629         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  630 
  631         // Utf8 locale can sync both
  632         fakeFolder.remoteModifier().insert("A/tößt");
  633         fakeFolder.remoteModifier().insert("A/t𠜎t");
  634         QVERIFY(fakeFolder.syncOnce());
  635         QVERIFY(fakeFolder.currentLocalState().find("A/tößt"));
  636         QVERIFY(fakeFolder.currentLocalState().find("A/t𠜎t"));
  637 
  638 #if !defined(Q_OS_MAC) && !defined(Q_OS_WIN)
  639         // Try again with a locale that can represent ö but not 𠜎 (4-byte utf8).
  640         auto codec = QTextCodec::codecForName("ISO-8859-15");
  641         QVERIFY(codec);
  642         QTextCodec::setCodecForLocale(codec);
  643         QCOMPARE(QTextCodec::codecForLocale()->mibEnum(), 111);
  644 
  645         fakeFolder.remoteModifier().insert("B/tößt");
  646         fakeFolder.remoteModifier().insert("B/t𠜎t");
  647         QVERIFY(fakeFolder.syncOnce());
  648         QVERIFY(fakeFolder.currentLocalState().find("B/tößt"));
  649         QVERIFY(!fakeFolder.currentLocalState().find("B/t𠜎t"));
  650         QVERIFY(!fakeFolder.currentLocalState().find("B/t?t"));
  651         QVERIFY(!fakeFolder.currentLocalState().find("B/t??t"));
  652         QVERIFY(!fakeFolder.currentLocalState().find("B/t???t"));
  653         QVERIFY(!fakeFolder.currentLocalState().find("B/t????t"));
  654         QVERIFY(fakeFolder.syncOnce());
  655         QVERIFY(fakeFolder.currentRemoteState().find("B/tößt"));
  656         QVERIFY(fakeFolder.currentRemoteState().find("B/t𠜎t"));
  657 
  658         // Try again with plain ascii
  659         codec = QTextCodec::codecForName("ASCII");
  660         if (codec) {
  661             QTextCodec::setCodecForLocale(codec);
  662             QCOMPARE(QTextCodec::codecForLocale()->mibEnum(), 3);
  663 
  664             fakeFolder.remoteModifier().insert("C/tößt");
  665             QVERIFY(fakeFolder.syncOnce());
  666             QVERIFY(!fakeFolder.currentLocalState().find("C/tößt"));
  667             QVERIFY(!fakeFolder.currentLocalState().find("C/t??t"));
  668             QVERIFY(!fakeFolder.currentLocalState().find("C/t????t"));
  669             QVERIFY(fakeFolder.syncOnce());
  670             QVERIFY(fakeFolder.currentRemoteState().find("C/tößt"));
  671         } else {
  672             qDebug() << "Skipping test for ASCII, ASCII is not available, available encodings are:" << QTextCodec::availableCodecs();
  673         }
  674 
  675         QTextCodec::setCodecForLocale(utf8Locale);
  676 #endif
  677     }
  678 
  679     // Aborting has had bugs when there are parallel upload jobs
  680     void testUploadV1Multiabort()
  681     {
  682         FakeFolder fakeFolder{ FileInfo{} };
  683         SyncOptions options;
  684         options._initialChunkSize = 10;
  685         options._maxChunkSize = 10;
  686         options._minChunkSize = 10;
  687         fakeFolder.syncEngine().setSyncOptions(options);
  688 
  689         QObject parent;
  690         int nPUT = 0;
  691         fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  692             if (op == QNetworkAccessManager::PutOperation) {
  693                 ++nPUT;
  694                 return new FakeHangingReply(op, request, &parent);
  695             }
  696             return nullptr;
  697         });
  698 
  699         fakeFolder.localModifier().insert("file", 100, 'W');
  700         QTimer::singleShot(100, &fakeFolder.syncEngine(), [&]() { fakeFolder.syncEngine().abort(); });
  701         QVERIFY(!fakeFolder.syncOnce());
  702 
  703         QCOMPARE(nPUT, 3);
  704     }
  705 
  706 #ifndef Q_OS_WIN
  707     void testPropagatePermissions()
  708     {
  709         FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  710         auto perm = QFileDevice::Permission(0x7704); // user/owner: rwx, group: r, other: -
  711         QFile::setPermissions(fakeFolder.localPath() + "A/a1", perm);
  712         QFile::setPermissions(fakeFolder.localPath() + "A/a2", perm);
  713         fakeFolder.syncOnce(); // get the metadata-only change out of the way
  714         fakeFolder.remoteModifier().appendByte("A/a1");
  715         fakeFolder.remoteModifier().appendByte("A/a2");
  716         fakeFolder.localModifier().appendByte("A/a2");
  717         fakeFolder.localModifier().appendByte("A/a2");
  718         fakeFolder.syncOnce(); // perms should be preserved
  719         QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1").permissions(), perm);
  720         QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a2").permissions(), perm);
  721 
  722         auto conflictName = fakeFolder.syncJournal().conflictRecord(fakeFolder.syncJournal().conflictRecordPaths().first()).path;
  723         QVERIFY(conflictName.contains("A/a2"));
  724         QCOMPARE(QFileInfo(fakeFolder.localPath() + conflictName).permissions(), perm);
  725     }
  726 #endif
  727 
  728     void testEmptyLocalButHasRemote()
  729     {
  730         FakeFolder fakeFolder{ FileInfo{} };
  731         fakeFolder.remoteModifier().mkdir("foo");
  732 
  733         QVERIFY(fakeFolder.syncOnce());
  734         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  735 
  736         QVERIFY(fakeFolder.currentLocalState().find("foo"));
  737 
  738     }
  739 
  740     // Check that server mtime is set on directories on initial propagation
  741     void testDirectoryInitialMtime()
  742     {
  743         FakeFolder fakeFolder{ FileInfo{} };
  744         fakeFolder.remoteModifier().mkdir("foo");
  745         fakeFolder.remoteModifier().insert("foo/bar");
  746         auto datetime = QDateTime::currentDateTime();
  747         datetime.setSecsSinceEpoch(datetime.toSecsSinceEpoch()); // wipe ms
  748         fakeFolder.remoteModifier().find("foo")->lastModified = datetime;
  749 
  750         QVERIFY(fakeFolder.syncOnce());
  751         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  752 
  753         QCOMPARE(QFileInfo(fakeFolder.localPath() + "foo").lastModified(), datetime);
  754     }
  755 };
  756 
  757 QTEST_GUILESS_MAIN(TestSyncEngine)
  758 #include "testsyncengine.moc"