syncenginetestutils.h 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  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. QString name;
  245. bool isDir = true;
  246. bool isShared = false;
  247. QDateTime lastModified = QDateTime::currentDateTime().addDays(-7);
  248. QString etag = generateEtag();
  249. QByteArray fileId = generateFileId();
  250. qint64 size = 0;
  251. char contentChar = 'W';
  252. // Sorted by name to be able to compare trees
  253. QMap<QString, FileInfo> children;
  254. QString parentPath;
  255. private:
  256. FileInfo *findInvalidatingEtags(const PathComponents &pathComponents) {
  257. return find(pathComponents, true);
  258. }
  259. void fixupParentPathRecursively() {
  260. auto p = path();
  261. for (auto it = children.begin(); it != children.end(); ++it) {
  262. Q_ASSERT(it.key() == it->name);
  263. it->parentPath = p;
  264. it->fixupParentPathRecursively();
  265. }
  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. Q_ASSERT(fileInfo);
  286. QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
  287. // Don't care about the request and just return a full propfind
  288. const QString davUri{QStringLiteral("DAV:")};
  289. const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
  290. QBuffer buffer{&payload};
  291. buffer.open(QIODevice::WriteOnly);
  292. QXmlStreamWriter xml( &buffer );
  293. xml.writeNamespace(davUri, "d");
  294. xml.writeNamespace(ocUri, "oc");
  295. xml.writeStartDocument();
  296. xml.writeStartElement(davUri, QStringLiteral("multistatus"));
  297. auto writeFileResponse = [&](const FileInfo &fileInfo) {
  298. xml.writeStartElement(davUri, QStringLiteral("response"));
  299. xml.writeTextElement(davUri, QStringLiteral("href"), prefix + fileInfo.path());
  300. xml.writeStartElement(davUri, QStringLiteral("propstat"));
  301. xml.writeStartElement(davUri, QStringLiteral("prop"));
  302. if (fileInfo.isDir) {
  303. xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
  304. xml.writeEmptyElement(davUri, QStringLiteral("collection"));
  305. xml.writeEndElement(); // resourcetype
  306. } else
  307. xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
  308. auto gmtDate = fileInfo.lastModified.toTimeZone(QTimeZone("GMT"));
  309. auto stringDate = gmtDate.toString("ddd, dd MMM yyyy HH:mm:ss 'GMT'");
  310. xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
  311. xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
  312. xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag);
  313. xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
  314. xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId);
  315. xml.writeEndElement(); // prop
  316. xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
  317. xml.writeEndElement(); // propstat
  318. xml.writeEndElement(); // response
  319. };
  320. writeFileResponse(*fileInfo);
  321. foreach(const FileInfo &childFileInfo, fileInfo->children)
  322. writeFileResponse(childFileInfo);
  323. xml.writeEndElement(); // multistatus
  324. xml.writeEndDocument();
  325. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  326. }
  327. Q_INVOKABLE void respond() {
  328. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  329. setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
  330. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
  331. setFinished(true);
  332. emit metaDataChanged();
  333. if (bytesAvailable())
  334. emit readyRead();
  335. emit finished();
  336. }
  337. void abort() override { }
  338. qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
  339. qint64 readData(char *data, qint64 maxlen) override {
  340. qint64 len = std::min(qint64{payload.size()}, maxlen);
  341. strncpy(data, payload.constData(), len);
  342. payload.remove(0, len);
  343. return len;
  344. }
  345. };
  346. class FakePutReply : public QNetworkReply
  347. {
  348. Q_OBJECT
  349. FileInfo *fileInfo;
  350. public:
  351. FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
  352. : QNetworkReply{parent} {
  353. setRequest(request);
  354. setUrl(request.url());
  355. setOperation(op);
  356. open(QIODevice::ReadOnly);
  357. QString fileName = getFilePathFromUrl(request.url());
  358. Q_ASSERT(!fileName.isEmpty());
  359. if ((fileInfo = remoteRootFileInfo.find(fileName))) {
  360. fileInfo->size = putPayload.size();
  361. fileInfo->contentChar = putPayload.at(0);
  362. } else {
  363. // Assume that the file is filled with the same character
  364. fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
  365. }
  366. if (!fileInfo) {
  367. abort();
  368. return;
  369. }
  370. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  371. }
  372. Q_INVOKABLE void respond() {
  373. emit uploadProgress(fileInfo->size, fileInfo->size);
  374. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  375. setRawHeader("ETag", fileInfo->etag.toLatin1());
  376. setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
  377. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  378. emit metaDataChanged();
  379. emit finished();
  380. }
  381. void abort() override { }
  382. qint64 readData(char *, qint64) override { return 0; }
  383. };
  384. class FakeMkcolReply : public QNetworkReply
  385. {
  386. Q_OBJECT
  387. FileInfo *fileInfo;
  388. public:
  389. FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  390. : QNetworkReply{parent} {
  391. setRequest(request);
  392. setUrl(request.url());
  393. setOperation(op);
  394. open(QIODevice::ReadOnly);
  395. QString fileName = getFilePathFromUrl(request.url());
  396. Q_ASSERT(!fileName.isEmpty());
  397. fileInfo = remoteRootFileInfo.createDir(fileName);
  398. if (!fileInfo) {
  399. abort();
  400. return;
  401. }
  402. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  403. }
  404. Q_INVOKABLE void respond() {
  405. setRawHeader("OC-FileId", fileInfo->fileId);
  406. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  407. emit metaDataChanged();
  408. emit finished();
  409. }
  410. void abort() override { }
  411. qint64 readData(char *, qint64) override { return 0; }
  412. };
  413. class FakeDeleteReply : public QNetworkReply
  414. {
  415. Q_OBJECT
  416. public:
  417. FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  418. : QNetworkReply{parent} {
  419. setRequest(request);
  420. setUrl(request.url());
  421. setOperation(op);
  422. open(QIODevice::ReadOnly);
  423. QString fileName = getFilePathFromUrl(request.url());
  424. Q_ASSERT(!fileName.isEmpty());
  425. remoteRootFileInfo.remove(fileName);
  426. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  427. }
  428. Q_INVOKABLE void respond() {
  429. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
  430. emit metaDataChanged();
  431. emit finished();
  432. }
  433. void abort() override { }
  434. qint64 readData(char *, qint64) override { return 0; }
  435. };
  436. class FakeMoveReply : public QNetworkReply
  437. {
  438. Q_OBJECT
  439. public:
  440. FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  441. : QNetworkReply{parent} {
  442. setRequest(request);
  443. setUrl(request.url());
  444. setOperation(op);
  445. open(QIODevice::ReadOnly);
  446. QString fileName = getFilePathFromUrl(request.url());
  447. Q_ASSERT(!fileName.isEmpty());
  448. QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  449. Q_ASSERT(!dest.isEmpty());
  450. remoteRootFileInfo.rename(fileName, dest);
  451. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  452. }
  453. Q_INVOKABLE void respond() {
  454. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  455. emit metaDataChanged();
  456. emit finished();
  457. }
  458. void abort() override { }
  459. qint64 readData(char *, qint64) override { return 0; }
  460. };
  461. class FakeGetReply : public QNetworkReply
  462. {
  463. Q_OBJECT
  464. public:
  465. const FileInfo *fileInfo;
  466. QByteArray payload;
  467. FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  468. : QNetworkReply{parent} {
  469. setRequest(request);
  470. setUrl(request.url());
  471. setOperation(op);
  472. open(QIODevice::ReadOnly);
  473. QString fileName = getFilePathFromUrl(request.url());
  474. Q_ASSERT(!fileName.isEmpty());
  475. fileInfo = remoteRootFileInfo.find(fileName);
  476. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  477. }
  478. Q_INVOKABLE void respond() {
  479. payload.fill(fileInfo->contentChar, fileInfo->size);
  480. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  481. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  482. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  483. setRawHeader("ETag", fileInfo->etag.toLatin1());
  484. setRawHeader("OC-FileId", fileInfo->fileId);
  485. emit metaDataChanged();
  486. if (bytesAvailable())
  487. emit readyRead();
  488. emit finished();
  489. }
  490. void abort() override { }
  491. qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
  492. qint64 readData(char *data, qint64 maxlen) override {
  493. qint64 len = std::min(qint64{payload.size()}, maxlen);
  494. strncpy(data, payload.constData(), len);
  495. payload.remove(0, len);
  496. return len;
  497. }
  498. };
  499. class FakeChunkMoveReply : public QNetworkReply
  500. {
  501. Q_OBJECT
  502. FileInfo *fileInfo;
  503. public:
  504. FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
  505. QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  506. QObject *parent) : QNetworkReply{parent} {
  507. setRequest(request);
  508. setUrl(request.url());
  509. setOperation(op);
  510. open(QIODevice::ReadOnly);
  511. QString source = getFilePathFromUrl(request.url());
  512. Q_ASSERT(!source.isEmpty());
  513. Q_ASSERT(source.endsWith("/.file"));
  514. source = source.left(source.length() - qstrlen("/.file"));
  515. auto sourceFolder = uploadsFileInfo.find(source);
  516. Q_ASSERT(sourceFolder);
  517. Q_ASSERT(sourceFolder->isDir);
  518. int count = 0;
  519. int size = 0;
  520. char payload = '*';
  521. do {
  522. if (!sourceFolder->children.contains(QString::number(count)))
  523. break;
  524. auto &x = sourceFolder->children[QString::number(count)];
  525. Q_ASSERT(!x.isDir);
  526. Q_ASSERT(x.size > 0); // There should not be empty chunks
  527. size += x.size;
  528. payload = x.contentChar;
  529. ++count;
  530. } while(true);
  531. Q_ASSERT(count > 1); // There should be at least two chunks, otherwise why would we use chunking?
  532. QCOMPARE(sourceFolder->children.count(), count); // There should not be holes or extra files
  533. QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  534. Q_ASSERT(!fileName.isEmpty());
  535. if ((fileInfo = remoteRootFileInfo.find(fileName))) {
  536. QCOMPARE(request.rawHeader("If"), QByteArray("<" + request.rawHeader("Destination") + "> ([\"" + fileInfo->etag.toLatin1() + "\"])"));
  537. fileInfo->size = size;
  538. fileInfo->contentChar = payload;
  539. } else {
  540. Q_ASSERT(!request.hasRawHeader("If"));
  541. // Assume that the file is filled with the same character
  542. fileInfo = remoteRootFileInfo.create(fileName, size, payload);
  543. }
  544. if (!fileInfo) {
  545. abort();
  546. return;
  547. }
  548. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  549. }
  550. Q_INVOKABLE void respond() {
  551. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  552. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  553. setRawHeader("ETag", fileInfo->etag.toLatin1());
  554. setRawHeader("OC-FileId", fileInfo->fileId);
  555. emit metaDataChanged();
  556. emit finished();
  557. }
  558. void abort() override { }
  559. qint64 readData(char *, qint64) override { return 0; }
  560. };
  561. class FakeErrorReply : public QNetworkReply
  562. {
  563. Q_OBJECT
  564. public:
  565. FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  566. : QNetworkReply{parent} {
  567. setRequest(request);
  568. setUrl(request.url());
  569. setOperation(op);
  570. open(QIODevice::ReadOnly);
  571. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  572. }
  573. Q_INVOKABLE void respond() {
  574. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 500);
  575. setError(InternalServerError, "Internal Server Fake Error");
  576. emit metaDataChanged();
  577. emit finished();
  578. }
  579. void abort() override { }
  580. qint64 readData(char *, qint64) override { return 0; }
  581. };
  582. class FakeQNAM : public QNetworkAccessManager
  583. {
  584. FileInfo _remoteRootFileInfo;
  585. FileInfo _uploadFileInfo;
  586. QStringList _errorPaths;
  587. public:
  588. FakeQNAM(FileInfo initialRoot) : _remoteRootFileInfo{std::move(initialRoot)} { }
  589. FileInfo &currentRemoteState() { return _remoteRootFileInfo; }
  590. FileInfo &uploadState() { return _uploadFileInfo; }
  591. QStringList &errorPaths() { return _errorPaths; }
  592. protected:
  593. QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
  594. QIODevice *outgoingData = 0) {
  595. const QString fileName = getFilePathFromUrl(request.url());
  596. Q_ASSERT(!fileName.isNull());
  597. if (_errorPaths.contains(fileName))
  598. return new FakeErrorReply{op, request, this};
  599. bool isUpload = request.url().path().startsWith(sUploadUrl.path());
  600. FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
  601. auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
  602. if (verb == QLatin1String("PROPFIND"))
  603. // Ignore outgoingData always returning somethign good enough, works for now.
  604. return new FakePropfindReply{info, op, request, this};
  605. else if (verb == QLatin1String("GET"))
  606. return new FakeGetReply{info, op, request, this};
  607. else if (verb == QLatin1String("PUT"))
  608. return new FakePutReply{info, op, request, outgoingData->readAll(), this};
  609. else if (verb == QLatin1String("MKCOL"))
  610. return new FakeMkcolReply{info, op, request, this};
  611. else if (verb == QLatin1String("DELETE"))
  612. return new FakeDeleteReply{info, op, request, this};
  613. else if (verb == QLatin1String("MOVE") && !isUpload)
  614. return new FakeMoveReply{info, op, request, this};
  615. else if (verb == QLatin1String("MOVE") && isUpload)
  616. return new FakeChunkMoveReply{info, _remoteRootFileInfo, op, request, this};
  617. else {
  618. qDebug() << verb << outgoingData;
  619. Q_UNREACHABLE();
  620. }
  621. }
  622. };
  623. class FakeCredentials : public OCC::AbstractCredentials
  624. {
  625. QNetworkAccessManager *_qnam;
  626. public:
  627. FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { }
  628. virtual QString authType() const { return "test"; }
  629. virtual QString user() const { return "admin"; }
  630. virtual QNetworkAccessManager* getQNAM() const { return _qnam; }
  631. virtual bool ready() const { return true; }
  632. virtual void fetchFromKeychain() { }
  633. virtual void askFromUser() { }
  634. virtual bool stillValid(QNetworkReply *) { return true; }
  635. virtual void persist() { }
  636. virtual void invalidateToken() { }
  637. virtual void forgetSensitiveData() { }
  638. };
  639. class FakeFolder
  640. {
  641. QTemporaryDir _tempDir;
  642. DiskFileModifier _localModifier;
  643. // FIXME: Clarify ownership, double delete
  644. FakeQNAM *_fakeQnam;
  645. OCC::AccountPtr _account;
  646. std::unique_ptr<OCC::SyncJournalDb> _journalDb;
  647. std::unique_ptr<OCC::SyncEngine> _syncEngine;
  648. public:
  649. FakeFolder(const FileInfo &fileTemplate)
  650. : _localModifier(_tempDir.path())
  651. {
  652. // Needs to be done once
  653. OCC::SyncEngine::minimumFileAgeForUpload = 0;
  654. csync_set_log_level(11);
  655. QDir rootDir{_tempDir.path()};
  656. toDisk(rootDir, fileTemplate);
  657. _fakeQnam = new FakeQNAM(fileTemplate);
  658. _account = OCC::Account::create();
  659. _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
  660. _account->setCredentials(new FakeCredentials{_fakeQnam});
  661. _journalDb.reset(new OCC::SyncJournalDb(localPath() + "._sync_test.db"));
  662. _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), "", _journalDb.get()));
  663. // A new folder will update the local file state database on first sync.
  664. // To have a state matching what users will encounter, we have to a sync
  665. // using an identical local/remote file tree first.
  666. syncOnce();
  667. }
  668. OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
  669. FileModifier &localModifier() { return _localModifier; }
  670. FileModifier &remoteModifier() { return _fakeQnam->currentRemoteState(); }
  671. FileInfo currentLocalState() {
  672. QDir rootDir{_tempDir.path()};
  673. FileInfo rootTemplate;
  674. fromDisk(rootDir, rootTemplate);
  675. return rootTemplate;
  676. }
  677. FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
  678. FileInfo &uploadState() { return _fakeQnam->uploadState(); }
  679. QStringList &serverErrorPaths() { return _fakeQnam->errorPaths(); }
  680. QString localPath() const {
  681. // SyncEngine wants a trailing slash
  682. if (_tempDir.path().endsWith('/'))
  683. return _tempDir.path();
  684. return _tempDir.path() + '/';
  685. }
  686. void scheduleSync() {
  687. // Have to be done async, else, an error before exec() does not terminate the event loop.
  688. QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
  689. }
  690. void execUntilBeforePropagation() {
  691. QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&)));
  692. QVERIFY(spy.wait());
  693. }
  694. void execUntilItemCompleted(const QString &relativePath) {
  695. QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
  696. QElapsedTimer t;
  697. t.start();
  698. while (t.elapsed() < 5000) {
  699. spy.clear();
  700. QVERIFY(spy.wait());
  701. for(const QList<QVariant> &args : spy) {
  702. auto item = args[0].value<OCC::SyncFileItem>();
  703. if (item.destination() == relativePath)
  704. return;
  705. }
  706. }
  707. QVERIFY(false);
  708. }
  709. bool execUntilFinished() {
  710. QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
  711. bool ok = spy.wait(60000);
  712. Q_ASSERT(ok && "Sync timed out");
  713. return spy[0][0].toBool();
  714. }
  715. bool syncOnce() {
  716. scheduleSync();
  717. return execUntilFinished();
  718. }
  719. private:
  720. static void toDisk(QDir &dir, const FileInfo &templateFi) {
  721. foreach (const FileInfo &child, templateFi.children) {
  722. if (child.isDir) {
  723. QDir subDir(dir);
  724. dir.mkdir(child.name);
  725. subDir.cd(child.name);
  726. toDisk(subDir, child);
  727. } else {
  728. QFile file{dir.filePath(child.name)};
  729. file.open(QFile::WriteOnly);
  730. file.write(QByteArray{}.fill(child.contentChar, child.size));
  731. file.close();
  732. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
  733. }
  734. }
  735. }
  736. static void fromDisk(QDir &dir, FileInfo &templateFi) {
  737. foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
  738. if (diskChild.isDir()) {
  739. QDir subDir = dir;
  740. subDir.cd(diskChild.fileName());
  741. templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName()});
  742. fromDisk(subDir, templateFi.children.last());
  743. } else {
  744. QFile f{diskChild.filePath()};
  745. f.open(QFile::ReadOnly);
  746. char contentChar = f.read(1).at(0);
  747. templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
  748. }
  749. }
  750. }
  751. };
  752. // QTest::toString overloads
  753. namespace OCC {
  754. inline char *toString(const SyncFileStatus &s) {
  755. return QTest::toString(QString("SyncFileStatus(" + s.toSocketAPIString() + ")"));
  756. }
  757. }
  758. inline void addFiles(QStringList &dest, const FileInfo &fi)
  759. {
  760. if (fi.isDir) {
  761. dest += QString("%1 - dir").arg(fi.name);
  762. foreach (const FileInfo &fi, fi.children)
  763. addFiles(dest, fi);
  764. } else {
  765. dest += QString("%1 - %2 %3-bytes").arg(fi.name).arg(fi.size).arg(fi.contentChar);
  766. }
  767. }
  768. inline char *toString(const FileInfo &fi)
  769. {
  770. QStringList files;
  771. foreach (const FileInfo &fi, fi.children)
  772. addFiles(files, fi);
  773. return QTest::toString(QString("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(", ")));
  774. }