File indexing completed on 2024-09-15 09:23:55
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2014, 2020 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 "applicationlauncherjobtest.h" 0009 #include "applicationlauncherjob.h" 0010 #include <kprocessrunner_p.h> 0011 0012 #include "kiotesthelper.h" // createTestFile etc. 0013 #include "mockcoredelegateextensions.h" 0014 #include "mockguidelegateextensions.h" 0015 0016 #include <KConfigGroup> 0017 #include <KDesktopFile> 0018 #include <KJobUiDelegate> 0019 #include <KService> 0020 0021 #ifdef Q_OS_UNIX 0022 #include <signal.h> // kill 0023 #endif 0024 0025 #include <QRegularExpression> 0026 #include <QStandardPaths> 0027 #include <QTemporaryDir> 0028 #include <QTest> 0029 0030 QTEST_GUILESS_MAIN(ApplicationLauncherJobTest) 0031 0032 void ApplicationLauncherJobTest::initTestCase() 0033 { 0034 QStandardPaths::setTestModeEnabled(true); 0035 m_tempService = createTempService(); 0036 } 0037 0038 void ApplicationLauncherJobTest::cleanupTestCase() 0039 { 0040 std::for_each(m_filesToRemove.cbegin(), m_filesToRemove.cend(), [](const QString &f) { 0041 QFile::remove(f); 0042 }); 0043 } 0044 0045 static const char s_tempServiceName[] = "applicationlauncherjobtest_service.desktop"; 0046 0047 static void createSrcFile(const QString path) 0048 { 0049 QFile srcFile(path); 0050 QVERIFY2(srcFile.open(QFile::WriteOnly), qPrintable(srcFile.errorString())); 0051 srcFile.write("Hello world\n"); 0052 } 0053 0054 void ApplicationLauncherJobTest::startProcess_data() 0055 { 0056 QTest::addColumn<bool>("tempFile"); 0057 QTest::addColumn<bool>("useExec"); 0058 QTest::addColumn<int>("numFiles"); 0059 0060 QTest::newRow("1_file_exec") << false << true << 1; 0061 QTest::newRow("1_file_waitForStarted") << false << false << 1; 0062 QTest::newRow("1_tempfile_exec") << true << true << 1; 0063 QTest::newRow("1_tempfile_waitForStarted") << true << false << 1; 0064 0065 QTest::newRow("2_files_exec") << false << true << 2; 0066 QTest::newRow("2_files_waitForStarted") << false << false << 2; 0067 QTest::newRow("2_tempfiles_exec") << true << true << 2; 0068 QTest::newRow("2_tempfiles_waitForStarted") << true << false << 2; 0069 } 0070 0071 void ApplicationLauncherJobTest::startProcess() 0072 { 0073 QFETCH(bool, tempFile); 0074 QFETCH(bool, useExec); 0075 QFETCH(int, numFiles); 0076 0077 // Given a service desktop file and a number of source files 0078 QTemporaryDir tempDir; 0079 const QString srcDir = tempDir.path(); 0080 QList<QUrl> urls; 0081 for (int i = 0; i < numFiles; ++i) { 0082 const QString srcFile = srcDir + "/srcfile" + QString::number(i + 1); 0083 createSrcFile(srcFile); 0084 QVERIFY(QFile::exists(srcFile)); 0085 urls.append(QUrl::fromLocalFile(srcFile)); 0086 } 0087 0088 // When running a ApplicationLauncherJob 0089 KService::Ptr servicePtr(new KService(m_tempService)); 0090 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); 0091 job->setUrls(urls); 0092 if (tempFile) { 0093 job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); 0094 } 0095 if (useExec) { 0096 QVERIFY2(job->exec(), qPrintable(job->errorString())); 0097 } else { 0098 job->start(); 0099 QVERIFY(job->waitForStarted()); 0100 } 0101 const QVector<qint64> pids = job->pids(); 0102 0103 // Then the service should be executed (which copies the source file to "dest") 0104 QCOMPARE(pids.count(), numFiles); 0105 QVERIFY(!pids.contains(0)); 0106 for (int i = 0; i < numFiles; ++i) { 0107 const QString dest = srcDir + "/dest_srcfile" + QString::number(i + 1); 0108 QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); 0109 QVERIFY(QFile::exists(srcDir + "/srcfile" + QString::number(i + 1))); // if tempfile is true, kioexec will delete it... in 3 minutes. 0110 QVERIFY(QFile::remove(dest)); // cleanup 0111 } 0112 0113 #ifdef Q_OS_UNIX 0114 // Kill the running kioexec processes 0115 for (qint64 pid : pids) { 0116 ::kill(pid, SIGTERM); 0117 } 0118 #endif 0119 0120 // The kioexec processes that are waiting for 3 minutes and got killed above, 0121 // will now trigger KProcessRunner::slotProcessError, KProcessRunner::slotProcessExited and delete the KProcessRunner. 0122 // We wait for that to happen otherwise it gets confusing to see that output from later tests. 0123 QTRY_COMPARE(KProcessRunner::instanceCount(), 0); 0124 } 0125 0126 void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile_data() 0127 { 0128 QTest::addColumn<bool>("withHandler"); 0129 QTest::addColumn<bool>("handlerRetVal"); 0130 QTest::addColumn<bool>("useExec"); 0131 QTest::addColumn<bool>("serviceAction"); 0132 0133 QTest::newRow("no_handler_exec") << false << false << true << false; 0134 QTest::newRow("handler_false_exec") << true << false << true << false; 0135 QTest::newRow("handler_true_exec") << true << true << true << false; 0136 QTest::newRow("no_handler_waitForStarted") << false << false << false << false; 0137 QTest::newRow("handler_false_waitForStarted") << true << false << false << false; 0138 QTest::newRow("handler_true_waitForStarted") << true << true << false << false; 0139 0140 // Test the ctor that takes a KServiceAction 0141 QTest::newRow("serviceaction_no_handler_exec") << false << false << true << true; 0142 QTest::newRow("serviceaction_handler_false_exec") << true << false << true << true; 0143 QTest::newRow("serviceaction_handler_true_exec") << true << true << true << true; 0144 QTest::newRow("serviceaction_no_handler_waitForStarted") << false << false << false << true; 0145 QTest::newRow("serviceaction_handler_false_waitForStarted") << true << false << false << true; 0146 QTest::newRow("serviceaction_handler_true_waitForStarted") << true << true << false << true; 0147 } 0148 0149 void ApplicationLauncherJobTest::shouldFailOnNonExecutableDesktopFile() 0150 { 0151 QFETCH(bool, useExec); 0152 QFETCH(bool, withHandler); 0153 QFETCH(bool, handlerRetVal); 0154 QFETCH(bool, serviceAction); 0155 0156 // Given a .desktop file in a temporary directory (outside the trusted paths) 0157 QTemporaryDir tempDir; 0158 const QString srcDir = tempDir.path(); 0159 const QString desktopFilePath = srcDir + "/shouldfail.desktop"; 0160 writeTempServiceDesktopFile(desktopFilePath); 0161 m_filesToRemove.append(desktopFilePath); 0162 0163 const QString srcFile = srcDir + "/srcfile"; 0164 createSrcFile(srcFile); 0165 const QList<QUrl> urls{QUrl::fromLocalFile(srcFile)}; 0166 KService::Ptr servicePtr(new KService(desktopFilePath)); 0167 0168 KIO::ApplicationLauncherJob *job = 0169 !serviceAction ? new KIO::ApplicationLauncherJob(servicePtr, this) : new KIO::ApplicationLauncherJob(servicePtr->actions().at(0), this); 0170 0171 job->setUrls(urls); 0172 job->setUiDelegate(new KJobUiDelegate); 0173 MockUntrustedProgramHandler *handler = withHandler ? new MockUntrustedProgramHandler(job->uiDelegate()) : nullptr; 0174 if (handler) { 0175 handler->setRetVal(handlerRetVal); 0176 } 0177 bool success; 0178 if (useExec) { 0179 success = job->exec(); 0180 } else { 0181 job->start(); 0182 success = job->waitForStarted(); 0183 } 0184 if (!withHandler) { 0185 QVERIFY(!success); 0186 QCOMPARE(job->error(), KJob::UserDefinedError); 0187 QCOMPARE(job->errorString(), QStringLiteral("You are not authorized to execute this file.")); 0188 } else { 0189 if (handlerRetVal) { 0190 QVERIFY(success); 0191 // check that the handler was called (before any event loop deletes the job...) 0192 QCOMPARE(handler->m_calls.count(), 1); 0193 QCOMPARE(handler->m_calls.at(0), QStringLiteral("KRunUnittestService")); 0194 0195 const QString dest = srcDir + (!serviceAction ? QLatin1String{"/dest_srcfile"} : QLatin1String{"/actionDest_srcfile"}); 0196 QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); 0197 0198 // The actual shell process will race against the deletion of the QTemporaryDir, 0199 // so don't be surprised by stderr like getcwd: cannot access parent directories: No such file or directory 0200 QTest::qWait(50); // this helps a bit 0201 } else { 0202 QVERIFY(!success); 0203 QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); 0204 } 0205 } 0206 } 0207 0208 void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable_data() 0209 { 0210 QTest::addColumn<bool>("tempFile"); 0211 QTest::addColumn<bool>("fullPath"); 0212 0213 QTest::newRow("file") << false << false; 0214 QTest::newRow("tempFile") << true << false; 0215 QTest::newRow("file_fullPath") << false << true; 0216 QTest::newRow("tempFile_fullPath") << true << true; 0217 } 0218 0219 void ApplicationLauncherJobTest::shouldFailOnNonExistingExecutable() 0220 { 0221 QFETCH(bool, tempFile); 0222 QFETCH(bool, fullPath); 0223 0224 const QString desktopFilePath = 0225 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/non_existing_executable.desktop"); 0226 KDesktopFile file(desktopFilePath); 0227 KConfigGroup group = file.desktopGroup(); 0228 group.writeEntry("Name", "KRunUnittestService"); 0229 group.writeEntry("Type", "Service"); 0230 if (fullPath) { 0231 group.writeEntry("Exec", "/usr/bin/does_not_exist %f %d/dest_%n"); 0232 } else { 0233 group.writeEntry("Exec", "does_not_exist %f %d/dest_%n"); 0234 } 0235 file.sync(); 0236 0237 KService::Ptr servicePtr(new KService(desktopFilePath)); 0238 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); 0239 job->setUrls({QUrl::fromLocalFile(desktopFilePath)}); // just to have one URL as argument, as the desktop file expects 0240 if (tempFile) { 0241 job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles); 0242 } 0243 QTest::ignoreMessage(QtWarningMsg, QRegularExpression("Could not find the program '.*'")); // from KProcessRunner 0244 QVERIFY(!job->exec()); 0245 QCOMPARE(job->error(), KJob::UserDefinedError); 0246 if (fullPath) { 0247 QCOMPARE(job->errorString(), QStringLiteral("Could not find the program '/usr/bin/does_not_exist'")); 0248 } else { 0249 QCOMPARE(job->errorString(), QStringLiteral("Could not find the program 'does_not_exist'")); 0250 } 0251 QFile::remove(desktopFilePath); 0252 } 0253 0254 void ApplicationLauncherJobTest::shouldFailOnInvalidService() 0255 { 0256 const QString desktopFilePath = 0257 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); 0258 KDesktopFile file(desktopFilePath); 0259 KConfigGroup group = file.desktopGroup(); 0260 group.writeEntry("Name", "KRunUnittestService"); 0261 group.writeEntry("Type", "NoSuchType"); 0262 group.writeEntry("Exec", "does_not_exist"); 0263 file.sync(); 0264 0265 QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file \".*\" has Type.*\"NoSuchType\" instead of \"Application\" or \"Service\"")); 0266 KService::Ptr servicePtr(new KService(desktopFilePath)); 0267 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); 0268 QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The desktop entry file.*is not valid")); // from KProcessRunner 0269 QVERIFY(!job->exec()); 0270 QCOMPARE(job->error(), KJob::UserDefinedError); 0271 const QString expectedError = QStringLiteral("The desktop entry file\n%1\nis not valid.").arg(desktopFilePath); 0272 QCOMPARE(job->errorString(), expectedError); 0273 0274 QFile::remove(desktopFilePath); 0275 } 0276 0277 void ApplicationLauncherJobTest::shouldFailOnServiceWithNoExec() 0278 { 0279 const QString desktopFilePath = 0280 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); 0281 KDesktopFile file(desktopFilePath); 0282 KConfigGroup group = file.desktopGroup(); 0283 group.writeEntry("Name", "KRunUnittestServiceNoExec"); 0284 group.writeEntry("Type", "Service"); 0285 file.sync(); 0286 0287 QTest::ignoreMessage(QtWarningMsg, qPrintable(QString("No Exec field in \"%1\"").arg(desktopFilePath))); // from KService 0288 KService::Ptr servicePtr(new KService(desktopFilePath)); 0289 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); 0290 QTest::ignoreMessage(QtWarningMsg, QRegularExpression("No Exec field in .*")); // from KProcessRunner 0291 QVERIFY(!job->exec()); 0292 QCOMPARE(job->error(), KJob::UserDefinedError); 0293 QCOMPARE(job->errorString(), QStringLiteral("No Exec field in %1").arg(desktopFilePath)); 0294 0295 QFile::remove(desktopFilePath); 0296 } 0297 0298 void ApplicationLauncherJobTest::shouldFailOnExecutableWithoutPermissions() 0299 { 0300 #ifdef Q_OS_UNIX 0301 // Given an executable shell script that copies "src" to "dest" (we'll cheat with the MIME type to treat it like a native binary) 0302 QTemporaryDir tempDir; 0303 const QString dir = tempDir.path(); 0304 const QString scriptFilePath = dir + QStringLiteral("/script.sh"); 0305 QFile scriptFile(scriptFilePath); 0306 QVERIFY(scriptFile.open(QIODevice::WriteOnly)); 0307 scriptFile.write("#!/bin/sh\ncp src dest"); 0308 scriptFile.close(); 0309 // Note that it's missing executable permissions 0310 0311 const QString desktopFilePath = 0312 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/invalid_service.desktop"); 0313 KDesktopFile file(desktopFilePath); 0314 KConfigGroup group = file.desktopGroup(); 0315 group.writeEntry("Name", "KRunUnittestServiceNoPermission"); 0316 group.writeEntry("Type", "Service"); 0317 group.writeEntry("Exec", scriptFilePath); 0318 file.sync(); 0319 0320 KService::Ptr servicePtr(new KService(desktopFilePath)); 0321 KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(servicePtr, this); 0322 QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The program .* is missing executable permissions.")); // from KProcessRunner 0323 QVERIFY(!job->exec()); 0324 QCOMPARE(job->error(), KJob::UserDefinedError); 0325 QCOMPARE(job->errorString(), QStringLiteral("The program '%1' is missing executable permissions.").arg(scriptFilePath)); 0326 0327 QFile::remove(desktopFilePath); 0328 #else 0329 QSKIP("This test is not run on Windows"); 0330 #endif 0331 } 0332 0333 void ApplicationLauncherJobTest::showOpenWithDialog_data() 0334 { 0335 QTest::addColumn<bool>("withHandler"); 0336 QTest::addColumn<bool>("handlerRetVal"); 0337 QTest::addColumn<bool>("nullService"); 0338 0339 for (bool nullService : {false, true}) { 0340 const char *nullServiceStr = nullService ? "pass_null_service" : "default_ctor"; 0341 QTest::addRow("without_handler_%s", nullServiceStr) << false << false << nullService; 0342 QTest::addRow("false_canceled_%s", nullServiceStr) << true << false << nullService; 0343 QTest::addRow("true_service_selected_%s", nullServiceStr) << true << true << nullService; 0344 } 0345 } 0346 0347 void ApplicationLauncherJobTest::showOpenWithDialog() 0348 { 0349 #ifdef Q_OS_UNIX 0350 QFETCH(bool, withHandler); 0351 QFETCH(bool, handlerRetVal); 0352 QFETCH(bool, nullService); 0353 0354 // Given a local text file (we could test multiple files, too...) 0355 QTemporaryDir tempDir; 0356 const QString srcDir = tempDir.path(); 0357 const QString srcFile = srcDir + QLatin1String("/file.txt"); 0358 createSrcFile(srcFile); 0359 0360 KIO::ApplicationLauncherJob *job = nullService ? new KIO::ApplicationLauncherJob(KService::Ptr(), this) : new KIO::ApplicationLauncherJob(this); 0361 job->setUrls({QUrl::fromLocalFile(srcFile)}); 0362 job->setUiDelegate(new KJobUiDelegate); 0363 MockOpenWithHandler *openWithHandler = withHandler ? new MockOpenWithHandler(job->uiDelegate()) : nullptr; 0364 KService::Ptr service = KService::serviceByDesktopName(QString(s_tempServiceName).remove(".desktop")); 0365 QVERIFY(service); 0366 if (withHandler) { 0367 openWithHandler->m_chosenService = handlerRetVal ? service : KService::Ptr{}; 0368 } 0369 0370 const bool success = job->exec(); 0371 0372 // Then --- it depends on what the user says via the handler 0373 if (withHandler) { 0374 QCOMPARE(openWithHandler->m_urls.count(), 1); 0375 QCOMPARE(openWithHandler->m_mimeTypes.count(), 1); 0376 QCOMPARE(openWithHandler->m_mimeTypes.at(0), QStringLiteral("text/plain")); // the job doesn't have the information 0377 if (handlerRetVal) { 0378 QVERIFY2(success, qPrintable(job->errorString())); 0379 // If the user chose a service, it should be executed (it writes to "dest") 0380 const QString dest = srcDir + "/dest_file.txt"; 0381 QTRY_VERIFY2(QFile::exists(dest), qPrintable(dest)); 0382 } else { 0383 QVERIFY(!success); 0384 QCOMPARE(job->error(), KIO::ERR_USER_CANCELED); 0385 } 0386 } else { 0387 QVERIFY(!success); 0388 QCOMPARE(job->error(), KJob::UserDefinedError); 0389 } 0390 #else 0391 QSKIP("Test skipped on Windows because the code ends up in QDesktopServices::openUrl"); 0392 #endif 0393 } 0394 0395 void ApplicationLauncherJobTest::writeTempServiceDesktopFile(const QString &filePath) 0396 { 0397 if (QFile::exists(filePath)) { 0398 return; 0399 } 0400 0401 KDesktopFile file(filePath); 0402 KConfigGroup group = file.desktopGroup(); 0403 group.writeEntry("Name", "KRunUnittestService"); 0404 group.writeEntry("Type", "Service"); 0405 #ifdef Q_OS_WIN 0406 group.writeEntry("Exec", "copy.exe %f %d/dest_%n"); 0407 #else 0408 group.writeEntry("Exec", "cd %d ; cp %f %d/dest_%n"); // cd is just to show that we can't do QFile::exists(binary) 0409 #endif 0410 0411 // Add a desktop Action 0412 group.writeEntry("Actions", "SubServiceAction"); 0413 0414 KConfigGroup actGroup = file.actionGroup(QStringLiteral("SubServiceAction")); 0415 actGroup.writeEntry("Name", "ServiceActionTest"); 0416 #ifdef Q_OS_WIN 0417 actGroup.writeEntry("Exec", "copy.exe %f %d/actionDest_%n"); 0418 #else 0419 actGroup.writeEntry("Exec", "cd %d ; cp %f %d/actionDest_%n"); // cd is just to show that we can't do QFile::exists(binary) 0420 #endif 0421 0422 file.sync(); 0423 } 0424 0425 QString ApplicationLauncherJobTest::createTempService() 0426 { 0427 const QString fileName = s_tempServiceName; 0428 const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kservices5/") + fileName; 0429 writeTempServiceDesktopFile(fakeService); 0430 m_filesToRemove.append(fakeService); 0431 return fakeService; 0432 } 0433 0434 #include "moc_applicationlauncherjobtest.cpp"