diff options
Diffstat (limited to 'tests/auto/packager-tool/tst_packager-tool.cpp')
-rw-r--r-- | tests/auto/packager-tool/tst_packager-tool.cpp | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/tests/auto/packager-tool/tst_packager-tool.cpp b/tests/auto/packager-tool/tst_packager-tool.cpp new file mode 100644 index 00000000..7826caee --- /dev/null +++ b/tests/auto/packager-tool/tst_packager-tool.cpp @@ -0,0 +1,414 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Copyright (C) 2019 Luxoft Sweden AB +** Copyright (C) 2018 Pelagicore AG +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtApplicationManager module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtTest> +#include <QCoreApplication> + +#include "global.h" +#include "applicationmanager.h" +#include "application.h" +#include "qtyaml.h" +#include "exception.h" +#include "packagedatabase.h" +#include "packagemanager.h" +#include "packagingjob.h" +#include "qmlinprocessruntime.h" +#include "runtimefactory.h" +#include "utilities.h" + +#include "../error-checking.h" + +QT_USE_NAMESPACE_AM + +static int spyTimeout = 5000; // shorthand for specifying QSignalSpy timeouts + +class tst_PackagerTool : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanup(); + + void test(); + void brokenMetadata_data(); + void brokenMetadata(); + void iconFileName(); + +private: + QString pathTo(const char *file) + { + return QDir(m_workDir.path()).absoluteFilePath(QLatin1String(file)); + } + + bool createInfoYaml(QTemporaryDir &tmp, const QString &changeField = QString(), const QVariant &toValue = QVariant()); + bool createIconPng(QTemporaryDir &tmp); + bool createCode(QTemporaryDir &tmp); + void createDummyFile(QTemporaryDir &tmp, const QString &fileName, const char *data); + + void installPackage(const QString &filePath); + + PackageManager *m_pm = nullptr; + QTemporaryDir m_workDir; + + QString m_devPassword; + QString m_devCertificate; + QString m_storePassword; + QString m_storeCertificate; + QStringList m_caFiles; + QString m_hardwareId; +}; + +void tst_PackagerTool::initTestCase() +{ + if (!QDir(qL1S(AM_TESTDATA_DIR "/packages")).exists()) + QSKIP("No test packages available in the data/ directory"); + + spyTimeout *= timeoutFactor(); + + QVERIFY(m_workDir.isValid()); + QVERIFY(QDir::root().mkpath(pathTo("internal-0"))); + QVERIFY(QDir::root().mkpath(pathTo("documents-0"))); + + m_hardwareId = qSL("foobar"); + + PackageDatabase *pdb = new PackageDatabase({}, pathTo("internal-0")); + try { + m_pm = PackageManager::createInstance(pdb, pathTo("documents-0")); + m_pm->setHardwareId(m_hardwareId); + m_pm->enableInstaller(); + } catch (const Exception &e) { + QVERIFY2(false, e.what()); + } + + QVERIFY(ApplicationManager::createInstance(true)); + + + // crypto stuff - we need to load the root CA and developer CA certificates + + QFile devcaFile(qL1S(AM_TESTDATA_DIR "certificates/devca.crt")); + QFile caFile(qL1S(AM_TESTDATA_DIR "certificates/ca.crt")); + QVERIFY2(devcaFile.open(QIODevice::ReadOnly), qPrintable(devcaFile.errorString())); + QVERIFY2(caFile.open(QIODevice::ReadOnly), qPrintable(devcaFile.errorString())); + + QList<QByteArray> chainOfTrust; + chainOfTrust << devcaFile.readAll() << caFile.readAll(); + QVERIFY(!chainOfTrust.at(0).isEmpty()); + QVERIFY(!chainOfTrust.at(1).isEmpty()); + m_pm->setCACertificates(chainOfTrust); + + m_caFiles << devcaFile.fileName() << caFile.fileName(); + + m_devPassword = qSL("password"); + m_devCertificate = qL1S(AM_TESTDATA_DIR "certificates/dev1.p12"); + m_storePassword = qSL("password"); + m_storeCertificate = qL1S(AM_TESTDATA_DIR "certificates/store.p12"); + + RuntimeFactory::instance()->registerRuntime(new QmlInProcessRuntimeManager(qSL("qml"))); +} + +void tst_PackagerTool::cleanup() +{ + recursiveOperation(pathTo("internal-0"), safeRemove); + recursiveOperation(pathTo("documents-0"), safeRemove); + + QDir dir(m_workDir.path()); + QStringList fileNames = dir.entryList(QDir::Files); + for (auto fileName : fileNames) + dir.remove(fileName); +} + +// exceptions are nice -- just not for unit testing :) +static bool packagerCheck(PackagingJob *p, QString &errorString) +{ + bool result = false; + try { + p->execute(); + errorString.clear(); + result = (p->resultCode() == 0); + if (!result) + errorString = p->output(); + } catch (const Exception &e) { \ + errorString = e.errorString(); + } + delete p; + return result; +} + +void tst_PackagerTool::test() +{ + QTemporaryDir tmp; + QString errorString; + + // no valid destination + QVERIFY(!packagerCheck(PackagingJob::create(pathTo("test.appkg"), pathTo("test.appkg")), errorString)); + QVERIFY2(errorString.contains(qL1S("is not a directory")), qPrintable(errorString)); + + // no valid info.yaml + QVERIFY(!packagerCheck(PackagingJob::create(pathTo("test.appkg"), tmp.path()), errorString)); + QVERIFY2(errorString.contains(qL1S("Cannot open for reading")), qPrintable(errorString)); + + // add an info.yaml file + createInfoYaml(tmp); + + // no icon + QVERIFY(!packagerCheck(PackagingJob::create(pathTo("test.appkg"), tmp.path()), errorString)); + QVERIFY2(errorString.contains(qL1S("missing the file referenced by the 'icon' field")), qPrintable(errorString)); + + // add an icon + createIconPng(tmp); + + // no valid code + QVERIFY(!packagerCheck(PackagingJob::create(pathTo("test.appkg"), tmp.path()), errorString)); + QVERIFY2(errorString.contains(qL1S("missing the file referenced by the 'code' field")), qPrintable(errorString)); + + // add a code file + createCode(tmp); + + // invalid destination + QVERIFY(!packagerCheck(PackagingJob::create(tmp.path(), tmp.path()), errorString)); + QVERIFY2(errorString.contains(qL1S("could not create package file")), qPrintable(errorString)); + + // now everything is correct - try again + QVERIFY2(packagerCheck(PackagingJob::create(pathTo("test.appkg"), tmp.path()), errorString), qPrintable(errorString)); + + // invalid source package + QVERIFY(!packagerCheck(PackagingJob::developerSign( + pathTo("no-such-file"), + pathTo("test.dev-signed.appkg"), + m_devCertificate, + m_devPassword), errorString)); + QVERIFY2(errorString.contains(qL1S("does not exist")), qPrintable(errorString)); + + // invalid destination package + QVERIFY(!packagerCheck(PackagingJob::developerSign( + pathTo("test.appkg"), + pathTo("."), + m_devCertificate, + m_devPassword), errorString)); + QVERIFY2(errorString.contains(qL1S("could not create package file")), qPrintable(errorString)); + + + // invalid dev key + QVERIFY(!packagerCheck(PackagingJob::developerSign( + pathTo("test.appkg"), + pathTo("test.dev-signed.appkg"), + m_devCertificate, + qSL("wrong-password")), errorString)); + QVERIFY2(errorString.contains(qL1S("could not create signature")), qPrintable(errorString)); + + // invalid store key + QVERIFY(!packagerCheck(PackagingJob::storeSign( + pathTo("test.appkg"), + pathTo("test.store-signed.appkg"), + m_storeCertificate, + qSL("wrong-password"), + m_hardwareId), errorString)); + QVERIFY2(errorString.contains(qL1S("could not create signature")), qPrintable(errorString)); + + // sign + QVERIFY2(packagerCheck(PackagingJob::developerSign( + pathTo("test.appkg"), + pathTo("test.dev-signed.appkg"), + m_devCertificate, + m_devPassword), errorString), qPrintable(errorString)); + + QVERIFY2(packagerCheck(PackagingJob::storeSign( + pathTo("test.appkg"), + pathTo("test.store-signed.appkg"), + m_storeCertificate, + m_storePassword, + m_hardwareId), errorString), qPrintable(errorString)); + + // verify + QVERIFY2(packagerCheck(PackagingJob::developerVerify( + pathTo("test.dev-signed.appkg"), + m_caFiles), errorString), qPrintable(errorString)); + + QVERIFY2(packagerCheck(PackagingJob::storeVerify( + pathTo("test.store-signed.appkg"), + m_caFiles, + m_hardwareId), errorString), qPrintable(errorString)); + + // now that we have it, see if the package actually installs correctly + + installPackage(pathTo("test.dev-signed.appkg")); + + QDir checkDir(pathTo("internal-0")); + QVERIFY(checkDir.cd(qSL("com.pelagicore.test"))); + + for (const QString &file : { qSL("info.yaml"), qSL("icon.png"), qSL("test.qml") }) { + QVERIFY(checkDir.exists(file)); + QFile src(QDir(tmp.path()).absoluteFilePath(file)); + QVERIFY(src.open(QFile::ReadOnly)); + QFile dst(checkDir.absoluteFilePath(file)); + QVERIFY(dst.open(QFile::ReadOnly)); + QCOMPARE(src.readAll(), dst.readAll()); + } +} + +void tst_PackagerTool::brokenMetadata_data() +{ + QTest::addColumn<QString>("yamlField"); + QTest::addColumn<QVariant>("yamlValue"); + QTest::addColumn<QString>("errorString"); + + QTest::newRow("missing-name") << qSL("name") << QVariant() << "~.*Required fields are missing: name.*"; + QTest::newRow("missing-runtime") << qSL("runtime") << QVariant() << "~.*Required fields are missing: runtime"; + QTest::newRow("missing-identifier") << qSL("id") << QVariant() << "~.*Required fields are missing: id"; + QTest::newRow("missing-code") << qSL("code") << QVariant() << "~.*Required fields are missing: code"; +} + +void tst_PackagerTool::brokenMetadata() +{ + QFETCH(QString, yamlField); + QFETCH(QVariant, yamlValue); + QFETCH(QString, errorString); + + QTemporaryDir tmp; + + createCode(tmp); + createIconPng(tmp); + createInfoYaml(tmp, yamlField, yamlValue); + + // check if packaging actually fails with the expected error + + QString error; + QVERIFY2(!packagerCheck(PackagingJob::create(pathTo("test.appkg"), tmp.path()), error), qPrintable(error)); + AM_CHECK_ERRORSTRING(error, errorString); +} + +/* + Specify an icon whose name is different from "icon.png". + Packaging should work fine + */ +void tst_PackagerTool::iconFileName() +{ + QTemporaryDir tmp; + QString errorString; + + createInfoYaml(tmp, qSL("icon"), qSL("foo.bar")); + createCode(tmp); + createDummyFile(tmp, qSL("foo.bar"), "this-is-a-dummy-icon-file"); + + QVERIFY2(packagerCheck(PackagingJob::create(pathTo("test-foobar-icon.appkg"), tmp.path()), errorString), + qPrintable(errorString)); + + // see if the package installs correctly + + m_pm->setAllowInstallationOfUnsignedPackages(true); + installPackage(pathTo("test-foobar-icon.appkg")); + m_pm->setAllowInstallationOfUnsignedPackages(false); + + QDir checkDir(pathTo("internal-0")); + QVERIFY(checkDir.cd(qSL("com.pelagicore.test"))); + + for (const QString &file : { qSL("info.yaml"), qSL("foo.bar"), qSL("test.qml") }) { + QVERIFY(checkDir.exists(file)); + QFile src(QDir(tmp.path()).absoluteFilePath(file)); + QVERIFY(src.open(QFile::ReadOnly)); + QFile dst(checkDir.absoluteFilePath(file)); + QVERIFY(dst.open(QFile::ReadOnly)); + QCOMPARE(src.readAll(), dst.readAll()); + } +} + + +bool tst_PackagerTool::createInfoYaml(QTemporaryDir &tmp, const QString &changeField, const QVariant &toValue) +{ + QByteArray yaml = + "formatVersion: 1\n" + "formatType: am-application\n" + "---\n" + "id: com.pelagicore.test\n" + "name: { en_US: 'test' }\n" + "icon: icon.png\n" + "code: test.qml\n" + "runtime: qml\n"; + + if (!changeField.isEmpty()) { + QVector<QVariant> docs; + try { + docs = YamlParser::parseAllDocuments(yaml); + } catch (...) { + } + + QVariantMap map = docs.at(1).toMap(); + if (!toValue.isValid()) + map.remove(changeField); + else + map[changeField] = toValue; + yaml = QtYaml::yamlFromVariantDocuments({ docs.at(0), map }); + } + + QFile infoYaml(QDir(tmp.path()).absoluteFilePath(qSL("info.yaml"))); + return infoYaml.open(QFile::WriteOnly) && infoYaml.write(yaml) == yaml.size(); +} + +bool tst_PackagerTool::createIconPng(QTemporaryDir &tmp) +{ + QFile iconPng(QDir(tmp.path()).absoluteFilePath(qSL("icon.png"))); + return iconPng.open(QFile::WriteOnly) && iconPng.write("\x89PNG") == 4; +} + +bool tst_PackagerTool::createCode(QTemporaryDir &tmp) +{ + QFile code(QDir(tmp.path()).absoluteFilePath(qSL("test.qml"))); + return code.open(QFile::WriteOnly) && code.write("// test") == 7LL; +} + +void tst_PackagerTool::createDummyFile(QTemporaryDir &tmp, const QString &fileName, const char *data) +{ + QFile code(QDir(tmp.path()).absoluteFilePath(fileName)); + QVERIFY(code.open(QFile::WriteOnly)); + + auto written = code.write(data); + + QCOMPARE(written, static_cast<qint64>(strlen(data))); +} + +void tst_PackagerTool::installPackage(const QString &filePath) +{ + QSignalSpy finishedSpy(m_pm, &PackageManager::taskFinished); + + m_pm->setDevelopmentMode(true); // allow packages without store signature + + QString taskId = m_pm->startPackageInstallation(QUrl::fromLocalFile(filePath)); + m_pm->acknowledgePackageInstallation(taskId); + + QVERIFY(finishedSpy.wait(2 * spyTimeout)); + QCOMPARE(finishedSpy.first()[0].toString(), taskId); + + m_pm->setDevelopmentMode(false); +} + +QTEST_GUILESS_MAIN(tst_PackagerTool) + +#include "tst_packager-tool.moc" |