syncenginetestutils.h 38 KB

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