From d201c0b54db8265eb432e73f851bdcefeec80b24 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Thu, 12 Nov 2026 18:57:33 +0300 Subject: [PATCH 1/2] Seed upgrade fixtures with the installed release libraries --- scripts/test_update_smoke.py | 23 +++++++++++++++++ scripts/update_smoke.py | 48 +++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/scripts/test_update_smoke.py b/scripts/test_update_smoke.py index 8a95235..6c5bf67 100644 --- a/scripts/test_update_smoke.py +++ b/scripts/test_update_smoke.py @@ -1,8 +1,10 @@ import base64 import json import os +import subprocess as sp import tempfile import unittest +import xml.etree.ElementTree as ET from pathlib import Path from unittest.mock import patch @@ -13,6 +15,27 @@ class UpdateSmokeSafetyTests(unittest.TestCase): + def test_fixture_references_installed_release_instead_of_current_projects(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + application, data, logs = root / "old app", root / "data", root / "logs" + with patch.object(update_smoke, "dotnet") as dotnet: + update_smoke.seed_settings(application, data, logs) + project = logs / "baseline-fixture/BaselineFixture.csproj" + document = ET.parse(project) + self.assertEqual(document.findall(".//ProjectReference"), []) + for reference in document.findall(".//Reference"): + self.assertEqual(Path(reference.findtext("HintPath")), application / (reference.attrib["Include"] + ".dll")) + dotnet.assert_called_once_with("run", "--project", str(project), "-c", "Release", "--", str(data)) + + def test_service_failure_prints_redirected_diagnostic_and_still_fails(self): + result = sp.CompletedProcess(["service.exe"], 1, stdout="Unsupported TLS fingerprint.") + with patch.object(update_smoke.sp, "run", return_value=result) as run, patch("builtins.print") as output: + with self.assertRaises(sp.CalledProcessError): + update_smoke.service_command(Path("installed"), "--configure-mode", "service") + self.assertEqual(run.call_args.kwargs["stderr"], sp.STDOUT) + output.assert_called_once_with(result.stdout, flush=True) + def test_first_release_skips_upgrade_without_installing(self): with tempfile.TemporaryDirectory() as temporary: with patch("sys.argv", ["update_smoke.py", "0.0.1"]), patch.dict(os.environ, {"GITHUB_ACTIONS": "true"}), \ diff --git a/scripts/update_smoke.py b/scripts/update_smoke.py index 6efe8ac..9f091ed 100644 --- a/scripts/update_smoke.py +++ b/scripts/update_smoke.py @@ -8,6 +8,7 @@ import shutil import subprocess as sp import time +import xml.etree.ElementTree as ET from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey @@ -22,6 +23,43 @@ from service_control import running_images, service_state +def seed_settings(application, data, logs): + # Compile against the installed release: current defaults may be unreadable by older versions. + project = logs / "baseline-fixture" + project.mkdir(parents=True, exist_ok=True) + document = ET.Element("Project", Sdk="Microsoft.NET.Sdk") + properties = ET.SubElement(document, "PropertyGroup") + ET.SubElement(properties, "OutputType").text = "Exe" + references = ET.SubElement(document, "ItemGroup") + for name in ("WindowsMegaProxy.Core", "WindowsMegaProxy.Runtime", "System.Security.Cryptography.ProtectedData"): + reference = ET.SubElement(references, "Reference", Include=name) + ET.SubElement(reference, "HintPath").text = str((application / (name + ".dll")).resolve()) + ET.indent(document, space=" ") + ET.ElementTree(document).write(project / "BaselineFixture.csproj", encoding="utf-8", xml_declaration=True) + (project / "Program.cs").write_text('''using System; +using WindowsMegaProxy; + +var store = new MachineSettingsStore(args[0]); +var profile = ProxyProfile.Create("Automatic update preservation check"); +profile.Proxy["host"] = "proxy.invalid"; +profile.Proxy["password"] = "synthetic-update-secret"; +store.Save(new Settings { Profiles = [profile], ActiveProfileId = profile.Id, AutoConnect = false, Language = "en" }); +var saved = store.Load(); +if (saved.AutoConnect || saved.ActiveProfile?.Proxy["password"]?.ToString() != "synthetic-update-secret") + throw new Exception("The installed release could not read its seeded settings."); +Console.WriteLine("Baseline settings seeded successfully; TLS fingerprint: " + saved.TlsFingerprint); +''', encoding="utf-8") + dotnet("run", "--project", str(project / "BaselineFixture.csproj"), "-c", "Release", "--", str(data)) + + +def service_command(application, *arguments, timeout=60): + result = sp.run([str(application / "WindowsMegaProxy.Service.exe"), *arguments], + stdout=sp.PIPE, stderr=sp.STDOUT, text=True, timeout=timeout) + if result.stdout: + print(result.stdout, flush=True) + result.check_returncode() + + def signed_installer(directory, target_version): manifest = (directory / "update-manifest.json").read_bytes() signature = (directory / "update-manifest.sig").read_bytes() @@ -75,8 +113,8 @@ def check(*arguments): try: sp.run([str(previous), "Scope=PerMachine", "-quiet", "-norestart", "Mode=manual", "AutomaticUpdates=true", "-log", f"{logs / 'previous-install.log'}"], check=True, timeout=600) - check("--update-preservation-check", "seed") - sp.run([str(destination / "WindowsMegaProxy.Service.exe"), "--configure-mode", "service"], check=True, timeout=60) + seed_settings(destination, data, logs) + service_command(destination, "--configure-mode", "service") if args.candidate: item = signed_installer(args.candidate, args.version) setup = args.candidate / item["file"] @@ -88,7 +126,7 @@ def check(*arguments): (cache / "state.json").write_text(json.dumps({ "Manifest": base64.b64encode((args.candidate / "update-manifest.json").read_bytes()).decode(), "Signature": base64.b64encode((args.candidate / "update-manifest.sig").read_bytes()).decode()}), encoding="utf-8") - sp.run([str(destination / "WindowsMegaProxy.Service.exe"), "--install-update", "now"], check=True, timeout=180) + service_command(destination, "--install-update", "now", timeout=180) def upgraded(): try: @@ -98,7 +136,7 @@ def upgraded(): return False # Files and service registration may be changing during installation. wait_until(upgraded, "The published updater did not install the signed release candidate.", 600) - sp.run([str(destination / "WindowsMegaProxy.Service.exe"), "--check-dependencies"], check=True, timeout=60) + service_command(destination, "--check-dependencies") else: check("--published-update-check", args.version) check("--update-preservation-check", "verify") @@ -114,6 +152,8 @@ def upgraded(): finally: if (local / "logs").is_dir(): shutil.copytree(local / "logs", logs / "client", dirs_exist_ok=True) + if (data / "logs").is_dir(): + shutil.copytree(data / "logs", logs / "service", dirs_exist_ok=True) temporary = known_folder(0x1c) / "Temp" for log in temporary.glob("Setup Log*.txt"): shutil.copy2(log, logs / log.name) From 2b02617df99143921b074208072c72aacb7547e2 Mon Sep 17 00:00:00 2001 From: Andrey Prokopyuk Date: Thu, 12 Nov 2026 19:03:28 +0300 Subject: [PATCH 2/2] Normalize Windows short paths in upgrade fixture test --- scripts/test_update_smoke.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test_update_smoke.py b/scripts/test_update_smoke.py index 6c5bf67..2cf7a62 100644 --- a/scripts/test_update_smoke.py +++ b/scripts/test_update_smoke.py @@ -25,7 +25,7 @@ def test_fixture_references_installed_release_instead_of_current_projects(self): document = ET.parse(project) self.assertEqual(document.findall(".//ProjectReference"), []) for reference in document.findall(".//Reference"): - self.assertEqual(Path(reference.findtext("HintPath")), application / (reference.attrib["Include"] + ".dll")) + self.assertEqual(Path(reference.findtext("HintPath")), (application / (reference.attrib["Include"] + ".dll")).resolve()) dotnet.assert_called_once_with("run", "--project", str(project), "-c", "Release", "--", str(data)) def test_service_failure_prints_redirected_diagnostic_and_still_fails(self):