desktop/test/testpermissions.cpp

960 строки
48 KiB
C++

/*
* 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 "syncenginetestutils.h"
#include <syncengine.h>
#include "common/ownsql.h"
#include <QtTest>
#if !defined(Q_OS_MACOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15
#include <filesystem>
#endif
#include <iostream>
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)
{
// The DB is opened in locked mode: close to allow us to access.
journal.close();
SqlDatabase db;
QVERIFY(db.openReadOnly(journal.databaseFilePath()));
SqlQuery q("SELECT count(*) from metadata where length(fileId) == 0", db);
QVERIFY(q.exec());
QVERIFY(q.next().hasData);
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
}
SyncFileItemPtr findDiscoveryItem(const SyncFileItemVector &spy, const QString &path)
{
for (const auto &item : spy) {
if (item->destination() == path)
return item;
}
return SyncFileItemPtr(new SyncFileItem);
}
bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
{
auto item = spy.findItem(path);
const auto checkHelper = [item, instr]() {
QCOMPARE(item->_instruction, instr);
};
checkHelper();
return item->_instruction == instr;
}
bool discoveryInstruction(const SyncFileItemVector &spy, const QString &path, const SyncInstructions instr)
{
auto item = findDiscoveryItem(spy, path);
const auto checkHelper = [item, instr]() {
QCOMPARE(item->_instruction, instr);
};
checkHelper();
return item->_instruction == instr;
}
class TestPermissions : public QObject
{
Q_OBJECT
private slots:
void initTestCase()
{
Logger::instance()->setLogFlush(true);
Logger::instance()->setLogDebug(true);
QStandardPaths::setTestModeEnabled(true);
}
#if !defined(Q_OS_MACOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15
void t7pl()
{
FakeFolder fakeFolder{ FileInfo() };
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
qDebug() << "aboutToRemoveRemnantsReadOnlyFolders called";
Q_UNUSED(folders);
Q_UNUSED(localPath);
callback(false);
});
// Some of this test depends on the order of discovery. With threading
// that order becomes effectively random, but we want to make sure to test
// all cases and thus disable threading.
auto syncOpts = fakeFolder.syncEngine().syncOptions();
syncOpts._parallelNetworkJobs = 1;
fakeFolder.syncEngine().setSyncOptions(syncOpts);
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");
const auto removeReadOnly = [&] (const QString &file) {
try {
const auto fileInfoToDelete = QFileInfo(fakeFolder.localPath() + file);
QFile(fakeFolder.localPath() + file).setPermissions(QFile::WriteOwner | QFile::ReadOwner);
const auto isReadOnly = !static_cast<bool>(std::filesystem::status(fileInfoToDelete.absolutePath().toStdWString()).permissions() & std::filesystem::perms::owner_write);
if (isReadOnly) {
std::filesystem::permissions(fileInfoToDelete.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
}
fakeFolder.localModifier().remove(file);
if (isReadOnly) {
std::filesystem::permissions(fileInfoToDelete.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::remove);
}
}
catch (const std::exception& e)
{
qWarning() << e.what();
}
};
const auto renameReadOnly = [&] (const QString &relativePath, const QString &relativeDestinationDirectory) {
try {
const auto sourceFileInfo = QFileInfo(fakeFolder.localPath() + relativePath);
const auto destinationFileInfo = QFileInfo(fakeFolder.localPath() + relativeDestinationDirectory);
const auto isSourceReadOnly = !static_cast<bool>(std::filesystem::status(sourceFileInfo.absolutePath().toStdWString()).permissions() & std::filesystem::perms::owner_write);
const auto isDestinationReadOnly = !static_cast<bool>(std::filesystem::status(destinationFileInfo.absolutePath().toStdWString()).permissions() & std::filesystem::perms::owner_write);
if (isSourceReadOnly) {
std::filesystem::permissions(sourceFileInfo.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
}
if (isDestinationReadOnly) {
std::filesystem::permissions(destinationFileInfo.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
}
fakeFolder.localModifier().rename(relativePath, relativeDestinationDirectory);
if (isSourceReadOnly) {
std::filesystem::permissions(sourceFileInfo.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::remove);
}
if (isDestinationReadOnly) {
std::filesystem::permissions(destinationFileInfo.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::remove);
}
}
catch (const std::exception& e)
{
qWarning() << e.what();
}
};
const auto insertReadOnly = [&] (const QString &file, const int fileSize) {
try {
const auto fileInfo = QFileInfo(fakeFolder.localPath() + file);
const auto isReadOnly = !static_cast<bool>(std::filesystem::status(fileInfo.absolutePath().toStdWString()).permissions() & std::filesystem::perms::owner_write);
if (isReadOnly) {
std::filesystem::permissions(fileInfo.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::add);
}
fakeFolder.localModifier().insert(file, fileSize);
if (isReadOnly) {
std::filesystem::permissions(fileInfo.absolutePath().toStdWString(), std::filesystem::perms::owner_write, std::filesystem::perm_options::remove);
}
}
catch (const std::exception& e)
{
qWarning() << e.what();
}
};
//1. remove the file than cannot be removed
// (they should be recovered)
fakeFolder.localModifier().remove("normalDirectory_PERM_CKDNV_/cannotBeRemoved_PERM_WVN_.data");
removeReadOnly("readonlyDirectory_PERM_M_/cannotBeRemoved_PERM_WVN_.data");
//2. remove the file that can be removed
// (they should properly be gone)
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"));
#ifdef Q_OS_WINDOWS
QEXPECT_FAIL("", "", Abort);
#endif
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());
removeReadOnly(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)
insertReadOnly("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.
removeReadOnly("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
removeReadOnly("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_"));
// the subdirectory had delete permissions, but, it was within the recovered directory, so must also get recovered
QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
applyPermissionsFromName(fakeFolder.remoteModifier());
QVERIFY(fakeFolder.syncOnce());
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
renameReadOnly("readonlyDirectory_PERM_M_/subdir_PERM_CK_", "normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_");
applyPermissionsFromName(fakeFolder.remoteModifier());
QVERIFY(fakeFolder.syncOnce());
currentLocalState = fakeFolder.currentLocalState();
// old name restored
QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_"));
// contents moved (had move permissions)
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());
// restore for further tests
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");
applyPermissionsFromName(fakeFolder.remoteModifier());
QVERIFY(fakeFolder.syncOnce());
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());
assertCsyncJournalOk(fakeFolder.syncJournal());
QVERIFY(fakeFolder.currentLocalState().find("readonlyDirectory_PERM_M_/subdir_PERM_CK_/subsubdir_PERM_CKDNV_/normalFile_PERM_WVND_.data" ));
//1. rename a directory in a read only folder
//Missing directory should be restored
//new directory should stay but not be uploaded
renameReadOnly("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)
renameReadOnly("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_", "readonlyDirectory_PERM_M_/moved_PERM_CK_" );
// error: can't upload to readonly!
QVERIFY(!fakeFolder.syncOnce());
currentLocalState = fakeFolder.currentLocalState();
//1.
// old name restored
QVERIFY(currentLocalState.find("readonlyDirectory_PERM_M_/subdir_PERM_CK_" ));
// including contents
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 locally for the future comparison
removeReadOnly("readonlyDirectory_PERM_M_/newname_PERM_CK_");
//2.
// old removed
QVERIFY(!currentLocalState.find("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_"));
// but still on the server: the rename causing an error meant the deletes didn't execute
QVERIFY(fakeFolder.currentRemoteState().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
removeReadOnly("readonlyDirectory_PERM_M_/moved_PERM_CK_");
fakeFolder.remoteModifier().remove("normalDirectory_PERM_CKDNV_/subdir_PERM_CKDNV_");
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
//######################################################################
qInfo( "multiple restores of a file create different conflict files" );
fakeFolder.remoteModifier().insert("readonlyDirectory_PERM_M_/cannotBeModified_PERM_DVN_.data");
applyPermissionsFromName(fakeFolder.remoteModifier());
QVERIFY(fakeFolder.syncOnce());
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'));
removeReadOnly(i->path());
currentLocalState = fakeFolder.currentLocalState();
count++;
}
QCOMPARE(count, 2);
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
#endif
static void setAllPerm(FileInfo *fi, OCC::RemotePermissions perm)
{
fi->permissions = perm;
for (auto &subFi : fi->children)
setAllPerm(&subFi, perm);
}
// What happens if the source can't be moved or the target can't be created?
void testForbiddenMoves()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
Q_UNUSED(folders)
Q_UNUSED(localPath)
callback(false);
});
// Some of this test depends on the order of discovery. With threading
// that order becomes effectively random, but we want to make sure to test
// all cases and thus disable threading.
auto syncOpts = fakeFolder.syncEngine().syncOptions();
syncOpts._parallelNetworkJobs = 1;
fakeFolder.syncEngine().setSyncOptions(syncOpts);
auto &lm = fakeFolder.localModifier();
auto &rm = fakeFolder.remoteModifier();
rm.mkdir("allowed");
rm.mkdir("norename");
rm.mkdir("nomove");
rm.mkdir("nocreatefile");
rm.mkdir("nocreatedir");
rm.mkdir("zallowed"); // order of discovery matters
rm.mkdir("allowed/sub");
rm.mkdir("allowed/sub2");
rm.insert("allowed/file");
rm.insert("allowed/sub/file");
rm.insert("allowed/sub2/file");
rm.mkdir("norename/sub");
rm.insert("norename/file");
rm.insert("norename/sub/file");
rm.mkdir("nomove/sub");
rm.insert("nomove/file");
rm.insert("nomove/sub/file");
rm.mkdir("zallowed/sub");
rm.mkdir("zallowed/sub2");
rm.insert("zallowed/file");
rm.insert("zallowed/sub/file");
rm.insert("zallowed/sub2/file");
setAllPerm(rm.find("norename"), RemotePermissions::fromServerString("WDVCK"));
setAllPerm(rm.find("nomove"), RemotePermissions::fromServerString("WDNCK"));
setAllPerm(rm.find("nocreatefile"), RemotePermissions::fromServerString("WDNVK"));
setAllPerm(rm.find("nocreatedir"), RemotePermissions::fromServerString("WDNVC"));
QVERIFY(fakeFolder.syncOnce());
// Renaming errors
lm.rename("norename/file", "norename/file_renamed");
lm.rename("norename/sub", "norename/sub_renamed");
// Moving errors
lm.rename("nomove/file", "allowed/file_moved");
lm.rename("nomove/sub", "allowed/sub_moved");
// Createfile errors
lm.rename("allowed/file", "nocreatefile/file");
lm.rename("zallowed/file", "nocreatefile/zfile");
lm.rename("allowed/sub", "nocreatefile/sub"); // TODO: probably forbidden because it contains file children?
// Createdir errors
lm.rename("allowed/sub2", "nocreatedir/sub2");
lm.rename("zallowed/sub2", "nocreatedir/zsub2");
// also hook into discovery!!
SyncFileItemVector discovery;
connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, this, [&discovery](auto v) { discovery = v; });
ItemCompletedSpy completeSpy(fakeFolder);
QVERIFY(!fakeFolder.syncOnce());
// if renaming doesn't work, just delete+create
QVERIFY(itemInstruction(completeSpy, "norename/file", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemInstruction(completeSpy, "norename/sub", CSYNC_INSTRUCTION_NONE));
QVERIFY(discoveryInstruction(discovery, "norename/sub", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemInstruction(completeSpy, "norename/file_renamed", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "norename/sub_renamed", CSYNC_INSTRUCTION_NEW));
// the contents can _move_
QVERIFY(itemInstruction(completeSpy, "norename/sub_renamed/file", CSYNC_INSTRUCTION_RENAME));
// simiilarly forbidding moves becomes delete+create
QVERIFY(itemInstruction(completeSpy, "nomove/file", CSYNC_INSTRUCTION_REMOVE));
QVERIFY(itemInstruction(completeSpy, "nomove/sub", CSYNC_INSTRUCTION_NONE));
QVERIFY(discoveryInstruction(discovery, "nomove/sub", CSYNC_INSTRUCTION_REMOVE));
// nomove/sub/file is removed as part of the dir
QVERIFY(itemInstruction(completeSpy, "allowed/file_moved", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "allowed/sub_moved", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "allowed/sub_moved/file", CSYNC_INSTRUCTION_NEW));
// when moving to an invalid target, the targets should be an error
QVERIFY(itemInstruction(completeSpy, "nocreatefile/file", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "nocreatefile/zfile", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "nocreatefile/sub", CSYNC_INSTRUCTION_RENAME)); // TODO: What does a real server say?
QVERIFY(itemInstruction(completeSpy, "nocreatedir/sub2", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "nocreatedir/zsub2", CSYNC_INSTRUCTION_ERROR));
// and the sources of the invalid moves should be restored, not deleted
// (depending on the order of discovery a follow-up sync is needed)
QVERIFY(itemInstruction(completeSpy, "allowed/file", CSYNC_INSTRUCTION_NONE));
QVERIFY(itemInstruction(completeSpy, "allowed/sub2", CSYNC_INSTRUCTION_NONE));
QVERIFY(itemInstruction(completeSpy, "zallowed/file", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "zallowed/sub2", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "zallowed/sub2/file", CSYNC_INSTRUCTION_NEW));
QCOMPARE(fakeFolder.syncEngine().isAnotherSyncNeeded(), ImmediateFollowUp);
// A follow-up sync will restore allowed/file and allowed/sub2 and maintain the nocreatedir/file errors
completeSpy.clear();
QVERIFY(!fakeFolder.syncOnce());
QVERIFY(itemInstruction(completeSpy, "nocreatefile/file", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "nocreatefile/zfile", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "nocreatedir/sub2", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "nocreatedir/zsub2", CSYNC_INSTRUCTION_ERROR));
QVERIFY(itemInstruction(completeSpy, "allowed/file", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "allowed/sub2", CSYNC_INSTRUCTION_NEW));
QVERIFY(itemInstruction(completeSpy, "allowed/sub2/file", CSYNC_INSTRUCTION_NEW));
auto cls = fakeFolder.currentLocalState();
QVERIFY(cls.find("allowed/file"));
QVERIFY(cls.find("allowed/sub2"));
QVERIFY(cls.find("zallowed/file"));
QVERIFY(cls.find("zallowed/sub2"));
QVERIFY(cls.find("zallowed/sub2/file"));
}
void testParentMoveNotAllowedChildrenRestored()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &lm = fakeFolder.localModifier();
auto &rm = fakeFolder.remoteModifier();
rm.mkdir("forbidden-move");
rm.mkdir("forbidden-move/sub1");
rm.insert("forbidden-move/sub1/file1.txt", 100);
rm.mkdir("forbidden-move/sub2");
rm.insert("forbidden-move/sub2/file2.txt", 100);
rm.find("forbidden-move")->permissions = RemotePermissions::fromServerString("WNCK");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
lm.rename("forbidden-move", "forbidden-move-new");
QVERIFY(fakeFolder.syncOnce());
// verify that original folder did not get wiped (files are still there)
QVERIFY(fakeFolder.currentRemoteState().find("forbidden-move/sub1/file1.txt"));
QVERIFY(fakeFolder.currentRemoteState().find("forbidden-move/sub2/file2.txt"));
// verify that the duplicate folder has been created when trying to rename a folder that has its move permissions forbidden
QVERIFY(fakeFolder.currentRemoteState().find("forbidden-move-new/sub1/file1.txt"));
QVERIFY(fakeFolder.currentRemoteState().find("forbidden-move-new/sub2/file2.txt"));
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
#if !defined(Q_OS_MACOS) || __MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_15
static void demo_perms(std::filesystem::perms p)
{
using std::filesystem::perms;
auto show = [=](char op, perms perm)
{
std::cout << (perms::none == (perm & p) ? '-' : op);
};
show('r', perms::owner_read);
show('w', perms::owner_write);
show('x', perms::owner_exec);
show('r', perms::group_read);
show('w', perms::group_write);
show('x', perms::group_exec);
show('r', perms::others_read);
show('w', perms::others_write);
show('x', perms::others_exec);
std::cout << std::endl;
}
void testReadOnlyFolderIsReallyReadOnly()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("readOnlyFolder");
remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto folderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/readOnlyFolder")).toStdWString());
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_read);
}
void testReadWriteFolderIsReallyReadWrite()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("readWriteFolder");
remote.find("readWriteFolder")->permissions = RemotePermissions::fromServerString("CKWDNVRSM");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto folderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/readWriteFolder")).toStdWString());
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_write);
}
void testChangePermissionsFolder()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("testFolder");
remote.find("testFolder")->permissions = RemotePermissions::fromServerString("M");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto folderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder")).toStdWString());
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(!static_cast<bool>(folderStatus.permissions() & std::filesystem::perms::owner_write));
remote.find("testFolder")->permissions = RemotePermissions::fromServerString("CKWDNVRSM");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
folderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder")).toStdWString());
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_write);
remote.find("testFolder")->permissions = RemotePermissions::fromServerString("M");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
folderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder")).toStdWString());
QVERIFY(folderStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(!static_cast<bool>(folderStatus.permissions() & std::filesystem::perms::owner_write));
}
void testChangePermissionsForFolderHierarchy()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("testFolder");
remote.find("testFolder")->permissions = RemotePermissions::fromServerString("M");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
remote.mkdir("testFolder/subFolderReadWrite");
remote.mkdir("testFolder/subFolderReadOnly");
remote.find("testFolder/subFolderReadOnly")->permissions = RemotePermissions::fromServerString("m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
auto testFolderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder")).toStdWString());
QVERIFY(testFolderStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(!static_cast<bool>(testFolderStatus.permissions() & std::filesystem::perms::owner_write));
auto subFolderReadWriteStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder/subFolderReadWrite")).toStdWString());
QVERIFY(subFolderReadWriteStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(subFolderReadWriteStatus.permissions() & std::filesystem::perms::owner_write);
auto subFolderReadOnlyStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder/subFolderReadOnly")).toStdWString());
QVERIFY(subFolderReadOnlyStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(!static_cast<bool>(subFolderReadOnlyStatus.permissions() & std::filesystem::perms::owner_write));
remote.find("testFolder/subFolderReadOnly")->permissions = RemotePermissions::fromServerString("CKWDNVRSm");
remote.find("testFolder/subFolderReadWrite")->permissions = RemotePermissions::fromServerString("m");
remote.mkdir("testFolder/newSubFolder");
remote.create("testFolder/testFile", 12, '9');
remote.create("testFolder/testReadOnlyFile", 13, '8');
remote.find("testFolder/testReadOnlyFile")->permissions = RemotePermissions::fromServerString("m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
subFolderReadWriteStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder/subFolderReadWrite")).toStdWString());
QVERIFY(subFolderReadWriteStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(!static_cast<bool>(subFolderReadWriteStatus.permissions() & std::filesystem::perms::owner_write));
subFolderReadOnlyStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder/subFolderReadOnly")).toStdWString());
QVERIFY(subFolderReadOnlyStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(subFolderReadOnlyStatus.permissions() & std::filesystem::perms::owner_write);
remote.rename("testFolder/subFolderReadOnly", "testFolder/subFolderReadWriteNew");
remote.rename("testFolder/subFolderReadWrite", "testFolder/subFolderReadOnlyNew");
remote.rename("testFolder/testFile", "testFolder/testFileNew");
remote.rename("testFolder/testReadOnlyFile", "testFolder/testReadOnlyFileNew");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
testFolderStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + QStringLiteral("/testFolder")).toStdWString());
QVERIFY(testFolderStatus.permissions() & std::filesystem::perms::owner_read);
QVERIFY(!static_cast<bool>(testFolderStatus.permissions() & std::filesystem::perms::owner_write));
}
void testDeleteChildItemsInReadOnlyFolder()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("readOnlyFolder");
remote.mkdir("readOnlyFolder/test");
remote.insert("readOnlyFolder/readOnlyFile.txt");
remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
remote.remove("readOnlyFolder/test");
remote.remove("readOnlyFolder/readOnlyFile.txt");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto ensureReadOnlyItem = [&fakeFolder] (const auto path) -> bool {
const auto itemStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + path).toStdWString());
return static_cast<bool>(itemStatus.permissions() & std::filesystem::perms::owner_read);
};
QVERIFY(ensureReadOnlyItem("/readOnlyFolder"));
}
void testRenameChildItemsInReadOnlyFolder()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("readOnlyFolder");
remote.mkdir("readOnlyFolder/test");
remote.insert("readOnlyFolder/readOnlyFile.txt");
remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
remote.rename("readOnlyFolder/test", "readOnlyFolder/testRename");
remote.rename("readOnlyFolder/readOnlyFile.txt", "readOnlyFolder/readOnlyFileRename.txt");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto ensureReadOnlyItem = [&fakeFolder] (const auto path) -> bool {
const auto itemStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + path).toStdWString());
return static_cast<bool>(itemStatus.permissions() & std::filesystem::perms::owner_read);
};
QVERIFY(ensureReadOnlyItem("/readOnlyFolder"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/testRename"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/readOnlyFileRename.txt"));
}
void testMoveChildItemsInReadOnlyFolder()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("readOnlyFolder");
remote.mkdir("readOnlyFolder/child");
remote.mkdir("readOnlyFolder/test");
remote.insert("readOnlyFolder/readOnlyFile.txt");
remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
remote.find("readOnlyFolder/child")->permissions = RemotePermissions::fromServerString("m");
remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
remote.rename("readOnlyFolder/test", "readOnlyFolder/child/test");
remote.rename("readOnlyFolder/readOnlyFile.txt", "readOnlyFolder/child/readOnlyFile.txt");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto ensureReadOnlyItem = [&fakeFolder] (const auto path) -> bool {
const auto itemStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + path).toStdWString());
return static_cast<bool>(itemStatus.permissions() & std::filesystem::perms::owner_read);
};
QVERIFY(ensureReadOnlyItem("/readOnlyFolder"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/child"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/child/test"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/child/readOnlyFile.txt"));
}
void testModifyChildItemsInReadOnlyFolder()
{
FakeFolder fakeFolder{FileInfo{}};
QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToRemoveRemnantsReadOnlyFolders,
[&](const QList<SyncFileItemPtr> &folders, const QString &localPath, std::function<void(bool)> callback) {
for(const auto &oneFolder : folders) {
FileSystem::removeRecursively(localPath + oneFolder->_file);
}
callback(false);
});
auto &remote = fakeFolder.remoteModifier();
remote.mkdir("readOnlyFolder");
remote.mkdir("readOnlyFolder/test");
remote.insert("readOnlyFolder/readOnlyFile.txt");
remote.find("readOnlyFolder")->permissions = RemotePermissions::fromServerString("M");
remote.find("readOnlyFolder/test")->permissions = RemotePermissions::fromServerString("m");
remote.find("readOnlyFolder/readOnlyFile.txt")->permissions = RemotePermissions::fromServerString("m");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
remote.insert("readOnlyFolder/test/newFile.txt");
remote.find("readOnlyFolder/test/newFile.txt")->permissions = RemotePermissions::fromServerString("m");
remote.mkdir("readOnlyFolder/test/newFolder");
remote.find("readOnlyFolder/test/newFolder")->permissions = RemotePermissions::fromServerString("m");
remote.appendByte("readOnlyFolder/readOnlyFile.txt");
QVERIFY(fakeFolder.syncOnce());
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
const auto ensureReadOnlyItem = [&fakeFolder] (const auto path) -> bool {
const auto itemStatus = std::filesystem::status(static_cast<QString>(fakeFolder.localPath() + path).toStdWString());
return static_cast<bool>(itemStatus.permissions() & std::filesystem::perms::owner_read);
};
QVERIFY(ensureReadOnlyItem("/readOnlyFolder"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/test"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/readOnlyFile.txt"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/test/newFile.txt"));
QVERIFY(ensureReadOnlyItem("/readOnlyFolder/newFolder"));
}
#endif
};
QTEST_GUILESS_MAIN(TestPermissions)
#include "testpermissions.moc"