syncenginetestutils.h 36 KB

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