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 { [Fact] public void Before_the_first_check_the_section_says_nothing() { using UpdateViewModel viewModel = Create(new FakeUpdateService()); Assert.Equal(UpdateStatus.Idle, viewModel.Status); Assert.False(viewModel.HasStatus); 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.True(viewModel.HasStatus); } // The app does not always start with a live network, and the user who never // asked about updates has no use for the complaint [Fact] public async Task A_check_at_startup_keeps_a_failure_to_itself() { var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") }; using UpdateViewModel viewModel = Create(updates); await viewModel.StartAsync(); Assert.Equal(UpdateStatus.Idle, viewModel.Status); Assert.False(viewModel.HasStatus); } [Fact] public async Task A_successful_check_is_remembered_in_the_settings() { var settings = new AppSettings(); var updates = new FakeUpdateService { Release = Release("2.0.0.0") }; using UpdateViewModel viewModel = Create(updates, settings); await viewModel.CheckCommand.ExecuteAsync(null); Assert.NotNull(settings.LastUpdateCheck); } [Fact] public async Task A_check_that_did_not_work_out_is_not_remembered() { var settings = new AppSettings(); var updates = new FakeUpdateService { Failure = new HttpRequestException("no network") }; using UpdateViewModel viewModel = Create(updates, settings); await viewModel.CheckCommand.ExecuteAsync(null); Assert.Null(settings.LastUpdateCheck); } [Fact] public async Task A_recent_check_is_not_repeated_at_startup() { var settings = new AppSettings { LastUpdateCheck = DateTimeOffset.UtcNow }; var updates = new FakeUpdateService(); using UpdateViewModel viewModel = Create(updates, settings); await viewModel.StartAsync(); Assert.Equal(0, updates.CheckCalls); } [Fact] public async Task A_check_of_yesterday_is_repeated_at_startup() { var settings = new AppSettings { LastUpdateCheck = DateTimeOffset.UtcNow - TimeSpan.FromDays(2) }; var updates = new FakeUpdateService(); using UpdateViewModel viewModel = Create(updates, settings); await viewModel.StartAsync(); Assert.Equal(1, updates.CheckCalls); } [Fact] public async Task A_ban_on_checking_by_itself_is_obeyed() { var settings = new AppSettings { CheckForUpdates = false }; var updates = new FakeUpdateService(); using UpdateViewModel viewModel = Create(updates, settings); await viewModel.StartAsync(); Assert.Equal(0, updates.CheckCalls); // The button still works: the setting is about the app doing it on its own await viewModel.CheckCommand.ExecuteAsync(null); Assert.Equal(1, updates.CheckCalls); } [Fact] public void The_ban_on_checking_travels_to_the_settings() { var settings = new AppSettings { CheckForUpdates = true }; using UpdateViewModel viewModel = Create(new FakeUpdateService(), settings); viewModel.CheckAutomatically = false; Assert.False(settings.CheckForUpdates); } [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, AppSettings? settings = null, ILocalizationService? localization = null) => new(updates, localization ?? new FakeLocalizationService(), settings ?? new AppSettings(), new UpdateOptions()); }