using System.Net; using System.Text; using CursorLang.Core.Models; using CursorLang.Core.Services; using CursorLang.Tests.Shared; namespace CursorLang.Core.Tests.Services; /// /// What the app does with a release once it has found one: whether it is newer /// at all, and what ends up on disk. /// public sealed class UpdateServiceTests { [Theory] [InlineData("1.0.0.0", "1.0.1.0", true)] [InlineData("1.0.0.0", "2.0.0.0", true)] [InlineData("1.0.0.0", "1.0.0.0", false)] [InlineData("1.0.1.0", "1.0.0.0", false)] public async Task Only_a_higher_version_counts_as_an_update(string current, string found, bool offered) { var feed = new FakeReleaseFeed { Release = Release(found) }; using TempFolder folder = new(); using UpdateService service = Create(feed, folder, current); ReleaseInfo? update = await service.CheckAsync(TestContext.Current.CancellationToken); Assert.Equal(offered, update is not null); } [Fact] public async Task An_empty_repository_leaves_the_app_with_nothing() { var feed = new FakeReleaseFeed { Release = null }; using TempFolder folder = new(); using UpdateService service = Create(feed, folder); Assert.Null(await service.CheckAsync(TestContext.Current.CancellationToken)); } [Fact] public async Task The_package_ends_up_on_disk_whole() { byte[] content = Encoding.UTF8.GetBytes(new string('p', 300_000)); using TempFolder folder = new(); using UpdateService service = Create(new FakeReleaseFeed(), folder, client: FakeHttpHandler.Bytes(content)); string path = await service.DownloadAsync(Release("2.0.0.0"), null, TestContext.Current.CancellationToken); Assert.Equal(content, await File.ReadAllBytesAsync(path, TestContext.Current.CancellationToken)); } // The name comes from the version, not from the answer: the app creates a // file with it, and the answer comes from the other side [Fact] public async Task The_name_of_the_file_is_built_by_the_app_itself() { using TempFolder folder = new(); using UpdateService service = Create(new FakeReleaseFeed(), folder, client: FakeHttpHandler.Bytes([1, 2, 3])); var release = new ReleaseInfo( new Version(2, 0, 0, 0), "v2.0.0", null, new ReleaseAsset(@"..\..\evil.msixbundle", new Uri("https://host/a"), 3)); string path = await service.DownloadAsync(release, null, TestContext.Current.CancellationToken); Assert.Equal("CursorLang-2.0.0.0.msixbundle", Path.GetFileName(path)); Assert.Equal(folder.Path, Path.GetDirectoryName(path)); } [Fact] public async Task The_download_reports_how_far_it_has_come() { byte[] content = new byte[500_000]; using TempFolder folder = new(); using UpdateService service = Create(new FakeReleaseFeed(), folder, client: FakeHttpHandler.Bytes(content)); var reported = new CollectingProgress(); await service.DownloadAsync(Release("2.0.0.0"), reported, TestContext.Current.CancellationToken); Assert.NotEmpty(reported.Values); Assert.All(reported.Values, value => Assert.InRange(value, 0, 1)); Assert.Equal(reported.Values, [.. reported.Values.Order()]); Assert.Equal(1, reported.Values[^1]); } [Fact] public async Task A_closed_repository_gets_the_token_with_the_download_too() { var feed = new FakeReleaseFeed(); using TempFolder folder = new(); using UpdateService service = Create(feed, folder, client: FakeHttpHandler.Bytes([1])); await service.DownloadAsync(Release("2.0.0.0"), null, TestContext.Current.CancellationToken); Assert.Single(feed.Authorized); } [Fact] public async Task A_refusal_of_the_hosting_service_leaves_no_package_behind() { using TempFolder folder = new(); using UpdateService service = Create( new FakeReleaseFeed(), folder, client: FakeHttpHandler.Status(HttpStatusCode.NotFound)); await Assert.ThrowsAsync( () => service.DownloadAsync(Release("2.0.0.0"), null, TestContext.Current.CancellationToken)); Assert.Empty(Directory.GetFiles(folder.Path)); } // A package left from an earlier download takes up room and is of no use // once it has been installed [Fact] public async Task An_older_download_is_cleared_away() { using TempFolder folder = new(); string leftover = folder.File("CursorLang-1.5.0.0.msixbundle"); await File.WriteAllTextAsync(leftover, "old", TestContext.Current.CancellationToken); using UpdateService service = Create(new FakeReleaseFeed(), folder, client: FakeHttpHandler.Bytes([1])); string path = await service.DownloadAsync(Release("2.0.0.0"), null, TestContext.Current.CancellationToken); Assert.False(File.Exists(leftover)); Assert.True(File.Exists(path)); } /// /// The reports as the download makes them. Progress<T> would /// hand them over to another thread, and a test has nowhere to wait for that. /// private sealed class CollectingProgress : IProgress { internal List Values { get; } = []; public void Report(double value) => Values.Add(value); } 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 UpdateService Create( IReleaseFeed feed, TempFolder folder, string current = "1.0.0.0", FakeHttpHandler? client = null) => new(feed, (client ?? FakeHttpHandler.Bytes([])).CreateClient(), Version.Parse(current), folder.Path); }