1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2014 David Faure <faure@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6 */
7 
8 #include <QDir>
9 #include <QMenu>
10 #include <QMimeData>
11 #include <QSignalSpy>
12 #include <QStandardPaths>
13 #include <QTemporaryDir>
14 #include <QTest>
15 
16 #include "jobuidelegatefactory.h"
17 #include "kiotesthelper.h"
18 #include "mockcoredelegateextensions.h"
19 #include <KConfigGroup>
20 #include <KDesktopFile>
21 #include <KFileItemListProperties>
22 #include <KIO/CopyJob>
23 #include <KIO/DeleteJob>
24 #include <KIO/DropJob>
25 #include <KIO/StatJob>
26 #include <KJobUiDelegate>
27 
28 Q_DECLARE_METATYPE(Qt::KeyboardModifiers)
Q_DECLARE_METATYPE(Qt::DropAction)29 Q_DECLARE_METATYPE(Qt::DropAction)
30 Q_DECLARE_METATYPE(Qt::DropActions)
31 Q_DECLARE_METATYPE(KFileItemListProperties)
32 
33 #ifndef Q_OS_WIN
34 void initLocale()
35 {
36     setenv("LC_ALL", "en_US.utf-8", 1);
37 }
38 Q_CONSTRUCTOR_FUNCTION(initLocale)
39 #endif
40 
41 class JobSpy : public QObject
42 {
43     Q_OBJECT
44 public:
JobSpy(KIO::Job * job)45     JobSpy(KIO::Job *job)
46         : QObject(nullptr)
47         , m_spy(job, &KJob::result)
48         , m_error(0)
49     {
50         connect(job, &KJob::result, this, [this](KJob *job) {
51             m_error = job->error();
52         });
53     }
54     // like job->exec(), but with a timeout (to avoid being stuck with a popup grabbing mouse and keyboard...)
waitForResult()55     bool waitForResult()
56     {
57         // implementation taken from QTRY_COMPARE, to move the QVERIFY to the caller
58         if (m_spy.isEmpty()) {
59             QTest::qWait(0);
60         }
61         for (int i = 0; i < 5000 && m_spy.isEmpty(); i += 50) {
62             QTest::qWait(50);
63         }
64         return !m_spy.isEmpty();
65     }
error() const66     int error() const
67     {
68         return m_error;
69     }
70 
71 private:
72     QSignalSpy m_spy;
73     int m_error;
74 };
75 
76 class DropJobTest : public QObject
77 {
78     Q_OBJECT
79 
80 private Q_SLOTS:
initTestCase()81     void initTestCase()
82     {
83         QStandardPaths::setTestModeEnabled(true);
84         qputenv("KIOSLAVE_ENABLE_TESTMODE", "1"); // ensure the ioslaves call QStandardPaths::setTestModeEnabled too
85 
86         // To avoid a runtime dependency on klauncher
87         qputenv("KDE_FORK_SLAVES", "yes");
88 
89         KIO::setDefaultJobUiDelegateFactory(nullptr);
90         KIO::setDefaultJobUiDelegateExtension(nullptr);
91 
92         const QString trashDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/Trash");
93         QDir(trashDir).removeRecursively();
94 
95         QVERIFY(m_tempDir.isValid());
96         QVERIFY(m_nonWritableTempDir.isValid());
97         QVERIFY(QFile(m_nonWritableTempDir.path()).setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::ExeOwner | QFile::ExeUser));
98         m_srcDir = m_tempDir.path();
99 
100         m_srcFile = m_srcDir + "/srcfile";
101         m_srcLink = m_srcDir + "/link";
102 
103         qRegisterMetaType<KIO::CopyJob *>();
104     }
105 
cleanupTestCase()106     void cleanupTestCase()
107     {
108         QVERIFY(QFile(m_nonWritableTempDir.path())
109                     .setPermissions(QFile::ReadOwner | QFile::ReadUser | QFile::WriteOwner | QFile::WriteUser | QFile::ExeOwner | QFile::ExeUser));
110     }
111 
112     // Before every test method, ensure the test file m_srcFile exists
init()113     void init()
114     {
115         if (QFile::exists(m_srcFile)) {
116             QVERIFY(QFileInfo(m_srcFile).isWritable());
117         } else {
118             QFile srcFile(m_srcFile);
119             QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
120             srcFile.write("Hello world\n");
121         }
122 #ifndef Q_OS_WIN
123         if (!QFile::exists(m_srcLink)) {
124             QVERIFY(QFile(m_srcFile).link(m_srcLink));
125             QVERIFY(QFileInfo(m_srcLink).isSymLink());
126         }
127 #endif
128         QVERIFY(QFileInfo(m_srcFile).isWritable());
129         m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(m_srcFile)});
130     }
131 
shouldDropToDesktopFile()132     void shouldDropToDesktopFile()
133     {
134         // Given an executable application desktop file and a source file
135         const QString desktopPath = m_srcDir + "/target.desktop";
136         KDesktopFile desktopFile(desktopPath);
137         KConfigGroup desktopGroup = desktopFile.desktopGroup();
138         desktopGroup.writeEntry("Type", "Application");
139         desktopGroup.writeEntry("StartupNotify", "false");
140 #ifdef Q_OS_WIN
141         desktopGroup.writeEntry("Exec", "copy.exe %f %d/dest");
142 #else
143         desktopGroup.writeEntry("Exec", "cp %f %d/dest");
144 #endif
145         desktopFile.sync();
146         QFile file(desktopPath);
147         file.setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser);
148 
149         // When dropping the source file onto the desktop file
150         QUrl destUrl = QUrl::fromLocalFile(desktopPath);
151         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
152         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
153         QSignalSpy spy(job, &KIO::DropJob::itemCreated);
154 
155         // Then the application is run with the source file as argument
156         // (in this example, it copies the source file to "dest")
157         QVERIFY2(job->exec(), qPrintable(job->errorString()));
158         QCOMPARE(spy.count(), 0);
159         const QString dest = m_srcDir + "/dest";
160         QTRY_VERIFY(QFile::exists(dest));
161 
162         QVERIFY(QFile::remove(desktopPath));
163         QVERIFY(QFile::remove(dest));
164     }
165 
shouldDropToDirectory_data()166     void shouldDropToDirectory_data()
167     {
168         QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
169         QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
170         QTest::addColumn<QString>("srcFile");
171         QTest::addColumn<QString>("dest"); // empty for a temp dir
172         QTest::addColumn<int>("expectedError");
173         QTest::addColumn<bool>("shouldSourceStillExist");
174 
175         QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile << QString() << 0 << true;
176         QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile << QString() << 0 << false;
177         QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile << QString() << 0 << true;
178         QTest::newRow("DropOnItself") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcDir << m_srcDir << int(KIO::ERR_DROP_ON_ITSELF) << true;
179         QTest::newRow("DropDirOnFile") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcDir << m_srcFile << int(KIO::ERR_ACCESS_DENIED)
180                                        << true;
181         QTest::newRow("NonWritableDest") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile << m_nonWritableTempDir.path()
182                                          << int(KIO::ERR_WRITE_ACCESS_DENIED) << true;
183     }
184 
shouldDropToDirectory()185     void shouldDropToDirectory()
186     {
187         QFETCH(Qt::KeyboardModifiers, modifiers);
188         QFETCH(Qt::DropAction, dropAction);
189         QFETCH(QString, srcFile);
190         QFETCH(QString, dest);
191         QFETCH(int, expectedError);
192         QFETCH(bool, shouldSourceStillExist);
193 
194         // Given a directory and a source file
195         QTemporaryDir tempDestDir;
196         QVERIFY(tempDestDir.isValid());
197         if (dest.isEmpty()) {
198             dest = tempDestDir.path();
199         }
200 
201         // When dropping the source file onto the directory
202         const QUrl destUrl = QUrl::fromLocalFile(dest);
203         m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
204         QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
205         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo | KIO::NoPrivilegeExecution);
206         JobSpy jobSpy(job);
207         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
208         QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
209 
210         // Then the file is copied
211         QVERIFY(jobSpy.waitForResult());
212         QCOMPARE(jobSpy.error(), expectedError);
213         if (expectedError == 0) {
214             QCOMPARE(copyJobSpy.count(), 1);
215             const QString destFile = dest + "/srcfile";
216             QCOMPARE(itemCreatedSpy.count(), 1);
217             QCOMPARE(itemCreatedSpy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(destFile));
218             QVERIFY(QFile::exists(destFile));
219             QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
220             if (dropAction == Qt::LinkAction) {
221                 QVERIFY(QFileInfo(destFile).isSymLink());
222             }
223         }
224     }
225 
shouldDropToTrash_data()226     void shouldDropToTrash_data()
227     {
228         QTest::addColumn<Qt::KeyboardModifiers>("modifiers");
229         QTest::addColumn<Qt::DropAction>("dropAction"); // Qt's dnd support sets it from the modifiers, we fake it here
230         QTest::addColumn<QString>("srcFile");
231 
232         QTest::newRow("Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcFile;
233         QTest::newRow("Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcFile;
234         QTest::newRow("Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcFile;
235         QTest::newRow("NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcFile;
236 #ifndef Q_OS_WIN
237         QTest::newRow("Link_Ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << Qt::CopyAction << m_srcLink;
238         QTest::newRow("Link_Shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << Qt::MoveAction << m_srcLink;
239         QTest::newRow("Link_Ctrl_Shift") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::ShiftModifier) << Qt::LinkAction << m_srcLink;
240         QTest::newRow("Link_NoModifiers") << Qt::KeyboardModifiers() << Qt::CopyAction << m_srcLink;
241 #endif
242     }
243 
shouldDropToTrash()244     void shouldDropToTrash()
245     {
246         // Given a source file
247         QFETCH(Qt::KeyboardModifiers, modifiers);
248         QFETCH(Qt::DropAction, dropAction);
249         QFETCH(QString, srcFile);
250         const bool isLink = QFileInfo(srcFile).isSymLink();
251 
252         // When dropping it into the trash, with <modifiers> pressed
253         m_mimeData.setUrls(QList<QUrl>{QUrl::fromLocalFile(srcFile)});
254         QDropEvent dropEvent(QPoint(10, 10), dropAction, &m_mimeData, Qt::LeftButton, modifiers);
255         KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
256         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
257         QSignalSpy itemCreatedSpy(job, &KIO::DropJob::itemCreated);
258 
259         // Then a confirmation dialog should appear
260         auto *uiDelegate = new KJobUiDelegate;
261         job->setUiDelegate(uiDelegate);
262         auto *askUserHandler = new MockAskUserInterface(uiDelegate);
263         askUserHandler->m_deleteResult = true;
264 
265         // And the file should be moved to the trash, no matter what the modifiers are
266         QVERIFY2(job->exec(), qPrintable(job->errorString()));
267         QCOMPARE(askUserHandler->m_askUserDeleteCalled, 1);
268         QCOMPARE(copyJobSpy.count(), 1);
269         QCOMPARE(itemCreatedSpy.count(), 1);
270         const QUrl trashUrl = itemCreatedSpy.at(0).at(0).value<QUrl>();
271         QCOMPARE(trashUrl.scheme(), QString("trash"));
272         KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
273         QVERIFY(statJob->exec());
274         if (isLink) {
275             QVERIFY(statJob->statResult().isLink());
276         }
277 
278         // clean up
279         KIO::DeleteJob *delJob = KIO::del(trashUrl, KIO::HideProgressInfo);
280         QVERIFY2(delJob->exec(), qPrintable(delJob->errorString()));
281     }
282 
shouldDropFromTrash()283     void shouldDropFromTrash()
284     {
285         // Given a file in the trash
286         const QFile::Permissions origPerms = QFileInfo(m_srcFile).permissions();
287         QVERIFY(QFileInfo(m_srcFile).isWritable());
288         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
289         QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
290         QVERIFY(copyJob->exec());
291         const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
292         QVERIFY(trashUrl.isValid());
293         QVERIFY(!QFile::exists(m_srcFile));
294 
295         // When dropping the trashed file into a local dir, without modifiers
296         m_mimeData.setUrls(QList<QUrl>{trashUrl});
297         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
298         KIO::DropJob *job = KIO::drop(&dropEvent, QUrl::fromLocalFile(m_srcDir), KIO::HideProgressInfo);
299         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
300         QSignalSpy spy(job, &KIO::DropJob::itemCreated);
301 
302         // Then the file should be moved, without a popup. No point in copying out of the trash, or linking to it.
303         QVERIFY2(job->exec(), qPrintable(job->errorString()));
304         QCOMPARE(copyJobSpy.count(), 1);
305         QCOMPARE(spy.count(), 1);
306         QCOMPARE(spy.at(0).at(0).value<QUrl>(), QUrl::fromLocalFile(m_srcFile));
307         QVERIFY(QFile::exists(m_srcFile));
308         QCOMPARE(int(QFileInfo(m_srcFile).permissions()), int(origPerms));
309         QVERIFY(QFileInfo(m_srcFile).isWritable());
310         KIO::StatJob *statJob = KIO::stat(trashUrl, KIO::HideProgressInfo);
311         QVERIFY(!statJob->exec());
312         QVERIFY(QFileInfo(m_srcFile).isWritable());
313     }
314 
shouldDropTrashRootWithoutMovingAllTrashedFiles()315     void shouldDropTrashRootWithoutMovingAllTrashedFiles() // #319660
316     {
317         // Given some stuff in the trash
318         const QUrl trashUrl(QStringLiteral("trash:/"));
319         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), trashUrl);
320         QVERIFY(copyJob->exec());
321         // and an empty destination directory
322         QTemporaryDir tempDestDir;
323         QVERIFY(tempDestDir.isValid());
324         const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
325 
326         // When dropping a link / icon of the trash...
327         m_mimeData.setUrls(QList<QUrl>{trashUrl});
328         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
329         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
330         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
331         QVERIFY2(job->exec(), qPrintable(job->errorString()));
332 
333         // Then a full move shouldn't happen, just a link
334         QCOMPARE(copyJobSpy.count(), 1);
335         const QStringList items = QDir(tempDestDir.path()).entryList();
336         QVERIFY2(!items.contains("srcfile"), qPrintable(items.join(',')));
337         QVERIFY2(items.contains("trash:" + QChar(0x2044) + ".desktop"), qPrintable(items.join(',')));
338     }
339 
shouldDropFromTrashToTrash()340     void shouldDropFromTrashToTrash() // #378051
341     {
342         // Given a file in the trash
343         QVERIFY(QFileInfo(m_srcFile).isWritable());
344         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(m_srcFile), QUrl(QStringLiteral("trash:/")));
345         QSignalSpy copyingDoneSpy(copyJob, &KIO::CopyJob::copyingDone);
346         QVERIFY(copyJob->exec());
347         const QUrl trashUrl = copyingDoneSpy.at(0).at(2).value<QUrl>();
348         QVERIFY(trashUrl.isValid());
349         QVERIFY(!QFile::exists(m_srcFile));
350 
351         // When dropping the trashed file in the trash
352         m_mimeData.setUrls(QList<QUrl>{trashUrl});
353         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
354         KIO::DropJob *job = KIO::drop(&dropEvent, QUrl(QStringLiteral("trash:/")), KIO::HideProgressInfo);
355         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
356         QSignalSpy spy(job, &KIO::DropJob::itemCreated);
357 
358         // Then an error should be reported and no files action should occur
359         QVERIFY(!job->exec());
360         QCOMPARE(job->error(), KIO::ERR_DROP_ON_ITSELF);
361     }
362 
shouldDropToDirectoryWithPopup_data()363     void shouldDropToDirectoryWithPopup_data()
364     {
365         QTest::addColumn<QString>("dest"); // empty for a temp dir
366         QTest::addColumn<Qt::DropActions>("offeredActions");
367         QTest::addColumn<int>("triggerActionNumber");
368         QTest::addColumn<int>("expectedError");
369         QTest::addColumn<Qt::DropAction>("expectedDropAction");
370         QTest::addColumn<bool>("shouldSourceStillExist");
371 
372         const Qt::DropActions threeActions = Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
373         const Qt::DropActions copyAndLink = Qt::CopyAction | Qt::LinkAction;
374         QTest::newRow("Move") << QString() << threeActions << 0 << 0 << Qt::MoveAction << false;
375         QTest::newRow("Copy") << QString() << threeActions << 1 << 0 << Qt::CopyAction << true;
376         QTest::newRow("Link") << QString() << threeActions << 2 << 0 << Qt::LinkAction << true;
377         QTest::newRow("SameDestCopy") << m_srcDir << copyAndLink << 0 << int(KIO::ERR_IDENTICAL_FILES) << Qt::CopyAction << true;
378         QTest::newRow("SameDestLink") << m_srcDir << copyAndLink << 1 << int(KIO::ERR_FILE_ALREADY_EXIST) << Qt::LinkAction << true;
379     }
380 
shouldDropToDirectoryWithPopup()381     void shouldDropToDirectoryWithPopup()
382     {
383         QFETCH(QString, dest);
384         QFETCH(Qt::DropActions, offeredActions);
385         QFETCH(int, triggerActionNumber);
386         QFETCH(int, expectedError);
387         QFETCH(Qt::DropAction, expectedDropAction);
388         QFETCH(bool, shouldSourceStillExist);
389 
390         // Given a directory and a source file
391         QTemporaryDir tempDestDir;
392         QVERIFY(tempDestDir.isValid());
393         if (dest.isEmpty()) {
394             dest = tempDestDir.path();
395         }
396         QVERIFY(!findPopup());
397 
398         // When dropping the source file onto the directory
399         QUrl destUrl = QUrl::fromLocalFile(dest);
400         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
401         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
402         JobSpy jobSpy(job);
403         qRegisterMetaType<KFileItemListProperties>();
404         QSignalSpy spyShow(job, &KIO::DropJob::popupMenuAboutToShow);
405         QSignalSpy copyJobSpy(job, &KIO::DropJob::copyJobStarted);
406         QVERIFY(spyShow.isValid());
407 
408         // Then a popup should appear, with the expected available actions
409         QVERIFY(spyShow.wait());
410         QTRY_VERIFY(findPopup());
411         QMenu *popup = findPopup();
412         QCOMPARE(int(popupDropActions(popup)), int(offeredActions));
413 
414         // And when selecting action number <triggerActionNumber>
415         QAction *action = popup->actions().at(triggerActionNumber);
416         QVERIFY(action);
417         QCOMPARE(int(action->data().value<Qt::DropAction>()), int(expectedDropAction));
418         const QRect actionGeom = popup->actionGeometry(action);
419         QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
420 
421         // Then the job should finish, and the chosen action should happen.
422         QVERIFY(jobSpy.waitForResult());
423         QCOMPARE(jobSpy.error(), expectedError);
424         if (expectedError == 0) {
425             QCOMPARE(copyJobSpy.count(), 1);
426             const QString destFile = dest + "/srcfile";
427             QVERIFY(QFile::exists(destFile));
428             QCOMPARE(QFile::exists(m_srcFile), shouldSourceStillExist);
429             if (expectedDropAction == Qt::LinkAction) {
430                 QVERIFY(QFileInfo(destFile).isSymLink());
431             }
432         }
433         QTRY_VERIFY(!findPopup()); // flush deferred delete, so we don't get this popup again in findPopup
434     }
435 
shouldAddApplicationActionsToPopup()436     void shouldAddApplicationActionsToPopup()
437     {
438         // Given a directory and a source file
439         QTemporaryDir tempDestDir;
440         QVERIFY(tempDestDir.isValid());
441         const QUrl destUrl = QUrl::fromLocalFile(tempDestDir.path());
442 
443         // When dropping the source file onto the directory
444         QDropEvent dropEvent(QPoint(10, 10), Qt::CopyAction /*unused*/, &m_mimeData, Qt::LeftButton, Qt::NoModifier);
445         KIO::DropJob *job = KIO::drop(&dropEvent, destUrl, KIO::HideProgressInfo);
446         QAction appAction1(QStringLiteral("action1"), this);
447         QAction appAction2(QStringLiteral("action2"), this);
448         QList<QAction *> appActions;
449         appActions << &appAction1 << &appAction2;
450         job->setApplicationActions(appActions);
451         JobSpy jobSpy(job);
452 
453         // Then a popup should appear, with the expected available actions
454         QTRY_VERIFY(findPopup());
455         QMenu *popup = findPopup();
456         const QList<QAction *> actions = popup->actions();
457         QVERIFY(actions.contains(&appAction1));
458         QVERIFY(actions.contains(&appAction2));
459         QVERIFY(actions.at(actions.indexOf(&appAction1) - 1)->isSeparator());
460         QVERIFY(actions.at(actions.indexOf(&appAction2) + 1)->isSeparator());
461 
462         // And when selecting action appAction1
463         const QRect actionGeom = popup->actionGeometry(&appAction1);
464         QTest::mouseClick(popup, Qt::LeftButton, Qt::NoModifier, actionGeom.center());
465 
466         // Then the menu should hide and the job terminate (without doing any copying)
467         QVERIFY(jobSpy.waitForResult());
468         QCOMPARE(jobSpy.error(), 0);
469         const QString destFile = tempDestDir.path() + "/srcfile";
470         QVERIFY(!QFile::exists(destFile));
471     }
472 
473 private:
findPopup()474     static QMenu *findPopup()
475     {
476         const QList<QWidget *> widgetsList = qApp->topLevelWidgets();
477         for (QWidget *widget : widgetsList) {
478             if (QMenu *menu = qobject_cast<QMenu *>(widget)) {
479                 return menu;
480             }
481         }
482         return nullptr;
483     }
popupDropActions(QMenu * menu)484     static Qt::DropActions popupDropActions(QMenu *menu)
485     {
486         Qt::DropActions actions;
487         const QList<QAction *> actionsList = menu->actions();
488         for (const QAction *action : actionsList) {
489             const QVariant userData = action->data();
490             if (userData.isValid()) {
491                 actions |= userData.value<Qt::DropAction>();
492             }
493         }
494         return actions;
495     }
496     QMimeData m_mimeData; // contains m_srcFile
497     QTemporaryDir m_tempDir;
498     QString m_srcDir;
499     QString m_srcFile;
500     QString m_srcLink;
501     QTemporaryDir m_nonWritableTempDir;
502 };
503 
504 QTEST_MAIN(DropJobTest)
505 
506 #include "dropjobtest.moc"
507