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);
}