testdownload.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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. #include <QtTest>
  8. #include "syncenginetestutils.h"
  9. #include <syncengine.h>
  10. #include <owncloudpropagator.h>
  11. using namespace OCC;
  12. static constexpr qint64 stopAfter = 3'123'668;
  13. /* A FakeGetReply that sends max 'fakeSize' bytes, but whose ContentLength has the correct size */
  14. class BrokenFakeGetReply : public FakeGetReply
  15. {
  16. Q_OBJECT
  17. public:
  18. using FakeGetReply::FakeGetReply;
  19. int fakeSize = stopAfter;
  20. [[nodiscard]] qint64 bytesAvailable() const override
  21. {
  22. if (aborted)
  23. return 0;
  24. return std::min(size, fakeSize) + QIODevice::bytesAvailable(); // NOLINT: This is intended to simulate the brokenness
  25. }
  26. qint64 readData(char *data, qint64 maxlen) override
  27. {
  28. qint64 len = std::min(qint64{ fakeSize }, maxlen);
  29. std::fill_n(data, len, payload);
  30. size -= len;
  31. fakeSize -= len;
  32. return len;
  33. }
  34. };
  35. SyncFileItemPtr getItem(const QSignalSpy &spy, const QString &path)
  36. {
  37. for (const QList<QVariant> &args : spy) {
  38. auto item = args[0].value<SyncFileItemPtr>();
  39. if (item->destination() == path)
  40. return item;
  41. }
  42. return {};
  43. }
  44. class TestDownload : public QObject
  45. {
  46. Q_OBJECT
  47. private slots:
  48. void testResume()
  49. {
  50. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  51. fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  52. QSignalSpy completeSpy(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted);
  53. auto size = 30 * 1000 * 1000;
  54. fakeFolder.remoteModifier().insert("A/a0", size);
  55. // First, download only the first 3 MB of the file
  56. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  57. if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/a0")) {
  58. return new BrokenFakeGetReply(fakeFolder.remoteModifier(), op, request, this);
  59. }
  60. return nullptr;
  61. });
  62. QVERIFY(!fakeFolder.syncOnce()); // The sync must fail because not all the file was downloaded
  63. QCOMPARE(getItem(completeSpy, "A/a0")->_status, SyncFileItem::SoftError);
  64. QCOMPARE(getItem(completeSpy, "A/a0")->_errorString, QString("The file could not be downloaded completely."));
  65. QVERIFY(fakeFolder.syncEngine().isAnotherSyncNeeded());
  66. // Now, we need to restart, this time, it should resume.
  67. QByteArray ranges;
  68. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  69. if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/a0")) {
  70. ranges = request.rawHeader("Range");
  71. }
  72. return nullptr;
  73. });
  74. QVERIFY(fakeFolder.syncOnce()); // now this succeeds
  75. QCOMPARE(ranges, QByteArray("bytes=" + QByteArray::number(stopAfter) + "-"));
  76. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  77. }
  78. void testErrorMessage () {
  79. // This test's main goal is to test that the error string from the server is shown in the UI
  80. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  81. fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  82. QSignalSpy completeSpy(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted);
  83. auto size = 3'500'000;
  84. fakeFolder.remoteModifier().insert("A/broken", size);
  85. QByteArray serverMessage = "The file was not downloaded because the tests wants so!";
  86. // First, download only the first 3 MB of the file
  87. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  88. if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/broken")) {
  89. return new FakeErrorReply(op, request, this, 400,
  90. "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
  91. "<d:error xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\">\n"
  92. "<s:exception>Sabre\\DAV\\Exception\\Forbidden</s:exception>\n"
  93. "<s:message>"+serverMessage+"</s:message>\n"
  94. "</d:error>");
  95. }
  96. return nullptr;
  97. });
  98. bool timedOut = false;
  99. QTimer::singleShot(10000, &fakeFolder.syncEngine(), [&]() { timedOut = true; fakeFolder.syncEngine().abort(); });
  100. QVERIFY(!fakeFolder.syncOnce()); // Fail because A/broken
  101. QVERIFY(!timedOut);
  102. QCOMPARE(getItem(completeSpy, "A/broken")->_status, SyncFileItem::NormalError);
  103. QVERIFY(getItem(completeSpy, "A/broken")->_errorString.contains(serverMessage));
  104. }
  105. void serverMaintenence() {
  106. // Server in maintenance must abort the sync.
  107. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  108. fakeFolder.remoteModifier().insert("A/broken");
  109. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  110. if (op == QNetworkAccessManager::GetOperation) {
  111. return new FakeErrorReply(op, request, this, 503,
  112. "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
  113. "<d:error xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\">\n"
  114. "<s:exception>Sabre\\DAV\\Exception\\ServiceUnavailable</s:exception>\n"
  115. "<s:message>System in maintenance mode.</s:message>\n"
  116. "</d:error>");
  117. }
  118. return nullptr;
  119. });
  120. QSignalSpy completeSpy(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted);
  121. QVERIFY(!fakeFolder.syncOnce()); // Fail because A/broken
  122. // FatalError means the sync was aborted, which is what we want
  123. QCOMPARE(getItem(completeSpy, "A/broken")->_status, SyncFileItem::FatalError);
  124. QVERIFY(getItem(completeSpy, "A/broken")->_errorString.contains("System in maintenance mode"));
  125. }
  126. void testMoveFailsInAConflict() {
  127. #ifdef Q_OS_WIN
  128. QSKIP("Not run on windows because permission on directory does not do what is expected");
  129. #endif
  130. // Test for https://github.com/owncloud/client/issues/7015
  131. // We want to test the case in which the renaming of the original to the conflict file succeeds,
  132. // but renaming the temporary file fails.
  133. // This tests uses the fact that a "touchedFile" notification will be sent at the right moment.
  134. // Note that there will be first a notification on the file and the conflict file before.
  135. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  136. fakeFolder.syncEngine().setIgnoreHiddenFiles(true);
  137. fakeFolder.remoteModifier().setContents("A/a1", 'A');
  138. fakeFolder.localModifier().setContents("A/a1", 'B');
  139. bool propConnected = false;
  140. QString conflictFile;
  141. auto transProgress = connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  142. [&](const ProgressInfo &pi) {
  143. auto propagator = fakeFolder.syncEngine().getPropagator();
  144. if (pi.status() != ProgressInfo::Propagation || propConnected || !propagator)
  145. return;
  146. propConnected = true;
  147. connect(propagator.data(), &OwncloudPropagator::touchedFile, [&](const QString &s) {
  148. if (s.contains("conflicted copy")) {
  149. QCOMPARE(conflictFile, QString());
  150. conflictFile = s;
  151. return;
  152. }
  153. if (!conflictFile.isEmpty()) {
  154. // Check that the temporary file is still there
  155. QCOMPARE(QDir(fakeFolder.localPath() + "A/").entryList({"*.~*"}, QDir::Files | QDir::Hidden).count(), 1);
  156. // Set the permission to read only on the folder, so the rename of the temporary file will fail
  157. QFile(fakeFolder.localPath() + "A/").setPermissions(QFile::Permissions(0x5555));
  158. }
  159. });
  160. });
  161. QVERIFY(!fakeFolder.syncOnce()); // The sync must fail because the rename failed
  162. QVERIFY(!conflictFile.isEmpty());
  163. // restore permissions
  164. QFile(fakeFolder.localPath() + "A/").setPermissions(QFile::Permissions(0x7777));
  165. QObject::disconnect(transProgress);
  166. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) -> QNetworkReply * {
  167. if (op == QNetworkAccessManager::GetOperation)
  168. QTest::qFail("There shouldn't be any download", __FILE__, __LINE__);
  169. return nullptr;
  170. });
  171. QVERIFY(fakeFolder.syncOnce());
  172. // The a1 file is still tere and have the right content
  173. QVERIFY(fakeFolder.currentRemoteState().find("A/a1"));
  174. QCOMPARE(fakeFolder.currentRemoteState().find("A/a1")->contentChar, 'A');
  175. QVERIFY(QFile::remove(conflictFile)); // So the comparison succeeds;
  176. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  177. }
  178. void testHttp2Resend() {
  179. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  180. fakeFolder.remoteModifier().insert("A/resendme", 300);
  181. QByteArray serverMessage = "Needs to be resend on a new connection!";
  182. int resendActual = 0;
  183. int resendExpected = 2;
  184. // First, download only the first 3 MB of the file
  185. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  186. if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/resendme") && resendActual < resendExpected) {
  187. auto errorReply = new FakeErrorReply(op, request, this, 400, "ignore this body");
  188. errorReply->setError(QNetworkReply::ContentReSendError, serverMessage);
  189. errorReply->setAttribute(QNetworkRequest::Http2WasUsedAttribute, true);
  190. errorReply->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, QVariant());
  191. resendActual += 1;
  192. return errorReply;
  193. }
  194. return nullptr;
  195. });
  196. QVERIFY(fakeFolder.syncOnce());
  197. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  198. QCOMPARE(resendActual, 2);
  199. fakeFolder.remoteModifier().appendByte("A/resendme");
  200. resendActual = 0;
  201. resendExpected = 10;
  202. QSignalSpy completeSpy(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted);
  203. QVERIFY(!fakeFolder.syncOnce());
  204. QCOMPARE(resendActual, 4); // the 4th fails because it only resends 3 times
  205. QCOMPARE(getItem(completeSpy, "A/resendme")->_status, SyncFileItem::NormalError);
  206. QVERIFY(getItem(completeSpy, "A/resendme")->_errorString.contains(serverMessage));
  207. }
  208. };
  209. QTEST_GUILESS_MAIN(TestDownload)
  210. #include "testdownload.moc"