syncenginetestutils.h 39 KB

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