Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/owncloud/client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorOlivier Goffart <ogoffart@woboq.com>2018-09-14 16:25:59 +0300
committerOlivier Goffart <ogoffart@woboq.com>2018-09-14 16:25:59 +0300
commit0290564d5c654d654856f04f1b04bad984f36aee (patch)
tree3b9a5eda39861d9467bc130a00e7effbc4ef7b6c /test
parent638f5c8752164ccc0429b1df60b79c3d3293bcd4 (diff)
parent3a335879fa1f00f094d82855126fec3f6ab59405 (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')
-rw-r--r--test/CMakeLists.txt9
-rw-r--r--test/csync/CMakeLists.txt4
-rw-r--r--test/csync/csync_tests/check_csync_exclude.cpp34
-rw-r--r--test/csync/csync_tests/check_csync_update.cpp11
-rwxr-xr-xtest/scripts/txpl/t7.pl291
-rw-r--r--test/syncenginetestutils.h59
-rw-r--r--test/testallfilesdeleted.cpp102
-rw-r--r--test/testblacklist.cpp193
-rw-r--r--test/testchecksumvalidator.cpp68
-rw-r--r--test/testchunkingng.cpp197
-rw-r--r--test/testdownload.cpp125
-rw-r--r--test/testexcludedfiles.cpp1
-rw-r--r--test/testfilesystem.cpp76
-rw-r--r--test/testfolderman.cpp15
-rw-r--r--test/testfolderwatcher.cpp3
-rw-r--r--test/testlocaldiscovery.cpp155
-rw-r--r--test/testoauth.cpp3
-rw-r--r--test/testownsql.cpp15
-rw-r--r--test/testpermissions.cpp292
-rw-r--r--test/testremotediscovery.cpp128
-rw-r--r--test/testsyncconflict.cpp97
-rw-r--r--test/testsyncengine.cpp47
-rw-r--r--test/testsyncjournaldb.cpp134
-rw-r--r--test/testsyncmove.cpp85
-rw-r--r--test/testsyncvirtualfiles.cpp631
-rw-r--r--test/testuploadreset.cpp1
-rw-r--r--test/testutility.cpp36
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)