syncenginetestutils.h 35 KB

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