Files
plezy/scripts/test_check_windows_installer.py
T
edde746 1165998dae fix(windows): elevate the installer when the install directory is read-only
PrivilegesRequired=lowest makes Inno Setup "always run in non
administrative install mode" — the launching token is irrelevant. So a
copy that ended up in C:\Program Files, which the destination page still
lets an elevated wizard run pick, is registered under HKCU while living
somewhere an ordinary process cannot write. UsePreviousAppDir then aims
every later run straight back at that directory.

WinSparkle launches the downloaded installer with plain ShellExecuteEx
and no verb, so nothing along the in-app update path ever asks for
elevation: the silent installer starts, cannot replace a single file, and
the only way out was to quit Plezy, fetch the installer by hand and pick
"Run as administrator". Inno's own PrivilegesRequiredOverridesAllowed
plus UsePreviousPrivileges does not help here, because it reads the
recorded install mode — which is exactly the non-administrative one that
cannot write.

Decide on write access instead. InitializeSetup probes the registered
install directory and, when it is not writable, relaunches setup through
ShellExec 'runas' pinned to that directory with /ALLUSERS, so the update
lands in place instead of forking a second per-user copy. The relaunch
carries a guard parameter and drops any conflicting mode override, and a
refused UAC prompt now explains itself and points at the releases page
rather than failing mutely. A machine-wide install that takes over a
per-user directory also clears the stale uninstall entry and Start Menu
group that would otherwise list Plezy twice in Apps & Features.

Fresh installs are unchanged: still per-user, still no prompt. Only
commandline is added to PrivilegesRequiredOverridesAllowed, since
allowing dialog would make a silent install with no previous copy stop
for the install-mode question — which is how winget installs.

The script carried two near-identical copies of the whole .iss, one per
architecture shape, so both would have needed this code. Collapse them
into one template parameterised by architecture, add -EmitScriptOnly to
generate the .iss without 7-Zip or Inno Setup, and guard the contract
with check_windows_installer.py so the elevation path, the single-source
AppId and the winget marker cannot rot.

close #1705
2026-07-28 23:57:59 +02:00

130 lines
4.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Behavior tests for the Windows installer elevation guard."""
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
CHECKER = ROOT / "scripts/check_windows_installer.py"
SCRIPT = ROOT / "windows/build-installer.ps1"
class WindowsInstallerGuardTest(unittest.TestCase):
def _run(self, script: str) -> subprocess.CompletedProcess[str]:
with tempfile.TemporaryDirectory(prefix="plezy-windows-installer-test-") as directory:
fixture = Path(directory) / "build-installer.ps1"
fixture.write_text(script, encoding="utf-8")
return subprocess.run(
[sys.executable, str(CHECKER), str(fixture)],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
def _script(self) -> str:
return SCRIPT.read_text(encoding="utf-8")
def _mutate(self, old: str, new: str) -> str:
script = self._script().replace(old, new, 1)
self.assertNotEqual(script, self._script(), f"fixture mutation no longer matches: {old!r}")
return script
def test_current_script_passes(self) -> None:
result = self._run(self._script())
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("elevation checks passed", result.stdout)
def test_missing_commandline_override_is_rejected(self) -> None:
# Without the override /ALLUSERS is silently ignored and the relaunched
# instance installs per-user again, which is issue #1705.
script = self._mutate(
"PrivilegesRequiredOverridesAllowed=commandline",
"PrivilegesRequiredOverridesAllowed=",
)
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("/ALLUSERS is inert", result.stderr)
def test_dialog_override_is_rejected(self) -> None:
script = self._mutate(
"PrivilegesRequiredOverridesAllowed=commandline",
"PrivilegesRequiredOverridesAllowed=commandline dialog",
)
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("winget installs that way", result.stderr)
def test_admin_default_is_rejected(self) -> None:
script = self._mutate("PrivilegesRequired=lowest", "PrivilegesRequired=admin")
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("must stay per-user", result.stderr)
def test_dropping_the_elevated_relaunch_is_rejected(self) -> None:
script = self._mutate(
" if ShellExec('runas', ExpandConstant('{srcexe}'), Params, '', SW_SHOW, ewNoWait, ErrorCode) then",
" if False then",
)
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("relaunch this installer elevated", result.stderr)
def test_dropping_the_recursion_guard_is_rejected(self) -> None:
script = self._mutate("/ELEVATED=1 /DIR=", "/DIR=")
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("/ELEVATED=1", result.stderr)
def test_silent_elevation_failure_is_rejected(self) -> None:
script = self._mutate(
" SuppressibleMsgBox(FmtMessage(CustomMessage('ElevationRequired'), [PreviousDir]),\n"
" mbCriticalError, MB_OK, IDOK);\n",
"",
)
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("explain itself", result.stderr)
def test_second_template_copy_is_rejected(self) -> None:
# The regression this guard exists for: the script used to hold one
# whole .iss per architecture shape, and they drifted.
script = self._script()
marker = "[Setup]\n"
self.assertIn(marker, script)
script = script.replace(marker, marker + "[Setup]\n", 1)
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("a second copy of the template will drift", result.stderr)
def test_losing_the_winget_marker_is_rejected(self) -> None:
script = self._mutate("{param:WINGET|0}", "{param:NOTWINGET|0}")
result = self._run(script)
self.assertNotEqual(result.returncode, 0)
self.assertIn("winget marker file", result.stderr)
if __name__ == "__main__":
unittest.main()