syncenginetestutils.cpp 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  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 "syncenginetestutils.h"
  8. #include <memory>
  9. PathComponents::PathComponents(const char *path)
  10. : PathComponents { QString::fromUtf8(path) }
  11. {
  12. }
  13. PathComponents::PathComponents(const QString &path)
  14. : QStringList { path.split(QLatin1Char('/'), QString::SkipEmptyParts) }
  15. {
  16. }
  17. PathComponents::PathComponents(const QStringList &pathComponents)
  18. : QStringList { pathComponents }
  19. {
  20. }
  21. PathComponents PathComponents::parentDirComponents() const
  22. {
  23. return PathComponents { mid(0, size() - 1) };
  24. }
  25. PathComponents PathComponents::subComponents() const &
  26. {
  27. return PathComponents { mid(1) };
  28. }
  29. void DiskFileModifier::remove(const QString &relativePath)
  30. {
  31. QFileInfo fi { _rootDir.filePath(relativePath) };
  32. if (fi.isFile())
  33. QVERIFY(_rootDir.remove(relativePath));
  34. else
  35. QVERIFY(QDir { fi.filePath() }.removeRecursively());
  36. }
  37. void DiskFileModifier::insert(const QString &relativePath, qint64 size, char contentChar)
  38. {
  39. QFile file { _rootDir.filePath(relativePath) };
  40. QVERIFY(!file.exists());
  41. file.open(QFile::WriteOnly);
  42. QByteArray buf(1024, contentChar);
  43. for (int x = 0; x < size / buf.size(); ++x) {
  44. file.write(buf);
  45. }
  46. file.write(buf.data(), size % buf.size());
  47. file.close();
  48. // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
  49. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc().addSecs(-30)));
  50. QCOMPARE(file.size(), size);
  51. }
  52. void DiskFileModifier::setContents(const QString &relativePath, char contentChar)
  53. {
  54. QFile file { _rootDir.filePath(relativePath) };
  55. QVERIFY(file.exists());
  56. qint64 size = file.size();
  57. file.open(QFile::WriteOnly);
  58. file.write(QByteArray {}.fill(contentChar, size));
  59. }
  60. void DiskFileModifier::appendByte(const QString &relativePath)
  61. {
  62. QFile file { _rootDir.filePath(relativePath) };
  63. QVERIFY(file.exists());
  64. file.open(QFile::ReadWrite);
  65. QByteArray contents = file.read(1);
  66. file.seek(file.size());
  67. file.write(contents);
  68. }
  69. void DiskFileModifier::mkdir(const QString &relativePath)
  70. {
  71. _rootDir.mkpath(relativePath);
  72. }
  73. void DiskFileModifier::rename(const QString &from, const QString &to)
  74. {
  75. QVERIFY(_rootDir.exists(from));
  76. QVERIFY(_rootDir.rename(from, to));
  77. }
  78. void DiskFileModifier::setModTime(const QString &relativePath, const QDateTime &modTime)
  79. {
  80. OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
  81. }
  82. FileInfo FileInfo::A12_B12_C12_S12()
  83. {
  84. FileInfo fi { QString {}, {
  85. { QStringLiteral("A"), { { QStringLiteral("a1"), 4 }, { QStringLiteral("a2"), 4 } } },
  86. { QStringLiteral("B"), { { QStringLiteral("b1"), 16 }, { QStringLiteral("b2"), 16 } } },
  87. { QStringLiteral("C"), { { QStringLiteral("c1"), 24 }, { QStringLiteral("c2"), 24 } } },
  88. } };
  89. FileInfo sharedFolder { QStringLiteral("S"), { { QStringLiteral("s1"), 32 }, { QStringLiteral("s2"), 32 } } };
  90. sharedFolder.isShared = true;
  91. sharedFolder.children[QStringLiteral("s1")].isShared = true;
  92. sharedFolder.children[QStringLiteral("s2")].isShared = true;
  93. fi.children.insert(sharedFolder.name, std::move(sharedFolder));
  94. return fi;
  95. }
  96. FileInfo::FileInfo(const QString &name, const std::initializer_list<FileInfo> &children)
  97. : name { name }
  98. {
  99. for (const auto &source : children)
  100. addChild(source);
  101. }
  102. void FileInfo::addChild(const FileInfo &info)
  103. {
  104. auto &dest = this->children[info.name] = info;
  105. dest.parentPath = path();
  106. dest.fixupParentPathRecursively();
  107. }
  108. void FileInfo::remove(const QString &relativePath)
  109. {
  110. const PathComponents pathComponents { relativePath };
  111. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  112. Q_ASSERT(parent);
  113. parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(),
  114. [&pathComponents](const FileInfo &fi) { return fi.name == pathComponents.fileName(); }));
  115. }
  116. void FileInfo::insert(const QString &relativePath, qint64 size, char contentChar)
  117. {
  118. create(relativePath, size, contentChar);
  119. }
  120. void FileInfo::setContents(const QString &relativePath, char contentChar)
  121. {
  122. FileInfo *file = findInvalidatingEtags(relativePath);
  123. Q_ASSERT(file);
  124. file->contentChar = contentChar;
  125. }
  126. void FileInfo::appendByte(const QString &relativePath)
  127. {
  128. FileInfo *file = findInvalidatingEtags(relativePath);
  129. Q_ASSERT(file);
  130. file->size += 1;
  131. }
  132. void FileInfo::mkdir(const QString &relativePath)
  133. {
  134. createDir(relativePath);
  135. }
  136. void FileInfo::rename(const QString &oldPath, const QString &newPath)
  137. {
  138. const PathComponents newPathComponents { newPath };
  139. FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents());
  140. Q_ASSERT(dir);
  141. Q_ASSERT(dir->isDir);
  142. const PathComponents pathComponents { oldPath };
  143. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  144. Q_ASSERT(parent);
  145. FileInfo fi = parent->children.take(pathComponents.fileName());
  146. fi.parentPath = dir->path();
  147. fi.name = newPathComponents.fileName();
  148. fi.fixupParentPathRecursively();
  149. dir->children.insert(newPathComponents.fileName(), std::move(fi));
  150. }
  151. void FileInfo::setModTime(const QString &relativePath, const QDateTime &modTime)
  152. {
  153. FileInfo *file = findInvalidatingEtags(relativePath);
  154. Q_ASSERT(file);
  155. file->lastModified = modTime;
  156. }
  157. FileInfo *FileInfo::find(PathComponents pathComponents, const bool invalidateEtags)
  158. {
  159. if (pathComponents.isEmpty()) {
  160. if (invalidateEtags) {
  161. etag = generateEtag();
  162. }
  163. return this;
  164. }
  165. QString childName = pathComponents.pathRoot();
  166. auto it = children.find(childName);
  167. if (it != children.end()) {
  168. auto file = it->find(std::move(pathComponents).subComponents(), invalidateEtags);
  169. if (file && invalidateEtags) {
  170. // Update parents on the way back
  171. etag = generateEtag();
  172. }
  173. return file;
  174. }
  175. return nullptr;
  176. }
  177. FileInfo *FileInfo::createDir(const QString &relativePath)
  178. {
  179. const PathComponents pathComponents { relativePath };
  180. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  181. Q_ASSERT(parent);
  182. FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo { pathComponents.fileName() };
  183. child.parentPath = parent->path();
  184. child.etag = generateEtag();
  185. return &child;
  186. }
  187. FileInfo *FileInfo::create(const QString &relativePath, qint64 size, char contentChar)
  188. {
  189. const PathComponents pathComponents { relativePath };
  190. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  191. Q_ASSERT(parent);
  192. FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo { pathComponents.fileName(), size };
  193. child.parentPath = parent->path();
  194. child.contentChar = contentChar;
  195. child.etag = generateEtag();
  196. return &child;
  197. }
  198. bool FileInfo::operator==(const FileInfo &other) const
  199. {
  200. // Consider files to be equal between local<->remote as a user would.
  201. return name == other.name
  202. && isDir == other.isDir
  203. && size == other.size
  204. && contentChar == other.contentChar
  205. && children == other.children;
  206. }
  207. QString FileInfo::path() const
  208. {
  209. return (parentPath.isEmpty() ? QString() : (parentPath + QLatin1Char('/'))) + name;
  210. }
  211. void FileInfo::fixupParentPathRecursively()
  212. {
  213. auto p = path();
  214. for (auto it = children.begin(); it != children.end(); ++it) {
  215. Q_ASSERT(it.key() == it->name);
  216. it->parentPath = p;
  217. it->fixupParentPathRecursively();
  218. }
  219. }
  220. FileInfo *FileInfo::findInvalidatingEtags(PathComponents pathComponents)
  221. {
  222. return find(std::move(pathComponents), true);
  223. }
  224. FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  225. : FakeReply { parent }
  226. {
  227. setRequest(request);
  228. setUrl(request.url());
  229. setOperation(op);
  230. open(QIODevice::ReadOnly);
  231. QString fileName = getFilePathFromUrl(request.url());
  232. Q_ASSERT(!fileName.isNull()); // for root, it should be empty
  233. const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  234. if (!fileInfo) {
  235. QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection);
  236. return;
  237. }
  238. QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
  239. // Don't care about the request and just return a full propfind
  240. const QString davUri { QStringLiteral("DAV:") };
  241. const QString ocUri { QStringLiteral("http://owncloud.org/ns") };
  242. QBuffer buffer { &payload };
  243. buffer.open(QIODevice::WriteOnly);
  244. QXmlStreamWriter xml(&buffer);
  245. xml.writeNamespace(davUri, QStringLiteral("d"));
  246. xml.writeNamespace(ocUri, QStringLiteral("oc"));
  247. xml.writeStartDocument();
  248. xml.writeStartElement(davUri, QStringLiteral("multistatus"));
  249. auto writeFileResponse = [&](const FileInfo &fileInfo) {
  250. xml.writeStartElement(davUri, QStringLiteral("response"));
  251. xml.writeTextElement(davUri, QStringLiteral("href"), prefix + QString::fromUtf8(QUrl::toPercentEncoding(fileInfo.path(), "/")));
  252. xml.writeStartElement(davUri, QStringLiteral("propstat"));
  253. xml.writeStartElement(davUri, QStringLiteral("prop"));
  254. if (fileInfo.isDir) {
  255. xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
  256. xml.writeEmptyElement(davUri, QStringLiteral("collection"));
  257. xml.writeEndElement(); // resourcetype
  258. } else
  259. xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
  260. auto gmtDate = fileInfo.lastModified.toUTC();
  261. auto stringDate = QLocale::c().toString(gmtDate, QStringLiteral("ddd, dd MMM yyyy HH:mm:ss 'GMT'"));
  262. xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
  263. xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
  264. xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(QString::fromLatin1(fileInfo.etag)));
  265. xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() ? QString(fileInfo.permissions.toString()) : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
  266. xml.writeTextElement(ocUri, QStringLiteral("id"), QString::fromUtf8(fileInfo.fileId));
  267. xml.writeTextElement(ocUri, QStringLiteral("checksums"), QString::fromUtf8(fileInfo.checksums));
  268. buffer.write(fileInfo.extraDavProperties);
  269. xml.writeEndElement(); // prop
  270. xml.writeTextElement(davUri, QStringLiteral("status"), QStringLiteral("HTTP/1.1 200 OK"));
  271. xml.writeEndElement(); // propstat
  272. xml.writeEndElement(); // response
  273. };
  274. writeFileResponse(*fileInfo);
  275. foreach (const FileInfo &childFileInfo, fileInfo->children)
  276. writeFileResponse(childFileInfo);
  277. xml.writeEndElement(); // multistatus
  278. xml.writeEndDocument();
  279. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  280. }
  281. void FakePropfindReply::respond()
  282. {
  283. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  284. setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml; charset=utf-8"));
  285. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
  286. setFinished(true);
  287. emit metaDataChanged();
  288. if (bytesAvailable())
  289. emit readyRead();
  290. emit finished();
  291. }
  292. void FakePropfindReply::respond404()
  293. {
  294. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404);
  295. setError(InternalServerError, QStringLiteral("Not Found"));
  296. emit metaDataChanged();
  297. emit finished();
  298. }
  299. qint64 FakePropfindReply::bytesAvailable() const
  300. {
  301. return payload.size() + QIODevice::bytesAvailable();
  302. }
  303. qint64 FakePropfindReply::readData(char *data, qint64 maxlen)
  304. {
  305. qint64 len = std::min(qint64 { payload.size() }, maxlen);
  306. std::copy(payload.cbegin(), payload.cbegin() + len, data);
  307. payload.remove(0, static_cast<int>(len));
  308. return len;
  309. }
  310. FakePutReply::FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
  311. : FakeReply { parent }
  312. {
  313. setRequest(request);
  314. setUrl(request.url());
  315. setOperation(op);
  316. open(QIODevice::ReadOnly);
  317. fileInfo = perform(remoteRootFileInfo, request, putPayload);
  318. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  319. }
  320. FileInfo *FakePutReply::perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload)
  321. {
  322. QString fileName = getFilePathFromUrl(request.url());
  323. Q_ASSERT(!fileName.isEmpty());
  324. FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  325. if (fileInfo) {
  326. fileInfo->size = putPayload.size();
  327. fileInfo->contentChar = putPayload.at(0);
  328. } else {
  329. // Assume that the file is filled with the same character
  330. fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
  331. }
  332. fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
  333. remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
  334. return fileInfo;
  335. }
  336. void FakePutReply::respond()
  337. {
  338. emit uploadProgress(fileInfo->size, fileInfo->size);
  339. setRawHeader("OC-ETag", fileInfo->etag);
  340. setRawHeader("ETag", fileInfo->etag);
  341. setRawHeader("OC-FileID", fileInfo->fileId);
  342. setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
  343. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  344. emit metaDataChanged();
  345. emit finished();
  346. }
  347. void FakePutReply::abort()
  348. {
  349. setError(OperationCanceledError, QStringLiteral("abort"));
  350. emit finished();
  351. }
  352. FakeMkcolReply::FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  353. : FakeReply { parent }
  354. {
  355. setRequest(request);
  356. setUrl(request.url());
  357. setOperation(op);
  358. open(QIODevice::ReadOnly);
  359. QString fileName = getFilePathFromUrl(request.url());
  360. Q_ASSERT(!fileName.isEmpty());
  361. fileInfo = remoteRootFileInfo.createDir(fileName);
  362. if (!fileInfo) {
  363. abort();
  364. return;
  365. }
  366. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  367. }
  368. void FakeMkcolReply::respond()
  369. {
  370. setRawHeader("OC-FileId", fileInfo->fileId);
  371. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  372. emit metaDataChanged();
  373. emit finished();
  374. }
  375. FakeDeleteReply::FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  376. : FakeReply { parent }
  377. {
  378. setRequest(request);
  379. setUrl(request.url());
  380. setOperation(op);
  381. open(QIODevice::ReadOnly);
  382. QString fileName = getFilePathFromUrl(request.url());
  383. Q_ASSERT(!fileName.isEmpty());
  384. remoteRootFileInfo.remove(fileName);
  385. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  386. }
  387. void FakeDeleteReply::respond()
  388. {
  389. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
  390. emit metaDataChanged();
  391. emit finished();
  392. }
  393. FakeMoveReply::FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  394. : FakeReply { parent }
  395. {
  396. setRequest(request);
  397. setUrl(request.url());
  398. setOperation(op);
  399. open(QIODevice::ReadOnly);
  400. QString fileName = getFilePathFromUrl(request.url());
  401. Q_ASSERT(!fileName.isEmpty());
  402. QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  403. Q_ASSERT(!dest.isEmpty());
  404. remoteRootFileInfo.rename(fileName, dest);
  405. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  406. }
  407. void FakeMoveReply::respond()
  408. {
  409. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  410. emit metaDataChanged();
  411. emit finished();
  412. }
  413. FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  414. : FakeReply { parent }
  415. {
  416. setRequest(request);
  417. setUrl(request.url());
  418. setOperation(op);
  419. open(QIODevice::ReadOnly);
  420. QString fileName = getFilePathFromUrl(request.url());
  421. Q_ASSERT(!fileName.isEmpty());
  422. fileInfo = remoteRootFileInfo.find(fileName);
  423. if (!fileInfo)
  424. qWarning() << "Could not find file" << fileName << "on the remote";
  425. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  426. }
  427. void FakeGetReply::respond()
  428. {
  429. if (aborted) {
  430. setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
  431. emit metaDataChanged();
  432. emit finished();
  433. return;
  434. }
  435. payload = fileInfo->contentChar;
  436. size = fileInfo->size;
  437. setHeader(QNetworkRequest::ContentLengthHeader, size);
  438. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  439. setRawHeader("OC-ETag", fileInfo->etag);
  440. setRawHeader("ETag", fileInfo->etag);
  441. setRawHeader("OC-FileId", fileInfo->fileId);
  442. emit metaDataChanged();
  443. if (bytesAvailable())
  444. emit readyRead();
  445. emit finished();
  446. }
  447. void FakeGetReply::abort()
  448. {
  449. setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
  450. aborted = true;
  451. }
  452. qint64 FakeGetReply::bytesAvailable() const
  453. {
  454. if (aborted)
  455. return 0;
  456. return size + QIODevice::bytesAvailable();
  457. }
  458. qint64 FakeGetReply::readData(char *data, qint64 maxlen)
  459. {
  460. qint64 len = std::min(qint64 { size }, maxlen);
  461. std::fill_n(data, len, payload);
  462. size -= len;
  463. return len;
  464. }
  465. FakeGetWithDataReply::FakeGetWithDataReply(FileInfo &remoteRootFileInfo, const QByteArray &data, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  466. : FakeReply { parent }
  467. {
  468. setRequest(request);
  469. setUrl(request.url());
  470. setOperation(op);
  471. open(QIODevice::ReadOnly);
  472. Q_ASSERT(!data.isEmpty());
  473. payload = data;
  474. QString fileName = getFilePathFromUrl(request.url());
  475. Q_ASSERT(!fileName.isEmpty());
  476. fileInfo = remoteRootFileInfo.find(fileName);
  477. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  478. if (request.hasRawHeader("Range")) {
  479. const QString range = QString::fromUtf8(request.rawHeader("Range"));
  480. const QRegularExpression bytesPattern(QStringLiteral("bytes=(?<start>\\d+)-(?<end>\\d+)"));
  481. const QRegularExpressionMatch match = bytesPattern.match(range);
  482. if (match.hasMatch()) {
  483. const int start = match.captured(QStringLiteral("start")).toInt();
  484. const int end = match.captured(QStringLiteral("end")).toInt();
  485. payload = payload.mid(start, end - start + 1);
  486. }
  487. }
  488. }
  489. void FakeGetWithDataReply::respond()
  490. {
  491. if (aborted) {
  492. setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
  493. emit metaDataChanged();
  494. emit finished();
  495. return;
  496. }
  497. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  498. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  499. setRawHeader("OC-ETag", fileInfo->etag);
  500. setRawHeader("ETag", fileInfo->etag);
  501. setRawHeader("OC-FileId", fileInfo->fileId);
  502. emit metaDataChanged();
  503. if (bytesAvailable())
  504. emit readyRead();
  505. emit finished();
  506. }
  507. void FakeGetWithDataReply::abort()
  508. {
  509. setError(OperationCanceledError, QStringLiteral("Operation Canceled"));
  510. aborted = true;
  511. }
  512. qint64 FakeGetWithDataReply::bytesAvailable() const
  513. {
  514. if (aborted)
  515. return 0;
  516. return payload.size() - offset + QIODevice::bytesAvailable();
  517. }
  518. qint64 FakeGetWithDataReply::readData(char *data, qint64 maxlen)
  519. {
  520. qint64 len = std::min(payload.size() - offset, quint64(maxlen));
  521. std::memcpy(data, payload.constData() + offset, len);
  522. offset += len;
  523. return len;
  524. }
  525. FakeChunkMoveReply::FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  526. : FakeReply { parent }
  527. {
  528. setRequest(request);
  529. setUrl(request.url());
  530. setOperation(op);
  531. open(QIODevice::ReadOnly);
  532. fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request);
  533. if (!fileInfo) {
  534. QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed);
  535. } else {
  536. QTimer::singleShot(0, this, &FakeChunkMoveReply::respond);
  537. }
  538. }
  539. FileInfo *FakeChunkMoveReply::perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request)
  540. {
  541. QString source = getFilePathFromUrl(request.url());
  542. Q_ASSERT(!source.isEmpty());
  543. Q_ASSERT(source.endsWith(QLatin1String("/.file")));
  544. source = source.left(source.length() - static_cast<int>(qstrlen("/.file")));
  545. auto sourceFolder = uploadsFileInfo.find(source);
  546. Q_ASSERT(sourceFolder);
  547. Q_ASSERT(sourceFolder->isDir);
  548. int count = 0;
  549. qlonglong size = 0;
  550. char payload = '\0';
  551. QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  552. Q_ASSERT(!fileName.isEmpty());
  553. // Compute the size and content from the chunks if possible
  554. for (auto chunkName : sourceFolder->children.keys()) {
  555. auto &x = sourceFolder->children[chunkName];
  556. Q_ASSERT(!x.isDir);
  557. Q_ASSERT(x.size > 0); // There should not be empty chunks
  558. size += x.size;
  559. Q_ASSERT(!payload || payload == x.contentChar);
  560. payload = x.contentChar;
  561. ++count;
  562. }
  563. Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files
  564. // NOTE: This does not actually assemble the file data from the chunks!
  565. FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  566. if (fileInfo) {
  567. // The client should put this header
  568. Q_ASSERT(request.hasRawHeader("If"));
  569. // And it should condition on the destination file
  570. auto start = QByteArray("<" + request.rawHeader("Destination") + ">");
  571. Q_ASSERT(request.rawHeader("If").startsWith(start));
  572. if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag + "\"])") {
  573. return nullptr;
  574. }
  575. fileInfo->size = size;
  576. fileInfo->contentChar = payload;
  577. } else {
  578. Q_ASSERT(!request.hasRawHeader("If"));
  579. // Assume that the file is filled with the same character
  580. fileInfo = remoteRootFileInfo.create(fileName, size, payload);
  581. }
  582. fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
  583. remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
  584. return fileInfo;
  585. }
  586. void FakeChunkMoveReply::respond()
  587. {
  588. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  589. setRawHeader("OC-ETag", fileInfo->etag);
  590. setRawHeader("ETag", fileInfo->etag);
  591. setRawHeader("OC-FileId", fileInfo->fileId);
  592. emit metaDataChanged();
  593. emit finished();
  594. }
  595. void FakeChunkMoveReply::respondPreconditionFailed()
  596. {
  597. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412);
  598. setError(InternalServerError, QStringLiteral("Precondition Failed"));
  599. emit metaDataChanged();
  600. emit finished();
  601. }
  602. void FakeChunkMoveReply::abort()
  603. {
  604. setError(OperationCanceledError, QStringLiteral("abort"));
  605. emit finished();
  606. }
  607. FakePayloadReply::FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &body, QObject *parent)
  608. : FakeReply { parent }
  609. , _body(body)
  610. {
  611. setRequest(request);
  612. setUrl(request.url());
  613. setOperation(op);
  614. open(QIODevice::ReadOnly);
  615. QTimer::singleShot(10, this, &FakePayloadReply::respond);
  616. }
  617. void FakePayloadReply::respond()
  618. {
  619. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  620. setHeader(QNetworkRequest::ContentLengthHeader, _body.size());
  621. emit metaDataChanged();
  622. emit readyRead();
  623. setFinished(true);
  624. emit finished();
  625. }
  626. qint64 FakePayloadReply::readData(char *buf, qint64 max)
  627. {
  628. max = qMin<qint64>(max, _body.size());
  629. memcpy(buf, _body.constData(), max);
  630. _body = _body.mid(max);
  631. return max;
  632. }
  633. qint64 FakePayloadReply::bytesAvailable() const
  634. {
  635. return _body.size();
  636. }
  637. FakeErrorReply::FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent, int httpErrorCode, const QByteArray &body)
  638. : FakeReply { parent }
  639. , _body(body)
  640. {
  641. setRequest(request);
  642. setUrl(request.url());
  643. setOperation(op);
  644. open(QIODevice::ReadOnly);
  645. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode);
  646. setError(InternalServerError, QStringLiteral("Internal Server Fake Error"));
  647. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  648. }
  649. void FakeErrorReply::respond()
  650. {
  651. emit metaDataChanged();
  652. emit readyRead();
  653. // finishing can come strictly after readyRead was called
  654. QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished);
  655. }
  656. void FakeErrorReply::slotSetFinished()
  657. {
  658. setFinished(true);
  659. emit finished();
  660. }
  661. qint64 FakeErrorReply::readData(char *buf, qint64 max)
  662. {
  663. max = qMin<qint64>(max, _body.size());
  664. memcpy(buf, _body.constData(), max);
  665. _body = _body.mid(max);
  666. return max;
  667. }
  668. qint64 FakeErrorReply::bytesAvailable() const
  669. {
  670. return _body.size();
  671. }
  672. FakeHangingReply::FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  673. : FakeReply(parent)
  674. {
  675. setRequest(request);
  676. setUrl(request.url());
  677. setOperation(op);
  678. open(QIODevice::ReadOnly);
  679. }
  680. void FakeHangingReply::abort()
  681. {
  682. // Follow more or less the implementation of QNetworkReplyImpl::abort
  683. close();
  684. setError(OperationCanceledError, tr("Operation canceled"));
  685. emit error(OperationCanceledError);
  686. setFinished(true);
  687. emit finished();
  688. }
  689. FakeQNAM::FakeQNAM(FileInfo initialRoot)
  690. : _remoteRootFileInfo { std::move(initialRoot) }
  691. {
  692. setCookieJar(new OCC::CookieJar);
  693. }
  694. QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
  695. {
  696. if (_override) {
  697. if (auto reply = _override(op, request, outgoingData))
  698. return reply;
  699. }
  700. const QString fileName = getFilePathFromUrl(request.url());
  701. Q_ASSERT(!fileName.isNull());
  702. if (_errorPaths.contains(fileName))
  703. return new FakeErrorReply { op, request, this, _errorPaths[fileName] };
  704. bool isUpload = request.url().path().startsWith(sUploadUrl.path());
  705. FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
  706. auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
  707. FakeReply *reply = nullptr;
  708. if (verb == QLatin1String("PROPFIND"))
  709. // Ignore outgoingData always returning somethign good enough, works for now.
  710. reply = new FakePropfindReply { info, op, request, this };
  711. else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
  712. reply = new FakeGetReply { info, op, request, this };
  713. else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
  714. reply = new FakePutReply { info, op, request, outgoingData->readAll(), this };
  715. else if (verb == QLatin1String("MKCOL"))
  716. reply = new FakeMkcolReply { info, op, request, this };
  717. else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
  718. reply = new FakeDeleteReply { info, op, request, this };
  719. else if (verb == QLatin1String("MOVE") && !isUpload)
  720. reply = new FakeMoveReply { info, op, request, this };
  721. else if (verb == QLatin1String("MOVE") && isUpload)
  722. reply = new FakeChunkMoveReply { info, _remoteRootFileInfo, op, request, this };
  723. else {
  724. qDebug() << verb << outgoingData;
  725. Q_UNREACHABLE();
  726. }
  727. return reply;
  728. }
  729. FakeFolder::FakeFolder(const FileInfo &fileTemplate)
  730. : _localModifier(_tempDir.path())
  731. {
  732. // Needs to be done once
  733. OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0);
  734. OCC::Logger::instance()->setLogFile(QStringLiteral("-"));
  735. QDir rootDir { _tempDir.path() };
  736. qDebug() << "FakeFolder operating on" << rootDir;
  737. toDisk(rootDir, fileTemplate);
  738. _fakeQnam = new FakeQNAM(fileTemplate);
  739. _account = OCC::Account::create();
  740. _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
  741. _account->setCredentials(new FakeCredentials { _fakeQnam });
  742. _account->setDavDisplayName(QStringLiteral("fakename"));
  743. _account->setServerVersion(QStringLiteral("10.0.0"));
  744. _journalDb = std::make_unique<OCC::SyncJournalDb>(localPath() + QStringLiteral(".sync_test.db"));
  745. _syncEngine = std::make_unique<OCC::SyncEngine>(_account, localPath(), QString(), _journalDb.get());
  746. // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it)
  747. _syncEngine->excludedFiles().addManualExclude(QStringLiteral("]*.~*"));
  748. // handle aboutToRemoveAllFiles with a timeout in case our test does not handle it
  749. QObject::connect(_syncEngine.get(), &OCC::SyncEngine::aboutToRemoveAllFiles, _syncEngine.get(), [this](OCC::SyncFileItem::Direction, std::function<void(bool)> callback) {
  750. QTimer::singleShot(1 * 1000, _syncEngine.get(), [callback] {
  751. callback(false);
  752. });
  753. });
  754. // Ensure we have a valid VfsOff instance "running"
  755. switchToVfs(_syncEngine->syncOptions()._vfs);
  756. // A new folder will update the local file state database on first sync.
  757. // To have a state matching what users will encounter, we have to a sync
  758. // using an identical local/remote file tree first.
  759. ENFORCE(syncOnce());
  760. }
  761. void FakeFolder::switchToVfs(QSharedPointer<OCC::Vfs> vfs)
  762. {
  763. auto opts = _syncEngine->syncOptions();
  764. opts._vfs->stop();
  765. QObject::disconnect(_syncEngine.get(), nullptr, opts._vfs.data(), nullptr);
  766. opts._vfs = vfs;
  767. _syncEngine->setSyncOptions(opts);
  768. OCC::VfsSetupParams vfsParams;
  769. vfsParams.filesystemPath = localPath();
  770. vfsParams.remotePath = QLatin1Char('/');
  771. vfsParams.account = _account;
  772. vfsParams.journal = _journalDb.get();
  773. vfsParams.providerName = QStringLiteral("OC-TEST");
  774. vfsParams.providerVersion = QStringLiteral("0.1");
  775. QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() {
  776. vfs->stop();
  777. vfs->unregisterFolder();
  778. });
  779. vfs->start(vfsParams);
  780. }
  781. FileInfo FakeFolder::currentLocalState()
  782. {
  783. QDir rootDir { _tempDir.path() };
  784. FileInfo rootTemplate;
  785. fromDisk(rootDir, rootTemplate);
  786. rootTemplate.fixupParentPathRecursively();
  787. return rootTemplate;
  788. }
  789. QString FakeFolder::localPath() const
  790. {
  791. // SyncEngine wants a trailing slash
  792. if (_tempDir.path().endsWith(QLatin1Char('/')))
  793. return _tempDir.path();
  794. return _tempDir.path() + QLatin1Char('/');
  795. }
  796. void FakeFolder::scheduleSync()
  797. {
  798. // Have to be done async, else, an error before exec() does not terminate the event loop.
  799. QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
  800. }
  801. void FakeFolder::execUntilBeforePropagation()
  802. {
  803. QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector &)));
  804. QVERIFY(spy.wait());
  805. }
  806. void FakeFolder::execUntilItemCompleted(const QString &relativePath)
  807. {
  808. QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  809. QElapsedTimer t;
  810. t.start();
  811. while (t.elapsed() < 5000) {
  812. spy.clear();
  813. QVERIFY(spy.wait());
  814. for (const QList<QVariant> &args : spy) {
  815. auto item = args[0].value<OCC::SyncFileItemPtr>();
  816. if (item->destination() == relativePath)
  817. return;
  818. }
  819. }
  820. QVERIFY(false);
  821. }
  822. void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
  823. {
  824. foreach (const FileInfo &child, templateFi.children) {
  825. if (child.isDir) {
  826. QDir subDir(dir);
  827. dir.mkdir(child.name);
  828. subDir.cd(child.name);
  829. toDisk(subDir, child);
  830. } else {
  831. QFile file { dir.filePath(child.name) };
  832. file.open(QFile::WriteOnly);
  833. file.write(QByteArray {}.fill(child.contentChar, child.size));
  834. file.close();
  835. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
  836. }
  837. }
  838. }
  839. void FakeFolder::fromDisk(QDir &dir, FileInfo &templateFi)
  840. {
  841. foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
  842. if (diskChild.isDir()) {
  843. QDir subDir = dir;
  844. subDir.cd(diskChild.fileName());
  845. FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo { diskChild.fileName() };
  846. fromDisk(subDir, subFi);
  847. } else {
  848. QFile f { diskChild.filePath() };
  849. f.open(QFile::ReadOnly);
  850. auto content = f.read(1);
  851. if (content.size() == 0) {
  852. qWarning() << "Empty file at:" << diskChild.filePath();
  853. continue;
  854. }
  855. char contentChar = content.at(0);
  856. templateFi.children.insert(diskChild.fileName(), FileInfo { diskChild.fileName(), diskChild.size(), contentChar });
  857. }
  858. }
  859. }
  860. FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components)
  861. {
  862. if (components.isEmpty())
  863. return base;
  864. auto childName = components.pathRoot();
  865. auto it = base.children.find(childName);
  866. if (it != base.children.end()) {
  867. return findOrCreateDirs(*it, components.subComponents());
  868. }
  869. auto &newDir = base.children[childName] = FileInfo { childName };
  870. newDir.parentPath = base.path();
  871. return findOrCreateDirs(newDir, components.subComponents());
  872. }
  873. FileInfo FakeFolder::dbState() const
  874. {
  875. FileInfo result;
  876. _journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) {
  877. auto components = PathComponents(QString::fromUtf8(record._path));
  878. auto &parentDir = findOrCreateDirs(result, components.parentDirComponents());
  879. auto name = components.fileName();
  880. auto &item = parentDir.children[name];
  881. item.name = name;
  882. item.parentPath = parentDir.path();
  883. item.size = record._fileSize;
  884. item.isDir = record._type == ItemTypeDirectory;
  885. item.permissions = record._remotePerm;
  886. item.etag = record._etag;
  887. item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime);
  888. item.fileId = record._fileId;
  889. item.checksums = record._checksumHeader;
  890. // item.contentChar can't be set from the db
  891. });
  892. return result;
  893. }
  894. OCC::SyncFileItemPtr ItemCompletedSpy::findItem(const QString &path) const
  895. {
  896. for (const QList<QVariant> &args : *this) {
  897. auto item = args[0].value<OCC::SyncFileItemPtr>();
  898. if (item->destination() == path)
  899. return item;
  900. }
  901. return OCC::SyncFileItemPtr::create();
  902. }
  903. FakeReply::FakeReply(QObject *parent)
  904. : QNetworkReply(parent)
  905. {
  906. setRawHeader(QByteArrayLiteral("Date"), QDateTime::currentDateTimeUtc().toString(Qt::RFC2822Date).toUtf8());
  907. }
  908. FakeReply::~FakeReply()
  909. = default;