mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### Brief overview of PR changes/additions - Renames the six test executables whose filenames trip Windows' UAC installer detection: `UpdaterChecksumTest` to `ReleaseChecksumPairingTest`, `UpdaterPlatformAssetTest` to `ReleasePlatformAssetTest`, `UpdaterTeardownTest` to `NewReleaseDialogTeardownTest`, `PackageSelfUninstallTest` to `PackageSelfRemovalTest`, `PackageUninstallSaveTeardownTest` to `PackageRemovalSaveTeardownTest`, `ActionSelfUninstallTest` to `ActionSelfRemovalTest`. Assertions are untouched. - Adds a configure-time gate in `test/CMakeLists.txt` that fails with an actionable message if any test executable name contains install, setup, update or patch. It checks both the targets a configuration builds and the test source filenames, so conditionally registered tests cannot slip past it. - Documents the naming rule in `test/README.md`. #### Motivation for adding to Mudlet Windows treats an unsigned executable named that way as an installer and refuses to start it, so those six tests reported `BAD_COMMAND` for anyone running the suite from an ordinary Windows shell - and because CI runners are elevated, nothing caught it as more tests were added. #### Other info (issues closed, discussion etc) Fixes #9748 The gate was verified to fire on a target named after the guard statement, on one in a subdirectory, on `EventDispatcherTest` (the message names the offending substring, since "dispatch" contains "patch"), on a test registered only under `USE_UPDATER` when configuring with the updater off, and to fail loudly if the walk ever stops finding executables. **Test case:** `cmake --build build && ctest --test-dir build` - 92/92 pass; adding a test named e.g. `FooUpdateTest` fails the configure with an explanation. Assisted-by: Claude:claude-opus-5
230 lines
12 KiB
C++
230 lines
12 KiB
C++
/***************************************************************************
|
|
* Copyright (C) 2026 by Vadim Peretokin - vadim.peretokin@mudlet.org *
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* This program is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
|
* GNU General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program; if not, write to the *
|
|
* Free Software Foundation, Inc., *
|
|
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
|
***************************************************************************/
|
|
|
|
#include "../src/updater/Feed.h"
|
|
#include "../src/updater/Release.h"
|
|
|
|
#include <QtTest/QtTest>
|
|
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
|
|
/*
|
|
* Covers the pairing of a release's assets with its SHA256SUMS.txt, which is what
|
|
* decides whether an update can be installed at all.
|
|
*
|
|
* Windows auto-update broke on the 2026-08-01 PTB: the release carried five
|
|
* binaries but a SHA256SUMS.txt covering only four, and the uncovered one was the
|
|
* .exe the Windows updater picks. Feed refuses to install a download it cannot
|
|
* verify, so users got "Could not verify the integrity of the download" and no
|
|
* update. The data below is that release's real asset list and checksum file.
|
|
*
|
|
* The publishing side of the fix - never overwriting SHA256SUMS.txt with a file
|
|
* that covers fewer binaries - is covered by test/ci/release-checksums-test.sh.
|
|
*/
|
|
|
|
namespace {
|
|
const auto tag = QStringLiteral("Mudlet-4.22.0-ptb-2026-08-01-dfdcb137");
|
|
const auto windowsAsset = QStringLiteral("Mudlet-4.22.0-ptb-2026-08-01-dfdcb137-windows-64.exe");
|
|
const auto linuxAsset = QStringLiteral("Mudlet-4.22.0-ptb-2026-08-01-dfdcb137-linux-x64.AppImage.tar");
|
|
const auto arm64Asset = QStringLiteral("Mudlet-4.22.0-ptb-2026-08-01-dfdcb137-arm64.dmg");
|
|
const auto intelMacAsset = QStringLiteral("Mudlet-4.22.0-ptb-2026-08-01-dfdcb137-x86_64.dmg");
|
|
// A re-run of the Windows build restamped the date and appended a rebuild counter
|
|
const auto rebuiltWindowsAsset = QStringLiteral("Mudlet-4.22.0-ptb-2026-08-02-dfdcb137rebuild2-windows-64.exe");
|
|
|
|
const auto windowsHash = QStringLiteral("72dba076741a245a994b553b1cf88dba5d7f5bb07f0d6887ecd58361402764e1");
|
|
const auto linuxHash = QStringLiteral("72a239146b07fc3b94f9e96955d2d6d52a6f0f40c8a5b7ee314b0bf875a55611");
|
|
const auto arm64Hash = QStringLiteral("7a7a75723e15a3443c4e8937fe2a85cee90b9dc8938e1e0580887e5c5e1fabbb");
|
|
const auto intelMacHash = QStringLiteral("d2426d7f799b619b0bfcb1e51e373f776288f77584bc9f08a79cc288cc58278c");
|
|
const auto rebuiltWindowsHash = QStringLiteral("9b3895247937d37f645dd31e7ded739e3126ccad6bfa4188cdd3b78f735c2f6f");
|
|
|
|
// Exactly what the 2026-08-01 PTB shipped: the .exe on the release is absent and a
|
|
// different build's .exe is listed in its place
|
|
QString publishedChecksums()
|
|
{
|
|
return QStringLiteral("%1 %2\n%3 %4\n%5 *%6\n%7 %8\n").arg(linuxHash, linuxAsset, intelMacHash, intelMacAsset, rebuiltWindowsHash, rebuiltWindowsAsset, arm64Hash, arm64Asset);
|
|
}
|
|
|
|
// What the release should have shipped, and does once the publishing scripts merge
|
|
// instead of overwrite
|
|
QString mergedChecksums()
|
|
{
|
|
return publishedChecksums() + QStringLiteral("%1 *%2\n").arg(windowsHash, windowsAsset);
|
|
}
|
|
|
|
// The 2026-08-01 PTB's assets, in the order the GitHub releases API returns them -
|
|
// which is what decided the bug: had the API listed the rebuilt .exe first, the
|
|
// updater would have chosen the one that *was* covered. published_at and size are
|
|
// filler, only the tag and the asset names and order are the release's real values.
|
|
QJsonObject releaseJson()
|
|
{
|
|
const QStringList assetNames{arm64Asset, linuxAsset, windowsAsset, intelMacAsset, rebuiltWindowsAsset, QStringLiteral("SHA256SUMS.txt")};
|
|
|
|
QJsonArray assets;
|
|
for (const auto& name : assetNames) {
|
|
QJsonObject asset;
|
|
asset.insert(QStringLiteral("name"), name);
|
|
asset.insert(QStringLiteral("browser_download_url"), QStringLiteral("https://github.com/Mudlet/Mudlet/releases/download/%1/%2").arg(tag, name));
|
|
asset.insert(QStringLiteral("size"), 137252032);
|
|
assets.append(asset);
|
|
}
|
|
|
|
QJsonObject release;
|
|
release.insert(QStringLiteral("tag_name"), tag);
|
|
release.insert(QStringLiteral("published_at"), QStringLiteral("2026-07-31T18:22:33Z"));
|
|
release.insert(QStringLiteral("prerelease"), true);
|
|
release.insert(QStringLiteral("draft"), false);
|
|
release.insert(QStringLiteral("assets"), assets);
|
|
return release;
|
|
}
|
|
} // namespace
|
|
|
|
class ReleaseChecksumPairingTest : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
void windowsDownloadIsThePlatformExe();
|
|
void publishedChecksumsDoNotCoverTheWindowsDownload();
|
|
void mergedChecksumsCoverTheWindowsDownload();
|
|
void everyOtherPlatformWasAlreadyCovered();
|
|
void binaryAndTextModeLinesBothParse();
|
|
void anotherBuildsEntryDoesNotCoverThisDownload();
|
|
void aLongerNameContainingThisOneDoesNotCoverIt();
|
|
void aPathPrefixedEntryStillCoversTheDownload();
|
|
void malformedLinesAreIgnored();
|
|
void emptyInputsYieldNoChecksum();
|
|
void entriesParsedTellsAnUnreadableFileFromAMissingEntry();
|
|
};
|
|
|
|
// The updater picks the first asset matching its platform, so this is the file
|
|
// whose checksum has to be present
|
|
void ReleaseChecksumPairingTest::windowsDownloadIsThePlatformExe()
|
|
{
|
|
const dblsqd::Release release(releaseJson(), QStringLiteral("win"), QStringLiteral("x86_64"));
|
|
|
|
QCOMPARE(release.getDownloadUrl().fileName(), windowsAsset);
|
|
QCOMPARE(release.getChecksumsUrl().fileName(), QStringLiteral("SHA256SUMS.txt"));
|
|
}
|
|
|
|
// The regression: this is why Windows auto-update failed
|
|
void ReleaseChecksumPairingTest::publishedChecksumsDoNotCoverTheWindowsDownload()
|
|
{
|
|
const dblsqd::Release release(releaseJson(), QStringLiteral("win"), QStringLiteral("x86_64"));
|
|
|
|
QVERIFY(dblsqd::Feed::findChecksum(publishedChecksums(), release.getDownloadUrl().fileName()).isEmpty());
|
|
}
|
|
|
|
void ReleaseChecksumPairingTest::mergedChecksumsCoverTheWindowsDownload()
|
|
{
|
|
const dblsqd::Release release(releaseJson(), QStringLiteral("win"), QStringLiteral("x86_64"));
|
|
|
|
QCOMPARE(dblsqd::Feed::findChecksum(mergedChecksums(), release.getDownloadUrl().fileName()), windowsHash);
|
|
}
|
|
|
|
void ReleaseChecksumPairingTest::everyOtherPlatformWasAlreadyCovered()
|
|
{
|
|
const dblsqd::Release linuxRelease(releaseJson(), QStringLiteral("linux"), QStringLiteral("x86_64"));
|
|
QCOMPARE(linuxRelease.getDownloadUrl().fileName(), linuxAsset);
|
|
QCOMPARE(dblsqd::Feed::findChecksum(publishedChecksums(), linuxRelease.getDownloadUrl().fileName()), linuxHash);
|
|
|
|
const dblsqd::Release intelMacRelease(releaseJson(), QStringLiteral("mac"), QStringLiteral("x86_64"));
|
|
QCOMPARE(intelMacRelease.getDownloadUrl().fileName(), intelMacAsset);
|
|
QCOMPARE(dblsqd::Feed::findChecksum(publishedChecksums(), intelMacRelease.getDownloadUrl().fileName()), intelMacHash);
|
|
|
|
const dblsqd::Release appleSiliconRelease(releaseJson(), QStringLiteral("mac"), QStringLiteral("arm64"));
|
|
QCOMPARE(appleSiliconRelease.getDownloadUrl().fileName(), arm64Asset);
|
|
QCOMPARE(dblsqd::Feed::findChecksum(publishedChecksums(), appleSiliconRelease.getDownloadUrl().fileName()), arm64Hash);
|
|
}
|
|
|
|
// sha256sum writes two spaces in text mode and " *" in binary mode; the Windows
|
|
// build produces the latter
|
|
void ReleaseChecksumPairingTest::binaryAndTextModeLinesBothParse()
|
|
{
|
|
QCOMPARE(dblsqd::Feed::findChecksum(QStringLiteral("%1 *%2").arg(windowsHash, windowsAsset), windowsAsset), windowsHash);
|
|
QCOMPARE(dblsqd::Feed::findChecksum(QStringLiteral("%1 %2").arg(windowsHash, windowsAsset), windowsAsset), windowsHash);
|
|
QCOMPARE(dblsqd::Feed::findChecksum(QStringLiteral("%1\t%2").arg(windowsHash, windowsAsset), windowsAsset), windowsHash);
|
|
// trailing CR from a file written on Windows must not become part of the name
|
|
QCOMPARE(dblsqd::Feed::findChecksum(QStringLiteral("%1 *%2\r\n").arg(windowsHash, windowsAsset), windowsAsset), windowsHash);
|
|
}
|
|
|
|
// The rebuilt installer's entry must not be accepted for a different file, or the
|
|
// updater would check the download against the wrong hash
|
|
void ReleaseChecksumPairingTest::anotherBuildsEntryDoesNotCoverThisDownload()
|
|
{
|
|
const QString rebuiltOnly = QStringLiteral("%1 *%2\n").arg(rebuiltWindowsHash, rebuiltWindowsAsset);
|
|
|
|
QVERIFY(dblsqd::Feed::findChecksum(rebuiltOnly, windowsAsset).isEmpty());
|
|
QCOMPARE(dblsqd::Feed::findChecksum(rebuiltOnly, rebuiltWindowsAsset), rebuiltWindowsHash);
|
|
}
|
|
|
|
// SHA256SUMS.txt accumulates entries across builds, so a name that merely contains
|
|
// the download's name must not hand back its hash - the updater would then reject a
|
|
// perfectly good download as corrupt
|
|
void ReleaseChecksumPairingTest::aLongerNameContainingThisOneDoesNotCoverIt()
|
|
{
|
|
const QString longerName = QStringLiteral("old-%1").arg(windowsAsset);
|
|
|
|
QVERIFY(dblsqd::Feed::findChecksum(QStringLiteral("%1 *%2\n").arg(rebuiltWindowsHash, longerName), windowsAsset).isEmpty());
|
|
QVERIFY(dblsqd::Feed::findChecksum(QStringLiteral("%1 %2.sha256\n").arg(rebuiltWindowsHash, windowsAsset), windowsAsset).isEmpty());
|
|
QVERIFY(dblsqd::Feed::findChecksum(QStringLiteral("%1 %2.tar\n").arg(rebuiltWindowsHash, linuxAsset), linuxAsset).isEmpty());
|
|
}
|
|
|
|
void ReleaseChecksumPairingTest::aPathPrefixedEntryStillCoversTheDownload()
|
|
{
|
|
QCOMPARE(dblsqd::Feed::findChecksum(QStringLiteral("%1 *upload/%2\n").arg(windowsHash, windowsAsset), windowsAsset), windowsHash);
|
|
}
|
|
|
|
void ReleaseChecksumPairingTest::malformedLinesAreIgnored()
|
|
{
|
|
// too short, non-hex, and no separator respectively, then the real entry
|
|
const QString data = QStringLiteral("abc123 %1\n"
|
|
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz %1\n"
|
|
"%2\n"
|
|
"%3 *%1\n")
|
|
.arg(windowsAsset, windowsHash, windowsHash);
|
|
|
|
QCOMPARE(dblsqd::Feed::findChecksum(data, windowsAsset), windowsHash);
|
|
}
|
|
|
|
void ReleaseChecksumPairingTest::emptyInputsYieldNoChecksum()
|
|
{
|
|
QVERIFY(dblsqd::Feed::findChecksum(QString(), windowsAsset).isEmpty());
|
|
QVERIFY(dblsqd::Feed::findChecksum(mergedChecksums(), QString()).isEmpty());
|
|
}
|
|
|
|
// A release that forgot one platform and a payload that was never a checksum file
|
|
// both yield no hash, but they need different messages
|
|
void ReleaseChecksumPairingTest::entriesParsedTellsAnUnreadableFileFromAMissingEntry()
|
|
{
|
|
int entriesParsed = -1;
|
|
QVERIFY(dblsqd::Feed::findChecksum(publishedChecksums(), windowsAsset, &entriesParsed).isEmpty());
|
|
QCOMPARE(entriesParsed, 4);
|
|
|
|
entriesParsed = -1;
|
|
QVERIFY(dblsqd::Feed::findChecksum(QStringLiteral("<html><body>503 Service Unavailable</body></html>"), windowsAsset, &entriesParsed).isEmpty());
|
|
QCOMPARE(entriesParsed, 0);
|
|
|
|
entriesParsed = -1;
|
|
QCOMPARE(dblsqd::Feed::findChecksum(mergedChecksums(), windowsAsset, &entriesParsed), windowsHash);
|
|
QCOMPARE(entriesParsed, 5);
|
|
}
|
|
|
|
#include "ReleaseChecksumPairingTest.moc"
|
|
QTEST_MAIN(ReleaseChecksumPairingTest)
|