Files
RedBear-OS/local/recipes/kde/kf6-kio/source/autotests/dropjobtest.cpp
T
2026-04-14 10:51:06 +01:00

516 lines
22 KiB
C++

/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
#include <QDir>
#include <QMenu>
#include <QMimeData>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTest>
#include "jobuidelegatefactory.h"
#include "kiotesthelper.h"
#include "mockcoredelegateextensions.h"
#include <KConfigGroup>
#include <KDesktopFile>
#include <KFileItemListProperties>
#include <KIO/CopyJob>
#include <KIO/DeleteJob>
#include <KIO/DropJob>
#include <KIO/StatJob>
#include <KJobUiDelegate>
Q_DECLARE_METATYPE(Qt::KeyboardModifiers)
Q_DECLARE_METATYPE(Qt::DropAction)
Q_DECLARE_METATYPE(Qt::DropActions)
Q_DECLARE_METATYPE(KFileItemListProperties)
#ifndef Q_OS_WIN
void initLocale()
{
setenv("LC_ALL", "en_US.utf-8", 1);
}
Q_CONSTRUCTOR_FUNCTION(initLocale)
#endif
class JobSpy : public QObject
{
Q_OBJECT
public:
explicit JobSpy(KIO::Job *job)
: QObject(nullptr)
, m_spy(job, &KJob::result)
, m_error(0)
{
connect(job, &KJob::result, this, [this](KJob *job) {
m_error = job->error();
});
}
// like job->exec(), but with a timeout (to avoid being stuck with a popup grabbing mouse and keyboard...)
bool waitForResult()
{
// implementation taken from QTRY_COMPARE, to move the QVERIFY to the caller
if (m_spy.isEmpty()) {
QTest::qWait(0);
}
for (int i = 0; i < 5000 && m_spy.isEmpty(); i += 50) {
QTest::qWait(50);
}
return !m_spy.isEmpty();
}
int error() const
{
return m_error;
}
private:
QSignalSpy m_spy;
int m_error;
};
class DropJobTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
qputenv("KIOWORKER_ENABLE_TESTMODE", "1"); // ensure the KIO workers call QStandardPaths::setTestModeEnabled too
KIO::setDefaultJobUiDelegateFactory(nullptr);
KIO::setDefaultJobUiDelegateExtension(nullptr);
m_trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/Trash");
QDir(m_trashDir).removeRecursively();
QVERIFY(m_tempDir.isValid());
QVERIFY(m_nonWritableTempDir.isValid());
QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ExeOwner | QFile::ExeUser));
m_srcDir = m_tempDir.path();
m_srcFile = m_srcDir + "/srcfile";
m_srcLink = m_srcDir + "/link";
qRegisterMetaType<KIO::CopyJob *>();
}
void cleanupTestCase()
{
QVERIFY(QFile(m_nonWritableTempDir.path())
.setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::WriteOwner | QFile::WriteUser | QFile::ExeOwner | QFile::ExeUser));
}
// Before every test method, ensure the test file m_srcFile exists
void init()
{
if (QFile::exists(m_srcFile)) {
QVERIFY(QFileInfo(m_srcFile).isWritable());
} else {
QFile srcFile(m_srcFile);
QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
srcFile.write("Hello world\n");
}
#ifndef Q_OS_WIN
if (!QFile::exists(m_srcLink)) {
QVERIFY(QFile(m_srcFile).link(m_srcLink));
QVERIFY(QFileInfo(m_srcLink).isSymLink());
}
#endif
QVERIFY(QFileInfo(m_srcFile).isWritable());
m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(m_srcFile)});
}
void shouldDropToDesktopFile()
{
// Given an executable application desktop file and a source file
const QString desktopPath = m_srcDir + "/target.desktop";
KDesktopFile desktopFile(desktopPath);
KConfigGroup desktopGroup = desktopFile.desktopGroup();
desktopGroup.writeEntry("Type", "Application");
desktopGroup.writeEntry("StartupNotify", "false");
#ifdef Q_OS_WIN
desktopGroup.writeEntry("Exec", "copy.exe %f %d/dest");
#else
desktopGroup.writeEntry("Exec", "cp %f %d/dest");
#endif
desktopFile.sync();
QFile file(desktopPath);
file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser);
// When dropping the source file onto the desktop file
QUrl destUrl = QUrl::fromLocalFile(desktopPath);
QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
QSignalSpy spy(job, &KIO::DropJob::itemCreated);
// Then the application is run with the source file as argument
// (in this example, it copies the source file to "dest")
QVERIFY2(job->exec(), qPrintable(job->errorString()));
QCOMPARE(spy.count(), 0);
const QString dest = m_srcDir + "/dest";
QTRY_VERIFY(QFile::exists(dest));
QVERIFY(QFile::remove(desktopPath));
QVERIFY(QFile::remove(dest));
}
void shouldDropToDirectory_data()
{
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
QTest::addColumn<QString>("srcFile");
QTest::addColumn<QString>("dest"); // empty for a temp dir
QTest::addColumn<int>("expectedError");
QTest::addColumn<bool>("shouldSourceStillExist");
QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile << QString() << 0 << true;
QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile << QString() << 0 << false;
QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile << QString() << 0 << true;
QTest::newRow("DropOnItself") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcDir << m_srcDir << int(KIO::ERR_DROP_ON_ITSELF) << true;
QTest::newRow("DropDirOnFile") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcDir << m_srcFile << int(KIO::ERR_ACCESS_DENIED)
<< true;
QTest::newRow("NonWritableDest") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile << m_nonWritableTempDir.path()
<< int(KIO::ERR_WRITE_ACCESS_DENIED) << true;
}
void shouldDropToDirectory()
{
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(Qt::DropAction, dropAction);
QFETCH(QString, srcFile);
QFETCH(QString, dest);
QFETCH(int, expectedError);
QFETCH(bool, shouldSourceStillExist);
// Given a directory and a source file
QTemporaryDir tempDestDir;
QVERIFY(tempDestDir.isValid());
if (dest.isEmpty()) {
dest = tempDestDir.path();
}
// When dropping the source file onto the directory
const QUrl destUrl = QUrl::fromLocalFile(dest);
m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo | KIO::NoPrivilegeExecution);
JobSpy jobSpy(job);
QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
// Then the file is copied
QVERIFY(jobSpy.waitForResult());
QCOMPARE(jobSpy.error(), expectedError);
if (expectedError == 0) {
QCOMPARE(copyJobSpy.count(), 1);
const QString destFile = dest + "/srcfile";
QCOMPARE(itemCreatedSpy.count(), 1);
QCOMPARE(itemCreatedSpy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(destFile));
QVERIFY(QFile::exists(destFile));
QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
if (dropAction == Qt::LinkAction) {
QVERIFY(QFileInfo(destFile).isSymLink());
}
}
}
void shouldDropToTrash_data()
{
QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
QTest::addColumn<QString>("srcFile");
QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile;
QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile;
QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile;
QTest::newRow("NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile;
#ifndef Q_OS_WIN
QTest::newRow("Link_Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcLink;
QTest::newRow("Link_Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcLink;
QTest::newRow("Link_Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcLink;
QTest::newRow("Link_NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcLink;
#endif
}
void shouldDropToTrash()
{
// Given a source file
QFETCH(Qt::KeyboardModifiers, modifiers);
QFETCH(Qt::DropAction, dropAction);
QFETCH(QString, srcFile);
const bool isLink = QFileInfo(srcFile).isSymLink();
// When dropping it into the trash, with <modifiers> pressed
m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
// Then a confirmation dialog should appear
auto *uiDelegate = new KJobUiDelegate;
job->setUiDelegate(uiDelegate);
auto *askUserHandler = new MockAskUserInterface(uiDelegate);
askUserHandler->m_deleteResult = true;
// And the file should be moved to the trash, no matter what the modifiers are
QVERIFY2(job->exec(), qPrintable(job->errorString()));
QCOMPARE(askUserHandler->m_askUserDeleteCalled, 1);
QCOMPARE(copyJobSpy.count(), 1);
QCOMPARE(itemCreatedSpy.count(), 1);
const QUrl trashUrl = itemCreatedSpy.at(0).at(0).value<QUrl>();
QCOMPARE(trashUrl.scheme(), QString("trash"));
KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
QVERIFY(statJob->exec());
if (isLink) {
QVERIFY(statJob->statResult().isLink());
}
// clean up
KIO::DeleteJob *delJob = KIO::del(trashUrl, KIO::HideProgressInfo);
QVERIFY2(delJob->exec(), qPrintable(delJob->errorString()));
}
void shouldDropFromTrash()
{
// Given a file in the trash
const QFileInfo srcInfo(m_srcFile);
const QFile::Permissions origPerms = srcInfo.permissions();
QVERIFY(QFileInfo(m_srcFile).isWritable());
KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
QVERIFY(copyJob->exec());
const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
QVERIFY(trashUrl.isValid());
QVERIFY(!QFile::exists(m_srcFile));
// trashinfo file was created
const QString infoFile(m_trashDir + QStringLiteral("/info/") + srcInfo.fileName() + QStringLiteral(".trashinfo"));
QVERIFY(QFileInfo::exists(infoFile));
// When dropping the trashed file into a local dir, without modifiers
m_mimeData.setUrls(QList<QUrl>{trashUrl});
QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
KIO::DropJob *job = KIO::drop(&dropEvent, QUrl::fromLocalFile(m_srcDir), KIO::HideProgressInfo);
QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
QSignalSpy spy(job, &KIO::DropJob::itemCreated);
// Then the file should be moved, without a popup. No point in copying out of the trash, or linking to it.
QVERIFY2(job->exec(), qPrintable(job->errorString()));
QCOMPARE(copyJobSpy.count(), 1);
QCOMPARE(spy.count(), 1);
QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(m_srcFile));
QVERIFY(QFile::exists(m_srcFile));
QCOMPARE(int(QFileInfo(m_srcFile).permissions()), int(origPerms));
QVERIFY(QFileInfo(m_srcFile).isWritable());
KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
QVERIFY(!statJob->exec());
QVERIFY(QFileInfo(m_srcFile).isWritable());
// trashinfo file was removed
QVERIFY(!QFileInfo::exists(infoFile));
QVERIFY(QFileInfo(m_srcFile).isWritable());
}
void shouldDropTrashRootWithoutMovingAllTrashedFiles() // #319660
{
// Given some stuff in the trash
const QUrl trashUrl(QStringLiteral("trash:/"));
KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), trashUrl);
QVERIFY(copyJob->exec());
// and an empty destination directory
QTemporaryDir tempDestDir;
QVERIFY(tempDestDir.isValid());
const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
// When dropping a link / icon of the trash...
m_mimeData.setUrls(QList<QUrl>{trashUrl});
QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
QVERIFY2(job->exec(), qPrintable(job->errorString()));
// Then a full move shouldn't happen, just a link
QCOMPARE(copyJobSpy.count(), 1);
const QStringList items = QDir(tempDestDir.path()).entryList();
QVERIFY2(!items.contains("srcfile"), qPrintable(items.join(',')));
QVERIFY2(items.contains("trash:" + QChar(0x2044) + ".desktop"), qPrintable(items.join(',')));
}
void shouldDropFromTrashToTrash() // #378051
{
// Given a file in the trash
QVERIFY(QFileInfo(m_srcFile).isWritable());
KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
QVERIFY(copyJob->exec());
const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
QVERIFY(trashUrl.isValid());
QVERIFY(!QFile::exists(m_srcFile));
// When dropping the trashed file in the trash
m_mimeData.setUrls(QList<QUrl>{trashUrl});
QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
QSignalSpy spy(job, &KIO::DropJob::itemCreated);
// Then an error should be reported and no files action should occur
QVERIFY(!job->exec());
QCOMPARE(job->error(), KIO::ERR_DROP_ON_ITSELF);
}
void shouldDropToDirectoryWithPopup_data()
{
QTest::addColumn<QString>("dest"); // empty for a temp dir
QTest::addColumn<Qt::DropActions>("offeredActions");
QTest::addColumn<int>("triggerActionNumber");
QTest::addColumn<int>("expectedError");
QTest::addColumn<Qt::DropAction>("expectedDropAction");
QTest::addColumn<bool>("shouldSourceStillExist");
const Qt::DropActions threeActions = Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
const Qt::DropActions copyAndLink = Qt::CopyAction | Qt::LinkAction;
QTest::newRow("Move") << QString() << threeActions << 0 << 0 << Qt::MoveAction << false;
QTest::newRow("Copy") << QString() << threeActions << 1 << 0 << Qt::CopyAction << true;
QTest::newRow("Link") << QString() << threeActions << 2 << 0 << Qt::LinkAction << true;
QTest::newRow("SameDestCopy") << m_srcDir << copyAndLink << 0 << int(KIO::ERR_IDENTICAL_FILES) << Qt::CopyAction << true;
QTest::newRow("SameDestLink") << m_srcDir << copyAndLink << 1 << int(KIO::ERR_FILE_ALREADY_EXIST) << Qt::LinkAction << true;
}
void shouldDropToDirectoryWithPopup()
{
QFETCH(QString, dest);
QFETCH(Qt::DropActions, offeredActions);
QFETCH(int, triggerActionNumber);
QFETCH(int, expectedError);
QFETCH(Qt::DropAction, expectedDropAction);
QFETCH(bool, shouldSourceStillExist);
// Given a directory and a source file
QTemporaryDir tempDestDir;
QVERIFY(tempDestDir.isValid());
if (dest.isEmpty()) {
dest = tempDestDir.path();
}
QVERIFY(!findPopup());
// When dropping the source file onto the directory
QUrl destUrl = QUrl::fromLocalFile(dest);
QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
JobSpy jobSpy(job);
qRegisterMetaType<KFileItemListProperties>();
QSignalSpy spyShow(job, &KIO::DropJob::popupMenuAboutToShow);
QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
QVERIFY(spyShow.isValid());
// Then a popup should appear, with the expected available actions
QVERIFY(spyShow.wait());
QTRY_VERIFY(findPopup());
QMenu *popup = findPopup();
QCOMPARE(int(popupDropActions(popup)), int(offeredActions));
// And when selecting action number <triggerActionNumber>
QAction *action = popup->actions().at(triggerActionNumber);
QVERIFY(action);
QCOMPARE(int(action->data().value<Qt::DropAction>()), int(expectedDropAction));
const QRect actionGeom = popup->actionGeometry(action);
QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
// Then the job should finish, and the chosen action should happen.
QVERIFY(jobSpy.waitForResult());
QCOMPARE(jobSpy.error(), expectedError);
if (expectedError == 0) {
QCOMPARE(copyJobSpy.count(), 1);
const QString destFile = dest + "/srcfile";
QVERIFY(QFile::exists(destFile));
QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
if (expectedDropAction == Qt::LinkAction) {
QVERIFY(QFileInfo(destFile).isSymLink());
}
}
QTRY_VERIFY(!findPopup()); // flush deferred delete, so we don't get this popup again in findPopup
}
void shouldAddApplicationActionsToPopup()
{
// Given a directory and a source file
QTemporaryDir tempDestDir;
QVERIFY(tempDestDir.isValid());
const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
// When dropping the source file onto the directory
QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
QAction appAction1(QStringLiteral("action1"), this);
QAction appAction2(QStringLiteral("action2"), this);
QList<QAction *> appActions;
appActions << &appAction1 << &appAction2;
job->setApplicationActions(appActions);
JobSpy jobSpy(job);
// Then a popup should appear, with the expected available actions
QTRY_VERIFY(findPopup());
QMenu *popup = findPopup();
const QList<QAction *> actions = popup->actions();
QVERIFY(actions.contains(&appAction1));
QVERIFY(actions.contains(&appAction2));
QVERIFY(actions.at(actions.indexOf(&appAction1) - 1)->isSeparator());
QVERIFY(actions.at(actions.indexOf(&appAction2) + 1)->isSeparator());
// And when selecting action appAction1
const QRect actionGeom = popup->actionGeometry(&appAction1);
QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
// Then the menu should hide and the job terminate (without doing any copying)
QVERIFY(jobSpy.waitForResult());
QCOMPARE(jobSpy.error(), 0);
const QString destFile = tempDestDir.path() + "/srcfile";
QVERIFY(!QFile::exists(destFile));
}
private:
static QMenu *findPopup()
{
const QList<QWidget *> widgetsList = qApp->topLevelWidgets();
for (QWidget *widget : widgetsList) {
if (QMenu *menu = qobject_cast<QMenu *>(widget)) {
return menu;
}
}
return nullptr;
}
static Qt::DropActions popupDropActions(QMenu *menu)
{
Qt::DropActions actions;
const QList<QAction *> actionsList = menu->actions();
for (const QAction *action : actionsList) {
const QVariant userData = action->data();
if (userData.isValid()) {
actions |= userData.value<Qt::DropAction>();
}
}
return actions;
}
QMimeData m_mimeData; // contains m_srcFile
QTemporaryDir m_tempDir;
QString m_srcDir;
QString m_srcFile;
QString m_srcLink;
QTemporaryDir m_nonWritableTempDir;
QString m_trashDir;
};
QTEST_MAIN(DropJobTest)
#include "dropjobtest.moc"