File indexing completed on 2024-03-24 03:59:38

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2008 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 "kmimeassociations_p.h"
0009 #include "ksycoca_p.h"
0010 #include "setupxdgdirs.h"
0011 #include <KConfigGroup>
0012 #include <KDesktopFile>
0013 #include <QDebug>
0014 #include <QDir>
0015 #include <QMimeDatabase>
0016 #include <QMimeType>
0017 #include <QSignalSpy>
0018 #include <QTemporaryDir>
0019 #include <QTemporaryFile>
0020 #include <QTest>
0021 #include <kapplicationtrader.h>
0022 #include <kbuildsycoca_p.h>
0023 #include <kservicefactory_p.h>
0024 #include <ksycoca.h>
0025 
0026 using namespace Qt::StringLiterals;
0027 
0028 // We need a factory that returns the same KService::Ptr every time it's asked for a given service.
0029 // Otherwise the changes to the service's serviceTypes by KMimeAssociationsTest have no effect
0030 class FakeServiceFactory : public KServiceFactory
0031 {
0032 public:
0033     FakeServiceFactory(KSycoca *db)
0034         : KServiceFactory(db)
0035     {
0036     }
0037     ~FakeServiceFactory() override;
0038 
0039     KService::Ptr findServiceByMenuId(const QString &name) override
0040     {
0041         // qDebug() << name;
0042         KService::Ptr result = m_cache.value(name);
0043         if (!result) {
0044             result = KServiceFactory::findServiceByMenuId(name);
0045             m_cache.insert(name, result);
0046         }
0047         // qDebug() << name << result.data();
0048         return result;
0049     }
0050     KService::Ptr findServiceByDesktopPath(const QString &name) override
0051     {
0052         KService::Ptr result = m_cache.value(name); // yeah, same cache, I don't care :)
0053         if (!result) {
0054             result = KServiceFactory::findServiceByDesktopPath(name);
0055             m_cache.insert(name, result);
0056         }
0057         return result;
0058     }
0059 
0060 private:
0061     QMap<QString, KService::Ptr> m_cache;
0062 };
0063 
0064 static QString menusDir()
0065 {
0066     return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String{"/menus"};
0067 }
0068 
0069 // Helper method for all the trader tests, comes from kmimetypetest.cpp
0070 static bool offerListHasService(const KService::List &offers, const QString &entryPath, bool expected /* if set, show error if not found */)
0071 {
0072     // ksycoca resolves to canonical paths, so do it here as well
0073     const QString realPath = QFileInfo(entryPath).canonicalFilePath();
0074     Q_ASSERT(!realPath.isEmpty());
0075 
0076     bool found = false;
0077     for (const KService::Ptr &serv : offers) {
0078         if (serv->entryPath() == realPath) {
0079             if (found) { // should be there only once
0080                 qWarning("ERROR: %s was found twice in the list", qPrintable(realPath));
0081                 return false; // make test fail
0082             }
0083             found = true;
0084         }
0085     }
0086     if (!found && expected) {
0087         qWarning() << "ERROR:" << realPath << "not found in offer list. Here's the full list:";
0088         for (const KService::Ptr &serv : offers) {
0089             qDebug() << serv->entryPath();
0090         }
0091     }
0092     return found;
0093 }
0094 
0095 static void writeAppDesktopFile(const QString &path, const QStringList &mimeTypes, int initialPreference = 1)
0096 {
0097     KDesktopFile file(path);
0098     KConfigGroup group = file.desktopGroup();
0099     group.writeEntry("Name", "FakeApplication");
0100     group.writeEntry("Type", "Application");
0101     group.writeEntry("Exec", "ls");
0102     group.writeEntry("OnlyShowIn", "KDE;UDE");
0103     group.writeEntry("NotShowIn", "GNOME");
0104     group.writeEntry("InitialPreference", initialPreference);
0105     group.writeXdgListEntry("MimeType", mimeTypes);
0106 }
0107 
0108 static void writeNonKDEAppDesktopFile(const QString &path, const QStringList &mimeTypes) // bug 427469
0109 {
0110     KDesktopFile file(path);
0111     KConfigGroup group = file.desktopGroup();
0112     group.writeEntry("Name", "FakeApplication");
0113     group.writeEntry("Type", "Application");
0114     group.writeEntry("Exec", "ls");
0115     group.writeEntry("NotShowIn", "KDE");
0116     group.writeXdgListEntry("MimeType", mimeTypes);
0117 }
0118 
0119 /**
0120  * This unit test verifies the parsing of mimeapps.list files, both directly
0121  * and via kbuildsycoca (and making trader queries).
0122  */
0123 class KMimeAssociationsTest : public QObject
0124 {
0125     Q_OBJECT
0126 private Q_SLOTS:
0127     void initTestCase()
0128     {
0129         setupXdgDirs();
0130         QStandardPaths::setTestModeEnabled(true);
0131         // The Plasma bit makes no sense, but this is just to test that this is treated as a colon-separated list
0132         qputenv("XDG_CURRENT_DESKTOP", "KDE:Plasma");
0133 
0134         m_localConfig = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/');
0135         QDir(m_localConfig).removeRecursively();
0136         QVERIFY(QDir().mkpath(m_localConfig));
0137         m_localApps = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/');
0138         QDir(m_localApps).removeRecursively();
0139         QVERIFY(QDir().mkpath(m_localApps));
0140         QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/');
0141         QDir(cacheDir).removeRecursively();
0142 
0143         QDir(menusDir()).removeRecursively();
0144         QDir(menusDir()).mkpath(QStringLiteral("."));
0145         QFile::copy(QFINDTESTDATA("test-applications.menu"), menusDir() + QLatin1String("/applications.menu"));
0146 
0147         // Create fake application (associated with text/plain in mimeapps.list)
0148         fakeTextApplication = m_localApps + QLatin1String{"faketextapplication.desktop"};
0149         writeAppDesktopFile(fakeTextApplication, QStringList() << QStringLiteral("text/plain"));
0150 
0151         // Create fake application (associated with text/plain in mimeapps.list)
0152         fakeTextApplicationPrefixed = m_localApps + QLatin1String{"fakepfx/faketextapplicationpfx.desktop"};
0153         writeAppDesktopFile(fakeTextApplicationPrefixed, QStringList() << QStringLiteral("text/plain"));
0154 
0155         // A fake "default" application for text/plain (high initial preference, but not in mimeapps.list)
0156         fakeDefaultTextApplication = m_localApps + QLatin1String{"fakedefaulttextapplication.desktop"};
0157         writeAppDesktopFile(fakeDefaultTextApplication, QStringList() << QStringLiteral("text/plain"), 9);
0158 
0159         // An app (like emacs) listing explicitly the derived mimetype (c-src); not in mimeapps.list
0160         // This interacted badly with mimeapps.list listing another app for text/plain, but the
0161         // lookup found this app first, due to c-src. The fix: ignoring derived mimetypes when
0162         // the base mimetype is already listed.
0163         //
0164         // Also include aliases (msword), to check they don't cancel each other out.
0165         fakeCSrcApplication = m_localApps + QLatin1String{"fakecsrcmswordapplication.desktop"};
0166         writeAppDesktopFile(fakeCSrcApplication,
0167                             QStringList() << QStringLiteral("text/plain") << QStringLiteral("text/c-src") << QStringLiteral("application/vnd.ms-word")
0168                                           << QStringLiteral("application/msword"),
0169                             8);
0170 
0171         fakeJpegApplication = m_localApps + QLatin1String{"fakejpegapplication.desktop"};
0172         writeAppDesktopFile(fakeJpegApplication, QStringList() << QStringLiteral("image/jpeg"));
0173 
0174         fakeArkApplication = m_localApps + QLatin1String{"fakearkapplication.desktop"};
0175         writeAppDesktopFile(fakeArkApplication, QStringList() << QStringLiteral("application/zip"));
0176 
0177         fakeHtmlApplication = m_localApps + QLatin1String{"fakehtmlapplication.desktop"};
0178         writeAppDesktopFile(fakeHtmlApplication, QStringList() << QStringLiteral("text/html"));
0179 
0180         fakeHtmlApplicationPrefixed = m_localApps + QLatin1String{"fakepfx/fakehtmlapplicationpfx.desktop"};
0181         writeAppDesktopFile(fakeHtmlApplicationPrefixed, QStringList() << QStringLiteral("text/html"));
0182 
0183         fakeOktetaApplication = m_localApps + QLatin1String{"fakeoktetaapplication.desktop"};
0184         writeAppDesktopFile(fakeOktetaApplication, QStringList() << QStringLiteral("application/octet-stream"));
0185 
0186         const QString fakeGnomeRoller = m_localApps + QLatin1String{"fake.org.gnome.FileRoller.desktop"};
0187         writeNonKDEAppDesktopFile(fakeGnomeRoller, QStringList() << QStringLiteral("application/x-7z-compressed"));
0188 
0189         const QString fakeNautilus = m_localApps + QLatin1String{"fake.org.gnome.Nautilus.desktop"};
0190         writeNonKDEAppDesktopFile(fakeNautilus, QStringList() << QStringLiteral("application/x-7z-compressed"));
0191 
0192         // Update ksycoca in ~/.qttest after creating the above
0193         runKBuildSycoca();
0194 
0195         // Create factory on the heap and don't delete it. This must happen after
0196         // Sycoca is built, in case it did not exist before.
0197         // It registers to KSycoca, which deletes it at end of program execution.
0198         KServiceFactory *factory = new FakeServiceFactory(KSycoca::self());
0199         KSycocaPrivate::self()->m_serviceFactory = factory;
0200         QCOMPARE(KSycocaPrivate::self()->serviceFactory(), factory);
0201 
0202         // For debugging: print all services and their storageId
0203 #if 0
0204         const KService::List lst = KService::allServices();
0205         QVERIFY(!lst.isEmpty());
0206         for (const KService::Ptr &serv : lst) {
0207             qDebug() << serv->entryPath() << serv->storageId() /*<< serv->desktopEntryName()*/;
0208         }
0209 #endif
0210 
0211         KService::Ptr fakeApplicationService = KService::serviceByStorageId(QStringLiteral("faketextapplication.desktop"));
0212         QVERIFY(fakeApplicationService);
0213 
0214         m_mimeAppsFileContents =
0215             "[Added Associations]\n"
0216             "image/jpeg=fakejpegapplication.desktop;\n"
0217             "text/html=fakehtmlapplication.desktop;fakehtmlapplicationpfx.desktop;\n"
0218             "text/plain=fakepfx-faketextapplicationpfx.desktop;gvim.desktop;wine.desktop;idontexist.desktop;\n"
0219             // test alias resolution
0220             "application/x-pdf=fakejpegapplication.desktop;\n"
0221             // test x-scheme-handler (#358159) (missing trailing ';' as per xdg-mime bug...)
0222             "x-scheme-handler/mailto=faketextapplication.desktop\n"
0223             // test association with octet-stream (#425154)
0224             "application/octet-stream=fakeoktetaapplication.desktop\n"
0225             // test a non-kde app (#427469)
0226             "application/x-7z-compressed=fake.org.gnome.FileRoller.desktop;\n"
0227             "[Added KParts/ReadOnlyPart Associations]\n"
0228             "text/plain=katepart.desktop;\n"
0229             "[Removed Associations]\n"
0230             "image/jpeg=firefox.desktop;\n"
0231             "text/html=gvim.desktop;abiword.desktop;\n"
0232             "[Default Applications]\n"
0233             "text/plain=faketextapplication.desktop;second-faketextapplicationpfx.desktop\n";
0234         // Expected results
0235         preferredApps[QStringLiteral("image/jpeg")] << QStringLiteral("fakejpegapplication.desktop");
0236         preferredApps[QStringLiteral("application/pdf")] << QStringLiteral("fakejpegapplication.desktop");
0237         preferredApps[QStringLiteral("text/plain")] << QStringLiteral("faketextapplication.desktop") << QStringLiteral("second-faketextapplicationpfx.desktop")
0238                                                     << QStringLiteral("fakepfx-faketextapplicationpfx.desktop") << QStringLiteral("gvim.desktop");
0239         preferredApps[QStringLiteral("text/x-csrc")] << QStringLiteral("faketextapplication.desktop")
0240                                                      << QStringLiteral("fakepfx-faketextapplicationpfx.desktop") << QStringLiteral("gvim.desktop");
0241         preferredApps[QStringLiteral("text/html")] << QStringLiteral("fakehtmlapplication.desktop") << QStringLiteral("fakepfx-fakehtmlapplicationpfx.desktop");
0242         preferredApps[QStringLiteral("application/msword")] << QStringLiteral("fakecsrcmswordapplication.desktop");
0243         preferredApps[QStringLiteral("x-scheme-handler/mailto")] << QStringLiteral("faketextapplication.desktop");
0244         preferredApps[QStringLiteral("text/x-python")] << QStringLiteral("faketextapplication.desktop");
0245         preferredApps[QStringLiteral("application/x-7z-compressed")] << QStringLiteral("fake.org.gnome.FileRoller.desktop");
0246         removedApps[QStringLiteral("application/x-7z-compressed")] << QStringLiteral("fake.org.gnome.Nautilus.desktop");
0247         removedApps[QStringLiteral("image/jpeg")] << QStringLiteral("firefox.desktop");
0248         removedApps[QStringLiteral("text/html")] << QStringLiteral("gvim.desktop") << QStringLiteral("abiword.desktop");
0249 
0250         // Clean-up non-existing apps
0251         removeNonExisting(preferredApps);
0252         removeNonExisting(removedApps);
0253     }
0254 
0255     void cleanupTestCase()
0256     {
0257         QFile::remove(m_localConfig + QLatin1String{"/mimeapps.list"});
0258         runKBuildSycoca();
0259     }
0260 
0261     void testDefaultInitialPreference()
0262     {
0263         KService::Ptr service = KApplicationTrader::preferredService(QStringLiteral("text/plain"));
0264         QCOMPARE(service->entryPath(), fakeDefaultTextApplication);
0265     }
0266 
0267     void testParseSingleFile()
0268     {
0269         KOfferHash offerHash;
0270         KMimeAssociations parser(offerHash, KSycocaPrivate::self()->serviceFactory());
0271 
0272         QTemporaryDir tempDir;
0273         QVERIFY(tempDir.isValid());
0274 
0275         QFile tempFile(tempDir.path() + QLatin1String{"/mimeapps.list"});
0276         QVERIFY(tempFile.open(QIODevice::WriteOnly));
0277         tempFile.write(m_mimeAppsFileContents);
0278         const QString fileName = tempFile.fileName();
0279         tempFile.close();
0280 
0281         // QTest::ignoreMessage(QtDebugMsg, "findServiceByDesktopPath: idontexist.desktop not found");
0282         parser.parseMimeAppsList(fileName, 100);
0283 
0284         for (auto it = preferredApps.cbegin(), endIt = preferredApps.cend(); it != endIt; ++it) {
0285             const QString mime = it.key();
0286             // The data for derived types and aliases isn't for this test (which only looks at mimeapps.list)
0287             if (mime == QLatin1String("text/x-csrc") //
0288                 || mime == QLatin1String("text/x-python") //
0289                 || mime == QLatin1String("application/msword")) {
0290                 continue;
0291             }
0292             const QList<KServiceOffer> offers = offerHash.offersFor(mime);
0293             for (const QString &service : it.value()) {
0294                 KService::Ptr serv = KService::serviceByStorageId(service);
0295                 if (serv && !offersContains(offers, serv)) {
0296                     qDebug() << "expected offer" << serv->entryPath() << "not in offers for" << mime << ":";
0297                     for (const KServiceOffer &offer : offers) {
0298                         qDebug() << offer.service()->storageId();
0299                     }
0300                     QFAIL("offer does not have servicetype");
0301                 }
0302             }
0303         }
0304 
0305         for (auto it = removedApps.cbegin(), end = removedApps.cend(); it != end; ++it) {
0306             const QString mime = it.key();
0307             const QList<KServiceOffer> offers = offerHash.offersFor(mime);
0308             for (const QString &service : it.value()) {
0309                 KService::Ptr serv = KService::serviceByStorageId(service);
0310                 if (serv && offersContains(offers, serv)) {
0311                     // qDebug() << serv.data() << serv->entryPath() << "does not have" << mime;
0312                     QFAIL("offer should not have servicetype");
0313                 }
0314             }
0315         }
0316     }
0317 
0318     void testGlobalAndLocalFiles()
0319     {
0320         KOfferHash offerHash;
0321         KMimeAssociations parser(offerHash, KSycocaPrivate::self()->serviceFactory());
0322 
0323         // Write global file
0324         QTemporaryDir tempDirGlobal;
0325         QVERIFY(tempDirGlobal.isValid());
0326 
0327         QFile tempFileGlobal(tempDirGlobal.path() + QLatin1String{"/mimeapps.list"});
0328         QVERIFY(tempFileGlobal.open(QIODevice::WriteOnly));
0329         QByteArray globalAppsFileContents =
0330             "[Added Associations]\n"
0331             "image/jpeg=firefox.desktop;\n" // removed by local config
0332             "text/html=firefox.desktop;\n" // mdv
0333             "image/png=fakejpegapplication.desktop;\n";
0334         tempFileGlobal.write(globalAppsFileContents);
0335         const QString globalFileName = tempFileGlobal.fileName();
0336         tempFileGlobal.close();
0337 
0338         // We didn't keep it, so we need to write the local file again
0339         QTemporaryDir tempDir;
0340         QVERIFY(tempDir.isValid());
0341 
0342         QFile tempFile(tempDir.path() + QLatin1String{"/mimeapps.list"});
0343         QVERIFY(tempFile.open(QIODevice::WriteOnly));
0344         tempFile.write(m_mimeAppsFileContents);
0345         const QString fileName = tempFile.fileName();
0346         tempFile.close();
0347 
0348         parser.parseMimeAppsList(globalFileName, 1000);
0349         parser.parseMimeAppsList(fileName, 1050); // += 50 is correct.
0350 
0351         QList<KServiceOffer> offers = offerHash.offersFor(QStringLiteral("image/jpeg"));
0352         std::stable_sort(offers.begin(), offers.end()); // like kbuildservicefactory.cpp does
0353         const QStringList expectedJpegApps = preferredApps[QStringLiteral("image/jpeg")];
0354         QCOMPARE(assembleOffers(offers), expectedJpegApps);
0355 
0356         offers = offerHash.offersFor(QStringLiteral("text/html"));
0357         std::stable_sort(offers.begin(), offers.end());
0358         QStringList textHtmlApps = preferredApps[QStringLiteral("text/html")];
0359         if (KService::serviceByStorageId(QStringLiteral("firefox.desktop"))) {
0360             textHtmlApps.append(QStringLiteral("firefox.desktop"));
0361         }
0362         qDebug() << assembleOffers(offers);
0363         QCOMPARE(assembleOffers(offers), textHtmlApps);
0364 
0365         offers = offerHash.offersFor(QStringLiteral("image/png"));
0366         std::stable_sort(offers.begin(), offers.end());
0367         QCOMPARE(assembleOffers(offers), QStringList() << QStringLiteral("fakejpegapplication.desktop"));
0368     }
0369 
0370     void testSetupRealFile()
0371     {
0372         writeToMimeApps(m_mimeAppsFileContents);
0373 
0374         // Test a trader query
0375         KService::List offers = KApplicationTrader::queryByMimeType(QStringLiteral("image/jpeg"));
0376         QVERIFY(!offers.isEmpty());
0377         QCOMPARE(offers.first()->storageId(), QStringLiteral("fakejpegapplication.desktop"));
0378 
0379         // Now the generic variant of the above test:
0380         // for each mimetype, check that the preferred apps are as specified
0381         for (auto it = preferredApps.cbegin(), endIt = preferredApps.cend(); it != endIt; ++it) {
0382             const QString mime = it.key();
0383             const KService::List offers = KApplicationTrader::queryByMimeType(mime);
0384             const QStringList offerIds = assembleServices(offers, it.value().count());
0385             if (offerIds != it.value()) {
0386                 qDebug() << "offers for" << mime << ":";
0387                 for (int i = 0; i < offers.count(); ++i) {
0388                     qDebug() << "   " << i << ":" << offers[i]->storageId();
0389                 }
0390                 qDebug() << " Expected:" << it.value();
0391                 const QStringList expectedPreferredServices = it.value();
0392                 for (int i = 0; i < expectedPreferredServices.count(); ++i) {
0393                     qDebug() << mime << i << expectedPreferredServices[i];
0394                     // QCOMPARE(expectedPreferredServices[i], offers[i]->storageId());
0395                 }
0396             }
0397             QCOMPARE(offerIds, it.value());
0398         }
0399         for (auto it = removedApps.constBegin(), endIt = removedApps.constEnd(); it != endIt; ++it) {
0400             const QString mime = it.key();
0401             const KService::List offers = KApplicationTrader::queryByMimeType(mime);
0402             const QStringList offerIds = assembleServices(offers);
0403             for (const QString &service : it.value()) {
0404                 const QString error = QStringLiteral("Offers for %1 should not contain %2").arg(mime, service);
0405                 QVERIFY2(!offerIds.contains(service), qPrintable(error));
0406             }
0407         }
0408     }
0409 
0410     void testMultipleInheritance()
0411     {
0412         // application/x-shellscript inherits from both text/plain and application/x-executable
0413         KService::List offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/x-shellscript"));
0414         QVERIFY(offerListHasService(offers, fakeTextApplication, true));
0415     }
0416 
0417     void testRemoveAssociationFromParent()
0418     {
0419         // I removed kate from text/plain, and it would still appear in text/x-java.
0420 
0421         // First, let's check our fake app is associated with text/plain
0422         KService::List offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"));
0423         QVERIFY(offerListHasService(offers, fakeTextApplication, true));
0424 
0425         writeToMimeApps(
0426             QByteArray("[Removed Associations]\n"
0427                        "text/plain=faketextapplication.desktop;\n"));
0428 
0429         offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"));
0430         QVERIFY(!offerListHasService(offers, fakeTextApplication, false));
0431 
0432         offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/x-java"));
0433         QVERIFY(!offerListHasService(offers, fakeTextApplication, false));
0434     }
0435 
0436     void testRemovedImplicitAssociation() // remove (implicit) assoc from derived mimetype
0437     {
0438         // #164584: Removing ark from opendocument.text didn't work
0439         const QString opendocument = QStringLiteral("application/vnd.oasis.opendocument.text");
0440 
0441         // [sanity checking of s-m-i installation]
0442         QMimeType mime = QMimeDatabase().mimeTypeForName(opendocument);
0443         QVERIFY(mime.isValid());
0444         if (!mime.inherits(QStringLiteral("application/zip"))) {
0445             // CentOS patches out the application/zip inheritance from application/vnd.oasis.opendocument.text!! Grmbl.
0446             QSKIP("Broken distro where application/vnd.oasis.opendocument.text doesn't inherit from application/zip");
0447         }
0448 
0449         KService::List offers = KApplicationTrader::queryByMimeType(opendocument);
0450         QVERIFY(offerListHasService(offers, fakeArkApplication, true));
0451 
0452         writeToMimeApps(
0453             QByteArray("[Removed Associations]\n"
0454                        "application/vnd.oasis.opendocument.text=fakearkapplication.desktop;\n"));
0455 
0456         offers = KApplicationTrader::queryByMimeType(opendocument);
0457         QVERIFY(!offerListHasService(offers, fakeArkApplication, false));
0458 
0459         offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/zip"));
0460         QVERIFY(offerListHasService(offers, fakeArkApplication, true));
0461     }
0462 
0463     void testRemovedImplicitAssociation178560()
0464     {
0465         // #178560: Removing ark from interface/x-winamp-skin didn't work
0466         // Using application/x-kns (another zip-derived mimetype) nowadays.
0467         const QString mime = QStringLiteral("application/x-kns");
0468 
0469         // That mimetype comes from kcoreaddons, let's make sure it's properly installed
0470         {
0471             QMimeDatabase db;
0472             QMimeType mime = db.mimeTypeForName(QStringLiteral("application/x-kns"));
0473             QVERIFY(mime.isValid());
0474             QCOMPARE(mime.name(), QStringLiteral("application/x-kns"));
0475             QVERIFY(mime.inherits(QStringLiteral("application/zip")));
0476         }
0477 
0478         KService::List offers = KApplicationTrader::queryByMimeType(mime);
0479         QVERIFY(offerListHasService(offers, fakeArkApplication, true));
0480 
0481         writeToMimeApps(
0482             QByteArray("[Removed Associations]\n"
0483                        "application/x-kns=fakearkapplication.desktop;\n"));
0484 
0485         offers = KApplicationTrader::queryByMimeType(mime);
0486         QVERIFY(!offerListHasService(offers, fakeArkApplication, false));
0487 
0488         offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/zip"));
0489         QVERIFY(offerListHasService(offers, fakeArkApplication, true));
0490     }
0491 
0492     // remove assoc from a mime which is both a parent and a derived mimetype
0493     void testRemovedMiddleAssociation()
0494     {
0495         // More tricky: x-theme inherits x-desktop inherits text/plain,
0496         // if we remove an association for x-desktop then x-theme shouldn't
0497         // get it from text/plain...
0498 
0499         KService::List offers;
0500         writeToMimeApps(
0501             QByteArray("[Removed Associations]\n"
0502                        "application/x-desktop=faketextapplication.desktop;\n"));
0503 
0504         offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"));
0505         QVERIFY(offerListHasService(offers, fakeTextApplication, true));
0506 
0507         offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/x-desktop"));
0508         QVERIFY(!offerListHasService(offers, fakeTextApplication, false));
0509 
0510         offers = KApplicationTrader::queryByMimeType(QStringLiteral("application/x-theme"));
0511         QVERIFY(!offerListHasService(offers, fakeTextApplication, false));
0512     }
0513 
0514     void testCorrectFallbackOrder()
0515     {
0516         // Verify that a "more specific type" is used before a "less-specifc"
0517         // application/ecmascript inherits from both application/x-executable and text/plain
0518         // In this example we should use both of these first which has an app set compared to the less
0519         // specifc application/octet-stream via application/x-executable
0520         auto queryOnlyDirect = [](const QString &mimeType) {
0521             return KApplicationTrader::queryByMimeType(mimeType, [&mimeType](const KService::Ptr &service) {
0522                 return service->mimeTypes().contains(mimeType);
0523             });
0524         };
0525         const auto offers = assembleServices(KApplicationTrader::queryByMimeType(u"text/javascript"_s));
0526         const auto textOffers = assembleServices(queryOnlyDirect(u"text/plain"_s));
0527         const auto appOffers = assembleServices(queryOnlyDirect(u"application/x-executable"_s));
0528         auto octetOffers = assembleServices(KApplicationTrader::queryByMimeType(u"application/octet-stream"_s));
0529         // Apps that only support octet-stream but not text/plain nor application/x-executable
0530         octetOffers.removeIf([&textOffers, &appOffers](const QString &app) {
0531             return textOffers.contains(app) || appOffers.contains(app);
0532         });
0533         QCOMPARE(offers.mid(offers.count() - octetOffers.size(), octetOffers.size()), octetOffers);
0534     }
0535 
0536 private:
0537     typedef QMap<QString /*mimetype*/, QStringList> ExpectedResultsMap;
0538 
0539     void runKBuildSycoca()
0540     {
0541         // Wait for notifyDatabaseChanged DBus signal
0542         // (The real KCM code simply does the refresh in a slot, asynchronously)
0543         QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
0544 
0545         KBuildSycoca builder;
0546         QVERIFY(builder.recreate());
0547         if (spy.isEmpty()) {
0548             spy.wait();
0549         }
0550     }
0551 
0552     void writeToMimeApps(const QByteArray &contents)
0553     {
0554         QString mimeAppsPath = m_localConfig + QLatin1String{"/mimeapps.list"};
0555         QFile mimeAppsFile(mimeAppsPath);
0556         QVERIFY(mimeAppsFile.open(QIODevice::WriteOnly));
0557         mimeAppsFile.write(contents);
0558         mimeAppsFile.close();
0559 
0560         runKBuildSycoca();
0561     }
0562 
0563     static bool offersContains(const QList<KServiceOffer> &offers, KService::Ptr serv)
0564     {
0565         for (const KServiceOffer &offer : offers) {
0566             if (offer.service()->storageId() == serv->storageId()) {
0567                 return true;
0568             }
0569         }
0570         return false;
0571     }
0572     static QStringList assembleOffers(const QList<KServiceOffer> &offers)
0573     {
0574         QStringList lst;
0575         for (const KServiceOffer &offer : offers) {
0576             lst.append(offer.service()->storageId());
0577         }
0578         return lst;
0579     }
0580     static QStringList assembleServices(const QList<KService::Ptr> &services, int maxCount = -1)
0581     {
0582         QStringList lst;
0583         for (const KService::Ptr &service : services) {
0584             lst.append(service->storageId());
0585             if (maxCount > -1 && lst.count() == maxCount) {
0586                 break;
0587             }
0588         }
0589         return lst;
0590     }
0591 
0592     void removeNonExisting(ExpectedResultsMap &erm)
0593     {
0594         for (auto it = erm.begin(), endIt = erm.end(); it != endIt; ++it) {
0595             QMutableStringListIterator serv_it(it.value());
0596             while (serv_it.hasNext()) {
0597                 if (!KService::serviceByStorageId(serv_it.next())) {
0598                     // qDebug() << "removing non-existing entry" << serv_it.value();
0599                     serv_it.remove();
0600                 }
0601             }
0602         }
0603     }
0604     QString m_localApps;
0605     QString m_localConfig;
0606     QByteArray m_mimeAppsFileContents;
0607     QString fakeTextApplication;
0608     QString fakeTextApplicationPrefixed;
0609     QString fakeDefaultTextApplication;
0610     QString fakeCSrcApplication;
0611     QString fakeJpegApplication;
0612     QString fakeHtmlApplication;
0613     QString fakeHtmlApplicationPrefixed;
0614     QString fakeArkApplication;
0615     QString fakeOktetaApplication;
0616 
0617     ExpectedResultsMap preferredApps;
0618     ExpectedResultsMap removedApps;
0619 };
0620 
0621 FakeServiceFactory::~FakeServiceFactory()
0622 {
0623 }
0624 
0625 QTEST_GUILESS_MAIN(KMimeAssociationsTest)
0626 
0627 #include "kmimeassociationstest.moc"