130 lines
4.5 KiB
Python
130 lines
4.5 KiB
Python
#!/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()
|