syncenginetestutils.h 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  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 <QtTest>
  16. static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
  17. namespace {
  18. QString generateEtag() {
  19. return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
  20. }
  21. class PathComponents : public QStringList {
  22. public:
  23. PathComponents(const QString &path) : QStringList{path.split('/', QString::SkipEmptyParts)} { }
  24. PathComponents(const QStringList &pathComponents) : QStringList{pathComponents} { }
  25. PathComponents parentDirComponents() const {
  26. return PathComponents{mid(0, size() - 1)};
  27. }
  28. PathComponents subComponents() const { return PathComponents{mid(1)}; }
  29. QString pathRoot() const { return first(); }
  30. QString fileName() const { return last(); }
  31. };
  32. }
  33. class FileModifier
  34. {
  35. public:
  36. virtual ~FileModifier() { }
  37. virtual void remove(const QString &relativePath) = 0;
  38. virtual void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') = 0;
  39. virtual void setContents(const QString &relativePath, char contentChar) = 0;
  40. virtual void appendByte(const QString &relativePath) = 0;
  41. virtual void mkdir(const QString &relativePath) = 0;
  42. };
  43. class DiskFileModifier : public FileModifier
  44. {
  45. QDir _rootDir;
  46. public:
  47. DiskFileModifier(const QString &rootDirPath) : _rootDir(rootDirPath) { }
  48. void remove(const QString &relativePath) override {
  49. QFileInfo fi{_rootDir.filePath(relativePath)};
  50. if (fi.isFile())
  51. QVERIFY(_rootDir.remove(relativePath));
  52. else
  53. QVERIFY(QDir{fi.filePath()}.removeRecursively());
  54. }
  55. void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
  56. QFile file{_rootDir.filePath(relativePath)};
  57. QVERIFY(!file.exists());
  58. file.open(QFile::WriteOnly);
  59. file.write(QByteArray{}.fill(contentChar, size));
  60. file.close();
  61. // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
  62. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTime().addSecs(-30)));
  63. }
  64. void setContents(const QString &relativePath, char contentChar) override {
  65. QFile file{_rootDir.filePath(relativePath)};
  66. QVERIFY(file.exists());
  67. qint64 size = file.size();
  68. file.open(QFile::WriteOnly);
  69. file.write(QByteArray{}.fill(contentChar, size));
  70. }
  71. void appendByte(const QString &relativePath) override {
  72. QFile file{_rootDir.filePath(relativePath)};
  73. QVERIFY(file.exists());
  74. file.open(QFile::ReadWrite);
  75. QByteArray contents = file.read(1);
  76. file.seek(file.size());
  77. file.write(contents);
  78. }
  79. void mkdir(const QString &relativePath) override {
  80. _rootDir.mkpath(relativePath);
  81. }
  82. };
  83. class FileInfo : public FileModifier
  84. {
  85. public:
  86. static FileInfo A12_B12_C12_S12() {
  87. FileInfo fi{QString{}, {
  88. {QStringLiteral("A"), {
  89. {QStringLiteral("a1"), 4},
  90. {QStringLiteral("a2"), 4}
  91. }},
  92. {QStringLiteral("B"), {
  93. {QStringLiteral("b1"), 16},
  94. {QStringLiteral("b2"), 16}
  95. }},
  96. {QStringLiteral("C"), {
  97. {QStringLiteral("c1"), 24},
  98. {QStringLiteral("c2"), 24}
  99. }},
  100. }};
  101. FileInfo sharedFolder{QStringLiteral("S"), {
  102. {QStringLiteral("s1"), 32},
  103. {QStringLiteral("s2"), 32}
  104. }};
  105. sharedFolder.isShared = true;
  106. sharedFolder.children[QStringLiteral("s1")].isShared = true;
  107. sharedFolder.children[QStringLiteral("s2")].isShared = true;
  108. fi.children.insert(sharedFolder.name, std::move(sharedFolder));
  109. return fi;
  110. }
  111. FileInfo() = default;
  112. FileInfo(const QString &name) : name{name} { }
  113. FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { }
  114. FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { }
  115. FileInfo(const QString &name, const std::initializer_list<FileInfo> &children) : name{name} {
  116. QString p = path();
  117. for (const auto &source : children) {
  118. auto &dest = this->children[source.name] = source;
  119. dest.parentPath = p;
  120. }
  121. }
  122. void remove(const QString &relativePath) override {
  123. const PathComponents pathComponents{relativePath};
  124. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  125. Q_ASSERT(parent);
  126. parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(),
  127. [&pathComponents](const FileInfo &fi){ return fi.name == pathComponents.fileName(); }));
  128. }
  129. void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
  130. create(relativePath, size, contentChar);
  131. }
  132. void setContents(const QString &relativePath, char contentChar) override {
  133. FileInfo *file = findInvalidatingEtags(relativePath);
  134. Q_ASSERT(file);
  135. file->contentChar = contentChar;
  136. }
  137. void appendByte(const QString &relativePath) override {
  138. FileInfo *file = findInvalidatingEtags(relativePath);
  139. Q_ASSERT(file);
  140. file->size += 1;
  141. }
  142. void mkdir(const QString &relativePath) override {
  143. createDir(relativePath);
  144. }
  145. FileInfo *find(const PathComponents &pathComponents, const bool invalidateEtags = false) {
  146. if (pathComponents.isEmpty()) {
  147. if (invalidateEtags)
  148. etag = generateEtag();
  149. return this;
  150. }
  151. QString childName = pathComponents.pathRoot();
  152. auto it = children.find(childName);
  153. if (it != children.end()) {
  154. auto file = it->find(pathComponents.subComponents(), invalidateEtags);
  155. if (file && invalidateEtags)
  156. // Update parents on the way back
  157. etag = file->etag;
  158. return file;
  159. }
  160. return nullptr;
  161. }
  162. FileInfo *createDir(const QString &relativePath) {
  163. const PathComponents pathComponents{relativePath};
  164. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  165. Q_ASSERT(parent);
  166. FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName()};
  167. child.parentPath = parent->path();
  168. child.etag = generateEtag();
  169. return &child;
  170. }
  171. FileInfo *create(const QString &relativePath, qint64 size, char contentChar) {
  172. const PathComponents pathComponents{relativePath};
  173. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  174. Q_ASSERT(parent);
  175. FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName(), size};
  176. child.parentPath = parent->path();
  177. child.contentChar = contentChar;
  178. child.etag = generateEtag();
  179. return &child;
  180. }
  181. bool operator<(const FileInfo &other) const {
  182. return name < other.name;
  183. }
  184. bool operator==(const FileInfo &other) const {
  185. // Consider files to be equal between local<->remote as a user would.
  186. return name == other.name
  187. && isDir == other.isDir
  188. && size == other.size
  189. && contentChar == other.contentChar
  190. && children == other.children;
  191. }
  192. QString path() const {
  193. return (parentPath.isEmpty() ? QString() : (parentPath + '/')) + name;
  194. }
  195. QString name;
  196. bool isDir = true;
  197. bool isShared = false;
  198. QDateTime lastModified = QDateTime::currentDateTime().addDays(-7);
  199. QString etag = generateEtag();
  200. qint64 size = 0;
  201. char contentChar = 'W';
  202. // Sorted by name to be able to compare trees
  203. QMap<QString, FileInfo> children;
  204. QString parentPath;
  205. private:
  206. FileInfo *findInvalidatingEtags(const PathComponents &pathComponents) {
  207. return find(pathComponents, true);
  208. }
  209. };
  210. class FakePropfindReply : public QNetworkReply
  211. {
  212. Q_OBJECT
  213. public:
  214. QByteArray payload;
  215. FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  216. : QNetworkReply{parent} {
  217. setRequest(request);
  218. setUrl(request.url());
  219. setOperation(op);
  220. open(QIODevice::ReadOnly);
  221. // Don't care about the request and just return a full propfind
  222. const QString davUri{QStringLiteral("DAV:")};
  223. const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
  224. QBuffer buffer{&payload};
  225. buffer.open(QIODevice::WriteOnly);
  226. QXmlStreamWriter xml( &buffer );
  227. xml.writeNamespace(davUri, "d");
  228. xml.writeNamespace(ocUri, "oc");
  229. xml.writeStartDocument();
  230. xml.writeStartElement(davUri, QStringLiteral("multistatus"));
  231. auto writeFileResponse = [&](const FileInfo &fileInfo) {
  232. xml.writeStartElement(davUri, QStringLiteral("response"));
  233. xml.writeTextElement(davUri, QStringLiteral("href"), "/owncloud/remote.php/webdav/" + fileInfo.path());
  234. xml.writeStartElement(davUri, QStringLiteral("propstat"));
  235. xml.writeStartElement(davUri, QStringLiteral("prop"));
  236. if (fileInfo.isDir) {
  237. xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
  238. xml.writeEmptyElement(davUri, QStringLiteral("collection"));
  239. xml.writeEndElement(); // resourcetype
  240. } else
  241. xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
  242. auto gmtDate = fileInfo.lastModified.toTimeZone(QTimeZone("GMT"));
  243. auto stringDate = gmtDate.toString("ddd, dd MMM yyyy HH:mm:ss 'GMT'");
  244. xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
  245. xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
  246. xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag);
  247. xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
  248. xml.writeEndElement(); // prop
  249. xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
  250. xml.writeEndElement(); // propstat
  251. xml.writeEndElement(); // response
  252. };
  253. Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
  254. QString fileName = request.url().path().mid(sRootUrl.path().length());
  255. const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  256. writeFileResponse(*fileInfo);
  257. foreach(const FileInfo &childFileInfo, fileInfo->children)
  258. writeFileResponse(childFileInfo);
  259. xml.writeEndElement(); // multistatus
  260. xml.writeEndDocument();
  261. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  262. }
  263. Q_INVOKABLE void respond() {
  264. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  265. setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
  266. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
  267. setFinished(true);
  268. emit metaDataChanged();
  269. if (bytesAvailable())
  270. emit readyRead();
  271. emit finished();
  272. }
  273. void abort() override { }
  274. qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
  275. qint64 readData(char *data, qint64 maxlen) override {
  276. qint64 len = std::min(qint64{payload.size()}, maxlen);
  277. strncpy(data, payload.constData(), len);
  278. payload.remove(0, len);
  279. return len;
  280. }
  281. };
  282. class FakePutReply : public QNetworkReply
  283. {
  284. Q_OBJECT
  285. FileInfo *fileInfo;
  286. public:
  287. FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
  288. : QNetworkReply{parent} {
  289. setRequest(request);
  290. setUrl(request.url());
  291. setOperation(op);
  292. open(QIODevice::ReadOnly);
  293. Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
  294. QString fileName = request.url().path().mid(sRootUrl.path().length());
  295. if ((fileInfo = remoteRootFileInfo.find(fileName))) {
  296. fileInfo->size = putPayload.size();
  297. fileInfo->contentChar = putPayload.at(0);
  298. } else {
  299. // Assume that the file is filled with the same character
  300. fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
  301. }
  302. if (!fileInfo) {
  303. abort();
  304. return;
  305. }
  306. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  307. }
  308. Q_INVOKABLE void respond() {
  309. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  310. setRawHeader("ETag", fileInfo->etag.toLatin1());
  311. setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
  312. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  313. emit metaDataChanged();
  314. emit finished();
  315. }
  316. void abort() override { }
  317. qint64 readData(char *, qint64) override { return 0; }
  318. };
  319. class FakeMkcolReply : public QNetworkReply
  320. {
  321. Q_OBJECT
  322. FileInfo *fileInfo;
  323. public:
  324. FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  325. : QNetworkReply{parent} {
  326. setRequest(request);
  327. setUrl(request.url());
  328. setOperation(op);
  329. open(QIODevice::ReadOnly);
  330. Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
  331. QString fileName = request.url().path().mid(sRootUrl.path().length());
  332. fileInfo = remoteRootFileInfo.createDir(fileName);
  333. if (!fileInfo) {
  334. abort();
  335. return;
  336. }
  337. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  338. }
  339. Q_INVOKABLE void respond() {
  340. // FIXME: setRawHeader("OC-FileId", fileInfo->???);
  341. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  342. emit metaDataChanged();
  343. emit finished();
  344. }
  345. void abort() override { }
  346. qint64 readData(char *, qint64) override { return 0; }
  347. };
  348. class FakeDeleteReply : public QNetworkReply
  349. {
  350. Q_OBJECT
  351. public:
  352. FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  353. : QNetworkReply{parent} {
  354. setRequest(request);
  355. setUrl(request.url());
  356. setOperation(op);
  357. open(QIODevice::ReadOnly);
  358. Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
  359. QString fileName = request.url().path().mid(sRootUrl.path().length());
  360. remoteRootFileInfo.remove(fileName);
  361. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  362. }
  363. Q_INVOKABLE void respond() {
  364. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
  365. emit metaDataChanged();
  366. emit finished();
  367. }
  368. void abort() override { }
  369. qint64 readData(char *, qint64) override { return 0; }
  370. };
  371. class FakeGetReply : public QNetworkReply
  372. {
  373. Q_OBJECT
  374. public:
  375. const FileInfo *fileInfo;
  376. QByteArray payload;
  377. FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  378. : QNetworkReply{parent} {
  379. setRequest(request);
  380. setUrl(request.url());
  381. setOperation(op);
  382. open(QIODevice::ReadOnly);
  383. Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
  384. QString fileName = request.url().path().mid(sRootUrl.path().length());
  385. fileInfo = remoteRootFileInfo.find(fileName);
  386. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  387. }
  388. Q_INVOKABLE void respond() {
  389. payload.fill(fileInfo->contentChar, fileInfo->size);
  390. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  391. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  392. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  393. setRawHeader("ETag", fileInfo->etag.toLatin1());
  394. emit metaDataChanged();
  395. if (bytesAvailable())
  396. emit readyRead();
  397. emit finished();
  398. }
  399. void abort() override { }
  400. qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
  401. qint64 readData(char *data, qint64 maxlen) override {
  402. qint64 len = std::min(qint64{payload.size()}, maxlen);
  403. strncpy(data, payload.constData(), len);
  404. payload.remove(0, len);
  405. return len;
  406. }
  407. };
  408. class FakeErrorReply : public QNetworkReply
  409. {
  410. Q_OBJECT
  411. public:
  412. FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  413. : QNetworkReply{parent} {
  414. setRequest(request);
  415. setUrl(request.url());
  416. setOperation(op);
  417. open(QIODevice::ReadOnly);
  418. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  419. }
  420. Q_INVOKABLE void respond() {
  421. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 500);
  422. emit metaDataChanged();
  423. emit finished();
  424. }
  425. void abort() override { }
  426. qint64 readData(char *, qint64) override { return 0; }
  427. };
  428. class FakeQNAM : public QNetworkAccessManager
  429. {
  430. FileInfo _remoteRootFileInfo;
  431. QStringList _errorPaths;
  432. public:
  433. FakeQNAM(FileInfo initialRoot) : _remoteRootFileInfo{std::move(initialRoot)} { }
  434. FileInfo &currentRemoteState() { return _remoteRootFileInfo; }
  435. QStringList &errorPaths() { return _errorPaths; }
  436. protected:
  437. QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
  438. QIODevice *outgoingData = 0) {
  439. const QString fileName = request.url().path().mid(sRootUrl.path().length());
  440. if (_errorPaths.contains(fileName))
  441. return new FakeErrorReply{op, request, this};
  442. auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
  443. if (verb == QLatin1String("PROPFIND"))
  444. // Ignore outgoingData always returning somethign good enough, works for now.
  445. return new FakePropfindReply{_remoteRootFileInfo, op, request, this};
  446. else if (verb == QLatin1String("GET"))
  447. return new FakeGetReply{_remoteRootFileInfo, op, request, this};
  448. else if (verb == QLatin1String("PUT"))
  449. return new FakePutReply{_remoteRootFileInfo, op, request, outgoingData->readAll(), this};
  450. else if (verb == QLatin1String("MKCOL"))
  451. return new FakeMkcolReply{_remoteRootFileInfo, op, request, this};
  452. else if (verb == QLatin1String("DELETE"))
  453. return new FakeDeleteReply{_remoteRootFileInfo, op, request, this};
  454. else {
  455. qDebug() << verb << outgoingData;
  456. Q_UNREACHABLE();
  457. }
  458. }
  459. };
  460. class FakeCredentials : public OCC::AbstractCredentials
  461. {
  462. QNetworkAccessManager *_qnam;
  463. public:
  464. FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { }
  465. virtual bool changed(AbstractCredentials *) const { return false; }
  466. virtual QString authType() const { return "test"; }
  467. virtual QString user() const { return "admin"; }
  468. virtual QNetworkAccessManager* getQNAM() const { return _qnam; }
  469. virtual bool ready() const { return true; }
  470. virtual void fetchFromKeychain() { }
  471. virtual void askFromUser() { }
  472. virtual bool stillValid(QNetworkReply *) { return true; }
  473. virtual void persist() { }
  474. virtual void invalidateToken() { }
  475. virtual void forgetSensitiveData() { }
  476. };
  477. class FakeFolder
  478. {
  479. QTemporaryDir _tempDir;
  480. DiskFileModifier _localModifier;
  481. // FIXME: Clarify ownership, double delete
  482. FakeQNAM *_fakeQnam;
  483. OCC::AccountPtr _account;
  484. std::unique_ptr<OCC::SyncJournalDb> _journalDb;
  485. std::unique_ptr<OCC::SyncEngine> _syncEngine;
  486. public:
  487. FakeFolder(const FileInfo &fileTemplate)
  488. : _localModifier(_tempDir.path())
  489. {
  490. // Needs to be done once
  491. OCC::SyncEngine::minimumFileAgeForUpload = 0;
  492. csync_set_log_level(11);
  493. QDir rootDir{_tempDir.path()};
  494. toDisk(rootDir, fileTemplate);
  495. _fakeQnam = new FakeQNAM(fileTemplate);
  496. _account = OCC::Account::create();
  497. _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
  498. _account->setCredentials(new FakeCredentials{_fakeQnam});
  499. _journalDb.reset(new OCC::SyncJournalDb(localPath()));
  500. _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), sRootUrl, "", _journalDb.get()));
  501. // A new folder will update the local file state database on first sync.
  502. // To have a state matching what users will encounter, we have to a sync
  503. // using an identical local/remote file tree first.
  504. syncOnce();
  505. }
  506. OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
  507. FileModifier &localModifier() { return _localModifier; }
  508. FileModifier &remoteModifier() { return _fakeQnam->currentRemoteState(); }
  509. FileInfo currentLocalState() {
  510. QDir rootDir{_tempDir.path()};
  511. FileInfo rootTemplate;
  512. fromDisk(rootDir, rootTemplate);
  513. return rootTemplate;
  514. }
  515. FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
  516. QStringList &serverErrorPaths() { return _fakeQnam->errorPaths(); }
  517. QString localPath() const {
  518. // SyncEngine wants a trailing slash
  519. if (_tempDir.path().endsWith('/'))
  520. return _tempDir.path();
  521. return _tempDir.path() + '/';
  522. }
  523. void scheduleSync() {
  524. // Have to be done async, else, an error before exec() does not terminate the event loop.
  525. QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
  526. }
  527. void execUntilBeforePropagation() {
  528. QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&)));
  529. QVERIFY(spy.wait());
  530. }
  531. void execUntilItemCompleted(const QString &relativePath) {
  532. QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
  533. QElapsedTimer t;
  534. t.start();
  535. while (t.elapsed() < 5000) {
  536. spy.clear();
  537. QVERIFY(spy.wait());
  538. for(const QList<QVariant> &args : spy) {
  539. auto item = args[0].value<OCC::SyncFileItem>();
  540. if (item.destination() == relativePath)
  541. return;
  542. }
  543. }
  544. QVERIFY(false);
  545. }
  546. void execUntilFinished() {
  547. QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
  548. QVERIFY(spy.wait());
  549. }
  550. void syncOnce() {
  551. scheduleSync();
  552. execUntilFinished();
  553. }
  554. private:
  555. static void toDisk(QDir &dir, const FileInfo &templateFi) {
  556. foreach (const FileInfo &child, templateFi.children) {
  557. if (child.isDir) {
  558. QDir subDir(dir);
  559. dir.mkdir(child.name);
  560. subDir.cd(child.name);
  561. toDisk(subDir, child);
  562. } else {
  563. QFile file{dir.filePath(child.name)};
  564. file.open(QFile::WriteOnly);
  565. file.write(QByteArray{}.fill(child.contentChar, child.size));
  566. file.close();
  567. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
  568. }
  569. }
  570. }
  571. static void fromDisk(QDir &dir, FileInfo &templateFi) {
  572. foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
  573. if (diskChild.isDir()) {
  574. QDir subDir = dir;
  575. subDir.cd(diskChild.fileName());
  576. templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName()});
  577. fromDisk(subDir, templateFi.children.last());
  578. } else {
  579. QFile f{diskChild.filePath()};
  580. f.open(QFile::ReadOnly);
  581. char contentChar = f.read(1).at(0);
  582. templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
  583. }
  584. }
  585. }
  586. };
  587. // QTest::toString overloads
  588. namespace OCC {
  589. inline char *toString(const SyncFileStatus &s) {
  590. return QTest::toString(QString("SyncFileStatus(" + s.toSocketAPIString() + ")"));
  591. }
  592. }
  593. inline void addFiles(QStringList &dest, const FileInfo &fi)
  594. {
  595. if (fi.isDir) {
  596. dest += QString("%1 - dir").arg(fi.name);
  597. foreach (const FileInfo &fi, fi.children)
  598. addFiles(dest, fi);
  599. } else {
  600. dest += QString("%1 - %2 %3-bytes").arg(fi.name).arg(fi.size).arg(fi.contentChar);
  601. }
  602. }
  603. inline char *toString(const FileInfo &fi)
  604. {
  605. QStringList files;
  606. foreach (const FileInfo &fi, fi.children)
  607. addFiles(files, fi);
  608. return QTest::toString(QString("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(", ")));
  609. }