File indexing completed on 2024-05-12 05:29:24

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