using System.IO; using System.Net.Http; using CursorLang.Core.Models; using CursorLang.Core.Services; using CursorLang.Settings.ViewModels; using CursorLang.Tests.Shared; namespace CursorLang.Settings.Tests.ViewModels; /// /// The updates section of the settings window: what it shows at every step and /// what it asks of the service behind it. /// public sealed class UpdateViewModelTests { // The section has something to say at every moment, the one before the first // answer included: the status line is never an empty spot in the window [Fact] public void Before_the_first_check_the_section_says_so() { using UpdateViewModel viewModel = Create(new FakeUpdateService()); Assert.Equal(UpdateStatus.Idle, viewModel.Status); Assert.Equal("en:UpdateNotChecked", viewModel.StatusText); Assert.False(viewModel.IsDownloadOffered); Assert.False(viewModel.IsInstallOffered); Assert.True(viewModel.CanCheck); } [Fact] public async Task An_update_found_is_offered_for_download() { var updates = new FakeUpdateService { Release = Release("2.0.0.0") }; using UpdateViewModel viewModel = Create(updates); await viewModel.CheckCommand.ExecuteAsync(null); Assert.Equal(UpdateStatus.Available, viewModel.Status); Assert.True(viewModel.IsDownloadOffered); Assert.False(viewModel.IsInstallOffered); Assert.Equal("https://host/releases/tag", viewModel.ReleaseUrl?.ToString()); Assert.True(viewModel.IsReleaseLinkShown); Assert.Equal("en:UpdateAvailable", viewModel.StatusText); } [Fact] public async Task With_the_latest_version_installed_there_is_nothing_to_offer() { using UpdateViewModel viewModel = Create(new FakeUpdateService()); await viewModel.CheckCommand.ExecuteAsync(null); Assert.Equal(UpdateStatus.UpToDate, viewModel.Status); Assert.False(viewModel.IsDownloadOffered); Assert.False(viewModel.IsReleaseLinkShown); } [Fact] public async Task A_check_by_the_button_says_when_it_did_not_work_out() { var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") }; using UpdateViewModel viewModel = Create(updates); await viewModel.CheckCommand.ExecuteAsync(null); Assert.Equal(UpdateStatus.Failed, viewModel.Status); Assert.Equal("en:UpdateFailed", viewModel.StatusText); } // The window is opened by hand, and the answer is what the section is there // for: a dead network is part of the answer rather than a reason to say nothing [Fact] public async Task A_check_when_the_window_opens_says_when_it_did_not_work_out() { var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") }; using UpdateViewModel viewModel = Create(updates); await viewModel.StartAsync(); Assert.Equal(UpdateStatus.Failed, viewModel.Status); Assert.Equal("en:UpdateFailed", viewModel.StatusText); } // Nothing is remembered between openings: an answer from yesterday is worth // less than today's, and the request costs nothing at this rate [Fact] public async Task Every_opening_of_the_window_asks_anew() { var updates = new FakeUpdateService(); using UpdateViewModel viewModel = Create(updates); await viewModel.StartAsync(); Assert.Equal(1, updates.CheckCalls); await viewModel.StartAsync(); Assert.Equal(2, updates.CheckCalls); } [Fact] public async Task A_package_from_Store_is_left_to_Store() { var updates = new FakeUpdateService { IsSupported = false, Release = Release("2.0.0.0") }; using UpdateViewModel viewModel = Create(updates); await viewModel.StartAsync(); await viewModel.CheckCommand.ExecuteAsync(null); Assert.False(viewModel.IsSupported); Assert.Equal(0, updates.CheckCalls); } [Fact] public async Task A_downloaded_package_is_offered_for_installation() { using TempFolder folder = new(); string package = folder.File("CursorLang-2.0.0.0.msixbundle"); await File.WriteAllTextAsync(package, "package", TestContext.Current.CancellationToken); var updates = new FakeUpdateService { Release = Release("2.0.0.0"), PackagePath = package }; using UpdateViewModel viewModel = Create(updates); await viewModel.CheckCommand.ExecuteAsync(null); await viewModel.DownloadCommand.ExecuteAsync(null); Assert.Equal(UpdateStatus.Ready, viewModel.Status); Assert.True(viewModel.IsInstallOffered); Assert.False(viewModel.IsDownloadOffered); viewModel.InstallCommand.Execute(null); Assert.Equal([package], updates.Installed); } [Fact] public async Task While_the_package_is_downloading_the_section_shows_it() { var updates = new FakeUpdateService { Release = Release("2.0.0.0"), DownloadGate = new TaskCompletionSource(), }; using UpdateViewModel viewModel = Create(updates); await viewModel.CheckCommand.ExecuteAsync(null); Task download = viewModel.DownloadCommand.ExecuteAsync(null); Assert.Equal(UpdateStatus.Downloading, viewModel.Status); Assert.True(viewModel.IsProgressShown); Assert.True(viewModel.IsBusy); Assert.False(viewModel.CanCheck); updates.DownloadGate.SetResult(); await download; } [Fact] public async Task A_download_that_did_not_work_out_is_told_about() { var updates = new FakeUpdateService { Release = Release("2.0.0.0") }; using UpdateViewModel viewModel = Create(updates); await viewModel.CheckCommand.ExecuteAsync(null); updates.Failure = new HttpRequestException("the connection dropped"); await viewModel.DownloadCommand.ExecuteAsync(null); Assert.Equal(UpdateStatus.Failed, viewModel.Status); } // The temp folder is cleared by Windows as it sees fit, and the app has no // business handing a file that is gone to the installer [Fact] public async Task A_package_gone_from_the_disk_is_offered_for_download_again() { var updates = new FakeUpdateService { Release = Release("2.0.0.0"), PackagePath = Path.Combine(Path.GetTempPath(), "CursorLang.Tests", "never-written.msixbundle"), }; using UpdateViewModel viewModel = Create(updates); await viewModel.CheckCommand.ExecuteAsync(null); await viewModel.DownloadCommand.ExecuteAsync(null); viewModel.InstallCommand.Execute(null); Assert.Empty(updates.Installed); Assert.Equal(UpdateStatus.Available, viewModel.Status); } [Fact] public async Task The_status_is_written_in_the_chosen_language() { var localization = new FakeLocalizationService(); using UpdateViewModel viewModel = Create(new FakeUpdateService(), localization: localization); await viewModel.CheckCommand.ExecuteAsync(null); Assert.Equal("en:UpdateUpToDate", viewModel.StatusText); localization.CurrentLanguage = "ru"; Assert.Equal("ru:UpdateUpToDate", viewModel.StatusText); } [Fact] public async Task Closing_unsubscribes_from_the_language() { var localization = new FakeLocalizationService(); UpdateViewModel viewModel = Create(new FakeUpdateService(), localization: localization); await viewModel.CheckCommand.ExecuteAsync(null); List changed = []; viewModel.PropertyChanged += (_, e) => changed.Add(e.PropertyName); viewModel.Dispose(); localization.CurrentLanguage = "ru"; Assert.Empty(changed); } private static ReleaseInfo Release(string version) => new( Version.Parse(version), $"v{version}", new Uri("https://host/releases/tag"), new ReleaseAsset($"CursorLang-{version}.msixbundle", new Uri("https://host/package"), 0)); private static UpdateViewModel Create( IUpdateService updates, ILocalizationService? localization = null) => new(updates, localization ?? new FakeLocalizationService()); }