syncenginetestutils.cpp 35 KB

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