2026-05-26 14:25:52 +08:00
|
|
|
using CommunityToolkit.Mvvm.Input;
|
|
|
|
|
using LanMountainDesktop.Models;
|
|
|
|
|
using LanMountainDesktop.PluginSdk;
|
|
|
|
|
using LanMountainDesktop.Services;
|
2026-06-01 19:48:51 +08:00
|
|
|
using LanMountainDesktop.Services.Plonds;
|
2026-05-26 14:25:52 +08:00
|
|
|
using LanMountainDesktop.Services.Settings;
|
|
|
|
|
using LanMountainDesktop.Services.Update;
|
|
|
|
|
using LanMountainDesktop.Shared.Contracts.Update;
|
|
|
|
|
using LanMountainDesktop.ViewModels;
|
|
|
|
|
using Xunit;
|
|
|
|
|
using UpdateDownloadResult = LanMountainDesktop.Services.Update.DownloadResult;
|
|
|
|
|
using SettingsUpdateState = LanMountainDesktop.Services.Settings.UpdateSettingsState;
|
|
|
|
|
|
|
|
|
|
namespace LanMountainDesktop.Tests;
|
|
|
|
|
|
|
|
|
|
public sealed class UpdateSettingsInterfaceTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task UpdateSettingsViewModel_RoutesActionsThroughUpdateSettingsService()
|
|
|
|
|
{
|
|
|
|
|
var update = new FakeUpdateSettingsService();
|
|
|
|
|
var viewModel = new UpdateSettingsViewModel(new FakeSettingsFacade(update));
|
|
|
|
|
|
|
|
|
|
Assert.Equal(0, update.SaveCalls);
|
|
|
|
|
|
|
|
|
|
update.CheckReport = new UpdateCheckReport(
|
|
|
|
|
true,
|
|
|
|
|
"1.2.3",
|
|
|
|
|
"1.0.0",
|
|
|
|
|
UpdatePayloadKind.DeltaPlonds,
|
|
|
|
|
"dist-1",
|
|
|
|
|
UpdateSettingsValues.ChannelStable,
|
|
|
|
|
DateTimeOffset.Parse("2026-05-06T00:00:00Z"),
|
|
|
|
|
42,
|
|
|
|
|
null,
|
|
|
|
|
null);
|
|
|
|
|
|
|
|
|
|
await ((IAsyncRelayCommand)viewModel.CheckCommand).ExecuteAsync(null);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(1, update.CheckCalls);
|
|
|
|
|
Assert.Equal("1.2.3", viewModel.LatestVersionText);
|
|
|
|
|
Assert.True(viewModel.IsDeltaUpdate);
|
|
|
|
|
|
|
|
|
|
update.SetPhase(UpdatePhase.Checked);
|
|
|
|
|
await ((IAsyncRelayCommand)viewModel.DownloadCommand).ExecuteAsync(null);
|
|
|
|
|
Assert.Equal(1, update.DownloadCalls);
|
|
|
|
|
|
|
|
|
|
update.SetPhase(UpdatePhase.Downloaded);
|
|
|
|
|
await ((IAsyncRelayCommand)viewModel.InstallCommand).ExecuteAsync(null);
|
|
|
|
|
Assert.Equal(1, update.InstallCalls);
|
|
|
|
|
|
|
|
|
|
update.SetPhase(UpdatePhase.Downloading);
|
|
|
|
|
await ((IAsyncRelayCommand)viewModel.PauseCommand).ExecuteAsync(null);
|
|
|
|
|
Assert.Equal(1, update.PauseCalls);
|
|
|
|
|
|
|
|
|
|
update.SetPhase(UpdatePhase.PausedDownloading);
|
|
|
|
|
await ((IAsyncRelayCommand)viewModel.ResumeCommand).ExecuteAsync(null);
|
|
|
|
|
Assert.Equal(1, update.ResumeCalls);
|
|
|
|
|
|
|
|
|
|
update.SetPhase(UpdatePhase.Downloading);
|
|
|
|
|
await ((IAsyncRelayCommand)viewModel.CancelCommand).ExecuteAsync(null);
|
|
|
|
|
Assert.Equal(1, update.CancelCalls);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void UpdateSettingsViewModel_SavesPreferencesThroughUpdateSettingsService()
|
|
|
|
|
{
|
|
|
|
|
var update = new FakeUpdateSettingsService();
|
|
|
|
|
var viewModel = new UpdateSettingsViewModel(new FakeSettingsFacade(update));
|
|
|
|
|
|
|
|
|
|
viewModel.SelectedUpdateChannelValue = UpdateSettingsValues.ChannelPreview;
|
|
|
|
|
viewModel.SelectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGitHub;
|
|
|
|
|
viewModel.SelectedUpdateModeValue = UpdateSettingsValues.ModeManual;
|
|
|
|
|
viewModel.DownloadThreadsSliderValue = 12;
|
|
|
|
|
viewModel.ForceReinstall = true;
|
|
|
|
|
|
|
|
|
|
Assert.True(update.SaveCalls >= 5);
|
|
|
|
|
Assert.Equal(UpdateSettingsValues.ChannelPreview, update.State.UpdateChannel);
|
|
|
|
|
Assert.Equal(UpdateSettingsValues.DownloadSourceGitHub, update.State.UpdateDownloadSource);
|
|
|
|
|
Assert.Equal(UpdateSettingsValues.ModeManual, update.State.UpdateMode);
|
|
|
|
|
Assert.Equal(12, update.State.UpdateDownloadThreads);
|
|
|
|
|
Assert.True(update.State.ForceUpdateReinstall);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void UpdateSettingsViewModel_RestoresPersistedPendingAndLastCheckedState()
|
|
|
|
|
{
|
|
|
|
|
var update = new FakeUpdateSettingsService
|
|
|
|
|
{
|
|
|
|
|
State = DefaultUpdateState() with
|
|
|
|
|
{
|
|
|
|
|
PendingUpdateVersion = "2.0.0",
|
|
|
|
|
PendingUpdatePublishedAtUtcMs = DateTimeOffset.Parse("2026-05-06T00:00:00Z").ToUnixTimeMilliseconds(),
|
|
|
|
|
LastUpdateCheckUtcMs = DateTimeOffset.Parse("2026-05-07T00:00:00Z").ToUnixTimeMilliseconds()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var viewModel = new UpdateSettingsViewModel(new FakeSettingsFacade(update));
|
|
|
|
|
|
|
|
|
|
Assert.True(viewModel.IsUpdateAvailable);
|
|
|
|
|
Assert.Equal("2.0.0", viewModel.LatestVersionText);
|
|
|
|
|
Assert.NotEmpty(viewModel.PublishedAtText);
|
|
|
|
|
Assert.Contains("Last checked", viewModel.LastCheckedText, StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-06-01 19:48:51 +08:00
|
|
|
public async Task UpdateSettingsService_WhenPlondsSelected_UsesPlondsServiceWithoutCreatingOrchestrator()
|
2026-05-26 14:25:52 +08:00
|
|
|
{
|
2026-06-01 19:48:51 +08:00
|
|
|
var settings = new FakeSettingsService
|
|
|
|
|
{
|
|
|
|
|
Snapshot =
|
|
|
|
|
{
|
|
|
|
|
UpdateDownloadSource = UpdateSettingsValues.DownloadSourcePlonds
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var plonds = new FakePlondsService
|
2026-05-26 14:25:52 +08:00
|
|
|
{
|
2026-06-01 19:48:51 +08:00
|
|
|
LatestResult = PlondsLatestResult.Available(
|
|
|
|
|
new Version(1, 0, 0),
|
|
|
|
|
new Version(9, 9, 9),
|
|
|
|
|
[new PlondsManifestCandidate(
|
|
|
|
|
new PlondsSourceDescriptor("s3", "s3", "https://s3.test/PLONDS.json", 100),
|
|
|
|
|
CreatePlondsManifest("9.9.9"))])
|
2026-05-26 14:25:52 +08:00
|
|
|
};
|
2026-06-01 19:48:51 +08:00
|
|
|
var orchestratorCreated = false;
|
|
|
|
|
var service = new UpdateSettingsService(
|
|
|
|
|
settings,
|
|
|
|
|
orchestratorFactory: () =>
|
|
|
|
|
{
|
|
|
|
|
orchestratorCreated = true;
|
|
|
|
|
throw new InvalidOperationException("UpdateOrchestrator should not be created for PLONDS.");
|
|
|
|
|
},
|
|
|
|
|
plondsService: plonds);
|
2026-05-26 14:25:52 +08:00
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
var report = await service.CheckAsync(CancellationToken.None);
|
2026-05-26 14:25:52 +08:00
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
Assert.True(report.IsUpdateAvailable);
|
|
|
|
|
Assert.Equal("9.9.9", report.LatestVersion);
|
|
|
|
|
Assert.Equal(1, plonds.FindLatestCalls);
|
|
|
|
|
Assert.False(orchestratorCreated);
|
|
|
|
|
}
|
2026-05-26 14:25:52 +08:00
|
|
|
|
2026-06-02 13:16:13 +08:00
|
|
|
[Fact]
|
|
|
|
|
public async Task UpdateSettingsService_WhenPlondsManifestRequiresCleanInstall_ReportsFullInstaller()
|
|
|
|
|
{
|
|
|
|
|
var settings = new FakeSettingsService
|
|
|
|
|
{
|
|
|
|
|
Snapshot =
|
|
|
|
|
{
|
|
|
|
|
UpdateDownloadSource = UpdateSettingsValues.DownloadSourcePlonds
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var plonds = new FakePlondsService
|
|
|
|
|
{
|
|
|
|
|
LatestResult = PlondsLatestResult.Available(
|
|
|
|
|
new Version(1, 0, 0),
|
|
|
|
|
new Version(9, 9, 9),
|
|
|
|
|
[new PlondsManifestCandidate(
|
|
|
|
|
new PlondsSourceDescriptor("s3", "s3", "https://s3.test/PLONDS.json", 100),
|
|
|
|
|
CreatePlondsManifest("9.9.9", requiresCleanInstall: true))])
|
|
|
|
|
};
|
|
|
|
|
var orchestratorCreated = false;
|
|
|
|
|
var service = new UpdateSettingsService(
|
|
|
|
|
settings,
|
|
|
|
|
orchestratorFactory: () =>
|
|
|
|
|
{
|
|
|
|
|
orchestratorCreated = true;
|
|
|
|
|
throw new InvalidOperationException("UpdateOrchestrator should not be created for PLONDS check.");
|
|
|
|
|
},
|
|
|
|
|
plondsService: plonds);
|
|
|
|
|
|
|
|
|
|
var report = await service.CheckAsync(CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.True(report.IsUpdateAvailable);
|
|
|
|
|
Assert.Equal(UpdatePayloadKind.FullInstaller, report.PayloadKind);
|
|
|
|
|
Assert.Equal("9.9.9", report.LatestVersion);
|
|
|
|
|
Assert.False(orchestratorCreated);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
[Fact]
|
|
|
|
|
public async Task UpdateSettingsService_WhenGitHubSelected_UsesOrchestrator()
|
|
|
|
|
{
|
|
|
|
|
var settings = new FakeSettingsService
|
|
|
|
|
{
|
|
|
|
|
Snapshot =
|
|
|
|
|
{
|
|
|
|
|
UpdateDownloadSource = UpdateSettingsValues.DownloadSourceGitHub
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var orchestrator = CreateTestOrchestrator(DefaultUpdateState() with
|
|
|
|
|
{
|
|
|
|
|
UpdateDownloadSource = UpdateSettingsValues.DownloadSourceGitHub
|
|
|
|
|
});
|
|
|
|
|
var orchestratorCreated = false;
|
|
|
|
|
var service = new UpdateSettingsService(
|
|
|
|
|
settings,
|
|
|
|
|
orchestratorFactory: () =>
|
|
|
|
|
{
|
|
|
|
|
orchestratorCreated = true;
|
|
|
|
|
return orchestrator;
|
|
|
|
|
},
|
|
|
|
|
plondsService: new FakePlondsService());
|
|
|
|
|
|
|
|
|
|
var _ = service.CurrentPhase;
|
|
|
|
|
|
|
|
|
|
Assert.False(orchestratorCreated);
|
2026-05-26 14:25:52 +08:00
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
var report = await service.CheckAsync(CancellationToken.None);
|
|
|
|
|
|
|
|
|
|
Assert.True(orchestratorCreated);
|
|
|
|
|
Assert.True(report.IsUpdateAvailable);
|
2026-05-26 14:25:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void FromFullInstaller_IncludesPreferredInstallerInMirrors()
|
|
|
|
|
{
|
|
|
|
|
var release = new GitHubReleaseInfo(
|
|
|
|
|
"v1.2.3",
|
|
|
|
|
"Release",
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
DateTimeOffset.Parse("2026-05-06T00:00:00Z"),
|
|
|
|
|
[new GitHubReleaseAsset("LanMountainDesktop-setup-x64.exe", "https://example.test/setup.exe", 123, "abc")]);
|
|
|
|
|
|
|
|
|
|
var manifest = UpdateManifestMapper.FromFullInstaller(release, UpdateSettingsValues.ChannelStable, "windows-x64");
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(manifest.InstallerMirrors);
|
|
|
|
|
var mirror = Assert.Single(manifest.InstallerMirrors!);
|
|
|
|
|
Assert.Equal("https://example.test/setup.exe", mirror.Url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ApplyDownloadSource_UsesGhProxyForGithubProxySource()
|
|
|
|
|
{
|
|
|
|
|
var url = "https://github.com/owner/repo/releases/download/v1/app.exe";
|
|
|
|
|
|
|
|
|
|
Assert.Equal(url, UpdateDownloadEngine.ApplyDownloadSource(url, UpdateSettingsValues.DownloadSourceGitHub));
|
|
|
|
|
Assert.Equal(
|
|
|
|
|
$"{UpdateSettingsValues.DefaultGhProxyBaseUrl}{url}",
|
|
|
|
|
UpdateDownloadEngine.ApplyDownloadSource(url, UpdateSettingsValues.DownloadSourceGhProxy));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static SettingsUpdateState DefaultUpdateState() => new(
|
|
|
|
|
IncludePrereleaseUpdates: false,
|
|
|
|
|
UpdateChannel: UpdateSettingsValues.ChannelStable,
|
|
|
|
|
UpdateMode: UpdateSettingsValues.ModeSilentDownload,
|
|
|
|
|
UpdateDownloadSource: UpdateSettingsValues.DownloadSourcePlonds,
|
|
|
|
|
UpdateDownloadThreads: UpdateSettingsValues.DefaultDownloadThreads,
|
|
|
|
|
ForceUpdateReinstall: false,
|
|
|
|
|
UseGhProxyMirror: false,
|
|
|
|
|
PendingUpdateInstallerPath: null,
|
|
|
|
|
PendingUpdateVersion: null,
|
|
|
|
|
PendingUpdatePublishedAtUtcMs: null,
|
|
|
|
|
LastUpdateCheckUtcMs: null,
|
|
|
|
|
PendingUpdateSha256: null);
|
|
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
private static UpdateOrchestrator CreateTestOrchestrator(SettingsUpdateState state)
|
|
|
|
|
{
|
|
|
|
|
return new UpdateOrchestrator(
|
|
|
|
|
new FakeManifestProvider("github"),
|
|
|
|
|
new UpdateDownloadEngine(new FakeManifestProvider("github"), new ResumableDownloadService(new HttpClient(new EmptyHandler()))),
|
|
|
|
|
new UpdateInstallGateway(),
|
|
|
|
|
new UpdateStateStore(new FakeSettingsFacade(new FakeUpdateSettingsService { State = state })));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 13:16:13 +08:00
|
|
|
private static PlondsClientManifest CreatePlondsManifest(string version, bool requiresCleanInstall = false)
|
2026-06-01 19:48:51 +08:00
|
|
|
{
|
|
|
|
|
return new PlondsClientManifest(
|
|
|
|
|
FormatVersion: "2.0",
|
|
|
|
|
CurrentVersion: version,
|
|
|
|
|
PreviousVersion: "1.0.0",
|
|
|
|
|
IsFullUpdate: false,
|
2026-06-02 13:16:13 +08:00
|
|
|
RequiresCleanInstall: requiresCleanInstall,
|
2026-06-01 19:48:51 +08:00
|
|
|
Channel: "stable",
|
|
|
|
|
Platform: "windows-x64",
|
|
|
|
|
UpdatedAt: DateTimeOffset.Parse("2026-06-01T00:00:00Z"),
|
|
|
|
|
FilesMap: new Dictionary<string, PlondsClientFileEntry>(),
|
|
|
|
|
ChangedFilesMap: new Dictionary<string, PlondsClientChangedFileEntry>(),
|
|
|
|
|
Checksums: new Dictionary<string, string>(),
|
|
|
|
|
Downloads: null,
|
|
|
|
|
Sources: []);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 14:25:52 +08:00
|
|
|
private sealed class FakeUpdateSettingsService : IUpdateSettingsService
|
|
|
|
|
{
|
|
|
|
|
public SettingsUpdateState State { get; set; } = DefaultUpdateState();
|
|
|
|
|
public UpdatePhase CurrentPhase { get; private set; } = UpdatePhase.Idle;
|
|
|
|
|
public UpdateCheckReport CheckReport { get; set; } = new(false, null, null, null, null, null, null, null, null, null);
|
|
|
|
|
public UpdateDownloadResult DownloadResult { get; set; } = new(true, "downloaded", null, true);
|
|
|
|
|
public InstallResult InstallResult { get; set; } = new(true, null, false);
|
|
|
|
|
public int SaveCalls { get; private set; }
|
|
|
|
|
public int CheckCalls { get; private set; }
|
|
|
|
|
public int DownloadCalls { get; private set; }
|
|
|
|
|
public int InstallCalls { get; private set; }
|
|
|
|
|
public int PauseCalls { get; private set; }
|
|
|
|
|
public int ResumeCalls { get; private set; }
|
|
|
|
|
public int CancelCalls { get; private set; }
|
|
|
|
|
|
|
|
|
|
public event Action<UpdatePhase>? PhaseChanged;
|
|
|
|
|
public event Action<UpdateProgressReport>? ProgressChanged;
|
|
|
|
|
|
|
|
|
|
public void SetPhase(UpdatePhase phase)
|
|
|
|
|
{
|
|
|
|
|
CurrentPhase = phase;
|
|
|
|
|
PhaseChanged?.Invoke(phase);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SettingsUpdateState Get() => State;
|
|
|
|
|
|
|
|
|
|
public void Save(SettingsUpdateState state)
|
|
|
|
|
{
|
|
|
|
|
SaveCalls++;
|
|
|
|
|
State = state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<UpdateCheckReport> CheckAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
CheckCalls++;
|
|
|
|
|
SetPhase(UpdatePhase.Checked);
|
|
|
|
|
return Task.FromResult(CheckReport);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<UpdateDownloadResult> DownloadAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
DownloadCalls++;
|
|
|
|
|
SetPhase(UpdatePhase.Downloaded);
|
|
|
|
|
return Task.FromResult(DownloadResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<InstallResult> InstallAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
InstallCalls++;
|
|
|
|
|
SetPhase(UpdatePhase.Installed);
|
|
|
|
|
return Task.FromResult(InstallResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task RollbackAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
|
|
|
|
|
|
|
|
public Task PauseAsync()
|
|
|
|
|
{
|
|
|
|
|
PauseCalls++;
|
|
|
|
|
SetPhase(UpdatePhase.PausedDownloading);
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<UpdateDownloadResult> ResumeAsync(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
ResumeCalls++;
|
|
|
|
|
SetPhase(UpdatePhase.Downloaded);
|
|
|
|
|
return Task.FromResult(DownloadResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task CancelAsync()
|
|
|
|
|
{
|
|
|
|
|
CancelCalls++;
|
|
|
|
|
SetPhase(UpdatePhase.Idle);
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task AutoCheckIfEnabledAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
|
|
|
|
|
|
|
|
|
public bool TryApplyOnExit() => false;
|
|
|
|
|
|
|
|
|
|
public Task<UpdateCheckResult> CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default)
|
|
|
|
|
=> Task.FromResult(new UpdateCheckResult(true, false, currentVersion.ToString(), string.Empty, null, null, null));
|
|
|
|
|
|
|
|
|
|
public Task<UpdateCheckResult> ForceCheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default)
|
|
|
|
|
=> CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
|
|
|
|
|
|
|
|
|
public Task<LanMountainDesktop.Services.UpdateDownloadResult> DownloadAssetAsync(
|
|
|
|
|
GitHubReleaseAsset asset,
|
|
|
|
|
string destinationFilePath,
|
|
|
|
|
string downloadSource,
|
|
|
|
|
int maxParallelSegments,
|
|
|
|
|
IProgress<double>? progress = null,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
=> Task.FromResult(new LanMountainDesktop.Services.UpdateDownloadResult(false, null, "not used", false));
|
|
|
|
|
|
|
|
|
|
public Task<LanMountainDesktop.Services.UpdateDownloadResult> RedownloadAssetAsync(
|
|
|
|
|
GitHubReleaseAsset asset,
|
|
|
|
|
string destinationFilePath,
|
|
|
|
|
string downloadSource,
|
|
|
|
|
int maxParallelSegments,
|
|
|
|
|
IProgress<double>? progress = null,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
=> Task.FromResult(new LanMountainDesktop.Services.UpdateDownloadResult(false, null, "not used", false));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
private sealed class FakePlondsService : IPlondsService
|
|
|
|
|
{
|
|
|
|
|
public PlondsLatestResult LatestResult { get; set; } = PlondsLatestResult.UpToDate(new Version(1, 0, 0), new Version(1, 0, 0));
|
|
|
|
|
public PlondsPrepareResult PrepareResult { get; set; } = PlondsPrepareResult.FailedForUi("not prepared");
|
|
|
|
|
public int FindLatestCalls { get; private set; }
|
|
|
|
|
public int PrepareLatestCalls { get; private set; }
|
|
|
|
|
|
|
|
|
|
public Task<PlondsLatestResult> FindLatestAsync(Version currentVersion, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
FindLatestCalls++;
|
|
|
|
|
return Task.FromResult(LatestResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<PlondsPrepareResult> FindAndPrepareLatestAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
PrepareLatestCalls++;
|
|
|
|
|
return Task.FromResult(PrepareResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<PlondsPrepareResult> FindAndPrepareLatestAsync(Version currentVersion, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
PrepareLatestCalls++;
|
|
|
|
|
return Task.FromResult(PrepareResult);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class FakeSettingsService : ISettingsService
|
|
|
|
|
{
|
|
|
|
|
public event EventHandler<SettingsChangedEvent>? Changed;
|
|
|
|
|
|
|
|
|
|
public AppSettingsSnapshot Snapshot { get; init; } = new();
|
|
|
|
|
|
|
|
|
|
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
|
|
|
|
|
{
|
|
|
|
|
if (typeof(T) == typeof(AppSettingsSnapshot))
|
|
|
|
|
{
|
|
|
|
|
return (T)(object)Snapshot.Clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new T();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SaveSnapshot<T>(
|
|
|
|
|
SettingsScope scope,
|
|
|
|
|
T snapshot,
|
|
|
|
|
string? subjectId = null,
|
|
|
|
|
string? placementId = null,
|
|
|
|
|
string? sectionId = null,
|
|
|
|
|
IReadOnlyCollection<string>? changedKeys = null)
|
|
|
|
|
{
|
|
|
|
|
if (snapshot is AppSettingsSnapshot appSettings)
|
|
|
|
|
{
|
|
|
|
|
CopyUpdateSettings(appSettings, Snapshot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Changed?.Invoke(this, new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public T LoadSection<T>(SettingsScope scope, string subjectId, string sectionId, string? placementId = null) where T : new()
|
|
|
|
|
=> new();
|
|
|
|
|
|
|
|
|
|
public void SaveSection<T>(
|
|
|
|
|
SettingsScope scope,
|
|
|
|
|
string subjectId,
|
|
|
|
|
string sectionId,
|
|
|
|
|
T section,
|
|
|
|
|
string? placementId = null,
|
|
|
|
|
IReadOnlyCollection<string>? changedKeys = null)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void DeleteSection(SettingsScope scope, string subjectId, string sectionId, string? placementId = null)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public T? GetValue<T>(SettingsScope scope, string key, string? subjectId = null, string? placementId = null, string? sectionId = null)
|
|
|
|
|
=> default;
|
|
|
|
|
|
|
|
|
|
public void SetValue<T>(
|
|
|
|
|
SettingsScope scope,
|
|
|
|
|
string key,
|
|
|
|
|
T value,
|
|
|
|
|
string? subjectId = null,
|
|
|
|
|
string? placementId = null,
|
|
|
|
|
string? sectionId = null,
|
|
|
|
|
IReadOnlyCollection<string>? changedKeys = null)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
|
|
|
|
|
=> throw new NotSupportedException();
|
|
|
|
|
|
|
|
|
|
private static void CopyUpdateSettings(AppSettingsSnapshot source, AppSettingsSnapshot target)
|
|
|
|
|
{
|
|
|
|
|
target.IncludePrereleaseUpdates = source.IncludePrereleaseUpdates;
|
|
|
|
|
target.UpdateChannel = source.UpdateChannel;
|
|
|
|
|
target.UpdateMode = source.UpdateMode;
|
|
|
|
|
target.UpdateDownloadSource = source.UpdateDownloadSource;
|
|
|
|
|
target.UpdateDownloadThreads = source.UpdateDownloadThreads;
|
|
|
|
|
target.ForceUpdateReinstall = source.ForceUpdateReinstall;
|
|
|
|
|
target.UseGhProxyMirror = source.UseGhProxyMirror;
|
|
|
|
|
target.PendingUpdateInstallerPath = source.PendingUpdateInstallerPath;
|
|
|
|
|
target.PendingUpdateVersion = source.PendingUpdateVersion;
|
|
|
|
|
target.PendingUpdatePublishedAtUtcMs = source.PendingUpdatePublishedAtUtcMs;
|
|
|
|
|
target.LastUpdateCheckUtcMs = source.LastUpdateCheckUtcMs;
|
|
|
|
|
target.PendingUpdateSha256 = source.PendingUpdateSha256;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 14:25:52 +08:00
|
|
|
private sealed class FakeManifestProvider(string providerName) : IUpdateManifestProvider
|
|
|
|
|
{
|
|
|
|
|
public string ProviderName { get; } = providerName;
|
|
|
|
|
public int GetLatestCalls { get; private set; }
|
|
|
|
|
|
|
|
|
|
public Task<UpdateManifest?> GetLatestAsync(string channel, string platform, Version currentVersion, CancellationToken ct)
|
|
|
|
|
{
|
|
|
|
|
GetLatestCalls++;
|
|
|
|
|
return Task.FromResult<UpdateManifest?>(CreateManifest(ProviderName, channel, platform));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<UpdateManifest?> GetByVersionAsync(string version, string channel, string platform, CancellationToken ct)
|
|
|
|
|
=> Task.FromResult<UpdateManifest?>(CreateManifest(ProviderName, channel, platform));
|
|
|
|
|
|
|
|
|
|
public Task<IReadOnlyList<UpdateManifest>> GetIncrementalChainAsync(string channel, string platform, Version fromVersion, Version toVersion, CancellationToken ct)
|
|
|
|
|
=> Task.FromResult<IReadOnlyList<UpdateManifest>>([CreateManifest(ProviderName, channel, platform)]);
|
|
|
|
|
|
|
|
|
|
private static UpdateManifest CreateManifest(string id, string channel, string platform) => new(
|
|
|
|
|
id,
|
|
|
|
|
"1.0.0",
|
|
|
|
|
"1.1.0",
|
|
|
|
|
platform,
|
|
|
|
|
channel,
|
|
|
|
|
DateTimeOffset.Parse("2026-05-06T00:00:00Z"),
|
|
|
|
|
UpdatePayloadKind.DeltaPlonds,
|
|
|
|
|
"https://example.test/filemap.json",
|
|
|
|
|
"https://example.test/filemap.json.sig",
|
|
|
|
|
null,
|
|
|
|
|
[],
|
|
|
|
|
null,
|
|
|
|
|
new Dictionary<string, string>());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 19:48:51 +08:00
|
|
|
private sealed class EmptyHandler : HttpMessageHandler
|
|
|
|
|
{
|
|
|
|
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.NotFound));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 14:25:52 +08:00
|
|
|
private sealed class FakeSettingsFacade(IUpdateSettingsService update) : ISettingsFacadeService
|
|
|
|
|
{
|
|
|
|
|
public ISettingsService Settings => throw new NotSupportedException();
|
|
|
|
|
public ISettingsCatalog Catalog => throw new NotSupportedException();
|
|
|
|
|
public IGridSettingsService Grid => throw new NotSupportedException();
|
|
|
|
|
public IWallpaperSettingsService Wallpaper => throw new NotSupportedException();
|
|
|
|
|
public IWallpaperMediaService WallpaperMedia => throw new NotSupportedException();
|
|
|
|
|
public IThemeAppearanceService Theme => throw new NotSupportedException();
|
|
|
|
|
public IStatusBarSettingsService StatusBar => throw new NotSupportedException();
|
|
|
|
|
public ITextCapsuleSettingsService TextCapsule => throw new NotSupportedException();
|
|
|
|
|
public IWeatherSettingsService Weather => throw new NotSupportedException();
|
|
|
|
|
public IRegionSettingsService Region { get; } = new FakeRegionSettingsService();
|
|
|
|
|
public IPrivacySettingsService Privacy => throw new NotSupportedException();
|
|
|
|
|
public IUpdateSettingsService Update { get; } = update;
|
|
|
|
|
public ILauncherCatalogService LauncherCatalog => throw new NotSupportedException();
|
|
|
|
|
public ILauncherPolicyService LauncherPolicy => throw new NotSupportedException();
|
|
|
|
|
public IPluginManagementSettingsService PluginManagement => throw new NotSupportedException();
|
|
|
|
|
public IPluginCatalogSettingsService PluginCatalog => throw new NotSupportedException();
|
|
|
|
|
public IApplicationInfoService ApplicationInfo { get; } = new FakeApplicationInfoService();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class FakeRegionSettingsService : IRegionSettingsService
|
|
|
|
|
{
|
|
|
|
|
public RegionSettingsState Get() => new("en-US", null);
|
|
|
|
|
public void Save(RegionSettingsState state) { }
|
|
|
|
|
public TimeZoneService GetTimeZoneService() => throw new NotSupportedException();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sealed class FakeApplicationInfoService : IApplicationInfoService
|
|
|
|
|
{
|
|
|
|
|
public string GetAppVersionText() => "1.0.0";
|
|
|
|
|
public string GetAppCodenameText() => "Test";
|
|
|
|
|
public AppRenderBackendInfo GetRenderBackendInfo() => throw new NotSupportedException();
|
|
|
|
|
}
|
|
|
|
|
}
|