Files
BiLauncher/tests/unit/tst_buildarchiveworker.cpp
galeon 41599fa82f
tests / linux (push) Failing after 2m18s
tests / windows (push) Skipped
tests / macos (push) Skipped
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / package-smoke-macos (push) Skipped
new unit tests, hehe 67
Signed-off-by: galeon <dertiellis@gmail.com>
2026-10-12 13:14:08 +03:00

237 lines
10 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "testenv.h"
#include "fixtures.h"
#include "buildarchiveworker.h"
#include "localization.h"
#include <QDir>
#include <QSignalSpy>
class TestBuildArchiveWorker : public QObject
{
Q_OBJECT
QString m_game;
QString m_work;
void populateGame()
{
Fixtures::write(m_game + "/options.txt", "fov:90");
Fixtures::write(m_game + "/mods/a.jar", "mod-a");
Fixtures::write(m_game + "/mods/sub/b.jar", "mod-b");
Fixtures::write(m_game + "/config/x/y.toml", "y=1");
Fixtures::write(m_game + "/saves/World/level.dat", "level"); // общее — исключается
Fixtures::write(m_game + "/versions/1.20.1/1.20.1.json", "{}"); // общее — исключается
}
// Последний вызов finished(): (ok, error)
static QPair<bool, QString> result(const QSignalSpy &spy)
{
if (spy.isEmpty())
return {false, QStringLiteral("finished не пришёл")};
return {spy.constLast().at(0).toBool(), spy.constLast().at(1).toString()};
}
private slots:
void init()
{
m_game = TestEnv::freshDir(QStringLiteral("game"));
m_work = TestEnv::freshDir(QStringLiteral("archives"));
}
void archiveClearRestoreRoundTrip()
{
populateGame();
const QStringList shared{"saves", "versions"};
const QString zip = m_work + "/build.zip";
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
QSignalSpy progress(&worker, &BuildArchiveWorker::progress);
worker.archive(m_game, zip, shared);
QCOMPARE(result(finished), qMakePair(true, QString()));
QCOMPARE(Fixtures::zipEntries(zip),
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar", "options.txt"}));
// progress: (0,total) в начале и (total,total) в конце.
QCOMPARE(progress.constFirst().at(0).toInt(), 0);
QCOMPARE(progress.constLast().at(0).toInt(), 4);
QCOMPARE(progress.constLast().at(1).toInt(), 4);
worker.clear(m_game, shared);
QCOMPARE(result(finished), qMakePair(true, QString()));
QCOMPARE(Fixtures::listTree(m_game),
QStringList({"saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
worker.restore(zip, m_game);
QCOMPARE(result(finished), qMakePair(true, QString()));
QCOMPARE(Fixtures::listTree(m_game),
QStringList({"config/x/y.toml", "mods/a.jar", "mods/sub/b.jar",
"options.txt", "saves/World/level.dat", "versions/1.20.1/1.20.1.json"}));
QCOMPARE(Fixtures::readFile(m_game + "/mods/sub/b.jar"), QByteArray("mod-b"));
}
void dotFilesSurviveRoundTrip()
{
// В .minecraft живут .fabric/, .mixin.out/, на macOS ещё .DS_Store.
Fixtures::write(m_game + "/.fabric/remappedJars/x.jar", "cache");
Fixtures::write(m_game + "/mods/a.jar", "a");
const QString zip = m_work + "/dots.zip";
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.archive(m_game, zip, {});
QVERIFY(result(finished).first);
worker.clear(m_game, {});
worker.restore(zip, m_game);
QVERIFY(result(finished).first);
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
// QZipReader при чтении срезает ведущие «.» и «/» в именах записей, поэтому
// после переключения сборок .fabric/ возвращается как fabric/.
QEXPECT_FAIL("", "QZipReader срезает ведущую точку: .fabric/ восстанавливается как fabric/", Continue);
QVERIFY(QFile::exists(m_game + "/.fabric/remappedJars/x.jar"));
}
void archiveMissingGameDir()
{
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
const QString missing = m_game + "/nope";
worker.archive(missing, m_work + "/x.zip", {});
QCOMPARE(result(finished),
qMakePair(false, Loc::text("switch.error.gameDirMissing").arg(QDir::toNativeSeparators(missing))));
}
void archiveCancelRemovesTempZip()
{
populateGame();
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.requestCancel();
worker.archive(m_game, m_work + "/cancel.zip", {});
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.archivingCancelled")));
QVERIFY(!QFile::exists(m_work + "/cancel.zip"));
worker.clearCancel();
worker.archive(m_game, m_work + "/ok.zip", {});
QVERIFY(result(finished).first);
}
void clearMissingDirIsOk()
{
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.clear(m_game + "/nope", {});
QVERIFY(result(finished).first);
}
void restoreRejectsUnreadableAndUnsafeArchives()
{
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
const QString missing = m_work + "/missing.zip";
worker.restore(missing, m_game);
QCOMPARE(result(finished),
qMakePair(false, Loc::text("switch.error.archiveOpenFailed").arg(QDir::toNativeSeparators(missing))));
// Zip-slip: запись с «..» не должна вылезти из папки игры.
const QString evil = m_work + "/evil.zip";
// Ведущие «../» QZipReader срезает сам, а «..» в середине пути — нет.
QVERIFY(Fixtures::makeZip(evil, {{"ok.txt", "ok"}, {"mods/../../escaped.txt", "pwned"}}));
QVERIFY(Fixtures::zipEntries(evil).contains("mods/../../escaped.txt"));
worker.restore(evil, m_game);
QCOMPARE(result(finished),
qMakePair(false, Loc::text("switch.error.archiveBadPath").arg("mods/../../escaped.txt")));
QVERIFY(!QFile::exists(QFileInfo(m_game).absolutePath() + "/escaped.txt"));
}
void restoreCancel()
{
const QString zip = m_work + "/r.zip";
QVERIFY(Fixtures::makeZip(zip, {{"a.txt", "a"}}));
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.requestCancel();
worker.restore(zip, m_game);
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.extractCancelled")));
}
void applyPackRemovesThenWrites()
{
populateGame();
Fixtures::write(m_game + "/mods/old/legacy.jar", "old");
Fixtures::write(m_game + "/mods/keep.jar", "keep");
const QString pack = m_work + "/pack.zip";
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"},
{"config/x/y.toml", "y=2"},
{"options.txt", "fov:70"}}));
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
QSignalSpy entries(&worker, &BuildArchiveWorker::packEntries);
worker.applyPack(pack, m_game,
{"mods/old/legacy.jar", "mods/missing.jar",
"../outside.txt", "versions/1.20.1/1.20.1.json"},
{"versions", "saves"});
QCOMPARE(result(finished), qMakePair(true, QString()));
QVERIFY(!QFile::exists(m_game + "/mods/old/legacy.jar"));
QVERIFY(!QDir(m_game + "/mods/old").exists()); // опустевшая папка убрана
QVERIFY(QFile::exists(m_game + "/mods/keep.jar"));
// Удаление в запретной зоне молча пропускается.
QVERIFY(QFile::exists(m_game + "/versions/1.20.1/1.20.1.json"));
QCOMPARE(Fixtures::readFile(m_game + "/mods/new.jar"), QByteArray("new"));
QCOMPARE(Fixtures::readFile(m_game + "/config/x/y.toml"), QByteArray("y=2"));
QCOMPARE(Fixtures::readFile(m_game + "/options.txt"), QByteArray("fov:70"));
QCOMPARE(entries.count(), 1);
QStringList applied = entries.constFirst().constFirst().toStringList();
applied.sort();
QCOMPARE(applied, QStringList({"config/x/y.toml", "mods/new.jar", "options.txt"}));
}
void applyPackRefusesLauncherFolders()
{
populateGame();
const QString pack = m_work + "/bad.zip";
QVERIFY(Fixtures::makeZip(pack, {{"mods/new.jar", "new"}, {"versions/hack/hack.json", "{}"}}));
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.applyPack(pack, m_game, {"mods/a.jar"}, {"versions"});
QCOMPARE(result(finished),
qMakePair(false, Loc::text("switch.error.packTouchesLauncher").arg("versions/hack/hack.json")));
// Проверка до любых изменений: ничего не удалено и не записано.
QVERIFY(QFile::exists(m_game + "/mods/a.jar"));
QVERIFY(!QFile::exists(m_game + "/mods/new.jar"));
}
void applyPackRefusesUnsafePaths()
{
const QString pack = m_work + "/slip.zip";
QVERIFY(Fixtures::makeZip(pack, {{"mods/../../../etc/x", "x"}}));
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.applyPack(pack, m_game, {}, {});
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.error.packBadPath").arg("mods/../../../etc/x")));
}
void applyPackCancel()
{
const QString pack = m_work + "/c.zip";
QVERIFY(Fixtures::makeZip(pack, {{"a.txt", "a"}}));
BuildArchiveWorker worker;
QSignalSpy finished(&worker, &BuildArchiveWorker::finished);
worker.requestCancel();
worker.applyPack(pack, m_game, {}, {});
QCOMPARE(result(finished), qMakePair(false, Loc::text("switch.status.installCancelled")));
QVERIFY(!QFile::exists(m_game + "/a.txt"));
}
};
BILAUNCHER_TEST_MAIN(TestBuildArchiveWorker)
#include "tst_buildarchiveworker.moc"