diff options
Diffstat (limited to 'tests/auto/packagemanager/tst_packagemanager.cpp')
-rw-r--r-- | tests/auto/packagemanager/tst_packagemanager.cpp | 811 |
1 files changed, 811 insertions, 0 deletions
diff --git a/tests/auto/packagemanager/tst_packagemanager.cpp b/tests/auto/packagemanager/tst_packagemanager.cpp new file mode 100644 index 00000000..3735171a --- /dev/null +++ b/tests/auto/packagemanager/tst_packagemanager.cpp @@ -0,0 +1,811 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2019 Luxoft Sweden AB +// Copyright (C) 2018 Pelagicore AG +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include <QtCore> +#include <QtTest> + +#include <functional> + +#include "packagemanager.h" +#include "packagedatabase.h" +#include "package.h" +#include "applicationinfo.h" +#include "sudo.h" +#include "utilities.h" +#include "error.h" +#include "private/packageutilities_p.h" +#include "runtimefactory.h" +#include "qmlinprocruntime.h" +#include "packageutilities.h" + +#include "../error-checking.h" + +using namespace Qt::StringLiterals; + +QT_USE_NAMESPACE_AM + + +static int spyTimeout = 5000; // shorthand for specifying QSignalSpy timeouts + +// RAII to reset the global attribute +class AllowInstallations +{ +public: + enum Type { + AllowUnsigned, + RequireDevSigned, + RequireStoreSigned + }; + + AllowInstallations(Type t) + : m_oldUnsigned(PackageManager::instance()->allowInstallationOfUnsignedPackages()) + , m_oldDevMode(PackageManager::instance()->developmentMode()) + { + switch (t) { + case AllowUnsigned: + PackageManager::instance()->setAllowInstallationOfUnsignedPackages(true); + PackageManager::instance()->setDevelopmentMode(false); + break; + case RequireDevSigned: + PackageManager::instance()->setAllowInstallationOfUnsignedPackages(false); + PackageManager::instance()->setDevelopmentMode(true); + break; + case RequireStoreSigned: + PackageManager::instance()->setAllowInstallationOfUnsignedPackages(false); + PackageManager::instance()->setDevelopmentMode(false); + break; + } + } + ~AllowInstallations() + { + PackageManager::instance()->setAllowInstallationOfUnsignedPackages(m_oldUnsigned); + PackageManager::instance()->setDevelopmentMode(m_oldDevMode); + } +private: + bool m_oldUnsigned; + bool m_oldDevMode; + + Q_DISABLE_COPY_MOVE(AllowInstallations) +}; + +class tst_PackageManager : public QObject +{ + Q_OBJECT + +public: + tst_PackageManager(QObject *parent = nullptr); + ~tst_PackageManager() override; + + static bool startedSudoServer; + static QString sudoServerError; + +private slots: + void initTestCase(); + void cleanupTestCase(); + + void init(); + void cleanup(); + + //TODO: test AI::cleanupBrokenInstallations() before calling cleanup() the first time! + + void packageInstallation_data(); + void packageInstallation(); + + void simulateErrorConditions_data(); + void simulateErrorConditions(); + + void cancelPackageInstallation_data(); + void cancelPackageInstallation(); + + void parallelPackageInstallation(); + void doublePackageInstallation(); + + void validateDnsName_data(); + void validateDnsName(); + + void compareVersions_data(); + void compareVersions(); + +public: + enum PathLocation { + Internal0, + Documents0, + + PathLocationCount + }; + +private: + QString pathTo(const QString &sub = QString()) + { + return pathTo(PathLocationCount, sub); + } + + QString pathTo(PathLocation pathLocation, const QString &sub = QString()) + { + QString base; + switch (pathLocation) { + case Internal0: base = u"internal"_s; break; + case Documents0: base = u"documents"_s; break; + default: break; + } + + QDir workDir(m_workDir.path()); + + if (base.isEmpty() && sub.isEmpty()) + base = workDir.absolutePath(); + else if (sub.isEmpty()) + base = workDir.absoluteFilePath(base); + else if (base.isEmpty()) + base = workDir.absoluteFilePath(sub); + else + base = workDir.absoluteFilePath(base + u'/' + sub); + + if (QDir(base).exists()) + return base + u'/'; + else + return base; + } + + void clearSignalSpies() + { + m_startedSpy->clear(); + m_requestingInstallationAcknowledgeSpy->clear(); + m_blockingUntilInstallationAcknowledgeSpy->clear(); + m_progressSpy->clear(); + m_finishedSpy->clear(); + m_failedSpy->clear(); + } + + static bool isDataTag(const char *tag) + { + return !qstrcmp(tag, QTest::currentDataTag()); + } + +private: + SudoClient *m_sudo = nullptr; + bool m_fakeSudo = false; + + QTemporaryDir m_workDir; + QString m_hardwareId; + PackageManager *m_pm = nullptr; + QSignalSpy *m_startedSpy = nullptr; + QSignalSpy *m_requestingInstallationAcknowledgeSpy = nullptr; + QSignalSpy *m_blockingUntilInstallationAcknowledgeSpy = nullptr; + QSignalSpy *m_progressSpy = nullptr; + QSignalSpy *m_finishedSpy = nullptr; + QSignalSpy *m_failedSpy = nullptr; +}; + +bool tst_PackageManager::startedSudoServer = false; +QString tst_PackageManager::sudoServerError; + +tst_PackageManager::tst_PackageManager(QObject *parent) + : QObject(parent) +{ } + +tst_PackageManager::~tst_PackageManager() +{ + if (m_workDir.isValid()) { + if (m_sudo) + m_sudo->removeRecursive(m_workDir.path()); + else + recursiveOperation(m_workDir.path(), safeRemove); + } + + delete m_failedSpy; + delete m_finishedSpy; + delete m_progressSpy; + delete m_blockingUntilInstallationAcknowledgeSpy; + delete m_requestingInstallationAcknowledgeSpy; + delete m_startedSpy; + + delete m_pm; +} + +void tst_PackageManager::initTestCase() +{ + if (!QDir(QString::fromLatin1(AM_TESTDATA_DIR "/packages")).exists()) + QSKIP("No test packages available in the data/ directory"); + + bool verbose = qEnvironmentVariableIsSet("AM_VERBOSE_TEST"); + if (!verbose) + QLoggingCategory::setFilterRules(u"am.installer.debug=false"_s); + qInfo() << "Verbose mode is" << (verbose ? "on" : "off") << "(change by (un)setting $AM_VERBOSE_TEST)"; + + spyTimeout *= timeoutFactor(); + + QVERIFY2(startedSudoServer, qPrintable(sudoServerError)); + m_sudo = SudoClient::instance(); + QVERIFY(m_sudo); + m_fakeSudo = m_sudo->isFallbackImplementation(); + + // create a temporary dir (plus sub-dirs) for everything created by this test run + QVERIFY(m_workDir.isValid()); + + // make sure we have a valid hardware-id + m_hardwareId = u"foobar"_s; + + for (int i = 0; i < PathLocationCount; ++i) + QVERIFY(QDir().mkdir(pathTo(PathLocation(i)))); + + // finally, instantiate the PackageManager and a bunch of signal-spies for its signals + try { + PackageDatabase *pdb = new PackageDatabase(QStringList(), pathTo(Internal0)); + m_pm = PackageManager::createInstance(pdb, pathTo(Documents0)); + m_pm->setHardwareId(m_hardwareId); + m_pm->enableInstaller(); + m_pm->registerPackages(); + + // simulate the ApplicationManager stopping blocked applications + connect(&m_pm->internalSignals, &PackageManagerInternalSignals::registerApplication, + this, [this](ApplicationInfo *ai, Package *package) { + connect(package, &Package::blockedChanged, this, [ai, package](bool blocked) { + if (package->info()->applications().contains(ai) && blocked) + package->applicationStoppedDueToBlock(ai->id()); + }); + }); + } catch (const Exception &e) { + QVERIFY2(false, e.what()); + } + + const QVariantMap iloc = m_pm->installationLocation(); + QCOMPARE(iloc.size(), 3); + QCOMPARE(iloc.value(u"path"_s).toString(), pathTo(Internal0)); + QVERIFY(iloc.value(u"deviceSize"_s).toLongLong() > 0); + QVERIFY(iloc.value(u"deviceFree"_s).toLongLong() > 0); + QVERIFY(iloc.value(u"deviceFree"_s).toLongLong() < iloc.value(u"deviceSize"_s).toLongLong()); + + const QVariantMap dloc = m_pm->documentLocation(); + QCOMPARE(dloc.size(), 3); + QCOMPARE(dloc.value(u"path"_s).toString(), pathTo(Documents0)); + QVERIFY(dloc.value(u"deviceSize"_s).toLongLong() > 0); + QVERIFY(dloc.value(u"deviceFree"_s).toLongLong() > 0); + QVERIFY(dloc.value(u"deviceFree"_s).toLongLong() < dloc.value(u"deviceSize"_s).toLongLong()); + + m_startedSpy = new QSignalSpy(m_pm, &PackageManager::taskStarted); + m_requestingInstallationAcknowledgeSpy = new QSignalSpy(m_pm, &PackageManager::taskRequestingInstallationAcknowledge); + m_blockingUntilInstallationAcknowledgeSpy = new QSignalSpy(m_pm, &PackageManager::taskBlockingUntilInstallationAcknowledge); + m_progressSpy = new QSignalSpy(m_pm, &PackageManager::taskProgressChanged); + m_finishedSpy = new QSignalSpy(m_pm, &PackageManager::taskFinished); + m_failedSpy = new QSignalSpy(m_pm, &PackageManager::taskFailed); + + // crypto stuff - we need to load the root CA and developer CA certificates + + QFile devcaFile(QString::fromLatin1(AM_TESTDATA_DIR "certificates/devca.crt")); + QFile storecaFile(QString::fromLatin1(AM_TESTDATA_DIR "certificates/store.crt")); + QFile caFile(QString::fromLatin1(AM_TESTDATA_DIR "certificates/ca.crt")); + QVERIFY2(devcaFile.open(QIODevice::ReadOnly), qPrintable(devcaFile.errorString())); + QVERIFY2(storecaFile.open(QIODevice::ReadOnly), qPrintable(storecaFile.errorString())); + QVERIFY2(caFile.open(QIODevice::ReadOnly), qPrintable(caFile.errorString())); + + QByteArrayList chainOfTrust; + chainOfTrust << devcaFile.readAll() << caFile.readAll(); + QVERIFY(!chainOfTrust.at(0).isEmpty()); + QVERIFY(!chainOfTrust.at(1).isEmpty()); + m_pm->setCACertificates(chainOfTrust); + + // we do not require valid store signatures for this test run + + m_pm->setDevelopmentMode(true); + + // make sure we have a valid runtime available. The important part is + // that we have a runtime called "native" - the functionality does not matter. + RuntimeFactory::instance()->registerRuntime(new QmlInProcRuntimeManager(u"native"_s)); +} + +void tst_PackageManager::cleanupTestCase() +{ + // the real cleanup happens in ~tst_PackageManager, since we also need + // to call this cleanup from the crash handler +} + +void tst_PackageManager::init() +{ + // start with a fresh App1 dir on each test run + + if (!QDir(pathTo(Internal0)).exists()) + QVERIFY(QDir().mkdir(pathTo(Internal0))); +} + +void tst_PackageManager::cleanup() +{ + // this helps with reducing the amount of cleanup work required + // at the end of each test + + try { + m_pm->cleanupBrokenInstallations(); + } catch (const Exception &e) { + QFAIL(e.what()); + } + + clearSignalSpies(); + recursiveOperation(pathTo(Internal0), safeRemove); +} + +void tst_PackageManager::packageInstallation_data() +{ + QTest::addColumn<QString>("packageName"); + QTest::addColumn<QString>("updatePackageName"); + QTest::addColumn<bool>("devSigned"); + QTest::addColumn<bool>("storeSigned"); + QTest::addColumn<bool>("expectedSuccess"); + QTest::addColumn<bool>("updateExpectedSuccess"); + QTest::addColumn<QVariantMap>("extraMetaData"); + QTest::addColumn<QString>("errorString"); // start with ~ to create a RegExp + + QVariantMap nomd { }; // no meta-data + QVariantMap extramd = QVariantMap { + { u"extra"_s, QVariantMap { + { u"array"_s, QVariantList { 1, 2 } }, + { u"foo"_s, u"bar"_s }, + { u"foo2"_s,u"bar2"_s }, + { u"key"_s, u"value"_s } } }, + { u"extraSigned"_s, QVariantMap { + { u"sfoo"_s, u"sbar"_s }, + { u"sfoo2"_s, u"sbar2"_s }, + { u"signed-key"_s, u"signed-value"_s }, + { u"signed-object"_s, QVariantMap { { u"k1"_s, u"v1"_s }, { u"k2"_s, u"v2"_s } } } + } } + }; + + QTest::newRow("normal") \ + << "test.appkg" << "test-update.appkg" + << false << false << true << true << nomd<< ""; + QTest::newRow("no-dev-signed") \ + << "test.appkg" << "" + << true << false << false << false << nomd << "cannot install unsigned packages"; + QTest::newRow("dev-signed") \ + << "test-dev-signed.appkg" << "test-update-dev-signed.appkg" + << true << false << true << true << nomd << ""; + QTest::newRow("no-store-signed") \ + << "test.appkg" << "" + << false << true << false << false << nomd << "cannot install unsigned packages"; + QTest::newRow("no-store-but-dev-signed") \ + << "test-dev-signed.appkg" << "" + << false << true << false << false << nomd << "cannot install development packages on consumer devices"; + QTest::newRow("store-signed") \ + << "test-store-signed.appkg" << "" + << false << true << true << false << nomd << ""; + QTest::newRow("extra-metadata") \ + << "test-extra.appkg" << "" + << false << false << true << false << extramd << ""; + QTest::newRow("extra-metadata-dev-signed") \ + << "test-extra-dev-signed.appkg" << "" + << true << false << true << false << extramd << ""; + QTest::newRow("invalid-file-order") \ + << "test-invalid-file-order.appkg" << "" + << false << false << false << false << nomd << "The package icon (as stated in info.yaml) must be the second file in the package. Expected 'icon.png', got 'test'"; + QTest::newRow("invalid-header-format") \ + << "test-invalid-header-formatversion.appkg" << "" + << false << false << false << false << nomd << "metadata has an invalid format specification: wrong header: expected type 'am-package-header', version '2' or type 'am-package-header', version '1', but instead got type 'am-package-header', version '0'"; + QTest::newRow("invalid-header-diskspaceused") \ + << "test-invalid-header-diskspaceused.appkg" << "" + << false << false << false << false << nomd << "metadata has an invalid diskSpaceUsed field (0)"; + QTest::newRow("invalid-header-id") \ + << "test-invalid-header-id.appkg" << "" + << false << false << false << false << nomd << "metadata has an invalid packageId field (:invalid)"; + QTest::newRow("non-matching-header-id") \ + << "test-non-matching-header-id.appkg" << "" + << false << false << false << false << nomd << "the package identifiers in --PACKAGE-HEADER--' and info.yaml do not match"; + QTest::newRow("tampered-extra-signed-header") \ + << "test-tampered-extra-signed-header.appkg" << "" + << false << false << false << false << nomd << "~package digest mismatch.*"; + QTest::newRow("invalid-info.yaml") \ + << "test-invalid-info.appkg" << "" + << false << false << false << false << nomd << "~.*did not find expected key.*"; + QTest::newRow("invalid-info.yaml-id") \ + << "test-invalid-info-id.appkg" << "" + << false << false << false << false << nomd << "~.*the identifier \\(:invalid\\) is not a valid package-id: must consist of printable ASCII characters only, except any of .*"; + QTest::newRow("invalid-footer-signature") \ + << "test-invalid-footer-signature.appkg" << "" + << true << false << false << false << nomd << "could not verify the package's developer signature"; +} + +// this test function is a bit of a kitchen sink, but the basic boiler plate +// code of testing the results of an installation is the biggest part and it +// is always the same. +void tst_PackageManager::packageInstallation() +{ + QFETCH(QString, packageName); + QFETCH(QString, updatePackageName); + QFETCH(bool, devSigned); + QFETCH(bool, storeSigned); + QFETCH(bool, expectedSuccess); + QFETCH(bool, updateExpectedSuccess); + QFETCH(QVariantMap, extraMetaData); + QFETCH(QString, errorString); + + QString installationDir = m_pm->installationLocation().value(u"path"_s).toString(); + QString documentDir = m_pm->documentLocation().value(u"path"_s).toString(); + + AllowInstallations allow(storeSigned ? AllowInstallations::RequireStoreSigned + : (devSigned ? AllowInstallations::RequireDevSigned + : AllowInstallations::AllowUnsigned)); + + int lastPass = (updatePackageName.isEmpty() ? 1 : 2); + // pass 1 is the installation / pass 2 is the update (if needed) + for (int pass = 1; pass <= lastPass; ++pass) { + // this makes the results a bit ugly to look at, but it helps with debugging a lot + if (pass > 1) + qInfo("Pass %d", pass); + + // install (or update) the package + + QUrl url = QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/") + + (pass == 1 ? packageName : updatePackageName)); + QString taskId = m_pm->startPackageInstallation(url); + QVERIFY(!taskId.isEmpty()); + m_pm->acknowledgePackageInstallation(taskId); + + // check received signals... + + if (pass == 1 ? !expectedSuccess : !updateExpectedSuccess) { + // ...in case of expected failure + + QVERIFY(m_failedSpy->wait(spyTimeout)); + QCOMPARE(m_failedSpy->first()[0].toString(), taskId); + + QT_AM_CHECK_ERRORSTRING(m_failedSpy->first()[2].toString(), errorString); + } else { + // ...in case of expected success + + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), taskId); + QVERIFY(!m_progressSpy->isEmpty()); + QCOMPARE(m_progressSpy->last()[0].toString(), taskId); + QCOMPARE(m_progressSpy->last()[1].toDouble(), double(1)); + + // check files + + //QDirIterator it(m_workDir.path(), QDirIterator::Subdirectories); + //while (it.hasNext()) { qDebug() << it.next(); } + + QVERIFY(QFile::exists(installationDir + u"/com.pelagicore.test/.installation-report.yaml"_s)); + QVERIFY(QDir(documentDir + u"/com.pelagicore.test"_s).exists()); + + QString fileCheckPath = installationDir + u"/com.pelagicore.test"_s; + + // now check the installed files + + QStringList files = QDir(fileCheckPath).entryList(QDir::AllEntries | QDir::NoDotAndDotDot); +#if defined(Q_OS_WIN) + // files starting with . are not considered hidden on Windows + files = files.filter(QRegularExpression(u"^[^.].*"_s)); +#elif defined(Q_OS_MACOS) + // starting with Qt7 file names will be reported as-is in macOS decomposed form + std::for_each(files.begin(), files.end(), [](QString &s) { s = s.normalized(QString::NormalizationForm_C); }); +#endif + files.sort(); + + QVERIFY2(files == QStringList({ u"icon.png"_s, u"info.yaml"_s, u"test"_s, QString::fromUtf8("t\xc3\xa4st") }), + qPrintable(files.join(u", "_s))); + + QFile f(fileCheckPath + u"/test"_s); + QVERIFY(f.open(QFile::ReadOnly)); + QCOMPARE(f.readAll(), QByteArray(pass == 1 ? "test\n" : "test update\n")); + f.close(); + + // check metadata + QCOMPARE(m_requestingInstallationAcknowledgeSpy->count(), 1); + QVariantMap extra = m_requestingInstallationAcknowledgeSpy->first()[2].toMap(); + QVariantMap extraSigned = m_requestingInstallationAcknowledgeSpy->first()[3].toMap(); + if (extraMetaData.value(u"extra"_s).toMap() != extra) { + qDebug() << "Actual: " << extra; + qDebug() << "Expected: " << extraMetaData.value(u"extra"_s).toMap(); + QVERIFY(extraMetaData == extra); + } + if (extraMetaData.value(u"extraSigned"_s).toMap() != extraSigned) { + qDebug() << "Actual: " << extraSigned; + qDebug() << "Expected: " << extraMetaData.value(u"extraSigned"_s).toMap(); + QVERIFY(extraMetaData == extraSigned); + } + + // check if the meta-data was saved to the installation report correctly + QVERIFY2(m_pm->installedPackageExtraMetaData(u"com.pelagicore.test"_s) == extra, + "Extra meta-data was not correctly saved to installation report"); + QVERIFY2(m_pm->installedPackageExtraSignedMetaData(u"com.pelagicore.test"_s) == extraSigned, + "Extra signed meta-data was not correctly saved to installation report"); + } + if (pass == lastPass && expectedSuccess) { + // remove package again + + clearSignalSpies(); + taskId = m_pm->removePackage(u"com.pelagicore.test"_s, false); + QVERIFY(!taskId.isEmpty()); + + // check signals + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), taskId); + } + clearSignalSpies(); + } + // check that all files are gone + + for (PathLocation pl: { Internal0, Documents0 }) { + QStringList entries = QDir(pathTo(pl)).entryList({ u"com.pelagicore.test*"_s }); + QVERIFY2(entries.isEmpty(), qPrintable(pathTo(pl) + u": "_s + entries.join(u", "_s))); + } +} + + +Q_DECLARE_METATYPE(std::function<bool()>) +typedef QMultiMap<QByteArray, std::function<bool()>> FunctionMap; +Q_DECLARE_METATYPE(FunctionMap) + +void tst_PackageManager::simulateErrorConditions_data() +{ + QTest::addColumn<bool>("testUpdate"); + QTest::addColumn<QString>("errorString"); + QTest::addColumn<FunctionMap>("functions"); + +#ifdef Q_OS_LINUX + QTest::newRow("applications-dir-read-only") \ + << false << "~could not create installation directory .*" \ + << FunctionMap { { "before-start", [this]() { return chmod(pathTo(Internal0).toLocal8Bit(), 0000) == 0; } }, + { "after-failed", [this]() { return chmod(pathTo(Internal0).toLocal8Bit(), 0777) == 0; } } }; + + QTest::newRow("documents-dir-read-only") \ + << false << "~could not create the document directory .*" \ + << FunctionMap { { "before-start", [this]() { return chmod(pathTo(Documents0).toLocal8Bit(), 0000) == 0; } }, + { "after-failed", [this]() { return chmod(pathTo(Documents0).toLocal8Bit(), 0777) == 0; } } }; +#endif +} + +void tst_PackageManager::simulateErrorConditions() +{ +#ifndef Q_OS_LINUX + QSKIP("Only tested on Linux"); +#endif + + QFETCH(bool, testUpdate); + QFETCH(QString, errorString); + QFETCH(FunctionMap, functions); + + QString taskId; + + if (testUpdate) { + // the check will run when updating a package, so we need to install it first + + taskId = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/test-dev-signed.appkg"))); + QVERIFY(!taskId.isEmpty()); + m_pm->acknowledgePackageInstallation(taskId); + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), taskId); + clearSignalSpies(); + } + + const auto beforeStart = functions.values("before-start"); + for (const auto &f : beforeStart) + QVERIFY(f()); + + taskId = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/test-dev-signed.appkg"))); + + const auto afterStart = functions.values("after-start"); + for (const auto &f : afterStart) + QVERIFY(f()); + + m_pm->acknowledgePackageInstallation(taskId); + + QVERIFY(m_failedSpy->wait(spyTimeout)); + QCOMPARE(m_failedSpy->first()[0].toString(), taskId); + QT_AM_CHECK_ERRORSTRING(m_failedSpy->first()[2].toString(), errorString); + clearSignalSpies(); + + const auto afterFailed = functions.values("after-failed"); + for (const auto &f : afterFailed) + QVERIFY(f()); + + if (testUpdate) { + taskId = m_pm->removePackage(u"com.pelagicore.test"_s, false); + + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), taskId); + } +} + + +void tst_PackageManager::cancelPackageInstallation_data() +{ + QTest::addColumn<bool>("expectedResult"); + + // please note that the data tag names are used in the actual test function below! + + QTest::newRow("before-started-signal") << true; + QTest::newRow("after-started-signal") << true; + QTest::newRow("after-blocking-until-installation-acknowledge-signal") << true; + QTest::newRow("after-finished-signal") << false; +} + +void tst_PackageManager::cancelPackageInstallation() +{ + QFETCH(bool, expectedResult); + + QString taskId = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/test-dev-signed.appkg"))); + QVERIFY(!taskId.isEmpty()); + + if (isDataTag("before-started-signal")) { + QCOMPARE(m_pm->cancelTask(taskId), expectedResult); + } else if (isDataTag("after-started-signal")) { + QVERIFY(m_startedSpy->wait(spyTimeout)); + QCOMPARE(m_startedSpy->first()[0].toString(), taskId); + QCOMPARE(m_pm->cancelTask(taskId), expectedResult); + } else if (isDataTag("after-blocking-until-installation-acknowledge-signal")) { + QVERIFY(m_blockingUntilInstallationAcknowledgeSpy->wait(spyTimeout)); + QCOMPARE(m_blockingUntilInstallationAcknowledgeSpy->first()[0].toString(), taskId); + QCOMPARE(m_pm->cancelTask(taskId), expectedResult); + } else if (isDataTag("after-finished-signal")) { + m_pm->acknowledgePackageInstallation(taskId); + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), taskId); + QCOMPARE(m_pm->cancelTask(taskId), expectedResult); + } + + if (expectedResult) { + if (!m_startedSpy->isEmpty()) { + QVERIFY(m_failedSpy->wait(spyTimeout)); + QCOMPARE(m_failedSpy->first()[0].toString(), taskId); + QCOMPARE(m_failedSpy->first()[1].toInt(), int(Error::Canceled)); + } + } else { + clearSignalSpies(); + + taskId = m_pm->removePackage(u"com.pelagicore.test"_s, false); + QVERIFY(!taskId.isEmpty()); + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), taskId); + } + clearSignalSpies(); +} + +void tst_PackageManager::parallelPackageInstallation() +{ + QString task1Id = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/test-dev-signed.appkg"))); + QVERIFY(!task1Id.isEmpty()); + QVERIFY(m_blockingUntilInstallationAcknowledgeSpy->wait(spyTimeout)); + QCOMPARE(m_blockingUntilInstallationAcknowledgeSpy->first()[0].toString(), task1Id); + + QString task2Id = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/bigtest-dev-signed.appkg"))); + QVERIFY(!task2Id.isEmpty()); + m_pm->acknowledgePackageInstallation(task2Id); + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), task2Id); + + clearSignalSpies(); + m_pm->acknowledgePackageInstallation(task1Id); + QVERIFY(m_finishedSpy->wait(spyTimeout)); + QCOMPARE(m_finishedSpy->first()[0].toString(), task1Id); + + clearSignalSpies(); +} + +void tst_PackageManager::doublePackageInstallation() +{ + QString task1Id = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/test-dev-signed.appkg"))); + QVERIFY(!task1Id.isEmpty()); + QVERIFY(m_blockingUntilInstallationAcknowledgeSpy->wait(spyTimeout)); + QCOMPARE(m_blockingUntilInstallationAcknowledgeSpy->first()[0].toString(), task1Id); + + QString task2Id = m_pm->startPackageInstallation(QUrl::fromLocalFile(QString::fromLatin1(AM_TESTDATA_DIR "packages/test-dev-signed.appkg"))); + QVERIFY(!task2Id.isEmpty()); + m_pm->acknowledgePackageInstallation(task2Id); + QVERIFY(m_failedSpy->wait(spyTimeout)); + QCOMPARE(m_failedSpy->first()[0].toString(), task2Id); + QCOMPARE(m_failedSpy->first()[2].toString(), u"Cannot install the same package com.pelagicore.test multiple times in parallel"); + + clearSignalSpies(); + m_pm->cancelTask(task1Id); + QVERIFY(m_failedSpy->wait(spyTimeout)); + QCOMPARE(m_failedSpy->first()[0].toString(), task1Id); + + clearSignalSpies(); +} + +void tst_PackageManager::validateDnsName_data() +{ + QTest::addColumn<QString>("dnsName"); + QTest::addColumn<int>("minParts"); + QTest::addColumn<bool>("valid"); + + // passes + QTest::newRow("normal") << "com.pelagicore.test" << 3 << true; + QTest::newRow("shortest") << "c.p.t" << 3 << true; + QTest::newRow("valid-chars") << "1-2.c-d.3.z" << 3 << true; + QTest::newRow("longest-part") << "com.012345678901234567890123456789012345678901234567890123456789012.test" << 3 << true; + QTest::newRow("longest-name") << "com.012345678901234567890123456789012345678901234567890123456789012.012345678901234567890123456789012345678901234567890123456789012.0123456789012.test" << 3 << true; + QTest::newRow("max-part-cnt") << "a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.0.1.2.3.4.5.6.7.8.9.a.0.12" << 3 << true; + QTest::newRow("one-part-only") << "c" << 1 << true; + + // failures + QTest::newRow("too-few-parts") << "com.pelagicore" << 3 << false; + QTest::newRow("empty-part") << "com..test" << 3 << false; + QTest::newRow("empty") << "" << 3 << false; + QTest::newRow("dot-only") << "." << 3 << false; + QTest::newRow("invalid-char1") << "com.pelagi_core.test" << 3 << false; + QTest::newRow("invalid-char2") << "com.pelagi#core.test" << 3 << false; + QTest::newRow("invalid-char3") << "com.pelagi$core.test" << 3 << false; + QTest::newRow("invalid-char4") << "com.pelagi@core.test" << 3 << false; + QTest::newRow("unicode-char") << QString::fromUtf8("c\xc3\xb6m.pelagicore.test") << 3 << false; + QTest::newRow("upper-case") << "com.Pelagicore.test" << 3 << false; + QTest::newRow("dash-at-start") << "com.-pelagicore.test" << 3 << false; + QTest::newRow("dash-at-end") << "com.pelagicore-.test" << 3 << false; + QTest::newRow("part-too-long") << "com.x012345678901234567890123456789012345678901234567890123456789012.test" << 3 << false; +} + +void tst_PackageManager::validateDnsName() +{ + QFETCH(QString, dnsName); + QFETCH(int, minParts); + QFETCH(bool, valid); + + QString errorString; + bool result = m_pm->validateDnsName(dnsName, minParts); + + QVERIFY2(valid == result, qPrintable(errorString)); +} + +void tst_PackageManager::compareVersions_data() +{ + QTest::addColumn<QString>("version1"); + QTest::addColumn<QString>("version2"); + QTest::addColumn<int>("result"); + + + QTest::newRow("1") << "" << "" << 0; + QTest::newRow("2") << "0" << "0" << 0; + QTest::newRow("3") << "foo" << "foo" << 0; + QTest::newRow("4") << "1foo" << "1foo" << 0; + QTest::newRow("5") << "foo1" << "foo1" << 0; + QTest::newRow("6") << "13.403.51-alpha2+git" << "13.403.51-alpha2+git" << 0; + QTest::newRow("7") << "1" << "2" << -1; + QTest::newRow("8") << "2" << "1" << 1; + QTest::newRow("9") << "1.0" << "2.0" << -1; + QTest::newRow("10") << "1.99" << "2.0" << -1; + QTest::newRow("11") << "1.9" << "11" << -1; + QTest::newRow("12") << "9" << "10" << -1; + QTest::newRow("13") << "9a" << "10" << -1; + QTest::newRow("14") << "9-a" << "10" << -1; + QTest::newRow("15") << "13.403.51-alpha2+gi" << "13.403.51-alpha2+git" << -1; + QTest::newRow("16") << "13.403.51-alpha1+git" << "13.403.51-alpha2+git" << -1; + QTest::newRow("17") << "13.403.51-alpha2+git" << "13.403.51-beta1+git" << -1; + QTest::newRow("18") << "13.403.51-alpha2+git" << "13.403.52" << -1; + QTest::newRow("19") << "13.403.51-alpha2+git" << "13.403.52-alpha2+git" << -1; + QTest::newRow("20") << "13.403.51-alpha2+git" << "13.404" << -1; + QTest::newRow("21") << "13.402" << "13.403.51-alpha2+git" << -1; + QTest::newRow("22") << "12.403.51-alpha2+git" << "13.403.51-alpha2+git" << -1; +} + +void tst_PackageManager::compareVersions() +{ + QFETCH(QString, version1); + QFETCH(QString, version2); + QFETCH(int, result); + + int cmp = m_pm->compareVersions(version1, version2); + QCOMPARE(cmp, result); + + if (result) { + cmp = m_pm->compareVersions(version2, version1); + QCOMPARE(cmp, -result); + } +} + +static tst_PackageManager *tstPackageManager = nullptr; + +int main(int argc, char **argv) +{ + try { + Sudo::forkServer(Sudo::DropPrivilegesPermanently); + tst_PackageManager::startedSudoServer = true; + } catch (const Exception &e) { + tst_PackageManager::sudoServerError = e.errorString(); + } + + QCoreApplication a(argc, argv); + tstPackageManager = new tst_PackageManager(&a); + + return QTest::qExec(tstPackageManager, argc, argv); +} + +#include "tst_packagemanager.moc" |