File indexing completed on 2024-05-12 16:59:22

0001 /* This file is part of the KDE project
0002     SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include <kservice.h>
0008 
0009 #include <kconfiggroup.h>
0010 #include <kdesktopfile.h>
0011 #include <ksycoca.h>
0012 
0013 // Qt
0014 #include <QDir>
0015 #include <QLoggingCategory>
0016 #include <QMimeDatabase>
0017 #include <QProcess>
0018 #include <QSignalSpy>
0019 #include <QStandardPaths>
0020 #include <QTest>
0021 
0022 #include <mimetypedata.h>
0023 #include <mimetypewriter.h>
0024 
0025 // Unfortunately this isn't available in non-developer builds of Qt...
0026 // extern Q_CORE_EXPORT int qmime_secondsBetweenChecks; // see qmimeprovider.cpp
0027 
0028 class FileTypesTest : public QObject
0029 {
0030     Q_OBJECT
0031 
0032 private Q_SLOTS:
0033     void initTestCase()
0034     {
0035         QLoggingCategory::setFilterRules(QStringLiteral("kf.coreaddons.kdirwatch.debug=true"));
0036 
0037         QStandardPaths::setTestModeEnabled(true);
0038 
0039         // update-mime-database needs to know about that test directory for the mimetype pattern change in testMimeTypePatterns to have an effect
0040         qputenv("XDG_DATA_HOME", QFile::encodeName(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)));
0041 
0042         m_mimeTypeCreatedSuccessfully = false;
0043         QStringList appsDirs = QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation);
0044         // qDebug() << appsDirs;
0045         m_localApps = appsDirs.first() + QLatin1Char('/');
0046         m_localConfig = QDir(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation));
0047         QVERIFY(QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/mime/packages")));
0048 
0049         QFile::remove(m_localConfig.filePath(QStringLiteral("mimeapps.list")));
0050         QFile::remove(m_localConfig.filePath(QStringLiteral("filetypesrc")));
0051 
0052         // Create fake applications for some tests below.
0053         fakeApplication = QStringLiteral("fakeapplication.desktop");
0054         createDesktopFile(m_localApps + fakeApplication, {QStringLiteral("image/png")});
0055         fakeApplication2 = QStringLiteral("fakeapplication2.desktop");
0056         createDesktopFile(m_localApps + fakeApplication2, {QStringLiteral("image/png"), QStringLiteral("text/plain")});
0057 
0058         // Cleanup after testMimeTypePatterns if it failed mid-way
0059         const QString packageFileName =
0060             QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/mime/") + QStringLiteral("packages/text-plain.xml");
0061         if (!packageFileName.isEmpty()) {
0062             QFile::remove(packageFileName);
0063             MimeTypeWriter::runUpdateMimeDatabase();
0064         }
0065 
0066         KService::Ptr fakeApplicationService = KService::serviceByStorageId(fakeApplication);
0067         QVERIFY(fakeApplicationService);
0068     }
0069 
0070     void testMimeTypeGroupAutoEmbed()
0071     {
0072         MimeTypeData data(QStringLiteral("text"));
0073         QCOMPARE(data.majorType(), QStringLiteral("text"));
0074         QCOMPARE(data.name(), QStringLiteral("text"));
0075         QVERIFY(data.isMeta());
0076         QCOMPARE(data.autoEmbed(), MimeTypeData::No); // text doesn't autoembed by default
0077         QVERIFY(!data.isDirty());
0078         data.setAutoEmbed(MimeTypeData::Yes);
0079         QCOMPARE(data.autoEmbed(), MimeTypeData::Yes);
0080         QVERIFY(data.isDirty());
0081         QVERIFY(!data.sync()); // save to disk. Should succeed, but return false (no need to run update-mime-database)
0082         QVERIFY(!data.isDirty());
0083         // Check what's on disk by creating another MimeTypeData instance
0084         MimeTypeData data2(QStringLiteral("text"));
0085         QCOMPARE(data2.autoEmbed(), MimeTypeData::Yes);
0086         QVERIFY(!data2.isDirty());
0087         data2.setAutoEmbed(MimeTypeData::No); // revert to default, for next time
0088         QVERIFY(data2.isDirty());
0089         QVERIFY(!data2.sync());
0090         QVERIFY(!data2.isDirty());
0091 
0092         // TODO test askSave after cleaning up the code
0093     }
0094 
0095     void testMimeTypeAutoEmbed()
0096     {
0097         QMimeDatabase db;
0098         MimeTypeData data(db.mimeTypeForName(QStringLiteral("text/plain")));
0099         QCOMPARE(data.majorType(), QStringLiteral("text"));
0100         QCOMPARE(data.minorType(), QStringLiteral("plain"));
0101         QCOMPARE(data.name(), QStringLiteral("text/plain"));
0102         QVERIFY(!data.isMeta());
0103         QCOMPARE(data.autoEmbed(), MimeTypeData::UseGroupSetting);
0104         QVERIFY(!data.isDirty());
0105         data.setAutoEmbed(MimeTypeData::Yes);
0106         QCOMPARE(data.autoEmbed(), MimeTypeData::Yes);
0107         QVERIFY(data.isDirty());
0108         QVERIFY(!data.sync()); // save to disk. Should succeed, but return false (no need to run update-mime-database)
0109         QVERIFY(!data.isDirty());
0110         // Check what's on disk by creating another MimeTypeData instance
0111         MimeTypeData data2(db.mimeTypeForName(QStringLiteral("text/plain")));
0112         QCOMPARE(data2.autoEmbed(), MimeTypeData::Yes);
0113         QVERIFY(!data2.isDirty());
0114         data2.setAutoEmbed(MimeTypeData::UseGroupSetting); // revert to default, for next time
0115         QVERIFY(data2.isDirty());
0116         QVERIFY(!data2.sync());
0117         QVERIFY(!data2.isDirty());
0118     }
0119 
0120     void testMimeTypePatterns()
0121     {
0122         // Given the text/plain mimetype
0123         QMimeDatabase db;
0124         MimeTypeData data(db.mimeTypeForName(QStringLiteral("text/plain")));
0125         QCOMPARE(data.name(), QStringLiteral("text/plain"));
0126         QCOMPARE(data.majorType(), QStringLiteral("text"));
0127         QCOMPARE(data.minorType(), QStringLiteral("plain"));
0128         QVERIFY(!data.isMeta());
0129         QStringList patterns = data.patterns();
0130         QVERIFY(patterns.contains(QStringLiteral("*.txt")));
0131         QVERIFY(!patterns.contains(QStringLiteral("*.toto")));
0132 
0133         // When the user changes the patterns
0134         const QStringList origPatterns = patterns;
0135         patterns.removeAll(QStringLiteral("*.txt"));
0136         patterns.append(QStringLiteral("*.toto")); // yes, a french guy wrote this, as you can see
0137         patterns.sort(); // for future comparisons
0138         QVERIFY(!data.isDirty());
0139         data.setPatterns(patterns);
0140         QVERIFY(data.isDirty());
0141         const bool needUpdateMimeDb = data.sync();
0142         QVERIFY(needUpdateMimeDb);
0143         MimeTypeWriter::runUpdateMimeDatabase();
0144 
0145         // Then the GUI and the QMimeDatabase API should show the new patterns
0146         QCOMPARE(data.patterns(), patterns);
0147         data.refresh(); // reload from the xml
0148         QCOMPARE(data.patterns(), patterns);
0149         // Check what's in QMimeDatabase
0150         QStringList newPatterns = db.mimeTypeForName(QStringLiteral("text/plain")).globPatterns();
0151         newPatterns.sort();
0152         QCOMPARE(newPatterns, patterns);
0153         if (newPatterns == patterns) { // TODO Qt6: remove the if (keep the QVERIFY!)
0154             QVERIFY(!data.isDirty());
0155         }
0156 
0157         // And then removing the custom file by hand should revert to the initial state
0158         const QString packageFileName =
0159             QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/mime/") + QStringLiteral("packages/text-plain.xml");
0160         QVERIFY(!packageFileName.isEmpty());
0161         QFile::remove(packageFileName);
0162         MimeTypeWriter::runUpdateMimeDatabase();
0163         // Check what's in QMimeDatabase
0164         newPatterns = db.mimeTypeForName(QStringLiteral("text/plain")).globPatterns();
0165         newPatterns.sort();
0166         QCOMPARE(newPatterns, origPatterns);
0167     }
0168 
0169     void testAddService()
0170     {
0171         QMimeDatabase db;
0172         QString mimeTypeName = QStringLiteral("application/rtf"); // use inherited mimetype to test #321706
0173         MimeTypeData data(db.mimeTypeForName(mimeTypeName));
0174         QStringList appServices = data.appServices();
0175         // qDebug() << appServices;
0176         QVERIFY(appServices.contains(fakeApplication2));
0177         QVERIFY(!appServices.contains(fakeApplication)); // already there? hmm can't really test then
0178         QVERIFY(!data.isDirty());
0179         appServices.prepend(fakeApplication);
0180         data.setAppServices(appServices);
0181         QVERIFY(data.isDirty());
0182         QVERIFY(!data.sync()); // success, but no need to run update-mime-database
0183         runKBuildSycoca();
0184         QVERIFY(!data.isDirty());
0185         // Check what's in ksycoca
0186         checkMimeTypeServices(mimeTypeName, appServices);
0187         // Check what's in mimeapps.list
0188         checkAddedAssociationsContains(mimeTypeName, fakeApplication);
0189 
0190         // Test reordering apps, i.e. move fakeApplication under oldPreferredApp
0191         appServices.removeFirst();
0192         appServices.insert(1, fakeApplication);
0193         data.setAppServices(appServices);
0194         QVERIFY(!data.sync()); // success, but no need to run update-mime-database
0195         runKBuildSycoca();
0196         QVERIFY(!data.isDirty());
0197         // Check what's in ksycoca
0198         checkMimeTypeServices(mimeTypeName, appServices);
0199         // Check what's in mimeapps.list
0200         checkAddedAssociationsContains(mimeTypeName, fakeApplication);
0201 
0202         // Then we get the signal that kbuildsycoca changed
0203         data.refresh();
0204 
0205         // Now test removing (in the same test, since it's inter-dependent)
0206         QVERIFY(appServices.removeAll(fakeApplication) > 0);
0207         data.setAppServices(appServices);
0208         QVERIFY(data.isDirty());
0209         QVERIFY(!data.sync()); // success, but no need to run update-mime-database
0210         runKBuildSycoca();
0211         // Check what's in ksycoca
0212         checkMimeTypeServices(mimeTypeName, appServices);
0213         // Check what's in mimeapps.list
0214         checkRemovedAssociationsContains(mimeTypeName, fakeApplication);
0215     }
0216 
0217     void testRemoveTwice()
0218     {
0219         QMimeDatabase db;
0220         // Remove fakeApplication from image/png
0221         QString mimeTypeName = QStringLiteral("image/png");
0222         MimeTypeData data(db.mimeTypeForName(mimeTypeName));
0223         QStringList appServices = data.appServices();
0224         qDebug() << "initial list for" << mimeTypeName << appServices;
0225         QVERIFY(appServices.removeAll(fakeApplication) > 0);
0226         data.setAppServices(appServices);
0227         QVERIFY(!data.sync()); // success, but no need to run update-mime-database
0228         runKBuildSycoca();
0229         // Check what's in ksycoca
0230         checkMimeTypeServices(mimeTypeName, appServices);
0231         // Check what's in mimeapps.list
0232         checkRemovedAssociationsContains(mimeTypeName, fakeApplication);
0233 
0234         // Remove fakeApplication2 from image/png; must keep the previous entry in "Removed Associations"
0235         qDebug() << "Removing fakeApplication2";
0236         QVERIFY(appServices.removeAll(fakeApplication2) > 0);
0237         data.setAppServices(appServices);
0238         QVERIFY(!data.sync()); // success, but no need to run update-mime-database
0239         runKBuildSycoca();
0240         // Check what's in ksycoca
0241         checkMimeTypeServices(mimeTypeName, appServices);
0242         // Check what's in mimeapps.list
0243         checkRemovedAssociationsContains(mimeTypeName, fakeApplication);
0244         // Check what's in mimeapps.list
0245         checkRemovedAssociationsContains(mimeTypeName, fakeApplication2);
0246 
0247         // And now re-add fakeApplication2...
0248         qDebug() << "Re-adding fakeApplication2";
0249         appServices.prepend(fakeApplication2);
0250         data.setAppServices(appServices);
0251         QVERIFY(!data.sync()); // success, but no need to run update-mime-database
0252         runKBuildSycoca();
0253         // Check what's in ksycoca
0254         checkMimeTypeServices(mimeTypeName, appServices);
0255         // Check what's in mimeapps.list
0256         checkRemovedAssociationsContains(mimeTypeName, fakeApplication);
0257         checkRemovedAssociationsDoesNotContain(mimeTypeName, fakeApplication2);
0258     }
0259 
0260     void testCreateMimeType()
0261     {
0262         QMimeDatabase db;
0263         const QString mimeTypeName = QStringLiteral("fake/unit-test-fake-mimetype");
0264         // Clean up after previous runs if necessary
0265         if (MimeTypeWriter::hasDefinitionFile(mimeTypeName)) {
0266             MimeTypeWriter::removeOwnMimeType(mimeTypeName);
0267         }
0268 
0269         MimeTypeData data(mimeTypeName, true);
0270         data.setComment(QStringLiteral("Fake MimeType"));
0271         QStringList patterns = QStringList() << QStringLiteral("*.pkg.tar.gz");
0272         data.setPatterns(patterns);
0273         QVERIFY(data.isDirty());
0274         QVERIFY(data.sync());
0275         MimeTypeWriter::runUpdateMimeDatabase();
0276         // QMimeDatabase doesn't even try to update the cache if less than
0277         // 5000 ms have passed (can't use qmime_secondsBetweenChecks)
0278         QTest::qSleep(5000);
0279         QMimeType mime = db.mimeTypeForName(mimeTypeName);
0280         QVERIFY(mime.isValid());
0281         QCOMPARE(mime.comment(), QStringLiteral("Fake MimeType"));
0282         QCOMPARE(mime.globPatterns(), patterns); // must sort them if more than one
0283 
0284         // Testcase for the shaman.xml bug
0285         QCOMPARE(db.mimeTypeForFile(QStringLiteral("/whatever/foo.pkg.tar.gz")).name(), QStringLiteral("fake/unit-test-fake-mimetype"));
0286 
0287         m_mimeTypeCreatedSuccessfully = true;
0288     }
0289 
0290     void testDeleteMimeType()
0291     {
0292         QMimeDatabase db;
0293         if (!m_mimeTypeCreatedSuccessfully) {
0294             QSKIP("This test relies on testCreateMimeType");
0295         }
0296         const QString mimeTypeName = QStringLiteral("fake/unit-test-fake-mimetype");
0297         QVERIFY(MimeTypeWriter::hasDefinitionFile(mimeTypeName));
0298         MimeTypeWriter::removeOwnMimeType(mimeTypeName);
0299         MimeTypeWriter::runUpdateMimeDatabase();
0300         // QMimeDatabase doesn't even try to update the cache if less than
0301         // 5000 ms have passed (can't use qmime_secondsBetweenChecks)
0302         QTest::qSleep(5000);
0303         const QMimeType mime = db.mimeTypeForName(mimeTypeName);
0304         QVERIFY2(!mime.isValid(), qPrintable(mimeTypeName));
0305     }
0306 
0307     void testModifyMimeTypeComment() // of a system mimetype. And check that it's re-read correctly.
0308     {
0309         QMimeDatabase db;
0310         QString mimeTypeName = QStringLiteral("image/png");
0311         MimeTypeData data(db.mimeTypeForName(mimeTypeName));
0312         QCOMPARE(data.comment(), QString::fromLatin1("PNG image"));
0313         QString fakeComment = QStringLiteral("PNG image [testing]");
0314         data.setComment(fakeComment);
0315         QVERIFY(data.isDirty());
0316         QVERIFY(data.sync());
0317         MimeTypeWriter::runUpdateMimeDatabase();
0318         QMimeType mime = db.mimeTypeForName(mimeTypeName);
0319         QVERIFY(mime.isValid());
0320         QCOMPARE(mime.comment(), fakeComment);
0321 
0322         // Cleanup
0323         QVERIFY(MimeTypeWriter::hasDefinitionFile(mimeTypeName));
0324         MimeTypeWriter::removeOwnMimeType(mimeTypeName);
0325     }
0326 
0327     void cleanupTestCase()
0328     {
0329         const QString localAppsDir = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
0330         QFile::remove(localAppsDir + QLatin1String("/fakeapplication.desktop"));
0331         QFile::remove(localAppsDir + QLatin1String("/fakeapplication2.desktop"));
0332     }
0333 
0334 private: // helper methods
0335     void checkAddedAssociationsContains(const QString &mimeTypeName, const QString &application)
0336     {
0337         const KConfig config(m_localConfig.filePath(QStringLiteral("mimeapps.list")), KConfig::NoGlobals);
0338         const KConfigGroup group(&config, "Added Associations");
0339         const QStringList addedEntries = group.readXdgListEntry(mimeTypeName);
0340         if (!addedEntries.contains(application)) {
0341             qWarning() << addedEntries << "does not contain" << application;
0342             QVERIFY(addedEntries.contains(application));
0343         }
0344     }
0345 
0346     void checkRemovedAssociationsContains(const QString &mimeTypeName, const QString &application)
0347     {
0348         const KConfig config(m_localConfig.filePath(QStringLiteral("mimeapps.list")), KConfig::NoGlobals);
0349         const KConfigGroup group(&config, "Removed Associations");
0350         const QStringList removedEntries = group.readXdgListEntry(mimeTypeName);
0351         if (!removedEntries.contains(application)) {
0352             qWarning() << removedEntries << "does not contain" << application;
0353             QVERIFY(removedEntries.contains(application));
0354         }
0355     }
0356 
0357     void checkRemovedAssociationsDoesNotContain(const QString &mimeTypeName, const QString &application)
0358     {
0359         const KConfig config(m_localConfig.filePath(QStringLiteral("mimeapps.list")), KConfig::NoGlobals);
0360         const KConfigGroup group(&config, "Removed Associations");
0361         const QStringList removedEntries = group.readXdgListEntry(mimeTypeName);
0362         if (removedEntries.contains(application)) {
0363             qWarning() << removedEntries << "contains" << application;
0364             QVERIFY(!removedEntries.contains(application));
0365         }
0366     }
0367 
0368     void runKBuildSycoca()
0369     {
0370         // Wait for notifyDatabaseChanged DBus signal
0371         // (The real KCM code simply does the refresh in a slot, asynchronously)
0372 
0373         QProcess proc;
0374         // proc.setProcessChannelMode(QProcess::ForwardedChannels);
0375         const QString kbuildsycoca = QStandardPaths::findExecutable(QLatin1String(KBUILDSYCOCA_EXENAME));
0376         QVERIFY(!kbuildsycoca.isEmpty());
0377         QStringList args;
0378         args << QStringLiteral("--testmode");
0379         proc.start(kbuildsycoca, args);
0380         QSignalSpy spy(KSycoca::self(), qOverload<>(&KSycoca::databaseChanged));
0381         proc.waitForFinished();
0382         qDebug() << "waiting for signal";
0383         QVERIFY(spy.wait(10000));
0384         qDebug() << "got signal";
0385     }
0386 
0387     void createDesktopFile(const QString &path, const QStringList &mimeTypes)
0388     {
0389         KDesktopFile file(path);
0390         KConfigGroup group = file.desktopGroup();
0391         group.writeEntry("Name", "FakeApplication");
0392         group.writeEntry("Type", "Application");
0393         group.writeEntry("Exec", "ls");
0394         group.writeXdgListEntry("MimeType", mimeTypes);
0395     }
0396 
0397     void checkMimeTypeServices(const QString &mimeTypeName, const QStringList &expectedServices)
0398     {
0399         QMimeDatabase db;
0400         MimeTypeData data2(db.mimeTypeForName(mimeTypeName));
0401         if (data2.appServices() != expectedServices) {
0402             qDebug() << "got" << data2.appServices() << "expected" << expectedServices;
0403         }
0404         QCOMPARE(data2.appServices(), expectedServices);
0405     }
0406 
0407     QString fakeApplication; // storage id of the fake application
0408     QString fakeApplication2; // storage id of the fake application2
0409     QString m_localApps;
0410     QDir m_localConfig;
0411     bool m_mimeTypeCreatedSuccessfully;
0412 };
0413 
0414 QTEST_MAIN(FileTypesTest)
0415 
0416 #include "filetypestest.moc"