File indexing completed on 2024-04-21 15:03:05

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