1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2020 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 "openurljobtest.h"
9 #include "openurljob.h"
10 #include <KApplicationTrader>
11 #include <kprocessrunner_p.h>
12 
13 #include "kiotesthelper.h" // createTestFile etc.
14 #include "mockcoredelegateextensions.h"
15 #include "mockguidelegateextensions.h"
16 
17 #include <KConfigGroup>
18 #include <KDesktopFile>
19 #include <KJobUiDelegate>
20 #include <KService>
21 
22 #ifdef Q_OS_UNIX
23 #include <signal.h> // kill
24 #endif
25 
26 #include <KSharedConfig>
27 #include <QStandardPaths>
28 #include <QTemporaryFile>
29 #include <QTest>
30 
31 QTEST_GUILESS_MAIN(OpenUrlJobTest)
32 
33 extern KSERVICE_EXPORT int ksycoca_ms_between_checks;
34 
35 static const char s_tempServiceName[] = "openurljobtest_service.desktop";
36 
initTestCase()37 void OpenUrlJobTest::initTestCase()
38 {
39     QStandardPaths::setTestModeEnabled(true);
40 
41     // Ensure no leftovers from other tests
42     QDir(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)).removeRecursively();
43     // (including a mimeapps.list file)
44     // Don't remove ConfigLocation completely, it's useful when enabling debug output with kdebugsettings --test-mode
45     const QString mimeApps = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/mimeapps.list");
46     QFile::remove(mimeApps);
47 
48     ksycoca_ms_between_checks = 0; // need it to check the ksycoca mtime
49     m_fakeService = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + s_tempServiceName;
50     // not using %d because of remote urls
51     const QByteArray cmd = QByteArray("echo %u > " + QFile::encodeName(m_tempDir.path()) + "/dest");
52     writeApplicationDesktopFile(m_fakeService, cmd);
53     m_fakeService = QFileInfo(m_fakeService).canonicalFilePath();
54     m_filesToRemove.append(m_fakeService);
55 
56     // Ensure our service is the preferred one
57     KConfig mimeAppsCfg(mimeApps);
58     KConfigGroup grp = mimeAppsCfg.group("Default Applications");
59     grp.writeEntry("text/plain", s_tempServiceName);
60     grp.writeEntry("text/html", s_tempServiceName);
61     grp.sync();
62 
63     // "text/plain" encompasses all scripts (shell, python, perl)
64     KService::Ptr preferredTextEditor = KApplicationTrader::preferredService(QStringLiteral("text/plain"));
65     QVERIFY(preferredTextEditor);
66     QCOMPARE(preferredTextEditor->entryPath(), m_fakeService);
67 
68     // As used for preferredService
69     QVERIFY(KService::serviceByDesktopName("openurljobtest_service"));
70 
71     ksycoca_ms_between_checks = 5000; // all done, speed up again
72 }
73 
cleanupTestCase()74 void OpenUrlJobTest::cleanupTestCase()
75 {
76     for (const QString &file : std::as_const(m_filesToRemove)) {
77         QFile::remove(file);
78     };
79 }
80 
init()81 void OpenUrlJobTest::init()
82 {
83     QFile::remove(m_tempDir.path() + "/dest");
84 }
85 
createSrcFile(const QString & path)86 static void createSrcFile(const QString &path)
87 {
88     QFile srcFile(path);
89     QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString()));
90     srcFile.write("Hello world\n");
91 }
92 
readFile(const QString & path)93 static QString readFile(const QString &path)
94 {
95     QFile file(path);
96     file.open(QIODevice::ReadOnly);
97     return QString::fromLocal8Bit(file.readAll()).trimmed();
98 }
99 
startProcess_data()100 void OpenUrlJobTest::startProcess_data()
101 {
102     QTest::addColumn<QString>("mimeType");
103     QTest::addColumn<QString>("fileName");
104 
105     // Known MIME type
106     QTest::newRow("text_file") << "text/plain"
107                                << "srcfile.txt";
108     QTest::newRow("directory_file") << "application/x-desktop"
109                                     << ".directory";
110     QTest::newRow("desktop_file_link") << "application/x-desktop"
111                                        << "srcfile.txt";
112     QTest::newRow("desktop_file_link_preferred_service") << "application/x-desktop"
113                                                          << "srcfile.html";
114     QTest::newRow("non_executable_script_running_not_allowed") << "application/x-shellscript"
115                                                                << "srcfile.sh";
116     QTest::newRow("executable_script_running_not_allowed") << "application/x-shellscript"
117                                                            << "srcfile.sh";
118 
119     // Require MIME type determination
120     QTest::newRow("text_file_no_mimetype") << QString() << "srcfile.txt";
121     QTest::newRow("directory_file_no_mimetype") << QString() << ".directory";
122 }
123 
startProcess()124 void OpenUrlJobTest::startProcess()
125 {
126     QFETCH(QString, mimeType);
127     QFETCH(QString, fileName);
128 
129     // Given a file to open
130     QTemporaryDir tempDir;
131     const QString srcDir = tempDir.path();
132     const QString srcFile = srcDir + QLatin1Char('/') + fileName;
133     createSrcFile(srcFile);
134     QVERIFY(QFile::exists(srcFile));
135     const bool isLink = QByteArray(QTest::currentDataTag()).startsWith("desktop_file_link");
136     QUrl url = QUrl::fromLocalFile(srcFile);
137     if (isLink) {
138         const QString desktopFilePath = srcDir + QLatin1String("/link.desktop");
139         KDesktopFile linkDesktopFile(desktopFilePath);
140         linkDesktopFile.desktopGroup().writeEntry("Type", "Link");
141         linkDesktopFile.desktopGroup().writeEntry("URL", url);
142         const bool linkHasPreferredService = QByteArray(QTest::currentDataTag()) == "desktop_file_link_preferred_service";
143         if (linkHasPreferredService) {
144             linkDesktopFile.desktopGroup().writeEntry("X-KDE-LastOpenedWith", "openurljobtest_service");
145         }
146         url = QUrl::fromLocalFile(desktopFilePath);
147     }
148     if (QByteArray(QTest::currentDataTag()).startsWith("executable")) {
149         QFile file(srcFile);
150         QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions()));
151         // Note however that running executables is not allowed by the OpenUrlJob below
152         // so this will end up opening it as a text file anyway.
153     }
154 
155     // When running a OpenUrlJob
156     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimeType, this);
157     QVERIFY2(job->exec(), qPrintable(job->errorString()));
158 
159     // Then m_fakeService should be executed, since it's associated with text/plain
160     // We can find out that it was executed because it writes to "dest".
161     const QString dest = m_tempDir.path() + "/dest";
162     QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest));
163     QCOMPARE(readFile(dest), srcFile);
164 }
165 
noServiceNoHandler()166 void OpenUrlJobTest::noServiceNoHandler()
167 {
168     QTemporaryFile tempFile;
169     QVERIFY(tempFile.open());
170     const QUrl url = QUrl::fromLocalFile(tempFile.fileName());
171     const QString mimeType = QStringLiteral("application/x-zerosize");
172     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, mimeType, this);
173     // This is going to try QDesktopServices::openUrl which will fail because we are no QGuiApplication, good.
174     QTest::ignoreMessage(QtWarningMsg, "QDesktopServices::openUrl: Application is not a GUI application");
175     QVERIFY(!job->exec());
176     QCOMPARE(job->error(), KJob::UserDefinedError);
177     QCOMPARE(job->errorString(), QStringLiteral("Failed to open the file."));
178 }
179 
invalidUrl()180 void OpenUrlJobTest::invalidUrl()
181 {
182     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl(":/"), QStringLiteral("text/plain"), this);
183     QVERIFY(!job->exec());
184     QCOMPARE(job->error(), KIO::ERR_MALFORMED_URL);
185     QCOMPARE(job->errorString(), QStringLiteral("Malformed URL\nRelative URL's path component contains ':' before any '/'; source was \":/\"; path = \":/\""));
186 
187     QUrl u;
188     u.setPath(QStringLiteral("/pathonly"));
189     KIO::OpenUrlJob *job2 = new KIO::OpenUrlJob(u, QStringLiteral("text/plain"), this);
190     QVERIFY(!job2->exec());
191     QCOMPARE(job2->error(), KIO::ERR_MALFORMED_URL);
192     QCOMPARE(job2->errorString(), QStringLiteral("Malformed URL\n/pathonly"));
193 }
194 
refuseRunningLocalBinaries_data()195 void OpenUrlJobTest::refuseRunningLocalBinaries_data()
196 {
197     QTest::addColumn<QString>("mimeType");
198 
199     // Executables under e.g. /usr/bin/ can be either of these two MIME types
200     // see https://gitlab.freedesktop.org/xdg/shared-mime-info/-/issues/11
201     QTest::newRow("x-sharedlib") << "application/x-sharedlib";
202     QTest::newRow("x-executable") << "application/x-executable";
203 
204     QTest::newRow("msdos_executable") << "application/x-ms-dos-executable";
205 }
206 
refuseRunningLocalBinaries()207 void OpenUrlJobTest::refuseRunningLocalBinaries()
208 {
209     QFETCH(QString, mimeType);
210 
211     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(QCoreApplication::applicationFilePath()), mimeType, this);
212     QVERIFY(!job->exec());
213     QCOMPARE(job->error(), KJob::UserDefinedError);
214     QVERIFY2(job->errorString().contains("For security reasons, launching executables is not allowed in this context."), qPrintable(job->errorString()));
215 }
216 
refuseRunningRemoteNativeExecutables_data()217 void OpenUrlJobTest::refuseRunningRemoteNativeExecutables_data()
218 {
219     QTest::addColumn<QString>("mimeType");
220     QTest::newRow("x-sharedlib") << "application/x-sharedlib";
221     QTest::newRow("x-executable") << "application/x-executable";
222 }
223 
refuseRunningRemoteNativeExecutables()224 void OpenUrlJobTest::refuseRunningRemoteNativeExecutables()
225 {
226     QFETCH(QString, mimeType);
227 
228     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl("protocol://host/path/exe"), mimeType, this);
229     job->setRunExecutables(true); // even with this enabled, an error will occur
230     QVERIFY(!job->exec());
231     QCOMPARE(job->error(), KJob::UserDefinedError);
232     QVERIFY2(job->errorString().contains("is located on a remote filesystem. For safety reasons it will not be started"), qPrintable(job->errorString()));
233 }
234 
235 KCONFIGCORE_EXPORT void loadUrlActionRestrictions(const KConfigGroup &cg);
236 
notAuthorized()237 void OpenUrlJobTest::notAuthorized()
238 {
239     KConfigGroup cg(KSharedConfig::openConfig(), "KDE URL Restrictions");
240     cg.writeEntry("rule_count", 1);
241     cg.writeEntry("rule_1", QStringList{"open", {}, {}, {}, "file", "", "", "false"});
242     cg.sync();
243     loadUrlActionRestrictions(cg);
244 
245     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl("file:///"), QStringLiteral("text/plain"), this);
246     QVERIFY(!job->exec());
247     QCOMPARE(job->error(), KIO::ERR_ACCESS_DENIED);
248     QCOMPARE(job->errorString(), QStringLiteral("Access denied to file:///."));
249 
250     cg.deleteEntry("rule_1");
251     cg.deleteEntry("rule_count");
252     cg.sync();
253     loadUrlActionRestrictions(cg);
254 }
255 
runScript_data()256 void OpenUrlJobTest::runScript_data()
257 {
258     QTest::addColumn<QString>("mimeType");
259 
260     // All text-based scripts inherit text/plain and application/x-executable, no need to test
261     // all flavours (python, perl, lua, awk ...etc), this sample should be enough
262     QTest::newRow("shellscript") << "application/x-shellscript";
263     QTest::newRow("pythonscript") << "text/x-python";
264     QTest::newRow("javascript") << "application/javascript";
265 }
266 
runScript()267 void OpenUrlJobTest::runScript()
268 {
269 #ifdef Q_OS_UNIX
270     QFETCH(QString, mimeType);
271 
272     // Given an executable shell script that copies "src" to "dest"
273     QTemporaryDir tempDir;
274     const QString dir = tempDir.path();
275     createSrcFile(dir + QLatin1String("/src"));
276     const QString scriptFile = dir + QLatin1String("/script.sh");
277     QFile file(scriptFile);
278     QVERIFY(file.open(QIODevice::WriteOnly));
279     file.write("#!/bin/sh\ncp src dest");
280     file.close();
281     QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions()));
282 
283     // When using OpenUrlJob to run the script
284     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(scriptFile), mimeType, this);
285     job->setRunExecutables(true); // startProcess and refuseRunningLocalBinaries test the case where this isn't set
286 
287     // Then it works :-)
288     QVERIFY2(job->exec(), qPrintable(job->errorString()));
289     QTRY_VERIFY(QFileInfo::exists(dir + QLatin1String("/dest"))); // TRY because CommandLineLauncherJob finishes immediately
290 #endif
291 }
292 
runNativeExecutable_data()293 void OpenUrlJobTest::runNativeExecutable_data()
294 {
295     QTest::addColumn<QString>("mimeType");
296     QTest::addColumn<bool>("withHandler");
297     QTest::addColumn<bool>("handlerRetVal");
298 
299     QTest::newRow("no_handler_x-sharedlib") << "application/x-sharedlib" << false << false;
300     QTest::newRow("handler_false_x-sharedlib") << "application/x-sharedlib" << true << false;
301     QTest::newRow("handler_true_x-sharedlib") << "application/x-sharedlib" << true << true;
302 
303     QTest::newRow("no_handler_x-executable") << "application/x-executable" << false << false;
304     QTest::newRow("handler_false_x-executable") << "application/x-executable" << true << false;
305     QTest::newRow("handler_true_x-executable") << "application/x-executable" << true << true;
306 }
307 
runNativeExecutable()308 void OpenUrlJobTest::runNativeExecutable()
309 {
310     QFETCH(QString, mimeType);
311     QFETCH(bool, withHandler);
312     QFETCH(bool, handlerRetVal);
313 
314 #ifdef Q_OS_UNIX
315     // Given an executable shell script that copies "src" to "dest" (we'll cheat with the MIME type to treat it like a native binary)
316     QTemporaryDir tempDir;
317     const QString dir = tempDir.path();
318     createSrcFile(dir + QLatin1String("/src"));
319     const QString scriptFile = dir + QLatin1String("/script.sh");
320     QFile file(scriptFile);
321     QVERIFY(file.open(QIODevice::WriteOnly));
322     file.write("#!/bin/sh\ncp src dest");
323     file.close();
324     // Note that it's missing executable permissions
325 
326     // When using OpenUrlJob to run the executable
327     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(scriptFile), mimeType, this);
328     job->setRunExecutables(true); // startProcess tests the case where this isn't set
329     job->setUiDelegate(new KJobUiDelegate);
330 
331     // Then --- it depends on what the user says via the handler
332     if (!withHandler) {
333         QVERIFY(!job->exec());
334         QCOMPARE((int)job->error(), (int)KJob::UserDefinedError);
335         QCOMPARE(job->errorString(), QStringLiteral("The program \"%1\" needs to have executable permission before it can be launched.").arg(scriptFile));
336     } else {
337         auto *handler = new MockUntrustedProgramHandler(job->uiDelegate());
338         handler->setRetVal(handlerRetVal);
339 
340         const bool success = job->exec();
341         if (handlerRetVal) {
342             QVERIFY(success);
343             QTRY_VERIFY(QFileInfo::exists(dir + QLatin1String("/dest"))); // TRY because CommandLineLauncherJob finishes immediately
344         } else {
345             QVERIFY(!success);
346             QCOMPARE((int)job->error(), (int)KIO::ERR_USER_CANCELED);
347         }
348     }
349 #endif
350 }
351 
openOrExecuteScript_data()352 void OpenUrlJobTest::openOrExecuteScript_data()
353 {
354     QTest::addColumn<QString>("dialogResult");
355 
356     QTest::newRow("execute_true") << "execute_true";
357     QTest::newRow("execute_false") << "execute_false";
358     QTest::newRow("canceled") << "canceled";
359 }
360 
openOrExecuteScript()361 void OpenUrlJobTest::openOrExecuteScript()
362 {
363 #ifdef Q_OS_UNIX
364     QFETCH(QString, dialogResult);
365 
366     // Given an executable shell script that copies "src" to "dest"
367     QTemporaryDir tempDir;
368     const QString dir = tempDir.path();
369     createSrcFile(dir + QLatin1String("/src"));
370     const QString scriptFile = dir + QLatin1String("/script.sh");
371     QFile file(scriptFile);
372     QVERIFY(file.open(QIODevice::WriteOnly));
373     file.write("#!/bin/sh\ncp src dest");
374     file.close();
375     // Set the executable bit, because OpenUrlJob will always open shell
376     // scripts that are not executable as text files
377     QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions()));
378 
379     // When using OpenUrlJob to open the script
380     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(scriptFile), QStringLiteral("application/x-shellscript"), this);
381     job->setShowOpenOrExecuteDialog(true);
382     job->setUiDelegate(new KJobUiDelegate);
383     auto *openOrExecuteFileHandler = new MockOpenOrExecuteHandler(job->uiDelegate());
384 
385     // Then --- it depends on what the user says via the handler
386     if (dialogResult == QLatin1String("execute_true")) {
387         job->setRunExecutables(false); // Overridden by the user's choice
388         openOrExecuteFileHandler->setExecuteFile(true);
389         QVERIFY(job->exec());
390         // TRY because CommandLineLauncherJob finishes immediately, and tempDir
391         // will go out of scope and get deleted before the copy operation actually finishes
392         QTRY_VERIFY(QFileInfo::exists(dir + QLatin1String("/dest")));
393     } else if (dialogResult == QLatin1String("execute_false")) {
394         job->setRunExecutables(true); // Overridden by the user's choice
395         openOrExecuteFileHandler->setExecuteFile(false);
396         QVERIFY(job->exec());
397         const QString testOpen = m_tempDir.path() + QLatin1String("/dest"); // see the .desktop file in writeApplicationDesktopFile
398         QTRY_VERIFY(QFileInfo::exists(testOpen));
399     } else if (dialogResult == QLatin1String("canceled")) {
400         openOrExecuteFileHandler->setCanceled();
401         QVERIFY(!job->exec());
402         QCOMPARE(job->error(), KIO::ERR_USER_CANCELED);
403     }
404 #endif
405 }
406 
openOrExecuteDesktop_data()407 void OpenUrlJobTest::openOrExecuteDesktop_data()
408 {
409     QTest::addColumn<QString>("dialogResult");
410 
411     QTest::newRow("execute_true") << "execute_true";
412     QTest::newRow("execute_false") << "execute_false";
413     QTest::newRow("canceled") << "canceled";
414 }
415 
openOrExecuteDesktop()416 void OpenUrlJobTest::openOrExecuteDesktop()
417 {
418 #ifdef Q_OS_UNIX
419     QFETCH(QString, dialogResult);
420 
421     // Given a .desktop file, with an Exec line that copies "src" to "dest"
422     QTemporaryDir tempDir;
423     const QString dir = tempDir.path();
424     const QString desktopFile = dir + QLatin1String("/testopenorexecute.desktop");
425     createSrcFile(dir + QLatin1String("/src"));
426     const QByteArray cmd("cp " + QFile::encodeName(dir) + "/src " + QFile::encodeName(dir) + "/dest-open-or-execute-desktop");
427     writeApplicationDesktopFile(desktopFile, cmd);
428     QFile file(desktopFile);
429     QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions())); // otherwise we'll get the untrusted program warning
430 
431     // When using OpenUrlJob to open the .desktop file
432     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(desktopFile), QStringLiteral("application/x-desktop"), this);
433     job->setShowOpenOrExecuteDialog(true);
434     job->setUiDelegate(new KJobUiDelegate);
435     auto *openOrExecuteFileHandler = new MockOpenOrExecuteHandler(job->uiDelegate());
436 
437     // Then --- it depends on what the user says via the handler
438     if (dialogResult == QLatin1String("execute_true")) {
439         job->setRunExecutables(false); // Overridden by the user's choice
440         openOrExecuteFileHandler->setExecuteFile(true);
441         QVERIFY2(job->exec(), qPrintable(job->errorString()));
442         // TRY because CommandLineLauncherJob finishes immediately, and tempDir
443         // will go out of scope and get deleted before the copy operation actually finishes
444         QTRY_VERIFY(QFileInfo::exists(dir + QLatin1String("/dest-open-or-execute-desktop")));
445     }
446     if (dialogResult == QLatin1String("execute_false")) {
447         job->setRunExecutables(true); // Overridden by the user's choice
448         openOrExecuteFileHandler->setExecuteFile(false);
449         QVERIFY2(job->exec(), qPrintable(job->errorString()));
450         const QString testOpen = m_tempDir.path() + QLatin1String("/dest"); // see the .desktop file in writeApplicationDesktopFile
451         QTRY_VERIFY(QFileInfo::exists(testOpen));
452     } else if (dialogResult == QLatin1String("canceled")) {
453         openOrExecuteFileHandler->setCanceled();
454         QVERIFY(!job->exec());
455         QCOMPARE(job->error(), KIO::ERR_USER_CANCELED);
456     }
457 #endif
458 }
459 
launchExternalBrowser_data()460 void OpenUrlJobTest::launchExternalBrowser_data()
461 {
462     QTest::addColumn<bool>("useBrowserApp");
463     QTest::addColumn<bool>("useSchemeHandler");
464 
465     QTest::newRow("browserapp") << true << false;
466     QTest::newRow("scheme_handler") << false << true;
467 }
468 
launchExternalBrowser()469 void OpenUrlJobTest::launchExternalBrowser()
470 {
471 #ifdef Q_OS_UNIX
472     QFETCH(bool, useBrowserApp);
473     QFETCH(bool, useSchemeHandler);
474 
475     QTemporaryDir tempDir;
476     const QString dir = tempDir.path();
477     createSrcFile(dir + QLatin1String("/src"));
478     const QString scriptFile = dir + QLatin1String("/browser.sh");
479     QFile file(scriptFile);
480     QVERIFY(file.open(QIODevice::WriteOnly));
481     file.write("#!/bin/sh\necho $1 > `dirname $0`/destbrowser");
482     file.close();
483     QVERIFY(file.setPermissions(QFile::ExeUser | file.permissions()));
484 
485     QUrl remoteImage("http://example.org/image.jpg");
486     if (useBrowserApp) {
487         KConfigGroup(KSharedConfig::openConfig(), "General").writeEntry("BrowserApplication", QString(QLatin1Char('!') + scriptFile));
488     } else if (useSchemeHandler) {
489         remoteImage.setScheme("scheme");
490     }
491 
492     // When using OpenUrlJob to run the script
493     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(remoteImage, this);
494 
495     // Then it works :-)
496     QVERIFY2(job->exec(), qPrintable(job->errorString()));
497     QString dest;
498     if (useBrowserApp) {
499         dest = dir + QLatin1String("/destbrowser");
500     } else if (useSchemeHandler) {
501         dest = m_tempDir.path() + QLatin1String("/dest"); // see the .desktop file in writeApplicationDesktopFile
502     }
503     QTRY_VERIFY(QFileInfo::exists(dest)); // TRY because CommandLineLauncherJob finishes immediately
504     QCOMPARE(readFile(dest), remoteImage.toString());
505 
506     // Restore settings
507     KConfigGroup(KSharedConfig::openConfig(), "General").deleteEntry("BrowserApplication");
508 #endif
509 }
510 
nonExistingFile()511 void OpenUrlJobTest::nonExistingFile()
512 {
513     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(QStringLiteral("/does/not/exist")), this);
514     QVERIFY(!job->exec());
515     QCOMPARE(job->error(), KIO::ERR_DOES_NOT_EXIST);
516     QCOMPARE(job->errorString(), "The file or folder /does/not/exist does not exist.");
517 }
518 
httpUrlWithKIO()519 void OpenUrlJobTest::httpUrlWithKIO()
520 {
521     // This tests the scanFileWithGet() code path
522     const QUrl url(QStringLiteral("http://www.google.com/"));
523     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, this);
524     job->setEnableExternalBrowser(false);
525     job->setFollowRedirections(false);
526     QVERIFY2(job->exec(), qPrintable(job->errorString()));
527 
528     // Then the service should be executed (which writes to "dest")
529     const QString dest = m_tempDir.path() + "/dest";
530     QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest));
531     QCOMPARE(readFile(dest), url.toString());
532 }
533 
ftpUrlWithKIO()534 void OpenUrlJobTest::ftpUrlWithKIO()
535 {
536     // This is just to test the statFile() code at least a bit
537     const QUrl url(QStringLiteral("ftp://localhost:2")); // unlikely that anything is running on that port
538     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(url, this);
539     QVERIFY(!job->exec());
540     QVERIFY(job->errorString() == QLatin1String("Could not connect to host localhost: Connection refused.")
541             || job->errorString() == QLatin1String("Could not connect to host localhost: Network unreachable."));
542 }
543 
takeOverAfterMimeTypeFound()544 void OpenUrlJobTest::takeOverAfterMimeTypeFound()
545 {
546     // Given a local image file
547     QTemporaryDir tempDir;
548     const QString srcDir = tempDir.path();
549     const QString srcFile = srcDir + QLatin1String("/image.jpg");
550     createSrcFile(srcFile);
551 
552     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(srcFile), this);
553     QString foundMime = QStringLiteral("NONE");
554     connect(job, &KIO::OpenUrlJob::mimeTypeFound, this, [&](const QString &mimeType) {
555         foundMime = mimeType;
556         job->kill();
557     });
558     QVERIFY(!job->exec());
559     QCOMPARE(job->error(), KJob::KilledJobError);
560     QCOMPARE(foundMime, "image/jpeg");
561 }
562 
runDesktopFileDirectly()563 void OpenUrlJobTest::runDesktopFileDirectly()
564 {
565     KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(m_fakeService), this);
566     job->setRunExecutables(true);
567     QVERIFY(job->exec());
568 
569     const QString dest = m_tempDir.path() + "/dest";
570     QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest));
571     QCOMPARE(readFile(dest), QString{});
572 }
573 
writeApplicationDesktopFile(const QString & filePath,const QByteArray & command)574 void OpenUrlJobTest::writeApplicationDesktopFile(const QString &filePath, const QByteArray &command)
575 {
576     KDesktopFile file(filePath);
577     KConfigGroup group = file.desktopGroup();
578     group.writeEntry("Name", "KRunUnittestService");
579     group.writeEntry("MimeType", "text/plain;application/x-shellscript;x-scheme-handler/scheme");
580     group.writeEntry("Type", "Application");
581     group.writeEntry("Exec", command);
582     QVERIFY(file.sync());
583 }
584