syncenginetestutils.h 33 KB

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