#include "testenv.h" #include "fixtures.h" #include "nettest.h" #include "javainstaller.h" #include "javalocator.h" #include "javaruntime.h" #include "launcherpaths.h" #include "localization.h" #include #include #include #include namespace { const QString kTemurin = QStringLiteral("https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21/"); const QString kMojangManifest = QStringLiteral("https://piston-meta.mojang.com/v1/packages/x/manifest.json"); const QString kMojangFiles = QStringLiteral("https://piston-data.mojang.com/v1/objects/"); JavaRuntimeEntry temurinEntry(const QString &archive, const QByteArray &data) { JavaRuntimeEntry entry; entry.id = QStringLiteral("temurin-jdk-21.0.2_13-test-") + QString(archive).replace(u'.', u'-'); entry.kind = JavaRuntimeKind::Jdk; entry.major = 21; entry.version = QStringLiteral("21.0.2+13"); entry.archive = archive; entry.url = QUrl(kTemurin + "OpenJDK21U-jdk." + archive); entry.checksum = Fixtures::sha256(data); entry.size = data.size(); return entry; } // Раскладка архива Temurin: один корневой каталог с bin/java внутри. QMap jdkTree() { return {{QStringLiteral("jdk-21.0.2+13/bin/") + Fixtures::javaExecutableName(), "#!/bin/sh\necho java\n"}, {QStringLiteral("jdk-21.0.2+13/lib/modules"), QByteArray(2048, 'm')}, {QStringLiteral("jdk-21.0.2+13/release"), "JAVA_VERSION=\"21.0.2\""}}; } } // namespace class TestJavaInstaller : public QObject { Q_OBJECT FakeInternet m_net; QString m_work; struct Run { explicit Run(JavaInstaller &installer) : started(&installer, &JavaInstaller::started) , finished(&installer, &JavaInstaller::finished) , failed(&installer, &JavaInstaller::failed) , canceled(&installer, &JavaInstaller::canceled) {} bool waitEnd() { return QTest::qWaitFor([&] { return finished.count() + failed.count() + canceled.count() > 0; }, 30000); } QString failure() const { return failed.isEmpty() ? QString() : failed.constFirst().at(1).toString(); } QSignalSpy started, finished, failed, canceled; }; void verifyRegistered(const JavaRuntimeEntry &entry, const Run &run) { QCOMPARE(run.finished.count(), 1); QCOMPARE(run.finished.constFirst().at(0).toString(), entry.id); const QString javaPath = run.finished.constFirst().at(1).toString(); QVERIFY(QFileInfo(javaPath).isFile()); QVERIFY(javaPath.startsWith(JavaRuntimeStore::dirFor(entry.id) + u'/')); #if !defined(Q_OS_WIN) QVERIFY(QFileInfo(javaPath).isExecutable()); #endif const auto installed = JavaRuntimeStore::find(entry.id); QVERIFY(installed.has_value()); QCOMPARE(installed->major, entry.major); QCOMPARE(installed->version, entry.version); QCOMPARE(installed->kind, javaKindKey(entry.kind)); // Лаунчер видит её при поиске Java. QStringList canonical; for (const QString &path : JavaLocator::findAll(m_work)) canonical << QFileInfo(path).canonicalFilePath(); QVERIFY(canonical.contains(QFileInfo(javaPath).canonicalFilePath())); // Скачанный архив за собой не оставляем. QVERIFY(QDir(LauncherPaths::javaDownloadDir()).entryList(QDir::Files).isEmpty()); } private slots: void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); } void init() { TestEnv::resetLauncherRoot(); m_net.reset(); m_work = TestEnv::freshDir(QStringLiteral("game")); } void installsZipArchive() { const QByteArray zip = Fixtures::zipBytes(jdkTree()); const JavaRuntimeEntry entry = temurinEntry("zip", zip); m_net.route(entry.url.toString(), FakeResponse::bytes(zip)); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(installer.isRunning()); QCOMPARE(run.started.count(), 1); QVERIFY(run.waitEnd()); QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure())); verifyRegistered(entry, run); QVERIFY(!installer.isRunning()); } void installsTarGzArchive() { #if defined(Q_OS_WIN) QSKIP("Temurin под Windows — только zip; tar.gz распаковывается системным tar из PATH"); #endif const QString archive = m_work + "/jdk.tar.gz"; if (!Fixtures::makeTarGz(archive, jdkTree())) QSKIP("системный tar недоступен"); const QByteArray data = Fixtures::readFile(archive); const JavaRuntimeEntry entry = temurinEntry("tar.gz", data); m_net.route(entry.url.toString(), FakeResponse::bytes(data)); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure())); verifyRegistered(entry, run); } void archiveChecksumMismatch() { const QByteArray zip = Fixtures::zipBytes(jdkTree()); JavaRuntimeEntry entry = temurinEntry("zip", zip); entry.checksum = QString(64, u'0'); m_net.route(entry.url.toString(), FakeResponse::bytes(zip)); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QCOMPARE(run.failure(), Loc::text("java.error.archiveChecksum")); QVERIFY(!QDir(JavaRuntimeStore::dirFor(entry.id)).exists()); } void archiveWithoutJavaFails() { const QByteArray zip = Fixtures::zipBytes({{"jdk/README", "no binaries here"}}); const JavaRuntimeEntry entry = temurinEntry("zip", zip); m_net.route(entry.url.toString(), FakeResponse::bytes(zip)); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QCOMPARE(run.failure(), Loc::text("java.error.noExecutable")); QVERIFY(!JavaRuntimeStore::find(entry.id).has_value()); } void zipSlipIsRejected() { const QByteArray zip = Fixtures::zipBytes({{"jdk/../../../escaped", "x"}}); const JavaRuntimeEntry entry = temurinEntry("zip", zip); m_net.route(entry.url.toString(), FakeResponse::bytes(zip)); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QCOMPARE(run.failure(), Loc::text("java.error.archiveBadPath").arg("jdk/../../../escaped")); QVERIFY(!QFile::exists(LauncherPaths::rootDir() + "/escaped")); } void unknownFormatAndInvalidEntry() { JavaInstaller installer; Run run(installer); installer.install(JavaRuntimeEntry{}); QCOMPARE(run.failure(), Loc::text("java.error.buildUnavailable")); JavaRuntimeEntry entry = temurinEntry("7z", "x"); Run run2(installer); installer.install(entry); QCOMPARE(run2.failure(), Loc::text("java.error.unknownArchiveFormat").arg("7z")); } void mojangComponent() { const QByteArray java = "#!/bin/sh\necho mojang-java\n"; const QByteArray lib = QByteArray(1024, 'l'); const QString binary = QStringLiteral("bin/") + Fixtures::javaExecutableName(); QJsonObject files{ {"bin", QJsonObject{{"type", "directory"}}}, {binary, QJsonObject{{"type", "file"}, {"executable", true}, {"downloads", QJsonObject{{"raw", QJsonObject{ {"url", kMojangFiles + Fixtures::sha1(java) + "/java"}, {"sha1", Fixtures::sha1(java)}, {"size", java.size()}}}}}}}, {"lib/modules", QJsonObject{{"type", "file"}, {"downloads", QJsonObject{{"raw", QJsonObject{ {"url", kMojangFiles + Fixtures::sha1(lib) + "/modules"}, {"sha1", Fixtures::sha1(lib)}, {"size", lib.size()}}}}}}}, {"lib/modules-link", QJsonObject{{"type", "link"}, {"target", "modules"}}}}; m_net.route(kMojangManifest, FakeResponse::json(QJsonObject{{"files", files}})); m_net.route(kMojangFiles + Fixtures::sha1(java) + "/java", FakeResponse::bytes(java)); int libAttempts = 0; m_net.route(kMojangFiles + Fixtures::sha1(lib) + "/modules", [&](const FakeRequest &) { return ++libAttempts == 1 ? FakeResponse::error(503) : FakeResponse::bytes(lib); // временный сбой }); JavaRuntimeEntry entry; entry.id = QStringLiteral("mojang-java-runtime-delta"); entry.kind = JavaRuntimeKind::Mojang; entry.major = 21; entry.version = QStringLiteral("21.0.7"); entry.component = QStringLiteral("java-runtime-delta"); entry.archive = QStringLiteral("mojang"); entry.url = QUrl(kMojangManifest); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure())); verifyRegistered(entry, run); QCOMPARE(libAttempts, 2); const QString dir = JavaRuntimeStore::dirFor(entry.id); QCOMPARE(Fixtures::readFile(dir + "/lib/modules"), lib); QVERIFY(QFileInfo::exists(dir + "/lib/modules-link")); } void mojangManifestEscapingIsRejected() { m_net.route(kMojangManifest, FakeResponse::json(QJsonObject{ {"files", QJsonObject{{"../../evil", QJsonObject{{"type", "directory"}}}}}})); JavaRuntimeEntry entry; entry.id = QStringLiteral("mojang-evil"); entry.kind = JavaRuntimeKind::Mojang; entry.major = 17; entry.version = QStringLiteral("17"); entry.archive = QStringLiteral("mojang"); entry.url = QUrl(kMojangManifest); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QCOMPARE(run.failure(), Loc::text("java.error.manifestBadPath").arg("../../evil")); } void cancelDuringDownload() { FakeResponse hang; hang.hang = true; const JavaRuntimeEntry entry = temurinEntry("zip", "whatever"); m_net.route(entry.url.toString(), hang); JavaInstaller installer; Run run(installer); installer.install(entry); QTRY_COMPARE_WITH_TIMEOUT(m_net.hits(entry.url.toString()), 1, 15000); installer.cancel(); QVERIFY(run.waitEnd()); QVERIFY(!QDir(JavaRuntimeStore::dirFor(entry.id)).exists()); QVERIFY(!installer.isRunning()); QVERIFY(run.failed.isEmpty()); // abort() синхронно доводит ответ до handleArchiveFinished(), который уже // шлёт canceled; затем cancel() видит m_reply == nullptr и шлёт его ещё раз. QEXPECT_FAIL("", "canceled приходит дважды: из handleArchiveFinished() и из cancel()", Continue); QCOMPARE(run.canceled.count(), 1); } void reinstallReplacesOld() { const QByteArray zip = Fixtures::zipBytes(jdkTree()); const JavaRuntimeEntry entry = temurinEntry("zip", zip); Fixtures::write(JavaRuntimeStore::dirFor(entry.id) + "/stale/file", "old"); m_net.route(entry.url.toString(), FakeResponse::bytes(zip)); JavaInstaller installer; Run run(installer); installer.install(entry); QVERIFY(run.waitEnd()); QVERIFY2(run.failed.isEmpty(), qPrintable(run.failure())); QVERIFY(!QFile::exists(JavaRuntimeStore::dirFor(entry.id) + "/stale/file")); } }; BILAUNCHER_TEST_MAIN(TestJavaInstaller) #include "tst_javainstaller.moc"