File indexing completed on 2024-04-21 03:54:46

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include <QDir>
0009 #include <QMenu>
0010 #include <QMimeData>
0011 #include <QSignalSpy>
0012 #include <QStandardPaths>
0013 #include <QTemporaryDir>
0014 #include <QTest>
0015 
0016 #include "jobuidelegatefactory.h"
0017 #include "kiotesthelper.h"
0018 #include "mockcoredelegateextensions.h"
0019 #include <KConfigGroup>
0020 #include <KDesktopFile>
0021 #include <KFileItemListProperties>
0022 #include <KIO/CopyJob>
0023 #include <KIO/DeleteJob>
0024 #include <KIO/DropJob>
0025 #include <KIO/StatJob>
0026 #include <KJobUiDelegate>
0027 
0028 Q_DECLARE_METATYPE(Qt::KeyboardModifiers)
0029 Q_DECLARE_METATYPE(Qt::DropAction)
0030 Q_DECLARE_METATYPE(Qt::DropActions)
0031 Q_DECLARE_METATYPE(KFileItemListProperties)
0032 
0033 #ifndef Q_OS_WIN
0034 void initLocale()
0035 {
0036     setenv("LC_ALL", "en_US.utf-8", 1);
0037 }
0038 Q_CONSTRUCTOR_FUNCTION(initLocale)
0039 #endif
0040 
0041 class JobSpy : public QObject
0042 {
0043     Q_OBJECT
0044 public:
0045     explicit JobSpy(KIO::Job *job)
0046         : QObject(nullptr)
0047         , m_spy(job, &KJob::result)
0048         , m_error(0)
0049     {
0050         connect(job, &KJob::result, this, [this](KJob *job) {
0051             m_error = job->error();
0052         });
0053     }
0054     // like job->exec(), but with a timeout (to avoid being stuck with a popup grabbing mouse and keyboard...)
0055     bool waitForResult()
0056     {
0057         // implementation taken from QTRY_COMPARE, to move the QVERIFY to the caller
0058         if (m_spy.isEmpty()) {
0059             QTest::qWait(0);
0060         }
0061         for (int i = 0; i < 5000 && m_spy.isEmpty(); i += 50) {
0062             QTest::qWait(50);
0063         }
0064         return !m_spy.isEmpty();
0065     }
0066     int error() const
0067     {
0068         return m_error;
0069     }
0070 
0071 private:
0072     QSignalSpy m_spy;
0073     int m_error;
0074 };
0075 
0076 class DropJobTest : public QObject
0077 {
0078     Q_OBJECT
0079 
0080 private Q_SLOTS:
0081     void initTestCase()
0082     {
0083         QStandardPaths::setTestModeEnabled(true);
0084         qputenv("KIOWORKER_ENABLE_TESTMODE", "1"); // ensure the KIO workers call QStandardPaths::setTestModeEnabled too
0085 
0086         KIO::setDefaultJobUiDelegateFactory(nullptr);
0087         KIO::setDefaultJobUiDelegateExtension(nullptr);
0088 
0089         m_trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/Trash");
0090         QDir(m_trashDir).removeRecursively();
0091 
0092         QVERIFY(m_tempDir.isValid());
0093         QVERIFY(m_nonWritableTempDir.isValid());
0094         QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ExeOwner | QFile::ExeUser));
0095         m_srcDir = m_tempDir.path();
0096 
0097         m_srcFile = m_srcDir + "/srcfile";
0098         m_srcLink = m_srcDir + "/link";
0099 
0100         qRegisterMetaType<KIO::CopyJob *>();
0101     }
0102 
0103     void cleanupTestCase()
0104     {
0105         QVERIFY(QFile(m_nonWritableTempDir.path())
0106                     .setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::WriteOwner | QFile::WriteUser | QFile::ExeOwner | QFile::ExeUser));
0107     }
0108 
0109     // Before every test method, ensure the test file m_srcFile exists
0110     void init()
0111     {
0112         if (QFile::exists(m_srcFile)) {
0113             QVERIFY(QFileInfo(m_srcFile).isWritable());
0114         } else {
0115             QFile srcFile(m_srcFile);
0116             QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
0117             srcFile.write("Hello world\n");
0118         }
0119 #ifndef Q_OS_WIN
0120         if (!QFile::exists(m_srcLink)) {
0121             QVERIFY(QFile(m_srcFile).link(m_srcLink));
0122             QVERIFY(QFileInfo(m_srcLink).isSymLink());
0123         }
0124 #endif
0125         QVERIFY(QFileInfo(m_srcFile).isWritable());
0126         m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(m_srcFile)});
0127     }
0128 
0129     void shouldDropToDesktopFile()
0130     {
0131         // Given an executable application desktop file and a source file
0132         const QString desktopPath = m_srcDir + "/target.desktop";
0133         KDesktopFile desktopFile(desktopPath);
0134         KConfigGroup desktopGroup = desktopFile.desktopGroup();
0135         desktopGroup.writeEntry("Type", "Application");
0136         desktopGroup.writeEntry("StartupNotify", "false");
0137 #ifdef Q_OS_WIN
0138         desktopGroup.writeEntry("Exec", "copy.exe %f %d/dest");
0139 #else
0140         desktopGroup.writeEntry("Exec", "cp %f %d/dest");
0141 #endif
0142         desktopFile.sync();
0143         QFile file(desktopPath);
0144         file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser);
0145 
0146         // When dropping the source file onto the desktop file
0147         QUrl destUrl = QUrl::fromLocalFile(desktopPath);
0148         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
0149         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
0150         QSignalSpy spy(job, &KIO::DropJob::itemCreated);
0151 
0152         // Then the application is run with the source file as argument
0153         // (in this example, it copies the source file to "dest")
0154         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0155         QCOMPARE(spy.count(), 0);
0156         const QString dest = m_srcDir + "/dest";
0157         QTRY_VERIFY(QFile::exists(dest));
0158 
0159         QVERIFY(QFile::remove(desktopPath));
0160         QVERIFY(QFile::remove(dest));
0161     }
0162 
0163     void shouldDropToDirectory_data()
0164     {
0165         QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
0166         QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
0167         QTest::addColumn<QString>("srcFile");
0168         QTest::addColumn<QString>("dest"); // empty for a temp dir
0169         QTest::addColumn<int>("expectedError");
0170         QTest::addColumn<bool>("shouldSourceStillExist");
0171 
0172         QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile << QString() << 0 << true;
0173         QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile << QString() << 0 << false;
0174         QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile << QString() << 0 << true;
0175         QTest::newRow("DropOnItself") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcDir << m_srcDir << int(KIO::ERR_DROP_ON_ITSELF) << true;
0176         QTest::newRow("DropDirOnFile") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcDir << m_srcFile << int(KIO::ERR_ACCESS_DENIED)
0177                                        << true;
0178         QTest::newRow("NonWritableDest") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile << m_nonWritableTempDir.path()
0179                                          << int(KIO::ERR_WRITE_ACCESS_DENIED) << true;
0180     }
0181 
0182     void shouldDropToDirectory()
0183     {
0184         QFETCH(Qt::KeyboardModifiers, modifiers);
0185         QFETCH(Qt::DropAction, dropAction);
0186         QFETCH(QString, srcFile);
0187         QFETCH(QString, dest);
0188         QFETCH(int, expectedError);
0189         QFETCH(bool, shouldSourceStillExist);
0190 
0191         // Given a directory and a source file
0192         QTemporaryDir tempDestDir;
0193         QVERIFY(tempDestDir.isValid());
0194         if (dest.isEmpty()) {
0195             dest = tempDestDir.path();
0196         }
0197 
0198         // When dropping the source file onto the directory
0199         const QUrl destUrl = QUrl::fromLocalFile(dest);
0200         m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
0201         QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
0202         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo | KIO::NoPrivilegeExecution);
0203         JobSpy jobSpy(job);
0204         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
0205         QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
0206 
0207         // Then the file is copied
0208         QVERIFY(jobSpy.waitForResult());
0209         QCOMPARE(jobSpy.error(), expectedError);
0210         if (expectedError == 0) {
0211             QCOMPARE(copyJobSpy.count(), 1);
0212             const QString destFile = dest + "/srcfile";
0213             QCOMPARE(itemCreatedSpy.count(), 1);
0214             QCOMPARE(itemCreatedSpy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(destFile));
0215             QVERIFY(QFile::exists(destFile));
0216             QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
0217             if (dropAction == Qt::LinkAction) {
0218                 QVERIFY(QFileInfo(destFile).isSymLink());
0219             }
0220         }
0221     }
0222 
0223     void shouldDropToTrash_data()
0224     {
0225         QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
0226         QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
0227         QTest::addColumn<QString>("srcFile");
0228 
0229         QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile;
0230         QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile;
0231         QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile;
0232         QTest::newRow("NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile;
0233 #ifndef Q_OS_WIN
0234         QTest::newRow("Link_Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcLink;
0235         QTest::newRow("Link_Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcLink;
0236         QTest::newRow("Link_Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcLink;
0237         QTest::newRow("Link_NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcLink;
0238 #endif
0239     }
0240 
0241     void shouldDropToTrash()
0242     {
0243         // Given a source file
0244         QFETCH(Qt::KeyboardModifiers, modifiers);
0245         QFETCH(Qt::DropAction, dropAction);
0246         QFETCH(QString, srcFile);
0247         const bool isLink = QFileInfo(srcFile).isSymLink();
0248 
0249         // When dropping it into the trash, with <modifiers> pressed
0250         m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
0251         QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
0252         KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0253         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
0254         QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
0255 
0256         // Then a confirmation dialog should appear
0257         auto *uiDelegate = new KJobUiDelegate;
0258         job->setUiDelegate(uiDelegate);
0259         auto *askUserHandler = new MockAskUserInterface(uiDelegate);
0260         askUserHandler->m_deleteResult = true;
0261 
0262         // And the file should be moved to the trash, no matter what the modifiers are
0263         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0264         QCOMPARE(askUserHandler->m_askUserDeleteCalled, 1);
0265         QCOMPARE(copyJobSpy.count(), 1);
0266         QCOMPARE(itemCreatedSpy.count(), 1);
0267         const QUrl trashUrl = itemCreatedSpy.at(0).at(0).value<QUrl>();
0268         QCOMPARE(trashUrl.scheme(), QString("trash"));
0269         KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
0270         QVERIFY(statJob->exec());
0271         if (isLink) {
0272             QVERIFY(statJob->statResult().isLink());
0273         }
0274 
0275         // clean up
0276         KIO::DeleteJob *delJob = KIO::del(trashUrl, KIO::HideProgressInfo);
0277         QVERIFY2(delJob->exec(), qPrintable(delJob->errorString()));
0278     }
0279 
0280     void shouldDropFromTrash()
0281     {
0282         // Given a file in the trash
0283         const QFileInfo srcInfo(m_srcFile);
0284         const QFile::Permissions origPerms = srcInfo.permissions();
0285         QVERIFY(QFileInfo(m_srcFile).isWritable());
0286         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
0287 
0288         QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
0289         QVERIFY(copyJob->exec());
0290         const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
0291         QVERIFY(trashUrl.isValid());
0292         QVERIFY(!QFile::exists(m_srcFile));
0293 
0294         // trashinfo file was created
0295         const QString infoFile(m_trashDir + QStringLiteral("/info/") + srcInfo.fileName() + QStringLiteral(".trashinfo"));
0296         QVERIFY(QFileInfo::exists(infoFile));
0297 
0298         // When dropping the trashed file into a local dir, without modifiers
0299         m_mimeData.setUrls(QList<QUrl>{trashUrl});
0300         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
0301         KIO::DropJob *job = KIO::drop(&dropEvent, QUrl::fromLocalFile(m_srcDir), KIO::HideProgressInfo);
0302         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
0303         QSignalSpy spy(job, &KIO::DropJob::itemCreated);
0304 
0305         // Then the file should be moved, without a popup. No point in copying out of the trash, or linking to it.
0306         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0307         QCOMPARE(copyJobSpy.count(), 1);
0308         QCOMPARE(spy.count(), 1);
0309         QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(m_srcFile));
0310         QVERIFY(QFile::exists(m_srcFile));
0311         QCOMPARE(int(QFileInfo(m_srcFile).permissions()), int(origPerms));
0312         QVERIFY(QFileInfo(m_srcFile).isWritable());
0313         KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
0314         QVERIFY(!statJob->exec());
0315         QVERIFY(QFileInfo(m_srcFile).isWritable());
0316 
0317         // trashinfo file was removed
0318         QVERIFY(!QFileInfo::exists(infoFile));
0319 
0320         QVERIFY(QFileInfo(m_srcFile).isWritable());
0321     }
0322 
0323     void shouldDropTrashRootWithoutMovingAllTrashedFiles() // #319660
0324     {
0325         // Given some stuff in the trash
0326         const QUrl trashUrl(QStringLiteral("trash:/"));
0327         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), trashUrl);
0328         QVERIFY(copyJob->exec());
0329         // and an empty destination directory
0330         QTemporaryDir tempDestDir;
0331         QVERIFY(tempDestDir.isValid());
0332         const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
0333 
0334         // When dropping a link / icon of the trash...
0335         m_mimeData.setUrls(QList<QUrl>{trashUrl});
0336         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
0337         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
0338         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
0339         QVERIFY2(job->exec(), qPrintable(job->errorString()));
0340 
0341         // Then a full move shouldn't happen, just a link
0342         QCOMPARE(copyJobSpy.count(), 1);
0343         const QStringList items = QDir(tempDestDir.path()).entryList();
0344         QVERIFY2(!items.contains("srcfile"), qPrintable(items.join(',')));
0345         QVERIFY2(items.contains("trash:" + QChar(0x2044) + ".desktop"), qPrintable(items.join(',')));
0346     }
0347 
0348     void shouldDropFromTrashToTrash() // #378051
0349     {
0350         // Given a file in the trash
0351         QVERIFY(QFileInfo(m_srcFile).isWritable());
0352         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
0353         QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
0354         QVERIFY(copyJob->exec());
0355         const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
0356         QVERIFY(trashUrl.isValid());
0357         QVERIFY(!QFile::exists(m_srcFile));
0358 
0359         // When dropping the trashed file in the trash
0360         m_mimeData.setUrls(QList<QUrl>{trashUrl});
0361         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
0362         KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
0363         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
0364         QSignalSpy spy(job, &KIO::DropJob::itemCreated);
0365 
0366         // Then an error should be reported and no files action should occur
0367         QVERIFY(!job->exec());
0368         QCOMPARE(job->error(), KIO::ERR_DROP_ON_ITSELF);
0369     }
0370 
0371     void shouldDropToDirectoryWithPopup_data()
0372     {
0373         QTest::addColumn<QString>("dest"); // empty for a temp dir
0374         QTest::addColumn<Qt::DropActions>("offeredActions");
0375         QTest::addColumn<int>("triggerActionNumber");
0376         QTest::addColumn<int>("expectedError");
0377         QTest::addColumn<Qt::DropAction>("expectedDropAction");
0378         QTest::addColumn<bool>("shouldSourceStillExist");
0379 
0380         const Qt::DropActions threeActions = Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
0381         const Qt::DropActions copyAndLink = Qt::CopyAction | Qt::LinkAction;
0382         QTest::newRow("Move") << QString() << threeActions << 0 << 0 << Qt::MoveAction << false;
0383         QTest::newRow("Copy") << QString() << threeActions << 1 << 0 << Qt::CopyAction << true;
0384         QTest::newRow("Link") << QString() << threeActions << 2 << 0 << Qt::LinkAction << true;
0385         QTest::newRow("SameDestCopy") << m_srcDir << copyAndLink << 0 << int(KIO::ERR_IDENTICAL_FILES) << Qt::CopyAction << true;
0386         QTest::newRow("SameDestLink") << m_srcDir << copyAndLink << 1 << int(KIO::ERR_FILE_ALREADY_EXIST) << Qt::LinkAction << true;
0387     }
0388 
0389     void shouldDropToDirectoryWithPopup()
0390     {
0391         QFETCH(QString, dest);
0392         QFETCH(Qt::DropActions, offeredActions);
0393         QFETCH(int, triggerActionNumber);
0394         QFETCH(int, expectedError);
0395         QFETCH(Qt::DropAction, expectedDropAction);
0396         QFETCH(bool, shouldSourceStillExist);
0397 
0398         // Given a directory and a source file
0399         QTemporaryDir tempDestDir;
0400         QVERIFY(tempDestDir.isValid());
0401         if (dest.isEmpty()) {
0402             dest = tempDestDir.path();
0403         }
0404         QVERIFY(!findPopup());
0405 
0406         // When dropping the source file onto the directory
0407         QUrl destUrl = QUrl::fromLocalFile(dest);
0408         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
0409         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
0410         JobSpy jobSpy(job);
0411         qRegisterMetaType<KFileItemListProperties>();
0412         QSignalSpy spyShow(job, &KIO::DropJob::popupMenuAboutToShow);
0413         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
0414         QVERIFY(spyShow.isValid());
0415 
0416         // Then a popup should appear, with the expected available actions
0417         QVERIFY(spyShow.wait());
0418         QTRY_VERIFY(findPopup());
0419         QMenu *popup = findPopup();
0420         QCOMPARE(int(popupDropActions(popup)), int(offeredActions));
0421 
0422         // And when selecting action number <triggerActionNumber>
0423         QAction *action = popup->actions().at(triggerActionNumber);
0424         QVERIFY(action);
0425         QCOMPARE(int(action->data().value<Qt::DropAction>()), int(expectedDropAction));
0426         const QRect actionGeom = popup->actionGeometry(action);
0427         QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
0428 
0429         // Then the job should finish, and the chosen action should happen.
0430         QVERIFY(jobSpy.waitForResult());
0431         QCOMPARE(jobSpy.error(), expectedError);
0432         if (expectedError == 0) {
0433             QCOMPARE(copyJobSpy.count(), 1);
0434             const QString destFile = dest + "/srcfile";
0435             QVERIFY(QFile::exists(destFile));
0436             QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
0437             if (expectedDropAction == Qt::LinkAction) {
0438                 QVERIFY(QFileInfo(destFile).isSymLink());
0439             }
0440         }
0441         QTRY_VERIFY(!findPopup()); // flush deferred delete, so we don't get this popup again in findPopup
0442     }
0443 
0444     void shouldAddApplicationActionsToPopup()
0445     {
0446         // Given a directory and a source file
0447         QTemporaryDir tempDestDir;
0448         QVERIFY(tempDestDir.isValid());
0449         const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
0450 
0451         // When dropping the source file onto the directory
0452         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
0453         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
0454         QAction appAction1(QStringLiteral("action1"), this);
0455         QAction appAction2(QStringLiteral("action2"), this);
0456         QList<QAction *> appActions;
0457         appActions << &appAction1 << &appAction2;
0458         job->setApplicationActions(appActions);
0459         JobSpy jobSpy(job);
0460 
0461         // Then a popup should appear, with the expected available actions
0462         QTRY_VERIFY(findPopup());
0463         QMenu *popup = findPopup();
0464         const QList<QAction *> actions = popup->actions();
0465         QVERIFY(actions.contains(&appAction1));
0466         QVERIFY(actions.contains(&appAction2));
0467         QVERIFY(actions.at(actions.indexOf(&appAction1) - 1)->isSeparator());
0468         QVERIFY(actions.at(actions.indexOf(&appAction2) + 1)->isSeparator());
0469 
0470         // And when selecting action appAction1
0471         const QRect actionGeom = popup->actionGeometry(&appAction1);
0472         QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
0473 
0474         // Then the menu should hide and the job terminate (without doing any copying)
0475         QVERIFY(jobSpy.waitForResult());
0476         QCOMPARE(jobSpy.error(), 0);
0477         const QString destFile = tempDestDir.path() + "/srcfile";
0478         QVERIFY(!QFile::exists(destFile));
0479     }
0480 
0481 private:
0482     static QMenu *findPopup()
0483     {
0484         const QList<QWidget *> widgetsList = qApp->topLevelWidgets();
0485         for (QWidget *widget : widgetsList) {
0486             if (QMenu *menu = qobject_cast<QMenu *>(widget)) {
0487                 return menu;
0488             }
0489         }
0490         return nullptr;
0491     }
0492     static Qt::DropActions popupDropActions(QMenu *menu)
0493     {
0494         Qt::DropActions actions;
0495         const QList<QAction *> actionsList = menu->actions();
0496         for (const QAction *action : actionsList) {
0497             const QVariant userData = action->data();
0498             if (userData.isValid()) {
0499                 actions |= userData.value<Qt::DropAction>();
0500             }
0501         }
0502         return actions;
0503     }
0504     QMimeData m_mimeData; // contains m_srcFile
0505     QTemporaryDir m_tempDir;
0506     QString m_srcDir;
0507     QString m_srcFile;
0508     QString m_srcLink;
0509     QTemporaryDir m_nonWritableTempDir;
0510     QString m_trashDir;
0511 };
0512 
0513 QTEST_MAIN(DropJobTest)
0514 
0515 #include "dropjobtest.moc"