File indexing completed on 2024-05-12 05:50:10

0001 /*
0002     SPDX-FileCopyrightText: 2010-2011 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0003     SPDX-FileCopyrightText: 2016 Elvis Angelaccio <elvis.angelaccio@kde.org>
0004 
0005     SPDX-License-Identifier: BSD-2-Clause
0006 */
0007 
0008 #include "jobs.h"
0009 #include "jsonarchiveinterface.h"
0010 
0011 #include <KPluginMetaData>
0012 
0013 #include <QDebug>
0014 #include <QEventLoop>
0015 #include <QTest>
0016 
0017 using namespace Kerfuffle;
0018 
0019 class JobsTest : public QObject
0020 {
0021     Q_OBJECT
0022 
0023 public:
0024     JobsTest();
0025 
0026 protected Q_SLOTS:
0027     void init();
0028     void slotNewEntry(Archive::Entry *entry);
0029 
0030 private Q_SLOTS:
0031     // ListJob-related tests
0032     void testLoadJob_data();
0033     void testLoadJob();
0034 
0035     // ExtractJob-related tests
0036     void testExtractJobAccessors();
0037     void testTempExtractJob();
0038 
0039     // DeleteJob-related tests
0040     void testRemoveEntries_data();
0041     void testRemoveEntries();
0042 
0043     // AddJob-related tests
0044     void testAddEntries_data();
0045     void testAddEntries();
0046 
0047 private:
0048     JSONArchiveInterface *createArchiveInterface(const QString &filePath);
0049     QVector<Archive::Entry *> listEntries(JSONArchiveInterface *iface);
0050     void startAndWaitForResult(KJob *job);
0051 
0052     QVector<Archive::Entry *> m_entries;
0053     QEventLoop m_eventLoop;
0054 };
0055 
0056 QTEST_GUILESS_MAIN(JobsTest)
0057 
0058 JobsTest::JobsTest()
0059     : QObject(nullptr)
0060     , m_eventLoop(this)
0061 {
0062 }
0063 
0064 void JobsTest::init()
0065 {
0066     m_entries.clear();
0067 }
0068 
0069 void JobsTest::slotNewEntry(Archive::Entry *entry)
0070 {
0071     m_entries.append(entry);
0072 }
0073 
0074 JSONArchiveInterface *JobsTest::createArchiveInterface(const QString &filePath)
0075 {
0076     JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath, QVariant().fromValue(KPluginMetaData())});
0077     if (!iface->open()) {
0078         qDebug() << "Could not open" << filePath;
0079         return nullptr;
0080     }
0081 
0082     return iface;
0083 }
0084 
0085 QVector<Archive::Entry *> JobsTest::listEntries(JSONArchiveInterface *iface)
0086 {
0087     m_entries.clear();
0088 
0089     auto job = new LoadJob(iface);
0090     connect(job, &Job::newEntry, this, &JobsTest::slotNewEntry);
0091 
0092     startAndWaitForResult(job);
0093 
0094     return m_entries;
0095 }
0096 
0097 void JobsTest::startAndWaitForResult(KJob *job)
0098 {
0099     connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit);
0100     job->start();
0101     m_eventLoop.exec();
0102 }
0103 
0104 void JobsTest::testLoadJob_data()
0105 {
0106     QTest::addColumn<QString>("jsonArchive");
0107     QTest::addColumn<qlonglong>("expectedExtractedFilesSize");
0108     QTest::addColumn<bool>("isPasswordProtected");
0109     QTest::addColumn<bool>("isSingleFolder");
0110     QTest::addColumn<QStringList>("expectedEntryNames");
0111 
0112     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << 0LL << false << false
0113                                      << QStringList{QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")};
0114 
0115     QTest::newRow("archive002.json") << QFINDTESTDATA("data/archive002.json") << 45959LL << false << false
0116                                      << QStringList{QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")};
0117 
0118     QTest::newRow("archive-deepsinglehierarchy.json") << QFINDTESTDATA("data/archive-deepsinglehierarchy.json") << 0LL << false << true
0119                                                       << QStringList{
0120                                                              // Depth-first order!
0121                                                              QStringLiteral("aDir/"),
0122                                                              QStringLiteral("aDir/aDirInside/"),
0123                                                              QStringLiteral("aDir/aDirInside/anotherDir/"),
0124                                                              QStringLiteral("aDir/aDirInside/anotherDir/file.txt"),
0125                                                              QStringLiteral("aDir/b.txt"),
0126                                                          };
0127 
0128     QTest::newRow("archive-multiplefolders.json") << QFINDTESTDATA("data/archive-multiplefolders.json") << 0LL << false << false
0129                                                   << QStringList{
0130                                                          QStringLiteral("aDir/"),
0131                                                          QStringLiteral("aDir/b.txt"),
0132                                                          QStringLiteral("anotherDir/"),
0133                                                          QStringLiteral("anotherDir/file.txt"),
0134                                                      };
0135 
0136     QTest::newRow("archive-nodir-manyfiles.json") << QFINDTESTDATA("data/archive-nodir-manyfiles.json") << 0LL << false << false
0137                                                   << QStringList{QStringLiteral("a.txt"), QStringLiteral("file.txt")};
0138 
0139     QTest::newRow("archive-onetopfolder.json") << QFINDTESTDATA("data/archive-onetopfolder.json") << 0LL << false << true
0140                                                << QStringList{QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt")};
0141 
0142     QTest::newRow("archive-password.json") << QFINDTESTDATA("data/archive-password.json") << 0LL << true
0143                                            << false
0144                                            // Possibly unexpected behavior of listing:
0145                                            // 1. Directories are listed before files, if they are empty!
0146                                            // 2. Files are sorted alphabetically.
0147                                            << QStringList{QStringLiteral("aDirectory/"), QStringLiteral("bar.txt"), QStringLiteral("foo.txt")};
0148 
0149     QTest::newRow("archive-singlefile.json") << QFINDTESTDATA("data/archive-singlefile.json") << 0LL << false << false << QStringList{QStringLiteral("a.txt")};
0150 
0151     QTest::newRow("archive-emptysinglefolder.json") << QFINDTESTDATA("data/archive-emptysinglefolder.json") << 0LL << false << true
0152                                                     << QStringList{QStringLiteral("aDir/")};
0153 
0154     QTest::newRow("archive-unorderedsinglefolder.json") << QFINDTESTDATA("data/archive-unorderedsinglefolder.json") << 0LL << false << true
0155                                                         << QStringList{
0156                                                                QStringLiteral("aDir/"),
0157                                                                QStringLiteral("aDir/anotherDir/"),
0158                                                                QStringLiteral("aDir/anotherDir/bar.txt"),
0159                                                                QStringLiteral("aDir/foo.txt"),
0160                                                            };
0161 }
0162 
0163 void JobsTest::testLoadJob()
0164 {
0165     QFETCH(QString, jsonArchive);
0166     JSONArchiveInterface *iface = createArchiveInterface(jsonArchive);
0167     QVERIFY(iface);
0168 
0169     auto loadJob = new LoadJob(iface);
0170     loadJob->setAutoDelete(false);
0171     startAndWaitForResult(loadJob);
0172 
0173     QFETCH(qlonglong, expectedExtractedFilesSize);
0174     QCOMPARE(loadJob->extractedFilesSize(), expectedExtractedFilesSize);
0175 
0176     QFETCH(bool, isPasswordProtected);
0177     QCOMPARE(loadJob->isPasswordProtected(), isPasswordProtected);
0178 
0179     QFETCH(bool, isSingleFolder);
0180     QCOMPARE(loadJob->isSingleFolderArchive(), isSingleFolder);
0181 
0182     QFETCH(QStringList, expectedEntryNames);
0183     auto archiveEntries = listEntries(iface);
0184 
0185     QCOMPARE(archiveEntries.size(), expectedEntryNames.size());
0186 
0187     for (int i = 0; i < archiveEntries.size(); i++) {
0188         QCOMPARE(archiveEntries.at(i)->fullPath(), expectedEntryNames.at(i));
0189     }
0190 
0191     loadJob->deleteLater();
0192 }
0193 
0194 void JobsTest::testExtractJobAccessors()
0195 {
0196     JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive001.json"));
0197     ExtractJob *job = new ExtractJob(QVector<Archive::Entry *>(), QStringLiteral("/tmp/some-dir"), ExtractionOptions(), iface);
0198 
0199     QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir"));
0200     QVERIFY(job->extractionOptions().preservePaths());
0201 
0202     job->setAutoDelete(false);
0203     startAndWaitForResult(job);
0204 
0205     QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir"));
0206     delete job;
0207 
0208     ExtractionOptions options;
0209     options.setPreservePaths(false);
0210 
0211     job = new ExtractJob(QVector<Archive::Entry *>(), QStringLiteral("/root"), options, iface);
0212 
0213     QCOMPARE(job->destinationDirectory(), QLatin1String("/root"));
0214     QVERIFY(!job->extractionOptions().preservePaths());
0215 
0216     job->setAutoDelete(false);
0217     startAndWaitForResult(job);
0218 
0219     QCOMPARE(job->destinationDirectory(), QLatin1String("/root"));
0220     QVERIFY(!job->extractionOptions().preservePaths());
0221     delete job;
0222 }
0223 
0224 void JobsTest::testTempExtractJob()
0225 {
0226     JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive-malicious.json"));
0227     PreviewJob *job = new PreviewJob(new Archive::Entry(this, QStringLiteral("anotherDir/../../file.txt")), false, iface);
0228 
0229     const QString tempDirPath = job->tempDir()->path();
0230     QVERIFY(QFileInfo::exists(tempDirPath));
0231     QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt")));
0232     QVERIFY(job->extractionOptions().preservePaths());
0233 
0234     job->setAutoDelete(false);
0235     startAndWaitForResult(job);
0236 
0237     QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt")));
0238     QVERIFY(job->extractionOptions().preservePaths());
0239 
0240     delete job->tempDir();
0241     QVERIFY(!QFileInfo::exists(tempDirPath));
0242 
0243     delete job;
0244 }
0245 
0246 void JobsTest::testRemoveEntries_data()
0247 {
0248     QTest::addColumn<QString>("jsonArchive");
0249     QTest::addColumn<QVector<Archive::Entry *>>("entries");
0250     QTest::addColumn<QVector<Archive::Entry *>>("entriesToDelete");
0251 
0252     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0253                                      << (QVector<Archive::Entry *>{
0254                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0255                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0256                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0257                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0258                                         })
0259                                      << QVector<Archive::Entry *>{
0260                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0261                                         };
0262 
0263     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0264                                      << (QVector<Archive::Entry *>{
0265                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0266                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0267                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0268                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0269                                         })
0270                                      << QVector<Archive::Entry *>{
0271                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0272                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0273                                         };
0274 
0275     // Error test: if we delete non-existent entries, the archive must not change.
0276     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0277                                      << (QVector<Archive::Entry *>{
0278                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0279                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0280                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0281                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0282                                         })
0283                                      << QVector<Archive::Entry *>{new Archive::Entry(this, QStringLiteral("foo.txt"))};
0284 }
0285 
0286 void JobsTest::testRemoveEntries()
0287 {
0288     QFETCH(QString, jsonArchive);
0289     JSONArchiveInterface *iface = createArchiveInterface(jsonArchive);
0290     QVERIFY(iface);
0291 
0292     QFETCH(QVector<Archive::Entry *>, entries);
0293     QFETCH(QVector<Archive::Entry *>, entriesToDelete);
0294     QStringList fullPathsToDelete = iface->entryFullPaths(entriesToDelete);
0295 
0296     QVector<Archive::Entry *> expectedRemainingEntries;
0297     for (Archive::Entry *entry : std::as_const(entries)) {
0298         if (!fullPathsToDelete.contains(entry->fullPath())) {
0299             expectedRemainingEntries.append(entry);
0300         }
0301     }
0302 
0303     DeleteJob *deleteJob = new DeleteJob(entriesToDelete, iface);
0304     startAndWaitForResult(deleteJob);
0305 
0306     auto remainingEntries = listEntries(iface);
0307     QCOMPARE(remainingEntries.size(), expectedRemainingEntries.size());
0308 
0309     for (int i = 0; i < remainingEntries.size(); i++) {
0310         QCOMPARE(*remainingEntries.at(i), *expectedRemainingEntries.at(i));
0311     }
0312 
0313     iface->deleteLater();
0314 }
0315 
0316 void JobsTest::testAddEntries_data()
0317 {
0318     QTest::addColumn<QString>("jsonArchive");
0319     QTest::addColumn<QVector<Archive::Entry *>>("originalEntries");
0320     QTest::addColumn<QVector<Archive::Entry *>>("entriesToAdd");
0321     QTest::addColumn<Archive::Entry *>("destinationEntry");
0322 
0323     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0324                                      << (QVector<Archive::Entry *>{
0325                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0326                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0327                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0328                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0329                                         })
0330                                      << (QVector<Archive::Entry *>{
0331                                             new Archive::Entry(this, QStringLiteral("foo.txt")),
0332                                         })
0333                                      << new Archive::Entry(this);
0334 
0335     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0336                                      << (QVector<Archive::Entry *>{
0337                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0338                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0339                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0340                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0341                                         })
0342                                      << (QVector<Archive::Entry *>{
0343                                             new Archive::Entry(this, QStringLiteral("foo.txt")),
0344                                             new Archive::Entry(this, QStringLiteral("bar.txt")),
0345                                         })
0346                                      << new Archive::Entry(this);
0347 
0348     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0349                                      << (QVector<Archive::Entry *>{
0350                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0351                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0352                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0353                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0354                                         })
0355                                      << (QVector<Archive::Entry *>{
0356                                             new Archive::Entry(this, QStringLiteral("foo.txt")),
0357                                             new Archive::Entry(this, QStringLiteral("bar.txt")),
0358                                         })
0359                                      << new Archive::Entry(this, QStringLiteral("aDir/"));
0360 
0361     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0362                                      << (QVector<Archive::Entry *>{
0363                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0364                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0365                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0366                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0367                                         })
0368                                      << QVector<Archive::Entry *>{new Archive::Entry(this, QStringLiteral("c.txt"))}
0369                                      << new Archive::Entry(this, QStringLiteral("aDir/"));
0370 
0371     // Error test: if we add an already existent entry, the archive must not change.
0372     QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json")
0373                                      << (QVector<Archive::Entry *>{
0374                                             new Archive::Entry(this, QStringLiteral("a.txt")),
0375                                             new Archive::Entry(this, QStringLiteral("aDir/")),
0376                                             new Archive::Entry(this, QStringLiteral("aDir/b.txt")),
0377                                             new Archive::Entry(this, QStringLiteral("c.txt")),
0378                                         })
0379                                      << QVector<Archive::Entry *>{new Archive::Entry(this, QStringLiteral("c.txt"))} << new Archive::Entry(this);
0380 }
0381 
0382 void JobsTest::testAddEntries()
0383 {
0384     QFETCH(QString, jsonArchive);
0385     JSONArchiveInterface *iface = createArchiveInterface(jsonArchive);
0386     QVERIFY(iface);
0387 
0388     QFETCH(QVector<Archive::Entry *>, originalEntries);
0389     QStringList originalFullPaths = QStringList();
0390     for (const Archive::Entry *entry : std::as_const(originalEntries)) {
0391         originalFullPaths.append(entry->fullPath());
0392     }
0393     auto currentEntries = listEntries(iface);
0394     QCOMPARE(currentEntries.size(), originalEntries.size());
0395 
0396     QFETCH(QVector<Archive::Entry *>, entriesToAdd);
0397     QFETCH(Archive::Entry *, destinationEntry);
0398     AddJob *addJob = new AddJob(entriesToAdd, destinationEntry, CompressionOptions(), iface);
0399     startAndWaitForResult(addJob);
0400 
0401     QStringList expectedAddedFullPaths = QStringList();
0402     const QString destinationPath = destinationEntry->fullPath();
0403     int expectedEntriesCount = originalEntries.size();
0404     for (const Archive::Entry *entry : std::as_const(entriesToAdd)) {
0405         const QString fullPath = destinationPath + entry->fullPath();
0406         if (!originalFullPaths.contains(fullPath)) {
0407             expectedEntriesCount++;
0408             expectedAddedFullPaths << destinationPath + entry->fullPath();
0409         }
0410     }
0411 
0412     currentEntries = listEntries(iface);
0413     QCOMPARE(currentEntries.size(), expectedEntriesCount);
0414 
0415     QStringList currentFullPaths = QStringList();
0416     for (const Archive::Entry *entry : std::as_const(currentEntries)) {
0417         currentFullPaths << entry->fullPath();
0418     }
0419 
0420     for (const QString &fullPath : std::as_const(expectedAddedFullPaths)) {
0421         QVERIFY(currentFullPaths.contains(fullPath));
0422     }
0423 
0424     iface->deleteLater();
0425 }
0426 
0427 #include "jobstest.moc"