diff options
author | Olivier Goffart <ogoffart@woboq.com> | 2018-09-14 16:25:59 +0300 |
---|---|---|
committer | Olivier Goffart <ogoffart@woboq.com> | 2018-09-14 16:25:59 +0300 |
commit | 0290564d5c654d654856f04f1b04bad984f36aee (patch) | |
tree | 3b9a5eda39861d9467bc130a00e7effbc4ef7b6c /test | |
parent | 638f5c8752164ccc0429b1df60b79c3d3293bcd4 (diff) | |
parent | 3a335879fa1f00f094d82855126fec3f6ab59405 (diff) |
Merge remote-tracking branch 'owncloud/master' into delta-sync
Conflicts:
.gitmodules
src/cmd/cmd.cpp
src/gui/generalsettings.ui
src/libsync/propagatedownload.cpp
src/libsync/propagateuploadng.cpp
Diffstat (limited to 'test')
27 files changed, 2315 insertions, 497 deletions
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d6a99c67d..71a38b4c4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -40,16 +40,21 @@ owncloud_add_test(ChecksumValidator "") owncloud_add_test(ExcludedFiles "") -owncloud_add_test(FileSystem "") owncloud_add_test(Utility "") owncloud_add_test(SyncEngine "syncenginetestutils.h") +owncloud_add_test(SyncVirtualFiles "syncenginetestutils.h") owncloud_add_test(SyncMove "syncenginetestutils.h") owncloud_add_test(SyncConflict "syncenginetestutils.h") owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h") +owncloud_add_test(Download "syncenginetestutils.h") owncloud_add_test(ChunkingNg "syncenginetestutils.h") owncloud_add_test(Zsync "syncenginetestutils.h") owncloud_add_test(UploadReset "syncenginetestutils.h") owncloud_add_test(AllFilesDeleted "syncenginetestutils.h") +owncloud_add_test(Blacklist "syncenginetestutils.h") +owncloud_add_test(LocalDiscovery "syncenginetestutils.h") +owncloud_add_test(RemoteDiscovery "syncenginetestutils.h") +owncloud_add_test(Permissions "syncenginetestutils.h") owncloud_add_test(FolderWatcher "${FolderWatcher_SRC}") if( UNIX AND NOT APPLE ) @@ -66,6 +71,8 @@ list(APPEND FolderMan_SRC ../src/gui/syncrunfilelog.cpp ) list(APPEND FolderMan_SRC ../src/gui/lockwatcher.cpp ) list(APPEND FolderMan_SRC ../src/gui/guiutility.cpp ) list(APPEND FolderMan_SRC ../src/gui/navigationpanehelper.cpp ) +list(APPEND FolderMan_SRC ../src/gui/connectionvalidator.cpp ) +list(APPEND FolderMan_SRC ../src/gui/clientproxy.cpp ) list(APPEND FolderMan_SRC ${FolderWatcher_SRC}) list(APPEND FolderMan_SRC stub.cpp ) owncloud_add_test(FolderMan "${FolderMan_SRC}") diff --git a/test/csync/CMakeLists.txt b/test/csync/CMakeLists.txt index a7ec9f133..4e9c1422c 100644 --- a/test/csync/CMakeLists.txt +++ b/test/csync/CMakeLists.txt @@ -11,9 +11,9 @@ include_directories( include_directories(${CHECK_INCLUDE_DIRS}) # create test library add_library(${TORTURE_LIBRARY} STATIC torture.c cmdline.c) -target_link_libraries(${TORTURE_LIBRARY} ${CMOCKA_LIBRARIES} ${CSYNC_LIBRARY}) +target_link_libraries(${TORTURE_LIBRARY} ${CMOCKA_LIBRARIES}) -set(TEST_TARGET_LIBRARIES ${TORTURE_LIBRARY} Qt5::Core ocsync) +set(TEST_TARGET_LIBRARIES ${TORTURE_LIBRARY} Qt5::Core "${csync_NAME}") # create tests diff --git a/test/csync/csync_tests/check_csync_exclude.cpp b/test/csync/csync_tests/check_csync_exclude.cpp index f719aa4aa..3969dcaca 100644 --- a/test/csync/csync_tests/check_csync_exclude.cpp +++ b/test/csync/csync_tests/check_csync_exclude.cpp @@ -22,6 +22,8 @@ #include <time.h> #include <sys/time.h> +#include <QTemporaryDir> + #define CSYNC_TEST 1 #include "csync_exclude.cpp" @@ -271,7 +273,8 @@ static void check_csync_excluded_traversal(void **) assert_int_equal(check_file_traversal("subdir/.sync_5bdd60bdfcfa.db"), CSYNC_FILE_SILENTLY_EXCLUDED); /* Other builtin excludes */ - assert_int_equal(check_file_traversal("foo/Desktop.ini"), CSYNC_FILE_SILENTLY_EXCLUDED); + assert_int_equal(check_file_traversal("foo/Desktop.ini"), CSYNC_NOT_EXCLUDED); + assert_int_equal(check_file_traversal("Desktop.ini"), CSYNC_FILE_SILENTLY_EXCLUDED); /* pattern ]*.directory - ignore and remove */ assert_int_equal(check_file_traversal("my.~directory"), CSYNC_FILE_EXCLUDE_AND_REMOVE); @@ -624,6 +627,34 @@ static void check_csync_exclude_expand_escapes(void **state) assert_true(0 == strcmp(line.constData(), "\\")); } +static void check_version_directive(void **state) +{ + (void)state; + + ExcludedFiles excludes; + excludes.setClientVersion(ExcludedFiles::Version(2, 5, 0)); + + std::vector<std::pair<const char *, bool>> tests = { + { "#!version == 2.5.0", true }, + { "#!version == 2.6.0", false }, + { "#!version < 2.6.0", true }, + { "#!version <= 2.6.0", true }, + { "#!version > 2.6.0", false }, + { "#!version >= 2.6.0", false }, + { "#!version < 2.4.0", false }, + { "#!version <= 2.4.0", false }, + { "#!version > 2.4.0", true }, + { "#!version >= 2.4.0", true }, + { "#!version < 2.5.0", false }, + { "#!version <= 2.5.0", true }, + { "#!version > 2.5.0", false }, + { "#!version >= 2.5.0", true }, + }; + for (auto test : tests) { + assert_true(excludes.versionDirectiveKeepNextLine(test.first) == test.second); + } +} + }; // class ExcludedFilesTest int torture_run_tests(void) @@ -642,6 +673,7 @@ int torture_run_tests(void) cmocka_unit_test_setup_teardown(T::check_csync_is_windows_reserved_word, T::setup_init, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_excluded_performance, T::setup_init, T::teardown), cmocka_unit_test(T::check_csync_exclude_expand_escapes), + cmocka_unit_test(T::check_version_directive), }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/test/csync/csync_tests/check_csync_update.cpp b/test/csync/csync_tests/check_csync_update.cpp index 504ea2096..b4eca074d 100644 --- a/test/csync/csync_tests/check_csync_update.cpp +++ b/test/csync/csync_tests/check_csync_update.cpp @@ -325,16 +325,6 @@ static void check_csync_detect_update_db_new(void **state) csync_set_status(csync, 0xFFFF); } -static void check_csync_detect_update_null(void **state) -{ - CSYNC *csync = (CSYNC*)*state; - std::unique_ptr<csync_file_stat_t> fs; - int rc; - - rc = _csync_detect_update(csync, NULL); - assert_int_equal(rc, -1); -} - static void check_csync_ftw(void **state) { CSYNC *csync = (CSYNC*)*state; @@ -370,7 +360,6 @@ int torture_run_tests(void) cmocka_unit_test_setup_teardown(check_csync_detect_update_db_eval, setup, teardown), cmocka_unit_test_setup_teardown(check_csync_detect_update_db_rename, setup, teardown), cmocka_unit_test_setup_teardown(check_csync_detect_update_db_new, setup, teardown_rm), - cmocka_unit_test_setup_teardown(check_csync_detect_update_null, setup, teardown_rm), cmocka_unit_test_setup_teardown(check_csync_ftw, setup_ftw, teardown_rm), cmocka_unit_test_setup_teardown(check_csync_ftw_empty_uri, setup_ftw, teardown_rm), diff --git a/test/scripts/txpl/t7.pl b/test/scripts/txpl/t7.pl deleted file mode 100755 index 6b68a06ef..000000000 --- a/test/scripts/txpl/t7.pl +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/perl -# -# Test script for the ownCloud module of csync. -# This script requires a running ownCloud instance accessible via HTTP. -# It does quite some fancy tests and asserts the results. -# -# Copyright (C) by Klaas Freitag <freitag@owncloud.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -# - -use lib "."; - - -use File::Copy; -use ownCloud::Test; - -use strict; - -print "Hello, this is t7, a tester for syncing of files in read only directory\n"; - -# Check if the expected rows in the DB are non-empty. Note that in some cases they might be, then we cannot use this function -# https://github.comowncloud/client/issues/2038 -sub assertCsyncJournalOk { - my $path = $_[0]; - - # FIXME: should test also remoteperm but it's not working with owncloud6 - # my $cmd = 'sqlite3 ' . $path . '._sync_*.db "SELECT count(*) from metadata where length(remotePerm) == 0 or length(fileId) == 0"'; - my $cmd = 'sqlite3 ' . $path . '._sync_*.db "SELECT count(*) from metadata where length(fileId) == 0"'; - my $result = `$cmd`; - assert($result == "0"); -} - -# IMPORTANT NOTE : -print "This test use the OWNCLOUD_TEST_PERMISSIONS environement variable and _PERM_xxx_ on filenames to set the permission. "; -print "It does not rely on real permission set on the server. This test is just for testing the propagation choices\n"; -# "It would be nice" to have a test that test with real permissions on the server - -$ENV{OWNCLOUD_TEST_PERMISSIONS} = "1"; - -initTesting(); - -printInfo( "Init" ); - -#create some files localy -my $tmpdir = "/tmp/t7/"; -mkdir($tmpdir); -createLocalFile( $tmpdir . "normalFile_PERM_WVND_.data", 100 ); -createLocalFile( $tmpdir . "cannotBeRemoved_PERM_WVN_.data", 101 ); -createLocalFile( $tmpdir . "canBeRemoved_PERM_D_.data", 102 ); -my $md5CanotBeModified = createLocalFile( $tmpdir . "canotBeModified_PERM_DVN_.data", 103 ); -createLocalFile( $tmpdir . "canBeModified_PERM_W_.data", 104 ); - -#put them in some directories -createRemoteDir( "normalDirectory_PERM_CKDNV_" ); -glob_put( "$tmpdir/*", "normalDirectory_PERM_CKDNV_" ); -createRemoteDir( "readonlyDirectory_PERM_M_" ); -glob_put( "$tmpdir/*", "readonlyDirectory_PERM_M_" ); -createRemoteDir( "readonlyDirectory_PERM_M_/subdir_PERM_CK_" ); -createRemoteDir( "readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_" ); -glob_put( "$tmpdir/normalFile_PERM_WVND_.data", "readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_" ); - - -csync(); -assertCsyncJournalOk(localDir()); -assertLocalAndRemoteDir( '', 0); - -system("sleep 1"); #make sure changes have different mtime - -printInfo( "Do some changes and see how they propagate" ); - -#1. remove the file than cannot be removed -# (they should be recovered) -unlink( localDir() . 'normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data' ); -unlink( localDir() . 'readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data' ); - -#2. remove the file that can be removed -# (they should properly be gone) -unlink( localDir() . 'normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_D_.data' ); -unlink( localDir() . 'readonlyDirectory_PERM_M_/canBeRemoved_PERM_D_.data' ); - -#3. Edit the files that cannot be modified -# (they should be recovered, and a conflict shall be created) -system("echo 'modified' > ". localDir() . "normalDirectory_PERM_CKDNV_/canotBeModified_PERM_DVN_.data"); -system("echo 'modified_' > ". localDir() . "readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN_.data"); - -#4. Edit other files -# (they should be uploaded) -system("echo '__modified' > ". localDir() . "normalDirectory_PERM_CKDNV_/canBeModified_PERM_W_.data"); -system("echo '__modified_' > ". localDir() . "readonlyDirectory_PERM_M_/canBeModified_PERM_W_.data"); - -#5. Create a new file in a read only folder -# (should be uploaded) -createLocalFile( localDir() . "normalDirectory_PERM_CKDNV_/newFile_PERM_WDNV_.data", 106 ); - -#do the sync -csync(); -assertCsyncJournalOk(localDir()); - -#1. -# File should be recovered -assert( -e localDir(). 'normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data' ); -assert( -e localDir(). 'readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data' ); - -#2. -# File should be deleted -assert( !-e localDir() . 'normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_D_.data' ); -assert( !-e localDir() . 'readonlyDirectory_PERM_M_/canBeRemoved_PERM_D_.data' ); - -#3. -# File should be recovered -assert($md5CanotBeModified eq md5OfFile( localDir().'normalDirectory_PERM_CKDNV_/canotBeModified_PERM_DVN_.data' )); -assert($md5CanotBeModified eq md5OfFile( localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN_.data' )); -# and conflict created -# TODO check that the conflict file has the right content -assert( -e glob(localDir().'normalDirectory_PERM_CKDNV_/canotBeModified_PERM_DVN__conflict-*.data' ) ); -assert( -e glob(localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN__conflict-*.data' ) ); -# remove the conflicts for the next assertLocalAndRemoteDir -system("rm " . localDir().'normalDirectory_PERM_CKDNV_/canotBeModified_PERM_DVN__conflict-*.data' ); -system("rm " . localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN__conflict-*.data' ); - -#4. File should be updated, that's tested by assertLocalAndRemoteDir - -#5. -# the file should be in the server and local -assert( -e localDir() . "normalDirectory_PERM_CKDNV_/newFile_PERM_WDNV_.data" ); - -### Both side should still be the same -assertLocalAndRemoteDir( '', 0); - -# Next test - -#6. Create a new file in a read only folder -# (they should not be uploaded) -createLocalFile( localDir() . "readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data", 105 ); - -# error: can't upload to readonly -csync(1); -assertCsyncJournalOk(localDir()); - -#6. -# The file should not exist on the remote -# TODO: test that the file is NOT on the server -# but still be there -assert( -e localDir() . "readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data" ); -# remove it so assertLocalAndRemoteDir succeed. -unlink(localDir() . "readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data"); - -### Both side should still be the same -assertLocalAndRemoteDir( '', 0); - - - - -####################################################################### -printInfo( "remove the read only directory" ); -# -> It must be recovered -system("rm -r " . localDir().'readonlyDirectory_PERM_M_' ); -csync(); -assertCsyncJournalOk(localDir()); -assert( -e localDir(). 'readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data' ); -assert( -e localDir(). 'readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data' ); -assertLocalAndRemoteDir( '', 0); - - -####################################################################### -printInfo( "move a directory in a outside read only folder" ); -system("sqlite3 " . localDir().'._sync_*.db .dump'); - -#Missing directory should be restored -#new directory should be uploaded -system("mv " . localDir().'readonlyDirectory_PERM_M_/subdir_PERM_CK_ ' . localDir().'normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_' ); - -csync(); -system("sqlite3 " . localDir().'._sync_*.db .dump'); -assertCsyncJournalOk(localDir()); - -# old name restored -assert( -e localDir(). 'readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/' ); -assert( -e localDir(). 'readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data' ); - -# new still exist -assert( -e localDir(). 'normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data' ); - -assertLocalAndRemoteDir( '', 0); - - - - - -####################################################################### -printInfo( "rename a directory in a read only folder and move a directory to a read-only" ); - -# do a sync to update the database -csync(); - -#1. rename a directory in a read only folder -#Missing directory should be restored -#new directory should stay but not be uploaded -system("mv " . localDir().'readonlyDirectory_PERM_M_/subdir_PERM_CK_ ' . localDir().'readonlyDirectory_PERM_M_/newname_PERM_CK_' ); - -#2. move a directory from read to read only (move the directory from previous step) -system("mv " . localDir().'normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_ ' . localDir().'readonlyDirectory_PERM_M_/moved_PERM_CK_' ); - -# error: can't upload to readonly! -csync(1); -assertCsyncJournalOk(localDir()); - -#1. -# old name restored -assert( -e localDir(). 'readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data' ); - -# new still exist -assert( -e localDir(). 'readonlyDirectory_PERM_M_/newname_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data' ); -# but is not on server: so remove for assertLocalAndRemoteDir -system("rm -r " . localDir(). "readonlyDirectory_PERM_M_/newname_PERM_CK_"); - -#2. -# old removed -assert( ! -e localDir(). 'normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_/' ); -# new still there -assert( -e localDir(). 'readonlyDirectory_PERM_M_/moved_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data' ); -#but not on server -system("rm -r " . localDir(). "readonlyDirectory_PERM_M_/moved_PERM_CK_"); - -assertLocalAndRemoteDir( '', 0); - -system("sqlite3 " . localDir().'._sync_*.db .dump'); - - -####################################################################### -printInfo( "multiple restores of a file create different conflict files" ); - -system("sleep 1"); #make sure changes have different mtime - -system("echo 'modified_1' > ". localDir() . "readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN_.data"); - -#do the sync -csync(); -assertCsyncJournalOk(localDir()); - -system("sleep 1"); #make sure changes have different mtime - -system("echo 'modified_2' > ". localDir() . "readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN_.data"); - -#do the sync -csync(); -assertCsyncJournalOk(localDir()); - -# there should be two conflict files -# TODO check that the conflict file has the right content -my @conflicts = glob(localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN__conflict-*.data' ); -assert( scalar @conflicts == 2 ); -# remove the conflicts for the next assertLocalAndRemoteDir -system("rm " . localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN__conflict-*.data' ); - -### Both side should still be the same -assertLocalAndRemoteDir( '', 0); - - - -cleanup(); - - - - - - - - - - - - - - - - - diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h index 1f0a12431..14f173ba4 100644 --- a/test/syncenginetestutils.h +++ b/test/syncenginetestutils.h @@ -317,6 +317,7 @@ public: QString name; bool isDir = true; bool isShared = false; + OCC::RemotePermissions permissions; // When uset, defaults to everything QDateTime lastModified = QDateTime::currentDateTime().addDays(-7); QString etag = generateEtag(); QByteArray fileId = generateFileId(); @@ -390,7 +391,9 @@ public: xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate); xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size)); xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag); - xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); + xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull() + ? QString(fileInfo.permissions.toString()) + : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW")); xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId); xml.writeTextElement(ocUri, QStringLiteral("checksums"), fileInfo.checksums); xml.writeTextElement(ocUri, QStringLiteral("zsync"), QStringLiteral("true")); @@ -475,6 +478,7 @@ public: emit uploadProgress(fileInfo->size, fileInfo->size); setRawHeader("OC-ETag", fileInfo->etag.toLatin1()); setRawHeader("ETag", fileInfo->etag.toLatin1()); + setRawHeader("OC-FileID", fileInfo->fileId); setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); emit metaDataChanged(); @@ -938,8 +942,8 @@ class FakeErrorReply : public QNetworkReply Q_OBJECT public: FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, - QObject *parent, int httpErrorCode) - : QNetworkReply{parent}, _httpErrorCode(httpErrorCode) { + QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray()) + : QNetworkReply{parent}, _httpErrorCode(httpErrorCode), _body(body) { setRequest(request); setUrl(request.url()); setOperation(op); @@ -951,13 +955,31 @@ public: setAttribute(QNetworkRequest::HttpStatusCodeAttribute, _httpErrorCode); setError(InternalServerError, "Internal Server Fake Error"); emit metaDataChanged(); + emit readyRead(); + // finishing can come strictly after readyRead was called + QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished); + } + +public slots: + void slotSetFinished() { + setFinished(true); emit finished(); } +public: void abort() override { } - qint64 readData(char *, qint64) override { return 0; } + qint64 readData(char *buf, qint64 max) override { + max = qMin<qint64>(max, _body.size()); + memcpy(buf, _body.constData(), max); + _body = _body.mid(max); + return max; + } + qint64 bytesAvailable() const override { + return _body.size(); + } int _httpErrorCode; + QByteArray _body; }; // A reply that never responds @@ -974,7 +996,14 @@ public: open(QIODevice::ReadOnly); } - void abort() override {} + void abort() override { + // Follow more or less the implementation of QNetworkReplyImpl::abort + close(); + setError(OperationCanceledError, tr("Operation canceled")); + emit error(OperationCanceledError); + setFinished(true); + emit finished(); + } qint64 readData(char *, qint64) override { return 0; } }; @@ -1103,6 +1132,7 @@ public: _account = OCC::Account::create(); _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud"))); _account->setCredentials(new FakeCredentials{_fakeQnam}); + _account->setDavDisplayName("fakename"); _journalDb.reset(new OCC::SyncJournalDb(localPath() + "._sync_test.db")); _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), "", _journalDb.get())); @@ -1216,13 +1246,30 @@ private: qWarning() << "Empty file at:" << diskChild.filePath(); continue; } - char contentChar = f.read(1).at(0); + char contentChar = content.at(0); templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar}); } } } }; +/* Return the FileInfo for a conflict file for the specified relative filename */ +inline const FileInfo *findConflict(FileInfo &dir, const QString &filename) +{ + QFileInfo info(filename); + const FileInfo *parentDir = dir.find(info.path()); + if (!parentDir) + return nullptr; + QString start = info.baseName() + " (conflicted copy"; + for (const auto &item : parentDir->children) { + if (item.name.startsWith(start)) { + return &item; + } + } + return nullptr; +} + + // QTest::toString overloads namespace OCC { inline char *toString(const SyncFileStatus &s) { diff --git a/test/testallfilesdeleted.cpp b/test/testallfilesdeleted.cpp index 856fe73e3..e0dbbdd44 100644 --- a/test/testallfilesdeleted.cpp +++ b/test/testallfilesdeleted.cpp @@ -151,6 +151,108 @@ private slots: QCOMPARE(fakeFolder.currentLocalState(), expectedState); QCOMPARE(fakeFolder.currentRemoteState(), expectedState); } + + void testResetServer() + { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + + int aboutToRemoveAllFilesCalled = 0; + QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveAllFiles, + [&](SyncFileItem::Direction dir, bool *cancel) { + QCOMPARE(aboutToRemoveAllFilesCalled, 0); + aboutToRemoveAllFilesCalled++; + QCOMPARE(dir, SyncFileItem::Down); + *cancel = false; + }); + + // Some small changes + fakeFolder.localModifier().mkdir("Q"); + fakeFolder.localModifier().insert("Q/q1"); + fakeFolder.localModifier().appendByte("B/b1"); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(aboutToRemoveAllFilesCalled, 0); + + // Do some change localy + fakeFolder.localModifier().appendByte("A/a1"); + + // reset the server. + fakeFolder.remoteModifier() = FileInfo::A12_B12_C12_S12(); + + // Now, aboutToRemoveAllFiles with down as a direction + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(aboutToRemoveAllFilesCalled, 1); + + } + + void testDataFingetPrint_data() + { + QTest::addColumn<bool>("hasInitialFingerPrint"); + QTest::newRow("initial finger print") << true; + QTest::newRow("no initial finger print") << false; + } + + void testDataFingetPrint() + { + QFETCH(bool, hasInitialFingerPrint); + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + fakeFolder.remoteModifier().setContents("C/c1", 'N'); + fakeFolder.remoteModifier().setModTime("C/c1", QDateTime::currentDateTimeUtc().addDays(-2)); + fakeFolder.remoteModifier().remove("C/c2"); + if (hasInitialFingerPrint) { + fakeFolder.remoteModifier().extraDavProperties = "<oc:data-fingerprint>initial_finger_print</oc:data-fingerprint>"; + } else { + //Server support finger print, but none is set. + fakeFolder.remoteModifier().extraDavProperties = "<oc:data-fingerprint></oc:data-fingerprint>"; + } + QVERIFY(fakeFolder.syncOnce()); + // First sync, we did not change the finger print, so the file should be downloaded as normal + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(fakeFolder.currentRemoteState().find("C/c1")->contentChar, 'N'); + QVERIFY(!fakeFolder.currentRemoteState().find("C/c2")); + + /* Simulate a backup restoration */ + + // A/a1 is an old file + fakeFolder.remoteModifier().setContents("A/a1", 'O'); + fakeFolder.remoteModifier().setModTime("A/a1", QDateTime::currentDateTimeUtc().addDays(-2)); + // B/b1 did not exist at the time of the backup + fakeFolder.remoteModifier().remove("B/b1"); + // B/b2 was uploaded by another user in the mean time. + fakeFolder.remoteModifier().setContents("B/b2", 'N'); + fakeFolder.remoteModifier().setModTime("B/b2", QDateTime::currentDateTimeUtc().addDays(2)); + + // C/c3 was removed since we made the backup + fakeFolder.remoteModifier().insert("C/c3_removed"); + // C/c4 was moved to A/a2 since we made the backup + fakeFolder.remoteModifier().rename("A/a2", "C/old_a2_location"); + + // The admin sets the data-fingerprint property + fakeFolder.remoteModifier().extraDavProperties = "<oc:data-fingerprint>new_finger_print</oc:data-fingerprint>"; + + QVERIFY(fakeFolder.syncOnce()); + auto currentState = fakeFolder.currentLocalState(); + // Altough the local file is kept as a conflict, the server file is downloaded + QCOMPARE(currentState.find("A/a1")->contentChar, 'O'); + auto conflict = findConflict(currentState, "A/a1"); + QVERIFY(conflict); + QCOMPARE(conflict->contentChar, 'W'); + fakeFolder.localModifier().remove(conflict->path()); + // b1 was restored (re-uploaded) + QVERIFY(currentState.find("B/b1")); + + // b2 has the new content (was not restored), since its mode time goes forward in time + QCOMPARE(currentState.find("B/b2")->contentChar, 'N'); + conflict = findConflict(currentState, "B/b2"); + QVERIFY(conflict); // Just to be sure, we kept the old file in a conflict + QCOMPARE(conflict->contentChar, 'W'); + fakeFolder.localModifier().remove(conflict->path()); + + // We actually do not remove files that technically should have been removed (we don't want data-loss) + QVERIFY(currentState.find("C/c3_removed")); + QVERIFY(currentState.find("C/old_a2_location")); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } }; QTEST_GUILESS_MAIN(TestAllFilesDeleted) diff --git a/test/testblacklist.cpp b/test/testblacklist.cpp new file mode 100644 index 000000000..cad9f35f2 --- /dev/null +++ b/test/testblacklist.cpp @@ -0,0 +1,193 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> + +using namespace OCC; + +SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path) +{ + for (const QList<QVariant> &args : spy) { + auto item = args[0].value<SyncFileItemPtr>(); + if (item->destination() == path) + return item; + } + return SyncFileItemPtr(new SyncFileItem); +} + +SyncJournalFileRecord journalRecord(FakeFolder &folder, const QByteArray &path) +{ + SyncJournalFileRecord rec; + folder.syncJournal().getFileRecord(path, &rec); + return rec; +} + +class TestBlacklist : public QObject +{ + Q_OBJECT + +private slots: + void testBlacklistBasic_data() + { + QTest::addColumn<bool>("remote"); + QTest::newRow("remote") << true; + QTest::newRow("local") << false; + } + + void testBlacklistBasic() + { + QFETCH(bool, remote); + + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto &modifier = remote ? fakeFolder.remoteModifier() : fakeFolder.localModifier(); + + int counter = 0; + QByteArray reqId; + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) -> QNetworkReply * { + reqId = req.rawHeader("X-Request-ID"); + if (!remote && op == QNetworkAccessManager::PutOperation) + ++counter; + if (remote && op == QNetworkAccessManager::GetOperation) + ++counter; + return nullptr; + }); + + auto cleanup = [&]() { + completeSpy.clear(); + }; + + auto initialEtag = journalRecord(fakeFolder, "A")._etag; + QVERIFY(!initialEtag.isEmpty()); + + // The first sync and the download will fail - the item will be blacklisted + modifier.insert("A/new"); + fakeFolder.serverErrorPaths().append("A/new", 500); // will be blacklisted + QVERIFY(!fakeFolder.syncOnce()); + { + auto it = findItem(completeSpy, "A/new"); + QVERIFY(it); + QCOMPARE(it->_status, SyncFileItem::NormalError); // initial error visible + QCOMPARE(it->_instruction, CSYNC_INSTRUCTION_NEW); + + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + QVERIFY(entry.isValid()); + QCOMPARE(entry._errorCategory, SyncJournalErrorBlacklistRecord::Normal); + QCOMPARE(entry._retryCount, 1); + QCOMPARE(counter, 1); + QVERIFY(entry._ignoreDuration > 0); + QCOMPARE(entry._requestId, reqId); + + if (remote) + QCOMPARE(journalRecord(fakeFolder, "A")._etag, initialEtag); + } + cleanup(); + + // Ignored during the second run - but soft errors are also errors + QVERIFY(!fakeFolder.syncOnce()); + { + auto it = findItem(completeSpy, "A/new"); + QVERIFY(it); + QCOMPARE(it->_status, SyncFileItem::BlacklistedError); + QCOMPARE(it->_instruction, CSYNC_INSTRUCTION_IGNORE); // no retry happened! + + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + QVERIFY(entry.isValid()); + QCOMPARE(entry._errorCategory, SyncJournalErrorBlacklistRecord::Normal); + QCOMPARE(entry._retryCount, 1); + QCOMPARE(counter, 1); + QVERIFY(entry._ignoreDuration > 0); + QCOMPARE(entry._requestId, reqId); + + if (remote) + QCOMPARE(journalRecord(fakeFolder, "A")._etag, initialEtag); + } + cleanup(); + + // Let's expire the blacklist entry to verify it gets retried + { + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + entry._ignoreDuration = 1; + entry._lastTryTime -= 1; + fakeFolder.syncJournal().setErrorBlacklistEntry(entry); + } + QVERIFY(!fakeFolder.syncOnce()); + { + auto it = findItem(completeSpy, "A/new"); + QVERIFY(it); + QCOMPARE(it->_status, SyncFileItem::BlacklistedError); // blacklisted as it's just a retry + QCOMPARE(it->_instruction, CSYNC_INSTRUCTION_NEW); // retry! + + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + QVERIFY(entry.isValid()); + QCOMPARE(entry._errorCategory, SyncJournalErrorBlacklistRecord::Normal); + QCOMPARE(entry._retryCount, 2); + QCOMPARE(counter, 2); + QVERIFY(entry._ignoreDuration > 0); + QCOMPARE(entry._requestId, reqId); + + if (remote) + QCOMPARE(journalRecord(fakeFolder, "A")._etag, initialEtag); + } + cleanup(); + + // When the file changes a retry happens immediately + modifier.appendByte("A/new"); + QVERIFY(!fakeFolder.syncOnce()); + { + auto it = findItem(completeSpy, "A/new"); + QVERIFY(it); + QCOMPARE(it->_status, SyncFileItem::BlacklistedError); + QCOMPARE(it->_instruction, CSYNC_INSTRUCTION_NEW); // retry! + + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + QVERIFY(entry.isValid()); + QCOMPARE(entry._errorCategory, SyncJournalErrorBlacklistRecord::Normal); + QCOMPARE(entry._retryCount, 3); + QCOMPARE(counter, 3); + QVERIFY(entry._ignoreDuration > 0); + QCOMPARE(entry._requestId, reqId); + + if (remote) + QCOMPARE(journalRecord(fakeFolder, "A")._etag, initialEtag); + } + cleanup(); + + // When the error goes away and the item is retried, the sync succeeds + fakeFolder.serverErrorPaths().clear(); + { + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + entry._ignoreDuration = 1; + entry._lastTryTime -= 1; + fakeFolder.syncJournal().setErrorBlacklistEntry(entry); + } + QVERIFY(fakeFolder.syncOnce()); + { + auto it = findItem(completeSpy, "A/new"); + QVERIFY(it); + QCOMPARE(it->_status, SyncFileItem::Success); + QCOMPARE(it->_instruction, CSYNC_INSTRUCTION_NEW); + + auto entry = fakeFolder.syncJournal().errorBlacklistEntry("A/new"); + QVERIFY(!entry.isValid()); + QCOMPARE(counter, 4); + + if (remote) + QCOMPARE(journalRecord(fakeFolder, "A")._etag, fakeFolder.currentRemoteState().find("A")->etag.toUtf8()); + } + cleanup(); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } +}; + +QTEST_GUILESS_MAIN(TestBlacklist) +#include "testblacklist.moc" diff --git a/test/testchecksumvalidator.cpp b/test/testchecksumvalidator.cpp index 041dc193c..36e32f897 100644 --- a/test/testchecksumvalidator.cpp +++ b/test/testchecksumvalidator.cpp @@ -15,15 +15,14 @@ #include "filesystem.h" #include "propagatorjobs.h" - using namespace OCC; +using namespace OCC::Utility; class TestChecksumValidator : public QObject { Q_OBJECT - private: - QString _root; + QTemporaryDir _root; QString _testfile; QString _expectedError; QByteArray _expected; @@ -48,17 +47,62 @@ using namespace OCC; _errorSeen = true; } + static QByteArray shellSum( const QByteArray& cmd, const QString& file ) + { + QProcess md5; + QStringList args; + args.append(file); + md5.start(cmd, args); + QByteArray sumShell; + qDebug() << "File: "<< file; + + if( md5.waitForFinished() ) { + + sumShell = md5.readAll(); + sumShell = sumShell.left( sumShell.indexOf(' ')); + } + return sumShell; + } + private slots: void initTestCase() { - _root = QDir::tempPath() + "/" + "test_" + QString::number(qrand()); - QDir rootDir(_root); - - rootDir.mkpath(_root ); - _testfile = _root+"/csFile"; + _testfile = _root.path()+"/csFile"; Utility::writeRandomFile( _testfile); } + void testMd5Calc() + { + QString file( _root.path() + "/file_a.bin"); + QVERIFY(writeRandomFile(file)); + QFileInfo fi(file); + QVERIFY(fi.exists()); + QByteArray sum = calcMd5(file); + + QByteArray sSum = shellSum("md5sum", file); + if (sSum.isEmpty()) + QSKIP("Couldn't execute md5sum to calculate checksum, executable missing?", SkipSingle); + + QVERIFY(!sum.isEmpty()); + QCOMPARE(sSum, sum); + } + + void testSha1Calc() + { + QString file( _root.path() + "/file_b.bin"); + writeRandomFile(file); + QFileInfo fi(file); + QVERIFY(fi.exists()); + QByteArray sum = calcSha1(file); + + QByteArray sSum = shellSum("sha1sum", file); + if (sSum.isEmpty()) + QSKIP("Couldn't execute sha1sum to calculate checksum, executable missing?", SkipSingle); + + QVERIFY(!sum.isEmpty()); + QCOMPARE(sSum, sum); + } + void testUploadChecksummingAdler() { #ifndef ZLIB_FOUND QSKIP("ZLIB not found.", SkipSingle); @@ -69,7 +113,7 @@ using namespace OCC; connect(vali, SIGNAL(done(QByteArray,QByteArray)), SLOT(slotUpValidated(QByteArray,QByteArray))); - _expected = FileSystem::calcAdler32( _testfile ); + _expected = calcAdler32( _testfile ); qDebug() << "XX Expected Checksum: " << _expected; vali->start(_testfile); @@ -88,7 +132,7 @@ using namespace OCC; vali->setChecksumType(_expectedType); connect(vali, SIGNAL(done(QByteArray,QByteArray)), this, SLOT(slotUpValidated(QByteArray,QByteArray))); - _expected = FileSystem::calcMd5( _testfile ); + _expected = calcMd5( _testfile ); vali->start(_testfile); QEventLoop loop; @@ -105,7 +149,7 @@ using namespace OCC; vali->setChecksumType(_expectedType); connect(vali, SIGNAL(done(QByteArray,QByteArray)), this, SLOT(slotUpValidated(QByteArray,QByteArray))); - _expected = FileSystem::calcSha1( _testfile ); + _expected = calcSha1( _testfile ); vali->start(_testfile); @@ -122,7 +166,7 @@ using namespace OCC; #else QByteArray adler = checkSumAdlerC; adler.append(":"); - adler.append(FileSystem::calcAdler32( _testfile )); + adler.append(calcAdler32( _testfile )); _successDown = false; ValidateChecksumHeader *vali = new ValidateChecksumHeader(this); diff --git a/test/testchunkingng.cpp b/test/testchunkingng.cpp index ce7880537..3439f7cfc 100644 --- a/test/testchunkingng.cpp +++ b/test/testchunkingng.cpp @@ -40,6 +40,15 @@ static void partialUpload(FakeFolder &fakeFolder, const QString &name, int size) [](int s, const FileInfo &i) { return s + i.size; })); } +// Reduce max chunk size a bit so we get more chunks +static void setChunkSize(SyncEngine &engine, quint64 size) +{ + SyncOptions options; + options._maxChunkSize = size; + options._initialChunkSize = size; + options._minChunkSize = size; + engine.setSyncOptions(options); +} class TestChunkingNG : public QObject { @@ -50,7 +59,9 @@ private slots: void testFileUpload() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 300 * 1000 * 1000; // 300 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + const int size = 10 * 1000 * 1000; // 10 MB + fakeFolder.localModifier().insert("A/a0", size); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); @@ -64,30 +75,133 @@ private slots: QCOMPARE(fakeFolder.uploadState().children.count(), 2); // the transfer was done with chunking } - - void testResume () { + // Test resuming when there's a confusing chunk added + void testResume1() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 300 * 1000 * 1000; // 300 MB + const int size = 10 * 1000 * 1000; // 10 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + partialUpload(fakeFolder, "A/a0", size); QCOMPARE(fakeFolder.uploadState().children.count(), 1); auto chunkingId = fakeFolder.uploadState().children.first().name; const auto &chunkMap = fakeFolder.uploadState().children.first().children; quint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](quint64 s, const FileInfo &f) { return s + f.size; }); - QVERIFY(uploadedSize > 50 * 1000 * 1000); // at least 50 MB + QVERIFY(uploadedSize > 2 * 1000 * 1000); // at least 2 MB - // Add a fake file to make sure it gets deleted + // Add a fake chunk to make sure it gets deleted fakeFolder.uploadState().children.first().insert("10000", size); fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (op == QNetworkAccessManager::PutOperation) { // Test that we properly resuming and are not sending past data again. Q_ASSERT(request.rawHeader("OC-Chunk-Offset").toULongLong() >= uploadedSize); + } else if (op == QNetworkAccessManager::DeleteOperation) { + Q_ASSERT(request.url().path().endsWith("/10000")); + } + return nullptr; + }); + + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size); + // The same chunk id was re-used + QCOMPARE(fakeFolder.uploadState().children.count(), 1); + QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId); + } + + // Test resuming when one of the uploaded chunks got removed + void testResume2() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + const int size = 150 * 1000 * 1000; // 30 MB + partialUpload(fakeFolder, "A/a0", size); + QCOMPARE(fakeFolder.uploadState().children.count(), 1); + auto chunkingId = fakeFolder.uploadState().children.first().name; + const auto &chunkMap = fakeFolder.uploadState().children.first().children; + quint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](quint64 s, const FileInfo &f) { return s + f.size; }); + QVERIFY(uploadedSize > 2 * 1000 * 1000); // at least 50 MB + QVERIFY(chunkMap.size() >= 3); // at least three chunks + + QStringList chunksToDelete; + + // Remove the second chunk, so all further chunks will be deleted and resent + auto firstChunk = chunkMap.first(); + auto secondChunk = *(chunkMap.begin() + 1); + for (const auto& name : chunkMap.keys().mid(2)) { + chunksToDelete.append(name); + } + fakeFolder.uploadState().children.first().remove(secondChunk.name); + + QStringList deletedPaths; + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + if (op == QNetworkAccessManager::PutOperation) { + // Test that we properly resuming, not resending the first chunk + Q_ASSERT(request.rawHeader("OC-Chunk-Offset").toLongLong() >= firstChunk.size); + } else if (op == QNetworkAccessManager::DeleteOperation) { + deletedPaths.append(request.url().path()); + } + return nullptr; + }); + + QVERIFY(fakeFolder.syncOnce()); + + for (const auto& toDelete : chunksToDelete) { + bool wasDeleted = false; + for (const auto& deleted : deletedPaths) { + if (deleted.mid(deleted.lastIndexOf('/') + 1) == toDelete) { + wasDeleted = true; + break; + } + } + QVERIFY(wasDeleted); + } + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size); + // The same chunk id was re-used + QCOMPARE(fakeFolder.uploadState().children.count(), 1); + QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId); + } + + // Test resuming when all chunks are already present + void testResume3() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); + const int size = 30 * 1000 * 1000; // 30 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + + partialUpload(fakeFolder, "A/a0", size); + QCOMPARE(fakeFolder.uploadState().children.count(), 1); + auto chunkingId = fakeFolder.uploadState().children.first().name; + const auto &chunkMap = fakeFolder.uploadState().children.first().children; + quint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](quint64 s, const FileInfo &f) { return s + f.size; }); + QVERIFY(uploadedSize > 5 * 1000 * 1000); // at least 5 MB + + // Add a chunk that makes the file completely uploaded + fakeFolder.uploadState().children.first().insert( + QString::number(chunkMap.size()).rightJustified(8, '0'), size - uploadedSize); + + bool sawPut = false; + bool sawDelete = false; + bool sawMove = false; + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + if (op == QNetworkAccessManager::PutOperation) { + sawPut = true; + } else if (op == QNetworkAccessManager::DeleteOperation) { + sawDelete = true; + } else if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { + sawMove = true; } return nullptr; }); QVERIFY(fakeFolder.syncOnce()); + QVERIFY(sawMove); + QVERIFY(!sawPut); + QVERIFY(!sawDelete); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size); @@ -96,19 +210,48 @@ private slots: QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId); } + // Test resuming (or rather not resuming!) for the error case of the sum of + // chunk sizes being larger than the file size + void testResume4() { + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); + const int size = 30 * 1000 * 1000; // 300 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + + partialUpload(fakeFolder, "A/a0", size); + QCOMPARE(fakeFolder.uploadState().children.count(), 1); + auto chunkingId = fakeFolder.uploadState().children.first().name; + const auto &chunkMap = fakeFolder.uploadState().children.first().children; + quint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](quint64 s, const FileInfo &f) { return s + f.size; }); + QVERIFY(uploadedSize > 5 * 1000 * 1000); // at least 5 MB + + // Add a chunk that makes the file more than completely uploaded + fakeFolder.uploadState().children.first().insert( + QString::number(chunkMap.size()).rightJustified(8, '0'), size - uploadedSize + 100); + + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size); + // Used a new transfer id but wiped the old one + QCOMPARE(fakeFolder.uploadState().children.count(), 1); + QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId); + } + // Check what happens when we abort during the final MOVE and the // the final MOVE takes longer than the abort-delay void testLateAbortHard() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } }); - const int size = 150 * 1000 * 1000; + const int size = 15 * 1000 * 1000; + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); // Make the MOVE never reply, but trigger a client-abort and apply the change remotely auto parent = new QObject; QByteArray moveChecksumHeader; int nGET = 0; - int responseDelay = 10000; // bigger than abort-wait timeout + int responseDelay = 100000; // bigger than abort-wait timeout fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); }); @@ -185,25 +328,20 @@ private slots: { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } }); - const int size = 150 * 1000 * 1000; + const int size = 15 * 1000 * 1000; + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); // Make the MOVE never reply, but trigger a client-abort and apply the change remotely auto parent = new QObject; - QByteArray moveChecksumHeader; - int nGET = 0; - int responseDelay = 2000; // smaller than abort-wait timeout + int responseDelay = 200; // smaller than abort-wait timeout fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); }); - moveChecksumHeader = request.rawHeader("OC-Checksum"); return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, parent); - } else if (op == QNetworkAccessManager::GetOperation) { - nGET++; } return nullptr; }); - // Test 1: NEW file aborted fakeFolder.localModifier().insert("A/a0", size); QVERIFY(fakeFolder.syncOnce()); @@ -220,7 +358,9 @@ private slots: FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 300 * 1000 * 1000; // 300 MB + const int size = 10 * 1000 * 1000; // 10 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + partialUpload(fakeFolder, "A/a0", size); QCOMPARE(fakeFolder.uploadState().children.count(), 1); auto chunkingId = fakeFolder.uploadState().children.first().name; @@ -243,7 +383,9 @@ private slots: FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 300 * 1000 * 1000; // 300 MB + const int size = 10 * 1000 * 1000; // 10 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); + partialUpload(fakeFolder, "A/a0", size); QCOMPARE(fakeFolder.uploadState().children.count(), 1); @@ -257,7 +399,8 @@ private slots: void testCreateConflictWhileSyncing() { FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 150 * 1000 * 1000; // 150 MB + const int size = 10 * 1000 * 1000; // 10 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); // Put a file on the server and download it. fakeFolder.remoteModifier().insert("A/a0", size); @@ -294,7 +437,7 @@ private slots: // There is a conflict file with our version auto &stateAChildren = localState.find("A")->children; auto it = std::find_if(stateAChildren.cbegin(), stateAChildren.cend(), [&](const FileInfo &fi) { - return fi.name.startsWith("a0_conflict"); + return fi.name.startsWith("a0 (conflicted copy"); }); QVERIFY(it != stateAChildren.cend()); QCOMPARE(it->contentChar, 'B'); @@ -312,7 +455,8 @@ private slots: FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 150 * 1000 * 1000; // 150 MB + const int size = 10 * 1000 * 1000; // 100 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); fakeFolder.localModifier().insert("A/a0", size); @@ -350,7 +494,8 @@ private slots: FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } }); - const int size = 300 * 1000 * 1000; // 300 MB + const int size = 30 * 1000 * 1000; // 30 MB + setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000); partialUpload(fakeFolder, "A/a0", size); QCOMPARE(fakeFolder.uploadState().children.count(), 1); auto chunkingId = fakeFolder.uploadState().children.first().name; @@ -380,7 +525,8 @@ private slots: QFETCH(bool, chunking); FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } }); - const int size = chunking ? 150 * 1000 * 1000 : 300; + const int size = chunking ? 1 * 1000 * 1000 : 300; + setChunkSize(fakeFolder.syncEngine(), 300 * 1000); // Make the MOVE never reply, but trigger a client-abort and apply the change remotely QByteArray checksumHeader; @@ -389,6 +535,10 @@ private slots: int responseDelay = AbstractNetworkJob::httpTimeout * 1000 * 1000; // much bigger than http timeout (so a timeout will occur) // This will perform the operation on the server, but the reply will not come to the client fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { + if (!chunking) { + Q_ASSERT(!request.url().path().contains("/uploads/") + && "Should not touch uploads endpoint when not chunking"); + } if (!chunking && op == QNetworkAccessManager::PutOperation) { checksumHeader = request.rawHeader("OC-Checksum"); return new DelayedReply<FakePutReply>(responseDelay, fakeFolder.remoteModifier(), op, request, outgoingData->readAll(), &fakeFolder.syncEngine()); @@ -401,7 +551,6 @@ private slots: return nullptr; }); - // Test 1: a NEW file fakeFolder.localModifier().insert("A/a0", size); QVERIFY(!fakeFolder.syncOnce()); // timeout! diff --git a/test/testdownload.cpp b/test/testdownload.cpp new file mode 100644 index 000000000..3301e0e6d --- /dev/null +++ b/test/testdownload.cpp @@ -0,0 +1,125 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> + +using namespace OCC; + +static constexpr quint64 stopAfter = 3'123'668; + +/* A FakeGetReply that sends max 'fakeSize' bytes, but whose ContentLength has the corect size */ +class BrokenFakeGetReply : public FakeGetReply +{ + Q_OBJECT +public: + using FakeGetReply::FakeGetReply; + int fakeSize = stopAfter; + + qint64 bytesAvailable() const override + { + if (aborted) + return 0; + return std::min(size, fakeSize) + QIODevice::bytesAvailable(); + } + + qint64 readData(char *data, qint64 maxlen) override + { + qint64 len = std::min(qint64{ fakeSize }, maxlen); + std::fill_n(data, len, payload); + size -= len; + fakeSize -= len; + return len; + } +}; + + +SyncFileItemPtr getItem(const QSignalSpy &spy, const QString &path) +{ + for (const QList<QVariant> &args : spy) { + auto item = args[0].value<SyncFileItemPtr>(); + if (item->destination() == path) + return item; + } + return {}; +} + + +class TestDownload : public QObject +{ + Q_OBJECT + +private slots: + + void testResume() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + auto size = 30 * 1000 * 1000; + fakeFolder.remoteModifier().insert("A/a0", size); + + // First, download only the first 3 MB of the file + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/a0")) { + return new BrokenFakeGetReply(fakeFolder.remoteModifier(), op, request, this); + } + return nullptr; + }); + + QVERIFY(!fakeFolder.syncOnce()); // The sync must fail because not all the file was downloaded + QCOMPARE(getItem(completeSpy, "A/a0")->_status, SyncFileItem::SoftError); + QCOMPARE(getItem(completeSpy, "A/a0")->_errorString, QString("The file could not be downloaded completely.")); + QVERIFY(fakeFolder.syncEngine().isAnotherSyncNeeded()); + + // Now, we need to restart, this time, it should resume. + QByteArray ranges; + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/a0")) { + ranges = request.rawHeader("Range"); + } + return nullptr; + }); + QVERIFY(fakeFolder.syncOnce()); // now this succeeds + QCOMPARE(ranges, QByteArray("bytes=" + QByteArray::number(stopAfter) + "-")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testErrorMessage () { + // This test's main goal is to test that the error string from the server is shown in the UI + + FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()}; + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + auto size = 3'500'000; + fakeFolder.remoteModifier().insert("A/broken", size); + + QByteArray serverMessage = "The file was not downloaded because the tests wants so!"; + + // First, download only the first 3 MB of the file + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + if (op == QNetworkAccessManager::GetOperation && request.url().path().endsWith("A/broken")) { + return new FakeErrorReply(op, request, this, 400, + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<d:error xmlns:d=\"DAV:\" xmlns:s=\"http://sabredav.org/ns\">\n" + "<s:exception>Sabre\\DAV\\Exception\\Forbidden</s:exception>\n" + "<s:message>"+serverMessage+"</s:message>\n" + "</d:error>"); + } + return nullptr; + }); + + bool timedOut = false; + QTimer::singleShot(10000, &fakeFolder.syncEngine(), [&]() { timedOut = true; fakeFolder.syncEngine().abort(); }); + QVERIFY(!fakeFolder.syncOnce()); // Fail because A/broken + QVERIFY(!timedOut); + QCOMPARE(getItem(completeSpy, "A/broken")->_status, SyncFileItem::NormalError); + QVERIFY(getItem(completeSpy, "A/broken")->_errorString.contains(serverMessage)); + } +}; + +QTEST_GUILESS_MAIN(TestDownload) +#include "testdownload.moc" diff --git a/test/testexcludedfiles.cpp b/test/testexcludedfiles.cpp index 5043768ec..11d0a3282 100644 --- a/test/testexcludedfiles.cpp +++ b/test/testexcludedfiles.cpp @@ -37,6 +37,7 @@ private slots: QVERIFY(!excluded.isExcluded("/a/.b", "/a", keepHidden)); QVERIFY(excluded.isExcluded("/a/.Trashes", "/a", keepHidden)); QVERIFY(excluded.isExcluded("/a/foo_conflict-bar", "/a", keepHidden)); + QVERIFY(excluded.isExcluded("/a/foo (conflicted copy bar)", "/a", keepHidden)); QVERIFY(excluded.isExcluded("/a/.b", "/a", excludeHidden)); } }; diff --git a/test/testfilesystem.cpp b/test/testfilesystem.cpp deleted file mode 100644 index 0b8b61246..000000000 --- a/test/testfilesystem.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - This software is in the public domain, furnished "as is", without technical - support, and with no warranty, express or implied, as to its usefulness for - any purpose. -*/ - -#include <QtTest> -#include <QDebug> - -#include "filesystem.h" -#include "common/utility.h" - -using namespace OCC::Utility; -using namespace OCC::FileSystem; - -class TestFileSystem : public QObject -{ - Q_OBJECT - - QTemporaryDir _root; - - - QByteArray shellSum( const QByteArray& cmd, const QString& file ) - { - QProcess md5; - QStringList args; - args.append(file); - md5.start(cmd, args); - QByteArray sumShell; - qDebug() << "File: "<< file; - - if( md5.waitForFinished() ) { - - sumShell = md5.readAll(); - sumShell = sumShell.left( sumShell.indexOf(' ')); - } - return sumShell; - } - -private slots: - void testMd5Calc() - { - QString file( _root.path() + "/file_a.bin"); - QVERIFY(writeRandomFile(file)); - QFileInfo fi(file); - QVERIFY(fi.exists()); - QByteArray sum = calcMd5(file); - - QByteArray sSum = shellSum("md5sum", file); - if (sSum.isEmpty()) - QSKIP("Couldn't execute md5sum to calculate checksum, executable missing?", SkipSingle); - - QVERIFY(!sum.isEmpty()); - QCOMPARE(sSum, sum); - } - - void testSha1Calc() - { - QString file( _root.path() + "/file_b.bin"); - writeRandomFile(file); - QFileInfo fi(file); - QVERIFY(fi.exists()); - QByteArray sum = calcSha1(file); - - QByteArray sSum = shellSum("sha1sum", file); - if (sSum.isEmpty()) - QSKIP("Couldn't execute sha1sum to calculate checksum, executable missing?", SkipSingle); - - QVERIFY(!sum.isEmpty()); - QCOMPARE(sSum, sum); - } - -}; - -QTEST_APPLESS_MAIN(TestFileSystem) -#include "testfilesystem.moc" diff --git a/test/testfolderman.cpp b/test/testfolderman.cpp index 15f044ba1..9aec8faf0 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -146,6 +146,12 @@ private slots: // Invalid paths QVERIFY(!folderman->checkPathValidityForNewFolder("").isNull()); + + + // REMOVE ownCloud2 from the filesystem, but keep a folder sync'ed to it. + QDir(dirPath + "/ownCloud2/").removeRecursively(); + QVERIFY(!folderman->checkPathValidityForNewFolder(dirPath + "/ownCloud2/blublu").isNull()); + QVERIFY(!folderman->checkPathValidityForNewFolder(dirPath + "/ownCloud2/sub/subsub/sub").isNull()); } void testFindGoodPathForNewSyncFolder() @@ -169,6 +175,7 @@ private slots: HttpCredentialsTest *cred = new HttpCredentialsTest("testuser", "secret"); account->setCredentials(cred); account->setUrl( url ); + url.setUserName(cred->user()); AccountStatePtr newAccountState(new AccountState(account)); FolderMan *folderman = FolderMan::instance(); @@ -190,6 +197,14 @@ private slots: QString(dirPath + "/ownCloud2/bar")); QCOMPARE(folderman->findGoodPathForNewSyncFolder(dirPath + "/sub", url), QString(dirPath + "/sub2")); + + // REMOVE ownCloud2 from the filesystem, but keep a folder sync'ed to it. + // We should still not suggest this folder as a new folder. + QDir(dirPath + "/ownCloud2/").removeRecursively(); + QCOMPARE(folderman->findGoodPathForNewSyncFolder(dirPath + "/ownCloud", url), + QString(dirPath + "/ownCloud3")); + QCOMPARE(folderman->findGoodPathForNewSyncFolder(dirPath + "/ownCloud2", url), + QString(dirPath + "/ownCloud22")); } }; diff --git a/test/testfolderwatcher.cpp b/test/testfolderwatcher.cpp index 0f8551db6..d90856828 100644 --- a/test/testfolderwatcher.cpp +++ b/test/testfolderwatcher.cpp @@ -113,7 +113,8 @@ public: Utility::writeRandomFile( _rootPath+"/a2/renamefile"); Utility::writeRandomFile( _rootPath+"/a1/movefile"); - _watcher.reset(new FolderWatcher(_rootPath)); + _watcher.reset(new FolderWatcher); + _watcher->init(_rootPath); _pathChangedSpy.reset(new QSignalSpy(_watcher.data(), SIGNAL(pathChanged(QString)))); } diff --git a/test/testlocaldiscovery.cpp b/test/testlocaldiscovery.cpp new file mode 100644 index 000000000..03754af51 --- /dev/null +++ b/test/testlocaldiscovery.cpp @@ -0,0 +1,155 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> +#include <localdiscoverytracker.h> + +using namespace OCC; + +class TestLocalDiscovery : public QObject +{ + Q_OBJECT + +private slots: + // Check correct behavior when local discovery is partially drawn from the db + void testLocalDiscoveryStyle() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + + LocalDiscoveryTracker tracker; + connect(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted, &tracker, &LocalDiscoveryTracker::slotItemCompleted); + connect(&fakeFolder.syncEngine(), &SyncEngine::finished, &tracker, &LocalDiscoveryTracker::slotSyncFinished); + + // More subdirectories are useful for testing + fakeFolder.localModifier().mkdir("A/X"); + fakeFolder.localModifier().mkdir("A/Y"); + fakeFolder.localModifier().insert("A/X/x1"); + fakeFolder.localModifier().insert("A/Y/y1"); + tracker.addTouchedPath("A/X"); + + tracker.startSyncFullDiscovery(); + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QVERIFY(tracker.localDiscoveryPaths().empty()); + + // Test begins + fakeFolder.localModifier().insert("A/a3"); + fakeFolder.localModifier().insert("A/X/x2"); + fakeFolder.localModifier().insert("A/Y/y2"); + fakeFolder.localModifier().insert("B/b3"); + fakeFolder.remoteModifier().insert("C/c3"); + tracker.addTouchedPath("A/X"); + + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); + tracker.startSyncPartialDiscovery(); + QVERIFY(fakeFolder.syncOnce()); + + QVERIFY(fakeFolder.currentRemoteState().find("A/a3")); + QVERIFY(fakeFolder.currentRemoteState().find("A/X/x2")); + QVERIFY(!fakeFolder.currentRemoteState().find("A/Y/y2")); + QVERIFY(!fakeFolder.currentRemoteState().find("B/b3")); + QVERIFY(fakeFolder.currentLocalState().find("C/c3")); + QCOMPARE(fakeFolder.syncEngine().lastLocalDiscoveryStyle(), LocalDiscoveryStyle::DatabaseAndFilesystem); + QVERIFY(tracker.localDiscoveryPaths().empty()); + + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(fakeFolder.syncEngine().lastLocalDiscoveryStyle(), LocalDiscoveryStyle::FilesystemOnly); + QVERIFY(tracker.localDiscoveryPaths().empty()); + } + + void testLocalDiscoveryDecision() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + auto &engine = fakeFolder.syncEngine(); + + QVERIFY(engine.shouldDiscoverLocally("")); + QVERIFY(engine.shouldDiscoverLocally("A")); + QVERIFY(engine.shouldDiscoverLocally("A/X")); + + fakeFolder.syncEngine().setLocalDiscoveryOptions( + LocalDiscoveryStyle::DatabaseAndFilesystem, + { "A/X", "foo bar space/touch", "foo/", "zzz" }); + + QVERIFY(engine.shouldDiscoverLocally("")); + QVERIFY(engine.shouldDiscoverLocally("A")); + QVERIFY(engine.shouldDiscoverLocally("A/X")); + QVERIFY(!engine.shouldDiscoverLocally("B")); + QVERIFY(!engine.shouldDiscoverLocally("A B")); + QVERIFY(!engine.shouldDiscoverLocally("B/X")); + QVERIFY(!engine.shouldDiscoverLocally("A/X/Y")); + QVERIFY(engine.shouldDiscoverLocally("foo bar space")); + QVERIFY(engine.shouldDiscoverLocally("foo")); + QVERIFY(!engine.shouldDiscoverLocally("foo bar")); + QVERIFY(!engine.shouldDiscoverLocally("foo bar/touch")); + + fakeFolder.syncEngine().setLocalDiscoveryOptions( + LocalDiscoveryStyle::DatabaseAndFilesystem, + {}); + + QVERIFY(!engine.shouldDiscoverLocally("")); + } + + // Check whether item success and item failure adjusts the + // tracker correctly. + void testTrackerItemCompletion() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + + LocalDiscoveryTracker tracker; + connect(&fakeFolder.syncEngine(), &SyncEngine::itemCompleted, &tracker, &LocalDiscoveryTracker::slotItemCompleted); + connect(&fakeFolder.syncEngine(), &SyncEngine::finished, &tracker, &LocalDiscoveryTracker::slotSyncFinished); + auto trackerContains = [&](const char *path) { + return tracker.localDiscoveryPaths().find(path) != tracker.localDiscoveryPaths().end(); + }; + + tracker.addTouchedPath("A/spurious"); + + fakeFolder.localModifier().insert("A/a3"); + tracker.addTouchedPath("A/a3"); + + fakeFolder.localModifier().insert("A/a4"); + fakeFolder.serverErrorPaths().append("A/a4"); + // We're not adding a4 as touched, it's in the same folder as a3 and will be seen. + // And due to the error it should be added to the explicit list while a3 gets removed. + + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); + tracker.startSyncPartialDiscovery(); + QVERIFY(!fakeFolder.syncOnce()); + + QVERIFY(fakeFolder.currentRemoteState().find("A/a3")); + QVERIFY(!fakeFolder.currentRemoteState().find("A/a4")); + QVERIFY(!trackerContains("A/a3")); + QVERIFY(trackerContains("A/a4")); + QVERIFY(trackerContains("A/spurious")); // not removed since overall sync not successful + + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::FilesystemOnly); + tracker.startSyncFullDiscovery(); + QVERIFY(!fakeFolder.syncOnce()); + + QVERIFY(!fakeFolder.currentRemoteState().find("A/a4")); + QVERIFY(trackerContains("A/a4")); // had an error, still here + QVERIFY(!trackerContains("A/spurious")); // removed due to full discovery + + fakeFolder.serverErrorPaths().clear(); + fakeFolder.syncJournal().wipeErrorBlacklist(); + tracker.addTouchedPath("A/newspurious"); // will be removed due to successful sync + + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, tracker.localDiscoveryPaths()); + tracker.startSyncPartialDiscovery(); + QVERIFY(fakeFolder.syncOnce()); + + QVERIFY(fakeFolder.currentRemoteState().find("A/a4")); + QVERIFY(tracker.localDiscoveryPaths().empty()); + } +}; + +QTEST_GUILESS_MAIN(TestLocalDiscovery) +#include "testlocaldiscovery.moc" diff --git a/test/testoauth.cpp b/test/testoauth.cpp index 49eb3ce6c..72126c55e 100644 --- a/test/testoauth.cpp +++ b/test/testoauth.cpp @@ -22,7 +22,7 @@ signals: void hooked(const QUrl &); public: DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); } -} desktopServiceHook; +}; static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud"); @@ -90,6 +90,7 @@ public: class OAuthTestCase : public QObject { Q_OBJECT + DesktopServiceHook desktopServiceHook; public: enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState; Q_ENUM(State); diff --git a/test/testownsql.cpp b/test/testownsql.cpp index 77541794a..86ec77d28 100644 --- a/test/testownsql.cpp +++ b/test/testownsql.cpp @@ -126,6 +126,21 @@ private slots: } } + void testDestructor() + { + // This test make sure that the destructor of SqlQuery works even if the SqlDatabase + // is destroyed before + QScopedPointer<SqlDatabase> db(new SqlDatabase()); + SqlQuery q1(_db); + SqlQuery q2(_db); + q2.prepare("SELECT * FROM addresses"); + SqlQuery q3("SELECT * FROM addresses", _db); + SqlQuery q4; + SqlQuery q5; + q5.initOrReset("SELECT * FROM addresses", _db); + db.reset(); + } + private: SqlDatabase _db; }; diff --git a/test/testpermissions.cpp b/test/testpermissions.cpp new file mode 100644 index 000000000..57aac6eaf --- /dev/null +++ b/test/testpermissions.cpp @@ -0,0 +1,292 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> +#include "common/ownsql.h" + +using namespace OCC; + +static void applyPermissionsFromName(FileInfo &info) { + static QRegularExpression rx("_PERM_([^_]*)_[^/]*$"); + auto m = rx.match(info.name); + if (m.hasMatch()) { + info.permissions = RemotePermissions::fromServerString(m.captured(1)); + } + + for (FileInfo &sub : info.children) + applyPermissionsFromName(sub); +} + +// Check if the expected rows in the DB are non-empty. Note that in some cases they might be, then we cannot use this function +// https://github.com/owncloud/client/issues/2038 +static void assertCsyncJournalOk(SyncJournalDb &journal) +{ + SqlDatabase db; + QVERIFY(db.openReadOnly(journal.databaseFilePath())); + SqlQuery q("SELECT count(*) from metadata where length(fileId) == 0", db); + QVERIFY(q.exec()); + QVERIFY(q.next()); + QCOMPARE(q.intValue(0), 0); +#if defined(Q_OS_WIN) // Make sure the file does not appear in the FileInfo + FileSystem::setFileHidden(journal.databaseFilePath() + "-shm", true); +#endif +} + + +class TestPermissions : public QObject +{ + Q_OBJECT + +private slots: + + void t7pl() + { + FakeFolder fakeFolder{ FileInfo() }; + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + const int cannotBeModifiedSize = 133; + const int canBeModifiedSize = 144; + + //create some files + auto insertIn = [&](const QString &dir) { + fakeFolder.remoteModifier().insert(dir + "normalFile_PERM_WVND_.data", 100 ); + fakeFolder.remoteModifier().insert(dir + "cannotBeRemoved_PERM_WVN_.data", 101 ); + fakeFolder.remoteModifier().insert(dir + "canBeRemoved_PERM_D_.data", 102 ); + fakeFolder.remoteModifier().insert(dir + "cannotBeModified_PERM_DVN_.data", cannotBeModifiedSize , 'A'); + fakeFolder.remoteModifier().insert(dir + "canBeModified_PERM_W_.data", canBeModifiedSize ); + }; + + //put them in some directories + fakeFolder.remoteModifier().mkdir("normalDirectory_PERM_CKDNV_"); + insertIn("normalDirectory_PERM_CKDNV_/"); + fakeFolder.remoteModifier().mkdir("readonlyDirectory_PERM_M_" ); + insertIn("readonlyDirectory_PERM_M_/" ); + fakeFolder.remoteModifier().mkdir("readonlyDirectory_PERM_M_/subdir_PERM_CK_"); + fakeFolder.remoteModifier().mkdir("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_"); + fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data", 100); + applyPermissionsFromName(fakeFolder.remoteModifier()); + + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + assertCsyncJournalOk(fakeFolder.syncJournal()); + qInfo("Do some changes and see how they propagate"); + + //1. remove the file than cannot be removed + // (they should be recovered) + fakeFolder.localModifier().remove("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data"); + fakeFolder.localModifier().remove("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data"); + + //2. remove the file that can be removed + // (they should properly be gone) + auto removeReadOnly = [&] (const QString &file) { + QVERIFY(!QFileInfo(fakeFolder.localPath() + file).permission(QFile::WriteOwner)); + QFile(fakeFolder.localPath() + file).setPermissions(QFile::WriteOwner | QFile::ReadOwner); + fakeFolder.localModifier().remove(file); + }; + removeReadOnly("normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_D_.data"); + removeReadOnly("readonlyDirectory_PERM_M_/canBeRemoved_PERM_D_.data"); + + //3. Edit the files that cannot be modified + // (they should be recovered, and a conflict shall be created) + auto editReadOnly = [&] (const QString &file) { + QVERIFY(!QFileInfo(fakeFolder.localPath() + file).permission(QFile::WriteOwner)); + QFile(fakeFolder.localPath() + file).setPermissions(QFile::WriteOwner | QFile::ReadOwner); + fakeFolder.localModifier().appendByte(file); + }; + editReadOnly("normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_DVN_.data"); + editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data"); + + //4. Edit other files + // (they should be uploaded) + fakeFolder.localModifier().appendByte("normalDirectory_PERM_CKDNV_/canBeModified_PERM_W_.data"); + fakeFolder.localModifier().appendByte("readonlyDirectory_PERM_M_/canBeModified_PERM_W_.data"); + + //5. Create a new file in a read write folder + // (should be uploaded) + fakeFolder.localModifier().insert("normalDirectory_PERM_CKDNV_/newFile_PERM_WDNV_.data", 106 ); + applyPermissionsFromName(fakeFolder.remoteModifier()); + + //do the sync + QVERIFY(fakeFolder.syncOnce()); + assertCsyncJournalOk(fakeFolder.syncJournal()); + auto currentLocalState = fakeFolder.currentLocalState(); + + //1. + // File should be recovered + QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data")); + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data")); + + //2. + // File should be deleted + QVERIFY(!currentLocalState.find("normalDirectory_PERM_CKDNV_/canBeRemoved_PERM_D_.data")); + QVERIFY(!currentLocalState.find("readonlyDirectory_PERM_M_/canBeRemoved_PERM_D_.data")); + + //3. + // File should be recovered + QCOMPARE(currentLocalState.find("normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_DVN_.data")->size, cannotBeModifiedSize); + QCOMPARE(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data")->size, cannotBeModifiedSize); + // and conflict created + auto c1 = findConflict(currentLocalState, "normalDirectory_PERM_CKDNV_/cannotBeModified_PERM_DVN_.data"); + QVERIFY(c1); + QCOMPARE(c1->size, cannotBeModifiedSize + 1); + auto c2 = findConflict(currentLocalState, "readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data"); + QVERIFY(c2); + QCOMPARE(c2->size, cannotBeModifiedSize + 1); + // remove the conflicts for the next state comparison + fakeFolder.localModifier().remove(c1->path()); + fakeFolder.localModifier().remove(c2->path()); + + //4. File should be updated, that's tested by assertLocalAndRemoteDir + QCOMPARE(currentLocalState.find("normalDirectory_PERM_CKDNV_/canBeModified_PERM_W_.data")->size, canBeModifiedSize + 1); + QCOMPARE(currentLocalState.find("readonlyDirectory_PERM_M_/canBeModified_PERM_W_.data")->size, canBeModifiedSize + 1); + + //5. + // the file should be in the server and local + QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/newFile_PERM_WDNV_.data")); + + // Both side should still be the same + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Next test + + //6. Create a new file in a read only folder + // (they should not be uploaded) + fakeFolder.localModifier().insert("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data", 105 ); + + applyPermissionsFromName(fakeFolder.remoteModifier()); + // error: can't upload to readonly + QVERIFY(!fakeFolder.syncOnce()); + + assertCsyncJournalOk(fakeFolder.syncJournal()); + currentLocalState = fakeFolder.currentLocalState(); + + //6. + // The file should not exist on the remote, but still be there + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data")); + QVERIFY(!fakeFolder.currentRemoteState().find("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data")); + // remove it so next test succeed. + fakeFolder.localModifier().remove("readonlyDirectory_PERM_M_/newFile_PERM_WDNV_.data"); + // Both side should still be the same + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + + //###################################################################### + qInfo( "remove the read only directory" ); + // -> It must be recovered + fakeFolder.localModifier().remove("readonlyDirectory_PERM_M_"); + applyPermissionsFromName(fakeFolder.remoteModifier()); + QVERIFY(fakeFolder.syncOnce()); + assertCsyncJournalOk(fakeFolder.syncJournal()); + currentLocalState = fakeFolder.currentLocalState(); + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data")); + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + + //###################################################################### + qInfo( "move a directory in a outside read only folder" ); + + //Missing directory should be restored + //new directory should be uploaded + fakeFolder.localModifier().rename("readonlyDirectory_PERM_M_/subdir_PERM_CK_", "normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_"); + applyPermissionsFromName(fakeFolder.remoteModifier()); + fakeFolder.syncOnce(); + if (fakeFolder.syncEngine().isAnotherSyncNeeded() == ImmediateFollowUp) { + QVERIFY(fakeFolder.syncOnce()); + } + assertCsyncJournalOk(fakeFolder.syncJournal()); + currentLocalState = fakeFolder.currentLocalState(); + + // old name restored + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_")); + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data")); + + // new still exist (and is uploaded) + QVERIFY(currentLocalState.find("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data")); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + //###################################################################### + qInfo( "rename a directory in a read only folder and move a directory to a read-only" ); + + // do a sync to update the database + applyPermissionsFromName(fakeFolder.remoteModifier()); + QVERIFY(fakeFolder.syncOnce()); + + //1. rename a directory in a read only folder + //Missing directory should be restored + //new directory should stay but not be uploaded + fakeFolder.localModifier().rename("readonlyDirectory_PERM_M_/subdir_PERM_CK_", "readonlyDirectory_PERM_M_/newname_PERM_CK_" ); + + //2. move a directory from read to read only (move the directory from previous step) + fakeFolder.localModifier().rename("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_", "readonlyDirectory_PERM_M_/moved_PERM_CK_" ); + + // error: can't upload to readonly! + QVERIFY(!fakeFolder.syncOnce()); + if (fakeFolder.syncEngine().isAnotherSyncNeeded() == ImmediateFollowUp) { + QVERIFY(!fakeFolder.syncOnce()); + } + assertCsyncJournalOk(fakeFolder.syncJournal()); + currentLocalState = fakeFolder.currentLocalState(); + + //1. + // old name restored + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" )); + // new still exist + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/newname_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" )); + // but is not on server: so remove it localy for the future comarison + fakeFolder.localModifier().remove("readonlyDirectory_PERM_M_/newname_PERM_CK_"); + + //2. + // old removed + QVERIFY(!currentLocalState.find("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_")); + // new still there + QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/moved_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" )); + //but not on server + fakeFolder.localModifier().remove("readonlyDirectory_PERM_M_/moved_PERM_CK_"); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + + //###################################################################### + qInfo( "multiple restores of a file create different conflict files" ); + + editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data"); + fakeFolder.localModifier().setContents("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data", 's'); + //do the sync + applyPermissionsFromName(fakeFolder.remoteModifier()); + QVERIFY(fakeFolder.syncOnce()); + assertCsyncJournalOk(fakeFolder.syncJournal()); + + QThread::sleep(1); // make sure changes have different mtime + editReadOnly("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data"); + fakeFolder.localModifier().setContents("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data", 'd'); + + //do the sync + applyPermissionsFromName(fakeFolder.remoteModifier()); + QVERIFY(fakeFolder.syncOnce()); + assertCsyncJournalOk(fakeFolder.syncJournal()); + + // there should be two conflict files + currentLocalState = fakeFolder.currentLocalState(); + int count = 0; + while (auto i = findConflict(currentLocalState, "readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data")) { + QVERIFY((i->contentChar == 's') || (i->contentChar == 'd')); + fakeFolder.localModifier().remove(i->path()); + currentLocalState = fakeFolder.currentLocalState(); + count++; + } + QCOMPARE(count, 2); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + +}; + +QTEST_GUILESS_MAIN(TestPermissions) +#include "testpermissions.moc" diff --git a/test/testremotediscovery.cpp b/test/testremotediscovery.cpp new file mode 100644 index 000000000..4945bbf4c --- /dev/null +++ b/test/testremotediscovery.cpp @@ -0,0 +1,128 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> +#include <localdiscoverytracker.h> + +using namespace OCC; + +struct FakeBrokenXmlPropfindReply : FakePropfindReply { + FakeBrokenXmlPropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, + const QNetworkRequest &request, QObject *parent) + : FakePropfindReply(remoteRootFileInfo, op, request, parent) { + QVERIFY(payload.size() > 50); + // turncate the XML + payload.chop(20); + } +}; + +struct MissingPermissionsPropfindReply : FakePropfindReply { + MissingPermissionsPropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, + const QNetworkRequest &request, QObject *parent) + : FakePropfindReply(remoteRootFileInfo, op, request, parent) { + // If the propfind contains a single file without permissions, this is a server error + const char toRemove[] = "<oc:permissions>RDNVCKW</oc:permissions>"; + auto pos = payload.indexOf(toRemove, payload.size()/2); + QVERIFY(pos > 0); + payload.remove(pos, sizeof(toRemove) - 1); + } +}; + + +enum ErrorKind : int { + // Lower code are corresponding to HTML error code + InvalidXML = 1000, + MissingPermissions, + Timeout, +}; + +Q_DECLARE_METATYPE(ErrorCategory) + +class TestRemoteDiscovery : public QObject +{ + Q_OBJECT + +private slots: + + void testRemoteDiscoveryError_data() + { + qRegisterMetaType<ErrorCategory>(); + QTest::addColumn<int>("errorKind"); + QTest::addColumn<QString>("expectedErrorString"); + + QTest::newRow("404") << 404 << "B"; // The filename should be in the error message + QTest::newRow("500") << 500 << "Internal Server Fake Error"; // the message from FakeErrorReply + QTest::newRow("503") << 503 << ""; + QTest::newRow("200") << 200 << ""; // 200 should be an error since propfind should return 207 + QTest::newRow("InvalidXML") << +InvalidXML << ""; + QTest::newRow("MissingPermissions") << +MissingPermissions << "missing data"; + QTest::newRow("Timeout") << +Timeout << ""; + } + + + // Check what happens when there is an error. + void testRemoteDiscoveryError() + { + QFETCH(int, errorKind); + QFETCH(QString, expectedErrorString); + bool syncSucceeds = errorKind == 503; // 503 just ignore the temporarily unavailable directory + + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + + // Do Some change as well + fakeFolder.localModifier().insert("A/z1"); + fakeFolder.localModifier().insert("B/z1"); + fakeFolder.localModifier().insert("C/z1"); + fakeFolder.remoteModifier().insert("A/z2"); + fakeFolder.remoteModifier().insert("B/z2"); + fakeFolder.remoteModifier().insert("C/z2"); + + auto oldLocalState = fakeFolder.currentLocalState(); + auto oldRemoteState = fakeFolder.currentRemoteState(); + + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) + -> QNetworkReply *{ + if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "PROPFIND" && req.url().path().endsWith("/B")) { + if (errorKind == InvalidXML) { + return new FakeBrokenXmlPropfindReply(fakeFolder.remoteModifier(), op, req, this); + } else if (errorKind == MissingPermissions) { + return new MissingPermissionsPropfindReply(fakeFolder.remoteModifier(), op, req, this); + } else if (errorKind == Timeout) { + return new FakeHangingReply(op, req, this); + } else if (errorKind < 1000) { + return new FakeErrorReply(op, req, this, errorKind); + } + } + return nullptr; + }); + + // So the test that test timeout finishes fast + QScopedValueRollback<int> setHttpTimeout(AbstractNetworkJob::httpTimeout, errorKind == Timeout ? 1 : 10000); + + QSignalSpy errorSpy(&fakeFolder.syncEngine(), &SyncEngine::syncError); + QCOMPARE(fakeFolder.syncOnce(), false); + qDebug() << "errorSpy=" << errorSpy; + + // The folder B should not have been sync'ed (and in particular not removed) + QCOMPARE(oldLocalState.children["B"], fakeFolder.currentLocalState().children["B"]); + QCOMPARE(oldRemoteState.children["B"], fakeFolder.currentRemoteState().children["B"]); + if (!syncSucceeds) { + // Check we got the right error + QCOMPARE(errorSpy.count(), 1); + QVERIFY(errorSpy[0][0].toString().contains(expectedErrorString)); + } else { + // The other folder should have been sync'ed as the sync just ignored the faulty dir + QCOMPARE(fakeFolder.currentRemoteState().children["A"], fakeFolder.currentLocalState().children["A"]); + QCOMPARE(fakeFolder.currentRemoteState().children["C"], fakeFolder.currentLocalState().children["C"]); + } + } +}; + +QTEST_GUILESS_MAIN(TestRemoteDiscovery) +#include "testremotediscovery.moc" diff --git a/test/testsyncconflict.cpp b/test/testsyncconflict.cpp index daf7eab0d..9052592ef 100644 --- a/test/testsyncconflict.cpp +++ b/test/testsyncconflict.cpp @@ -42,7 +42,7 @@ QStringList findConflicts(const FileInfo &dir) { QStringList conflicts; for (const auto &item : dir.children) { - if (item.name.contains("conflict")) { + if (item.name.contains("(conflicted copy")) { conflicts.append(item.path()); } } @@ -56,7 +56,7 @@ bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString pa if (!base) return false; for (const auto &item : base->children) { - if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("_conflict")) { + if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(conflicted copy")) { local.remove(item.path()); return true; } @@ -80,6 +80,12 @@ private slots: fakeFolder.remoteModifier().appendByte("A/a2"); fakeFolder.remoteModifier().appendByte("A/a2"); QVERIFY(fakeFolder.syncOnce()); + + // Verify that the conflict names don't have the user name + for (const auto &name : findConflicts(fakeFolder.currentLocalState().children["A"])) { + QVERIFY(!name.contains(fakeFolder.syncEngine().account()->davDisplayName())); + } + QVERIFY(expectAndWipeConflict(fakeFolder.localModifier(), fakeFolder.currentLocalState(), "A/a1")); QVERIFY(expectAndWipeConflict(fakeFolder.localModifier(), fakeFolder.currentLocalState(), "A/a2")); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); @@ -94,11 +100,15 @@ private slots: QMap<QByteArray, QString> conflictMap; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (op == QNetworkAccessManager::PutOperation) { - auto baseFileId = request.rawHeader("OC-ConflictBaseFileId"); - if (!baseFileId.isEmpty()) { + if (request.rawHeader("OC-Conflict") == "1") { + auto baseFileId = request.rawHeader("OC-ConflictBaseFileId"); auto components = request.url().toString().split('/'); QString conflictFile = components.mid(components.size() - 2).join('/'); conflictMap[baseFileId] = conflictFile; + [&] { + QVERIFY(!baseFileId.isEmpty()); + QCOMPARE(request.rawHeader("OC-ConflictInitialBasePath"), Utility::conflictFileBaseName(conflictFile.toUtf8())); + }(); } } return nullptr; @@ -121,6 +131,9 @@ private slots: QCOMPARE(conflictMap.size(), 2); QCOMPARE(Utility::conflictFileBaseName(conflictMap[a1FileId].toUtf8()), QByteArray("A/a1")); + // Check that the conflict file contains the username + QVERIFY(conflictMap[a1FileId].contains(QString("(conflicted copy %1 ").arg(fakeFolder.syncEngine().account()->davDisplayName()))); + QCOMPARE(remote.find(conflictMap[a1FileId])->contentChar, 'L'); QCOMPARE(remote.find("A/a1")->contentChar, 'R'); @@ -137,11 +150,15 @@ private slots: QMap<QByteArray, QString> conflictMap; fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { if (op == QNetworkAccessManager::PutOperation) { - auto baseFileId = request.rawHeader("OC-ConflictBaseFileId"); - if (!baseFileId.isEmpty()) { + if (request.rawHeader("OC-Conflict") == "1") { + auto baseFileId = request.rawHeader("OC-ConflictBaseFileId"); auto components = request.url().toString().split('/'); QString conflictFile = components.mid(components.size() - 2).join('/'); conflictMap[baseFileId] = conflictFile; + [&] { + QVERIFY(!baseFileId.isEmpty()); + QCOMPARE(request.rawHeader("OC-ConflictInitialBasePath"), Utility::conflictFileBaseName(conflictFile.toUtf8())); + }(); } } return nullptr; @@ -151,11 +168,12 @@ private slots: // file didn't finish in the same sync run that the conflict was created. // To do that we need to create a mock conflict record. auto a1FileId = fakeFolder.remoteModifier().find("A/a1")->fileId; - QString conflictName = QLatin1String("A/a1_conflict-me-1234"); + QString conflictName = QLatin1String("A/a1 (conflicted copy me 1234)"); fakeFolder.localModifier().insert(conflictName, 64, 'L'); ConflictRecord conflictRecord; conflictRecord.path = conflictName.toUtf8(); conflictRecord.baseFileId = a1FileId; + conflictRecord.initialBasePath = "A/a1"; fakeFolder.syncJournal().setConflictRecord(conflictRecord); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); @@ -201,12 +219,13 @@ private slots: QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // With no headers from the server - fakeFolder.remoteModifier().insert("A/a1_conflict-1234"); + fakeFolder.remoteModifier().insert("A/a1 (conflicted copy 1234)"); QVERIFY(fakeFolder.syncOnce()); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); - auto conflictRecord = fakeFolder.syncJournal().conflictRecord("A/a1_conflict-1234"); + auto conflictRecord = fakeFolder.syncJournal().conflictRecord("A/a1 (conflicted copy 1234)"); QVERIFY(conflictRecord.isValid()); QCOMPARE(conflictRecord.baseFileId, fakeFolder.remoteModifier().find("A/a1")->fileId); + QCOMPARE(conflictRecord.initialBasePath, QByteArray("A/a1")); // Now with server headers QObject parent; @@ -218,6 +237,7 @@ private slots: reply->setRawHeader("OC-ConflictBaseFileId", a2FileId); reply->setRawHeader("OC-ConflictBaseMtime", "1234"); reply->setRawHeader("OC-ConflictBaseEtag", "etag"); + reply->setRawHeader("OC-ConflictInitialBasePath", "A/original"); return reply; } return nullptr; @@ -230,6 +250,7 @@ private slots: QCOMPARE(conflictRecord.baseFileId, a2FileId); QCOMPARE(conflictRecord.baseModtime, 1234); QCOMPARE(conflictRecord.baseEtag, QByteArray("etag")); + QCOMPARE(conflictRecord.initialBasePath, QByteArray("A/original")); } // Check that conflict records are removed when the file is gone @@ -303,40 +324,72 @@ private slots: QTest::addColumn<QString>("input"); QTest::addColumn<QString>("output"); - QTest::newRow("") + QTest::newRow("nomatch1") << "a/b/foo" << ""; - QTest::newRow("") + QTest::newRow("nomatch2") << "a/b/foo.txt" << ""; - QTest::newRow("") + QTest::newRow("nomatch3") << "a/b/foo_conflict" << ""; - QTest::newRow("") + QTest::newRow("nomatch4") << "a/b/foo_conflict.txt" << ""; - QTest::newRow("") + QTest::newRow("match1") << "a/b/foo_conflict-123.txt" << "a/b/foo.txt"; - QTest::newRow("") + QTest::newRow("match2") << "a/b/foo_conflict-foo-123.txt" << "a/b/foo.txt"; - QTest::newRow("") + QTest::newRow("match3") << "a/b/foo_conflict-123" << "a/b/foo"; - QTest::newRow("") + QTest::newRow("match4") << "a/b/foo_conflict-foo-123" << "a/b/foo"; + // new style + QTest::newRow("newmatch1") + << "a/b/foo (conflicted copy 123).txt" + << "a/b/foo.txt"; + QTest::newRow("newmatch2") + << "a/b/foo (conflicted copy foo 123).txt" + << "a/b/foo.txt"; + + QTest::newRow("newmatch3") + << "a/b/foo (conflicted copy 123)" + << "a/b/foo"; + QTest::newRow("newmatch4") + << "a/b/foo (conflicted copy foo 123)" + << "a/b/foo"; + + QTest::newRow("newmatch5") + << "a/b/foo (conflicted copy foo 123) bla" + << "a/b/foo bla"; + + QTest::newRow("newmatch6") + << "a/b/foo (conflicted copy foo.bar 123)" + << "a/b/foo"; + // double conflict files - QTest::newRow("") + QTest::newRow("double1") << "a/b/foo_conflict-123_conflict-456.txt" << "a/b/foo_conflict-123.txt"; - QTest::newRow("") + QTest::newRow("double2") << "a/b/foo_conflict-foo-123_conflict-bar-456.txt" << "a/b/foo_conflict-foo-123.txt"; + QTest::newRow("double3") + << "a/b/foo (conflicted copy 123) (conflicted copy 456).txt" + << "a/b/foo (conflicted copy 123).txt"; + QTest::newRow("double4") + << "a/b/foo (conflicted copy 123)_conflict-456.txt" + << "a/b/foo (conflicted copy 123).txt"; + QTest::newRow("double5") + << "a/b/foo_conflict-123 (conflicted copy 456).txt" + << "a/b/foo_conflict-123.txt"; } void testConflictFileBaseName() @@ -500,8 +553,8 @@ private slots: auto conflicts = findConflicts(fakeFolder.currentLocalState()); std::sort(conflicts.begin(), conflicts.end()); QVERIFY(conflicts.size() == 2); - QVERIFY(conflicts[0].contains("A_conflict")); - QVERIFY(conflicts[1].contains("B_conflict")); + QVERIFY(conflicts[0].contains("A (conflicted copy")); + QVERIFY(conflicts[1].contains("B (conflicted copy")); for (auto conflict : conflicts) QDir(fakeFolder.localPath() + conflict).removeRecursively(); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); @@ -539,7 +592,7 @@ private slots: // inside of them! auto conflicts = findConflicts(fakeFolder.currentLocalState()); QVERIFY(conflicts.size() == 1); - QVERIFY(conflicts[0].contains("A_conflict")); + QVERIFY(conflicts[0].contains("A (conflicted copy")); for (auto conflict : conflicts) QDir(fakeFolder.localPath() + conflict).removeRecursively(); diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp index ff291cc39..5628befdc 100644 --- a/test/testsyncengine.cpp +++ b/test/testsyncengine.cpp @@ -172,6 +172,14 @@ private slots: fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, {"parentFolder/subFolderA/"}); fakeFolder.syncEngine().journal()->avoidReadFromDbOnNextSync(QByteArrayLiteral("parentFolder/subFolderA/")); + auto getEtag = [&](const QByteArray &file) { + SyncJournalFileRecord rec; + fakeFolder.syncJournal().getFileRecord(file, &rec); + return rec._etag; + }; + QVERIFY(getEtag("parentFolder") == "_invalid_"); + QVERIFY(getEtag("parentFolder/subFolderA") == "_invalid_"); + QVERIFY(getEtag("parentFolder/subFolderA/subsubFolder") != "_invalid_"); // But touch local file before the next sync, such that the local folder // can't be removed @@ -247,7 +255,8 @@ private slots: } else if(item->_file == "Y/Z/d3") { QVERIFY(item->_status != SyncFileItem::Success); } - QVERIFY(item->_file != "Y/Z/d9"); // we should have aborted the sync before d9 starts + // We do not know about the other files - maybe the sync was aborted, + // maybe they finished before the error caused the abort. } } @@ -563,40 +572,6 @@ private slots: QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt")); } - // Check correct behavior when local discovery is partially drawn from the db - void testLocalDiscoveryStyle() - { - FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; - - // More subdirectories are useful for testing - fakeFolder.localModifier().mkdir("A/X"); - fakeFolder.localModifier().mkdir("A/Y"); - fakeFolder.localModifier().insert("A/X/x1"); - fakeFolder.localModifier().insert("A/Y/y1"); - QVERIFY(fakeFolder.syncOnce()); - QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); - - // Test begins - fakeFolder.localModifier().insert("A/a3"); - fakeFolder.localModifier().insert("A/X/x2"); - fakeFolder.localModifier().insert("A/Y/y2"); - fakeFolder.localModifier().insert("B/b3"); - fakeFolder.remoteModifier().insert("C/c3"); - - fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, { "A/X" }); - QVERIFY(fakeFolder.syncOnce()); - QVERIFY(fakeFolder.currentRemoteState().find("A/a3")); - QVERIFY(fakeFolder.currentRemoteState().find("A/X/x2")); - QVERIFY(!fakeFolder.currentRemoteState().find("A/Y/y2")); - QVERIFY(!fakeFolder.currentRemoteState().find("B/b3")); - QVERIFY(fakeFolder.currentLocalState().find("C/c3")); - QCOMPARE(fakeFolder.syncEngine().lastLocalDiscoveryStyle(), LocalDiscoveryStyle::DatabaseAndFilesystem); - - QVERIFY(fakeFolder.syncOnce()); - QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); - QCOMPARE(fakeFolder.syncEngine().lastLocalDiscoveryStyle(), LocalDiscoveryStyle::FilesystemOnly); - } - void testDiscoveryHiddenFile() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; @@ -641,6 +616,7 @@ private slots: QVERIFY(fakeFolder.currentLocalState().find("A/tößt")); QVERIFY(fakeFolder.currentLocalState().find("A/t𠜎t")); +#if !defined(Q_OS_MAC) && !defined(Q_OS_WIN) // Try again with a locale that can represent ö but not 𠜎 (4-byte utf8). QTextCodec::setCodecForLocale(QTextCodec::codecForName("ISO-8859-15")); QVERIFY(QTextCodec::codecForLocale()->mibEnum() == 111); @@ -671,6 +647,7 @@ private slots: QVERIFY(fakeFolder.currentRemoteState().find("C/tößt")); QTextCodec::setCodecForLocale(utf8Locale); +#endif } }; diff --git a/test/testsyncjournaldb.cpp b/test/testsyncjournaldb.cpp index 389330ffa..4748f64f7 100644 --- a/test/testsyncjournaldb.cpp +++ b/test/testsyncjournaldb.cpp @@ -57,7 +57,7 @@ private slots: record._type = ItemTypeDirectory; record._etag = "789789"; record._fileId = "abcd"; - record._remotePerm = RemotePermissions("RW"); + record._remotePerm = RemotePermissions::fromDbValue("RW"); record._fileSize = 213089055; record._checksumHeader = "MD5:mychecksum"; QVERIFY(_db.setFileRecord(record)); @@ -79,7 +79,7 @@ private slots: record._type = ItemTypeFile; record._etag = "789FFF"; record._fileId = "efg"; - record._remotePerm = RemotePermissions("NV"); + record._remotePerm = RemotePermissions::fromDbValue("NV"); record._fileSize = 289055; _db.setFileRecordMetadata(record); QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo"), &storedRecord)); @@ -96,7 +96,7 @@ private slots: { SyncJournalFileRecord record; record._path = "foo-checksum"; - record._remotePerm = RemotePermissions("RW"); + record._remotePerm = RemotePermissions::fromDbValue(" "); record._checksumHeader = "MD5:mychecksum"; record._modtime = Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc()); QVERIFY(_db.setFileRecord(record)); @@ -117,7 +117,7 @@ private slots: { SyncJournalFileRecord record; record._path = "foo-nochecksum"; - record._remotePerm = RemotePermissions("RWN"); + record._remotePerm = RemotePermissions(); record._modtime = Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc()); QVERIFY(_db.setFileRecord(record)); @@ -176,11 +176,15 @@ private slots: // Typical 8-digit padded id record._fileId = "00000001abcd"; - QCOMPARE(record.numericFileId(), QByteArray("00000001")); + QCOMPARE(record.legacyDeriveNumericFileId(), QByteArray("00000001")); + + // Typical 8-digit padded id with instanceid that starts with a digit + record._fileId = "00000001999"; + QCOMPARE(record.legacyDeriveNumericFileId(), QByteArray("00000001")); // When the numeric id overflows the 8-digit boundary record._fileId = "123456789ocidblaabcd"; - QCOMPARE(record.numericFileId(), QByteArray("123456789")); + QCOMPARE(record.legacyDeriveNumericFileId(), QByteArray("123456789")); } void testConflictRecord() @@ -205,6 +209,124 @@ private slots: QVERIFY(!_db.conflictRecord(record.path).isValid()); } + void testAvoidReadFromDbOnNextSync() + { + auto invalidEtag = QByteArray("_invalid_"); + auto initialEtag = QByteArray("etag"); + auto makeEntry = [&](const QByteArray &path, ItemType type) { + SyncJournalFileRecord record; + record._path = path; + record._type = type; + record._etag = initialEtag; + _db.setFileRecord(record); + }; + auto getEtag = [&](const QByteArray &path) { + SyncJournalFileRecord record; + _db.getFileRecord(path, &record); + return record._etag; + }; + + const auto dirType = ItemTypeDirectory; + const auto fileType = ItemTypeFile; + + makeEntry("foodir", dirType); + makeEntry("otherdir", dirType); + makeEntry("foo%", dirType); // wildcards don't apply + makeEntry("foodi_", dirType); // wildcards don't apply + makeEntry("foodir/file", fileType); + makeEntry("foodir/subdir", dirType); + makeEntry("foodir/subdir/file", fileType); + makeEntry("foodir/otherdir", dirType); + makeEntry("fo", dirType); // prefix, but does not match + makeEntry("foodir/sub", dirType); // prefix, but does not match + makeEntry("foodir/subdir/subsubdir", dirType); + makeEntry("foodir/subdir/subsubdir/file", fileType); + makeEntry("foodir/subdir/otherdir", dirType); + + _db.avoidReadFromDbOnNextSync(QByteArray("foodir/subdir")); + + // Direct effects of parent directories being set to _invalid_ + QCOMPARE(getEtag("foodir"), invalidEtag); + QCOMPARE(getEtag("foodir/subdir"), invalidEtag); + QCOMPARE(getEtag("foodir/subdir/subsubdir"), initialEtag); + + QCOMPARE(getEtag("foodir/file"), initialEtag); + QCOMPARE(getEtag("foodir/subdir/file"), initialEtag); + QCOMPARE(getEtag("foodir/subdir/subsubdir/file"), initialEtag); + + QCOMPARE(getEtag("fo"), initialEtag); + QCOMPARE(getEtag("foo%"), initialEtag); + QCOMPARE(getEtag("foodi_"), initialEtag); + QCOMPARE(getEtag("otherdir"), initialEtag); + QCOMPARE(getEtag("foodir/otherdir"), initialEtag); + QCOMPARE(getEtag("foodir/sub"), initialEtag); + QCOMPARE(getEtag("foodir/subdir/otherdir"), initialEtag); + + // Indirect effects: setFileRecord() calls filter etags + initialEtag = "etag2"; + + makeEntry("foodir", dirType); + QCOMPARE(getEtag("foodir"), invalidEtag); + makeEntry("foodir/subdir", dirType); + QCOMPARE(getEtag("foodir/subdir"), invalidEtag); + makeEntry("foodir/subdir/subsubdir", dirType); + QCOMPARE(getEtag("foodir/subdir/subsubdir"), initialEtag); + makeEntry("fo", dirType); + QCOMPARE(getEtag("fo"), initialEtag); + makeEntry("foodir/sub", dirType); + QCOMPARE(getEtag("foodir/sub"), initialEtag); + } + + void testRecursiveDelete() + { + auto makeEntry = [&](const QByteArray &path) { + SyncJournalFileRecord record; + record._path = path; + _db.setFileRecord(record); + }; + + QByteArrayList elements; + elements + << "foo" + << "foo/file" + << "bar" + << "moo" + << "moo/file" + << "foo%bar" + << "foo bla bar/file" + << "fo_" + << "fo_/file"; + for (auto elem : elements) + makeEntry(elem); + + auto checkElements = [&]() { + bool ok = true; + for (auto elem : elements) { + SyncJournalFileRecord record; + _db.getFileRecord(elem, &record); + if (!record.isValid()) { + qWarning() << "Missing record: " << elem; + ok = false; + } + } + return ok; + }; + + _db.deleteFileRecord("moo", true); + elements.removeAll("moo"); + elements.removeAll("moo/file"); + QVERIFY(checkElements()); + + _db.deleteFileRecord("fo_", true); + elements.removeAll("fo_"); + elements.removeAll("fo_/file"); + QVERIFY(checkElements()); + + _db.deleteFileRecord("foo%bar", true); + elements.removeAll("foo%bar"); + QVERIFY(checkElements()); + } + private: SyncJournalDb _db; }; diff --git a/test/testsyncmove.cpp b/test/testsyncmove.cpp index 67d3c22be..4a80a6295 100644 --- a/test/testsyncmove.cpp +++ b/test/testsyncmove.cpp @@ -42,7 +42,7 @@ QStringList findConflicts(const FileInfo &dir) { QStringList conflicts; for (const auto &item : dir.children) { - if (item.name.contains("conflict")) { + if (item.name.contains("(conflicted copy")) { conflicts.append(item.path()); } } @@ -56,7 +56,7 @@ bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString pa if (!base) return false; for (const auto &item : base->children) { - if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("_conflict")) { + if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(conflicted copy")) { local.remove(item.path()); return true; } @@ -576,6 +576,87 @@ private slots: //QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } } + + // https://github.com/owncloud/client/issues/6629#issuecomment-402450691 + // When a file is moved and the server mtime was not in sync, the local mtime should be kept + void testMoveAndMTimeChange() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + int nPUT = 0; + int nDELETE = 0; + int nGET = 0; + int nMOVE = 0; + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) { + if (op == QNetworkAccessManager::PutOperation) + ++nPUT; + if (op == QNetworkAccessManager::DeleteOperation) + ++nDELETE; + if (op == QNetworkAccessManager::GetOperation) + ++nGET; + if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") + ++nMOVE; + return nullptr; + }); + + // Changing the mtime on the server (without invalidating the etag) + fakeFolder.remoteModifier().find("A/a1")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-50000); + fakeFolder.remoteModifier().find("A/a2")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-40000); + + // Move a few files + fakeFolder.remoteModifier().rename("A/a1", "A/a1_server_renamed"); + fakeFolder.localModifier().rename("A/a2", "A/a2_local_renamed"); + + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(nGET, 0); + QCOMPARE(nPUT, 0); + QCOMPARE(nMOVE, 1); + QCOMPARE(nDELETE, 0); + + // Another sync should do nothing + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(nGET, 0); + QCOMPARE(nPUT, 0); + QCOMPARE(nMOVE, 1); + QCOMPARE(nDELETE, 0); + + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + // Test for https://github.com/owncloud/client/issues/6694 + void testInvertFolderHierarchy() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + fakeFolder.remoteModifier().mkdir("A/Empty"); + fakeFolder.remoteModifier().mkdir("A/Empty/Foo"); + fakeFolder.remoteModifier().mkdir("C/AllEmpty"); + fakeFolder.remoteModifier().mkdir("C/AllEmpty/Bar"); + QVERIFY(fakeFolder.syncOnce()); + + // "Empty" is after "A", alphabetically + fakeFolder.localModifier().rename("A/Empty", "Empty"); + fakeFolder.localModifier().rename("A", "Empty/A"); + + // "AllEmpty" is before "C", alphabetically + fakeFolder.localModifier().rename("C/AllEmpty", "AllEmpty"); + fakeFolder.localModifier().rename("C", "AllEmpty/C"); + + auto expectedState = fakeFolder.currentLocalState(); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), expectedState); + QCOMPARE(fakeFolder.currentRemoteState(), expectedState); + + /* FIXME - likely addressed by ogoffart's sync code refactor + // Now, the revert, but "crossed" + fakeFolder.localModifier().rename("Empty/A", "A"); + fakeFolder.localModifier().rename("AllEmpty/C", "C"); + fakeFolder.localModifier().rename("Empty", "C/Empty"); + fakeFolder.localModifier().rename("AllEmpty", "A/AllEmpty"); + expectedState = fakeFolder.currentLocalState(); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), expectedState); + QCOMPARE(fakeFolder.currentRemoteState(), expectedState); + */ + } }; QTEST_GUILESS_MAIN(TestSyncMove) diff --git a/test/testsyncvirtualfiles.cpp b/test/testsyncvirtualfiles.cpp new file mode 100644 index 000000000..bc603e9a5 --- /dev/null +++ b/test/testsyncvirtualfiles.cpp @@ -0,0 +1,631 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> + +using namespace OCC; + +SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path) +{ + for (const QList<QVariant> &args : spy) { + auto item = args[0].value<SyncFileItemPtr>(); + if (item->destination() == path) + return item; + } + return SyncFileItemPtr(new SyncFileItem); +} + +bool itemInstruction(const QSignalSpy &spy, const QString &path, const csync_instructions_e instr) +{ + auto item = findItem(spy, path); + return item->_instruction == instr; +} + +SyncJournalFileRecord dbRecord(FakeFolder &folder, const QString &path) +{ + SyncJournalFileRecord record; + folder.syncJournal().getFileRecord(path, &record); + return record; +} + +class TestSyncVirtualFiles : public QObject +{ + Q_OBJECT + +private slots: + void testVirtualFileLifecycle_data() + { + QTest::addColumn<bool>("doLocalDiscovery"); + + QTest::newRow("full local discovery") << true; + QTest::newRow("skip local discovery") << false; + } + + void testVirtualFileLifecycle() + { + QFETCH(bool, doLocalDiscovery); + + FakeFolder fakeFolder{ FileInfo() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto cleanup = [&]() { + completeSpy.clear(); + if (!doLocalDiscovery) + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem); + }; + cleanup(); + + // Create a virtual file for a new remote file + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().insert("A/a1", 64); + auto someDate = QDateTime(QDate(1984, 07, 30), QTime(1,3,2)); + fakeFolder.remoteModifier().setModTime("A/a1", someDate); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1.owncloud").lastModified(), someDate); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_NEW)); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFile); + cleanup(); + + // Another sync doesn't actually lead to changes + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1.owncloud").lastModified(), someDate); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFile); + QVERIFY(completeSpy.isEmpty()); + cleanup(); + + // Not even when the remote is rediscovered + fakeFolder.syncJournal().forceRemoteDiscoveryNextSync(); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QCOMPARE(QFileInfo(fakeFolder.localPath() + "A/a1.owncloud").lastModified(), someDate); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFile); + QVERIFY(completeSpy.isEmpty()); + cleanup(); + + // Neither does a remote change + fakeFolder.remoteModifier().appendByte("A/a1"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_UPDATE_METADATA)); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFile); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._fileSize, 65); + cleanup(); + + // If the local virtual file file is removed, it'll just be recreated + if (!doLocalDiscovery) + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::DatabaseAndFilesystem, { "A" }); + fakeFolder.localModifier().remove("A/a1.owncloud"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_NEW)); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFile); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._fileSize, 65); + cleanup(); + + // Remote rename is propagated + fakeFolder.remoteModifier().rename("A/a1", "A/a1m"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1m")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1m.owncloud")); + QVERIFY(!fakeFolder.currentRemoteState().find("A/a1")); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1m")); + QVERIFY(itemInstruction(completeSpy, "A/a1m.owncloud", CSYNC_INSTRUCTION_RENAME)); + QCOMPARE(dbRecord(fakeFolder, "A/a1m.owncloud")._type, ItemTypeVirtualFile); + cleanup(); + + // Remote remove is propagated + fakeFolder.remoteModifier().remove("A/a1m"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1m.owncloud")); + QVERIFY(!fakeFolder.currentRemoteState().find("A/a1m")); + QVERIFY(itemInstruction(completeSpy, "A/a1m.owncloud", CSYNC_INSTRUCTION_REMOVE)); + QVERIFY(!dbRecord(fakeFolder, "A/a1.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a1m.owncloud").isValid()); + cleanup(); + + // Edge case: Local virtual file but no db entry for some reason + fakeFolder.remoteModifier().insert("A/a2", 64); + fakeFolder.remoteModifier().insert("A/a3", 64); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a3.owncloud")); + cleanup(); + + fakeFolder.syncEngine().journal()->deleteFileRecord("A/a2.owncloud"); + fakeFolder.syncEngine().journal()->deleteFileRecord("A/a3.owncloud"); + fakeFolder.remoteModifier().remove("A/a3"); + fakeFolder.syncEngine().setLocalDiscoveryOptions(LocalDiscoveryStyle::FilesystemOnly); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(itemInstruction(completeSpy, "A/a2.owncloud", CSYNC_INSTRUCTION_NEW)); + QVERIFY(dbRecord(fakeFolder, "A/a2.owncloud").isValid()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a3.owncloud")); + QVERIFY(!dbRecord(fakeFolder, "A/a3.owncloud").isValid()); + cleanup(); + } + + void testVirtualFileConflict() + { + FakeFolder fakeFolder{ FileInfo() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto cleanup = [&]() { + completeSpy.clear(); + }; + cleanup(); + + // Create a virtual file for a new remote file + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().insert("A/a1", 64); + fakeFolder.remoteModifier().insert("A/a2", 64); + fakeFolder.remoteModifier().mkdir("B"); + fakeFolder.remoteModifier().insert("B/b1", 64); + fakeFolder.remoteModifier().insert("B/b2", 64); + fakeFolder.remoteModifier().mkdir("C"); + fakeFolder.remoteModifier().insert("C/c1", 64); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/b2.owncloud")); + cleanup(); + + // A: the correct file and a conflicting file are added, virtual files stay + // B: same setup, but the virtual files are deleted by the user + // C: user adds a *directory* locally + fakeFolder.localModifier().insert("A/a1", 64); + fakeFolder.localModifier().insert("A/a2", 30); + fakeFolder.localModifier().insert("B/b1", 64); + fakeFolder.localModifier().insert("B/b2", 30); + fakeFolder.localModifier().remove("B/b1.owncloud"); + fakeFolder.localModifier().remove("B/b2.owncloud"); + fakeFolder.localModifier().mkdir("C/c1"); + fakeFolder.localModifier().insert("C/c1/foo"); + QVERIFY(fakeFolder.syncOnce()); + + // Everything is CONFLICT since mtimes are different even for a1/b1 + QVERIFY(itemInstruction(completeSpy, "A/a1", CSYNC_INSTRUCTION_CONFLICT)); + QVERIFY(itemInstruction(completeSpy, "A/a2", CSYNC_INSTRUCTION_CONFLICT)); + QVERIFY(itemInstruction(completeSpy, "B/b1", CSYNC_INSTRUCTION_CONFLICT)); + QVERIFY(itemInstruction(completeSpy, "B/b2", CSYNC_INSTRUCTION_CONFLICT)); + QVERIFY(itemInstruction(completeSpy, "C/c1", CSYNC_INSTRUCTION_CONFLICT)); + + // no virtual file files should remain + QVERIFY(!fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("B/b1.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("B/b2.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("C/c1.owncloud")); + + // conflict files should exist + QCOMPARE(fakeFolder.syncJournal().conflictRecordPaths().size(), 3); + + // nothing should have the virtual file tag + QCOMPARE(dbRecord(fakeFolder, "A/a1")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "A/a2")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "B/b1")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "B/b2")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "C/c1")._type, ItemTypeFile); + QVERIFY(!dbRecord(fakeFolder, "A/a1.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a2.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "B/b1.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "B/b2.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "C/c1.owncloud").isValid()); + + cleanup(); + } + + void testWithNormalSync() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto cleanup = [&]() { + completeSpy.clear(); + }; + cleanup(); + + // No effect sync + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + cleanup(); + + // Existing files are propagated just fine in both directions + fakeFolder.localModifier().appendByte("A/a1"); + fakeFolder.localModifier().insert("A/a3"); + fakeFolder.remoteModifier().appendByte("A/a2"); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + cleanup(); + + // New files on the remote create virtual files + fakeFolder.remoteModifier().insert("A/new"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/new")); + QVERIFY(fakeFolder.currentLocalState().find("A/new.owncloud")); + QVERIFY(fakeFolder.currentRemoteState().find("A/new")); + QVERIFY(itemInstruction(completeSpy, "A/new.owncloud", CSYNC_INSTRUCTION_NEW)); + QCOMPARE(dbRecord(fakeFolder, "A/new.owncloud")._type, ItemTypeVirtualFile); + cleanup(); + } + + void testVirtualFileDownload() + { + FakeFolder fakeFolder{ FileInfo() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto cleanup = [&]() { + completeSpy.clear(); + }; + cleanup(); + + auto triggerDownload = [&](const QByteArray &path) { + auto &journal = fakeFolder.syncJournal(); + SyncJournalFileRecord record; + journal.getFileRecord(path + ".owncloud", &record); + if (!record.isValid()) + return; + record._type = ItemTypeVirtualFileDownload; + journal.setFileRecord(record); + }; + + // Create a virtual file for remote files + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().insert("A/a1"); + fakeFolder.remoteModifier().insert("A/a2"); + fakeFolder.remoteModifier().insert("A/a3"); + fakeFolder.remoteModifier().insert("A/a4"); + fakeFolder.remoteModifier().insert("A/a5"); + fakeFolder.remoteModifier().insert("A/a6"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a3.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a4.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a5.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a6.owncloud")); + cleanup(); + + // Download by changing the db entry + triggerDownload("A/a1"); + triggerDownload("A/a2"); + triggerDownload("A/a3"); + triggerDownload("A/a4"); + triggerDownload("A/a5"); + triggerDownload("A/a6"); + fakeFolder.remoteModifier().appendByte("A/a2"); + fakeFolder.remoteModifier().remove("A/a3"); + fakeFolder.remoteModifier().rename("A/a4", "A/a4m"); + fakeFolder.localModifier().insert("A/a5"); + fakeFolder.localModifier().insert("A/a6"); + fakeFolder.localModifier().remove("A/a6.owncloud"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(itemInstruction(completeSpy, "A/a1", CSYNC_INSTRUCTION_NEW)); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_NONE)); + QVERIFY(itemInstruction(completeSpy, "A/a2", CSYNC_INSTRUCTION_NEW)); + QVERIFY(itemInstruction(completeSpy, "A/a2.owncloud", CSYNC_INSTRUCTION_NONE)); + QVERIFY(itemInstruction(completeSpy, "A/a3.owncloud", CSYNC_INSTRUCTION_REMOVE)); + QVERIFY(itemInstruction(completeSpy, "A/a4m", CSYNC_INSTRUCTION_NEW)); + QVERIFY(itemInstruction(completeSpy, "A/a4.owncloud", CSYNC_INSTRUCTION_REMOVE)); + QVERIFY(itemInstruction(completeSpy, "A/a5", CSYNC_INSTRUCTION_CONFLICT)); + QVERIFY(itemInstruction(completeSpy, "A/a5.owncloud", CSYNC_INSTRUCTION_NONE)); + QVERIFY(itemInstruction(completeSpy, "A/a6", CSYNC_INSTRUCTION_CONFLICT)); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(dbRecord(fakeFolder, "A/a1")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "A/a2")._type, ItemTypeFile); + QVERIFY(!dbRecord(fakeFolder, "A/a3").isValid()); + QCOMPARE(dbRecord(fakeFolder, "A/a4m")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "A/a5")._type, ItemTypeFile); + QCOMPARE(dbRecord(fakeFolder, "A/a6")._type, ItemTypeFile); + QVERIFY(!dbRecord(fakeFolder, "A/a1.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a2.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a3.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a4.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a5.owncloud").isValid()); + QVERIFY(!dbRecord(fakeFolder, "A/a6.owncloud").isValid()); + } + + void testVirtualFileDownloadResume() + { + FakeFolder fakeFolder{ FileInfo() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto cleanup = [&]() { + completeSpy.clear(); + fakeFolder.syncJournal().wipeErrorBlacklist(); + }; + cleanup(); + + auto triggerDownload = [&](const QByteArray &path) { + auto &journal = fakeFolder.syncJournal(); + SyncJournalFileRecord record; + journal.getFileRecord(path + ".owncloud", &record); + if (!record.isValid()) + return; + record._type = ItemTypeVirtualFileDownload; + journal.setFileRecord(record); + journal.avoidReadFromDbOnNextSync(record._path); + }; + + // Create a virtual file for remote files + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().insert("A/a1"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + cleanup(); + + // Download by changing the db entry + triggerDownload("A/a1"); + fakeFolder.serverErrorPaths().append("A/a1", 500); + QVERIFY(!fakeFolder.syncOnce()); + QVERIFY(itemInstruction(completeSpy, "A/a1", CSYNC_INSTRUCTION_NEW)); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_NONE)); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFileDownload); + QVERIFY(!dbRecord(fakeFolder, "A/a1").isValid()); + cleanup(); + + fakeFolder.serverErrorPaths().clear(); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(itemInstruction(completeSpy, "A/a1", CSYNC_INSTRUCTION_NEW)); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_NONE)); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QCOMPARE(dbRecord(fakeFolder, "A/a1")._type, ItemTypeFile); + QVERIFY(!dbRecord(fakeFolder, "A/a1.owncloud").isValid()); + } + + // Check what might happen if an older sync client encounters virtual files + void testOldVersion1() + { + FakeFolder fakeFolder{ FileInfo() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Create a virtual file + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().insert("A/a1"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + + // Simulate an old client by switching the type of all ItemTypeVirtualFile + // entries in the db to an invalid type. + auto &db = fakeFolder.syncJournal(); + SyncJournalFileRecord rec; + db.getFileRecord(QByteArray("A/a1.owncloud"), &rec); + QVERIFY(rec.isValid()); + QCOMPARE(rec._type, ItemTypeVirtualFile); + rec._type = static_cast<ItemType>(-1); + db.setFileRecord(rec); + + // Also switch off new files becoming virtual files + syncOptions._newFilesAreVirtual = false; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + + // A sync that doesn't do remote discovery has no effect + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QVERIFY(!fakeFolder.currentRemoteState().find("A/a1.owncloud")); + + // But with a remote discovery the virtual files will be removed and + // the remote files will be downloaded. + db.forceRemoteDiscoveryNextSync(); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1.owncloud")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + // Older versions may leave db entries for foo and foo.owncloud + void testOldVersion2() + { + FakeFolder fakeFolder{ FileInfo() }; + + // Sync a file + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().insert("A/a1"); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Create the virtual file too + // In the wild, the new version would create the virtual file and the db entry + // while the old version would download the plain file. + fakeFolder.localModifier().insert("A/a1.owncloud"); + auto &db = fakeFolder.syncJournal(); + SyncJournalFileRecord rec; + db.getFileRecord(QByteArray("A/a1"), &rec); + rec._type = ItemTypeVirtualFile; + rec._path = "A/a1.owncloud"; + db.setFileRecord(rec); + + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + + // Check that a sync removes the virtual file and its db entry + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1.owncloud")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QVERIFY(!dbRecord(fakeFolder, "A/a1.owncloud").isValid()); + } + + void testDownloadRecursive() + { + FakeFolder fakeFolder{ FileInfo() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Create a virtual file for remote files + fakeFolder.remoteModifier().mkdir("A"); + fakeFolder.remoteModifier().mkdir("A/Sub"); + fakeFolder.remoteModifier().mkdir("A/Sub/SubSub"); + fakeFolder.remoteModifier().mkdir("A/Sub2"); + fakeFolder.remoteModifier().mkdir("B"); + fakeFolder.remoteModifier().mkdir("B/Sub"); + fakeFolder.remoteModifier().insert("A/a1"); + fakeFolder.remoteModifier().insert("A/a2"); + fakeFolder.remoteModifier().insert("A/Sub/a3"); + fakeFolder.remoteModifier().insert("A/Sub/a4"); + fakeFolder.remoteModifier().insert("A/Sub/SubSub/a5"); + fakeFolder.remoteModifier().insert("A/Sub2/a6"); + fakeFolder.remoteModifier().insert("B/b1"); + fakeFolder.remoteModifier().insert("B/Sub/b2"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/a3.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/a4.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/SubSub/a5.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub2/a6.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/b1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/Sub/b2.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a2")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/a3")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/a4")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/SubSub/a5")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub2/a6")); + QVERIFY(!fakeFolder.currentLocalState().find("B/b1")); + QVERIFY(!fakeFolder.currentLocalState().find("B/Sub/b2")); + + + // Download All file in the directory A/Sub + // (as in Folder::downloadVirtualFile) + fakeFolder.syncJournal().markVirtualFileForDownloadRecursively("A/Sub"); + + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/a3.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/a4.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/SubSub/a5.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub2/a6.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/b1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/Sub/b2.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a2")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/a3")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/a4")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/SubSub/a5")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub2/a6")); + QVERIFY(!fakeFolder.currentLocalState().find("B/b1")); + QVERIFY(!fakeFolder.currentLocalState().find("B/Sub/b2")); + + // Add a file in a subfolder that was downloaded + // Currently, this continue to add it as a virtual file. + fakeFolder.remoteModifier().insert("A/Sub/SubSub/a7"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/SubSub/a7.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/SubSub/a7")); + + // Now download all files in "A" + fakeFolder.syncJournal().markVirtualFileForDownloadRecursively("A"); + QVERIFY(fakeFolder.syncOnce()); + QVERIFY(!fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/a3.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/a4.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/SubSub/a5.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub2/a6.owncloud")); + QVERIFY(!fakeFolder.currentLocalState().find("A/Sub/SubSub/a7.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/b1.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("B/Sub/b2.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a2")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/a3")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/a4")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/SubSub/a5")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub2/a6")); + QVERIFY(fakeFolder.currentLocalState().find("A/Sub/SubSub/a7")); + QVERIFY(!fakeFolder.currentLocalState().find("B/b1")); + QVERIFY(!fakeFolder.currentLocalState().find("B/Sub/b2")); + + // Now download remaining files in "B" + fakeFolder.syncJournal().markVirtualFileForDownloadRecursively("B"); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + void testRenameToVirtual() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + SyncOptions syncOptions; + syncOptions._newFilesAreVirtual = true; + fakeFolder.syncEngine().setSyncOptions(syncOptions); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &))); + + auto cleanup = [&]() { + completeSpy.clear(); + }; + cleanup(); + + // If a file is renamed to <name>.owncloud, it becomes virtual + fakeFolder.localModifier().rename("A/a1", "A/a1.owncloud"); + // If a file is renamed to <random>.owncloud, the file sticks around (to preserve user data) + fakeFolder.localModifier().rename("A/a2", "A/rand.owncloud"); + QVERIFY(fakeFolder.syncOnce()); + + QVERIFY(!fakeFolder.currentLocalState().find("A/a1")); + QVERIFY(fakeFolder.currentLocalState().find("A/a1.owncloud")); + QVERIFY(fakeFolder.currentRemoteState().find("A/a1")); + QVERIFY(itemInstruction(completeSpy, "A/a1.owncloud", CSYNC_INSTRUCTION_NEW)); + QCOMPARE(dbRecord(fakeFolder, "A/a1.owncloud")._type, ItemTypeVirtualFile); + + QVERIFY(!fakeFolder.currentLocalState().find("A/a2")); + QVERIFY(!fakeFolder.currentLocalState().find("A/a2.owncloud")); + QVERIFY(fakeFolder.currentLocalState().find("A/rand.owncloud")); + QVERIFY(!fakeFolder.currentRemoteState().find("A/a2")); + QVERIFY(itemInstruction(completeSpy, "A/a2", CSYNC_INSTRUCTION_REMOVE)); + QVERIFY(!dbRecord(fakeFolder, "A/rand.owncloud").isValid()); + + cleanup(); + } +}; + +QTEST_GUILESS_MAIN(TestSyncVirtualFiles) +#include "testsyncvirtualfiles.moc" diff --git a/test/testuploadreset.cpp b/test/testuploadreset.cpp index ba5489c3a..2250618e0 100644 --- a/test/testuploadreset.cpp +++ b/test/testuploadreset.cpp @@ -36,6 +36,7 @@ private slots: uploadInfo._transferid = 1; uploadInfo._valid = true; uploadInfo._modtime = Utility::qDateTimeToTime_t(modTime); + uploadInfo._size = size; fakeFolder.syncEngine().journal()->setUploadInfo("A/a0", uploadInfo); fakeFolder.uploadState().mkdir("1"); diff --git a/test/testutility.cpp b/test/testutility.cpp index c83b14199..20fb0a458 100644 --- a/test/testutility.cpp +++ b/test/testutility.cpp @@ -11,6 +11,10 @@ using namespace OCC::Utility; +namespace OCC { +OCSYNC_EXPORT extern bool fsCasePreserving_override; +} + class TestUtility : public QObject { Q_OBJECT @@ -150,12 +154,12 @@ private slots: void testFsCasePreserving() { - qputenv("OWNCLOUD_TEST_CASE_PRESERVING", "1"); + QVERIFY(isMac() || isWindows() ? fsCasePreserving() : ! fsCasePreserving()); + QScopedValueRollback<bool> scope(OCC::fsCasePreserving_override); + OCC::fsCasePreserving_override = 1; QVERIFY(fsCasePreserving()); - qputenv("OWNCLOUD_TEST_CASE_PRESERVING", "0"); + OCC::fsCasePreserving_override = 0; QVERIFY(! fsCasePreserving()); - qunsetenv("OWNCLOUD_TEST_CASE_PRESERVING"); - QVERIFY(isMac() || isWindows() ? fsCasePreserving() : ! fsCasePreserving()); } void testFileNamesEqual() @@ -178,16 +182,36 @@ private slots: QVERIFY(fileNamesEqual(a+"/test", b+"/test")); // both exist QVERIFY(fileNamesEqual(a+"/test/TESTI", b+"/test/../test/TESTI")); // both exist - qputenv("OWNCLOUD_TEST_CASE_PRESERVING", "1"); + QScopedValueRollback<bool> scope(OCC::fsCasePreserving_override, true); QVERIFY(fileNamesEqual(a+"/test", b+"/TEST")); // both exist QVERIFY(!fileNamesEqual(a+"/test", b+"/test/TESTI")); // both are different dir.remove(); - qunsetenv("OWNCLOUD_TEST_CASE_PRESERVING"); } + void testSanitizeForFileName_data() + { + QTest::addColumn<QString>("input"); + QTest::addColumn<QString>("output"); + + QTest::newRow("") + << "foobar" + << "foobar"; + QTest::newRow("") + << "a/b?c<d>e\\f:g*h|i\"j" + << "abcdefghij"; + QTest::newRow("") + << QString::fromLatin1("a\x01 b\x1f c\x80 d\x9f") + << "a b c d"; + } + void testSanitizeForFileName() + { + QFETCH(QString, input); + QFETCH(QString, output); + QCOMPARE(sanitizeForFileName(input), output); + } }; QTEST_GUILESS_MAIN(TestUtility) |