Files
SbLauncher/tests/ProfileUpdates.cs
T

157 lines
9.2 KiB
C#
Raw Normal View History

2026-10-10 14:58:04 +03:00
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using SbLauncher;
// Real first-run/import/save/restart/update paths using synthetic credentials.
public static class ProfileUpdates
{
[STAThread]
public static int Main()
{
string root = Path.Combine(Path.GetTempPath(), "SbLauncherUpdates-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(root);
var app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown };
MainVm vm = null;
try
{
var a = Shared("a", "AA", "127.0.0.1", "00000000-0000-4000-8000-000000000001");
var b = Shared("b", "BB", "127.0.0.1", "00000000-0000-4000-8000-000000000002");
WriteBase(root, "198.51.100.10", a, b);
Paths paths = new Paths(root);
vm = new MainVm();
vm.Load(paths);
Require(vm.Prof.Servers.Count == 2 && !File.Exists(paths.Profiles), "Fresh install loads shared servers without personal profiles");
vm.PrivateKey = Convert.ToBase64String(Enumerable.Range(1, 32).Select(delegate(int n) { return (byte)n; }).ToArray());
vm.LocalAddress = "10.10.0.2/24";
vm.Adapter = "Personal adapter";
Require(Wait(vm.AddFromLinkAsync("trojan://personal-secret@127.0.0.2:443?sni=example.com#Personal")), "UI import adds a personal server");
var own = vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; });
string ownCode = own.Code;
vm.Selected = vm.Servers.Single(delegate(ServerVm card) { return card.Cfg != null && card.Cfg.BaseId == "b"; });
vm.FlushLocal();
string localBefore = File.ReadAllText(paths.Local);
string personalBefore = File.ReadAllText(paths.Profiles);
Require(!personalBefore.Contains("00000000-0000-4000-8000-000000000001") &&
!personalBefore.Contains("00000000-0000-4000-8000-000000000002"), "Saving personal servers excludes all shared credentials");
vm.DisposeTimers();
vm = null;
Profiles removed = Profiles.Load(paths.Profiles);
removed.Remove(removed.Servers.Single(delegate(ServerCfg server) { return server.BaseId == "a"; }));
removed.Save(paths.Profiles);
string personalHidden = File.ReadAllText(paths.Profiles);
// Simulate replacement of files shipped by the next release.
var changedA = Shared("a", "AA", "127.0.0.3", "00000000-0000-4000-8000-000000000003");
var changedB = Shared("b", "BB", "127.0.0.4", "00000000-0000-4000-8000-000000000004");
var c = Shared("c", ownCode, "127.0.0.5", "00000000-0000-4000-8000-000000000005");
WriteBase(root, "198.51.100.20", changedA, changedB, c);
Require(File.ReadAllText(paths.Local) == localBefore && File.ReadAllText(paths.Profiles) == personalHidden,
"Replacing the release catalog does not write either personal file");
vm = new MainVm();
vm.Load(paths);
Require(vm.Prof.Servers.Count == 3 && vm.Prof.Wg.Server == "198.51.100.20", "Updated shared catalog and WireGuard endpoint load");
Require(vm.Prof.Servers.All(delegate(ServerCfg server) { return server.BaseId != "a"; }), "Hidden shared server stays hidden after its credentials change");
Require(vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId == "b"; }).Server == "127.0.0.4", "Existing shared server receives updated configuration");
Require(vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; }).Code == ownCode,
"Personal server survives a shared code collision");
Require(vm.Prof.Servers.Select(delegate(ServerCfg server) { return server.Code; }).Distinct(StringComparer.OrdinalIgnoreCase).Count() == 3,
"Merged server codes are unique");
Require(vm.Selected.Cfg.BaseId == "b", "Shared selection is restored by stable id");
vm.FlushLocal();
Require(File.ReadAllText(paths.Local) == localBefore, "WireGuard key, address, adapter and selection survive restart after update");
vm.DisposeTimers();
vm = null;
// Upgrade a v0.1.1 full list with one personal server and settings.
string legacyRoot = Path.Combine(root, "legacy");
Directory.CreateDirectory(legacyRoot);
WriteBase(legacyRoot, "198.51.100.10", a, b);
Profiles legacy = new Profiles();
legacy.Wg.Server = "198.51.100.10";
legacy.Wg.PeerPublicKey = "shared-public-key";
legacy.Servers.Add(new ServerCfg { Code = "AA", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000001" });
legacy.Servers.Add(new ServerCfg { Code = "BB", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000002" });
legacy.Servers.Add(own);
string legacyPath = Path.Combine(legacyRoot, "profiles.json");
legacy.Save(legacyPath);
File.WriteAllText(Path.Combine(legacyRoot, "local.json"), localBefore);
string legacyBefore = File.ReadAllText(legacyPath);
vm = new MainVm();
vm.Load(new Paths(legacyRoot));
Require(vm.Prof.Servers.Count == 3 && !vm.Prof.NeedsMigration, "Legacy list migrates without losing personal servers or duplicating shared servers");
Require(File.ReadAllText(Path.Combine(legacyRoot, "profiles.legacy-backup.json")) == legacyBefore,
"Migration preserves a complete legacy backup");
Require(!File.ReadAllText(legacyPath).Contains("00000000-0000-4000-8000-000000000001"), "Migration stores only personal servers");
vm.DisposeTimers();
vm = null;
WriteBase(legacyRoot, "198.51.100.20", changedB);
Profiles upgraded = Profiles.Load(legacyPath);
Require(upgraded.Servers.Count == 2 && upgraded.Wg.Server == "198.51.100.20", "Migrated installations receive later shared updates and removals");
Require(File.ReadAllText(Path.Combine(legacyRoot, "local.json")) == localBefore, "Legacy upgrade preserves the personal WireGuard settings");
string customRoot = Path.Combine(root, "custom");
Directory.CreateDirectory(customRoot);
WriteBase(customRoot, "198.51.100.10", a);
Profiles custom = new Profiles();
custom.Servers.Add(new ServerCfg { Code = "AA", Server = "127.0.0.1",
Uuid = "00000000-0000-4000-8000-000000000001", Sni = "custom.example.com" });
custom.Save(Path.Combine(customRoot, "profiles.json"));
Require(Profiles.Load(Path.Combine(customRoot, "profiles.json")).Servers.Count == 2,
"Legacy custom connection settings are preserved even with a matching endpoint and UUID");
upgraded.Remove(upgraded.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; }));
upgraded.Save(legacyPath);
Require(Profiles.Load(legacyPath).Servers.Count == 1, "Deleted personal server does not return on restart");
Console.WriteLine("Profile update checks passed.");
return 0;
}
catch (Exception ex) { Console.Error.WriteLine(ex); return 1; }
finally
{
if (vm != null) vm.DisposeTimers();
app.Shutdown();
string full = Path.GetFullPath(root);
string temp = Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (full.StartsWith(temp, StringComparison.OrdinalIgnoreCase)) Directory.Delete(full, true);
}
}
static Dictionary<string, object> Shared(string id, string code, string host, string uuid)
{
return new Dictionary<string, object> {
{ "id", id }, { "code", code }, { "name", code }, { "server", host },
{ "port", 443 }, { "uuid", uuid }, { "protocol", "vless" }, { "flow", "" }
};
}
static void WriteBase(string root, string wgHost, params Dictionary<string, object>[] servers)
{
J.Write(Path.Combine(root, "profiles.base.json"), J.ToJson(new Dictionary<string, object> {
{ "wireguard", new Dictionary<string, object> { { "server", wgHost }, { "peer_public_key", "shared-public-key" } } },
{ "servers", servers }
}));
}
static bool Wait(Task<bool> task)
{
var frame = new DispatcherFrame();
var started = DateTime.UtcNow;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) };
timer.Tick += delegate {
if (task.IsCompleted || (DateTime.UtcNow - started).TotalSeconds > 10) { timer.Stop(); frame.Continue = false; }
};
timer.Start();
Dispatcher.PushFrame(frame);
return task.IsCompleted && task.Result;
}
static void Require(bool condition, string message)
{
if (!condition) throw new InvalidOperationException(message);
Console.WriteLine("PASS " + message);
}
}