lightweight variant (#1)
Reviewed-on: #1 Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Core.Models;
|
||||
using CursorLang.Core.Services;
|
||||
using CursorLang.Tests.Shared;
|
||||
|
||||
namespace CursorLang.Core.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Reading the release list of Gitea. The answer of the server is not ours to
|
||||
/// shape, so what matters is what the app makes of it.
|
||||
/// </summary>
|
||||
public sealed class GiteaReleaseFeedTests
|
||||
{
|
||||
private const string Releases = """
|
||||
[
|
||||
{
|
||||
"tag_name": "v1.2.0",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
"html_url": "https://git.alrakis.kz/alrakis/cursor-lang/releases/tag/v1.2.0",
|
||||
"assets": [
|
||||
{
|
||||
"name": "CursorLang-1.2.0.0.msixbundle",
|
||||
"browser_download_url": "https://git.alrakis.kz/attachments/CursorLang-1.2.0.0.msixbundle",
|
||||
"size": 4096
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
""";
|
||||
|
||||
[Fact]
|
||||
public async Task A_release_is_read_whole()
|
||||
{
|
||||
ReleaseInfo? release = await Read(Releases);
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal(new Version(1, 2, 0, 0), release.Version);
|
||||
Assert.Equal("v1.2.0", release.Tag);
|
||||
Assert.Equal("https://git.alrakis.kz/alrakis/cursor-lang/releases/tag/v1.2.0", release.PageUrl?.ToString());
|
||||
Assert.Equal("CursorLang-1.2.0.0.msixbundle", release.Package.FileName);
|
||||
Assert.Equal(
|
||||
"https://git.alrakis.kz/attachments/CursorLang-1.2.0.0.msixbundle",
|
||||
release.Package.Url.ToString());
|
||||
Assert.Equal(4096, release.Package.Size);
|
||||
}
|
||||
|
||||
// The API of Gitea lives on the server itself, next to the pages of the
|
||||
// repository
|
||||
[Fact]
|
||||
public async Task The_request_goes_to_the_releases_of_the_project()
|
||||
{
|
||||
var handler = FakeHttpHandler.Json(Releases);
|
||||
var feed = new GiteaReleaseFeed(handler.CreateClient(), Options());
|
||||
|
||||
await feed.GetLatestAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Uri asked = Assert.Single(handler.Requests).RequestUri!;
|
||||
Assert.Equal("git.alrakis.kz", asked.Host);
|
||||
Assert.StartsWith(
|
||||
"/api/v1/repos/alrakis/cursor-lang/releases", asked.AbsolutePath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// A server sitting under a path of its own keeps that path: dropping it
|
||||
// would send the request to a place that answers nothing
|
||||
[Fact]
|
||||
public async Task A_server_behind_a_path_keeps_it()
|
||||
{
|
||||
var handler = FakeHttpHandler.Json(Releases);
|
||||
var feed = new GiteaReleaseFeed(
|
||||
handler.CreateClient(),
|
||||
new UpdateOptions { ServiceUri = new Uri("https://host.example.com/gitea"), Project = "team/app" });
|
||||
|
||||
await feed.GetLatestAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
Uri asked = Assert.Single(handler.Requests).RequestUri!;
|
||||
Assert.StartsWith("/gitea/api/v1/repos/team/app/releases", asked.AbsolutePath, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// «token» is the scheme of Gitea for keys of access
|
||||
[Fact]
|
||||
public void A_closed_repository_gets_the_token_it_asks_for()
|
||||
{
|
||||
var feed = new GiteaReleaseFeed(new HttpClient(), Options("secret"));
|
||||
using var request = new HttpRequestMessage();
|
||||
|
||||
feed.Authorize(request);
|
||||
|
||||
Assert.Equal("token", request.Headers.Authorization?.Scheme);
|
||||
Assert.Equal("secret", request.Headers.Authorization?.Parameter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_open_repository_is_asked_without_a_token()
|
||||
{
|
||||
var feed = new GiteaReleaseFeed(new HttpClient(), Options());
|
||||
using var request = new HttpRequestMessage();
|
||||
|
||||
feed.Authorize(request);
|
||||
|
||||
Assert.Null(request.Headers.Authorization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("1.2.3", "1.2.3.0")]
|
||||
[InlineData("v1.2.3", "1.2.3.0")]
|
||||
[InlineData("V1.2", "1.2.0.0")]
|
||||
[InlineData("1.2.3.4", "1.2.3.4")]
|
||||
public async Task A_version_is_read_out_of_the_tag(string tag, string expected)
|
||||
{
|
||||
ReleaseInfo? release = await Read(WithTag(tag));
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal(Version.Parse(expected), release.Version);
|
||||
}
|
||||
|
||||
// A pre-release version is not something the app offers by itself:
|
||||
// such a version is asked for on purpose
|
||||
[Theory]
|
||||
[InlineData("v1.2.3-beta")]
|
||||
[InlineData("nightly")]
|
||||
[InlineData("release-1")]
|
||||
public async Task A_tag_that_is_not_a_version_is_passed_over(string tag)
|
||||
{
|
||||
Assert.Null(await Read(WithTag(tag)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_draft_and_a_pre_release_are_passed_over()
|
||||
{
|
||||
const string releases = """
|
||||
[
|
||||
{ "tag_name": "v3.0.0", "draft": true, "assets": [
|
||||
{ "name": "a.msixbundle", "browser_download_url": "https://host/a.msixbundle" } ] },
|
||||
{ "tag_name": "v2.0.0", "prerelease": true, "assets": [
|
||||
{ "name": "b.msixbundle", "browser_download_url": "https://host/b.msixbundle" } ] },
|
||||
{ "tag_name": "v1.0.0", "assets": [
|
||||
{ "name": "c.msixbundle", "browser_download_url": "https://host/c.msixbundle" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
ReleaseInfo? release = await Read(releases);
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal(new Version(1, 0, 0, 0), release.Version);
|
||||
}
|
||||
|
||||
// The order of the releases belongs to the server, the highest number to
|
||||
// the app: a fix to an older branch can be the freshest release
|
||||
[Fact]
|
||||
public async Task The_highest_version_wins_over_the_order_of_the_answer()
|
||||
{
|
||||
const string releases = """
|
||||
[
|
||||
{ "tag_name": "v1.0.5", "assets": [
|
||||
{ "name": "a.msixbundle", "browser_download_url": "https://host/a.msixbundle" } ] },
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "b.msixbundle", "browser_download_url": "https://host/b.msixbundle" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
ReleaseInfo? release = await Read(releases);
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal(new Version(2, 0, 0, 0), release.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_release_without_a_package_is_passed_over()
|
||||
{
|
||||
const string releases = """
|
||||
[
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "notes.txt", "browser_download_url": "https://host/notes.txt" } ] },
|
||||
{ "tag_name": "v1.0.0", "assets": [
|
||||
{ "name": "a.msixbundle", "browser_download_url": "https://host/a.msixbundle" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
ReleaseInfo? release = await Read(releases);
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal(new Version(1, 0, 0, 0), release.Version);
|
||||
}
|
||||
|
||||
// The signature is what Windows checks, but a package offered over an open
|
||||
// connection is not worth downloading in the first place
|
||||
[Fact]
|
||||
public async Task A_package_offered_over_an_open_connection_is_passed_over()
|
||||
{
|
||||
const string releases = """
|
||||
[
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "a.msixbundle", "browser_download_url": "http://host/a.msixbundle" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
Assert.Null(await Read(releases));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_bundle_wins_over_the_packages_of_single_architectures()
|
||||
{
|
||||
const string releases = """
|
||||
[
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "CursorLang-2.0.0.0-x64.msix", "browser_download_url": "https://host/x64.msix" },
|
||||
{ "name": "CursorLang-2.0.0.0.msixbundle", "browser_download_url": "https://host/all.msixbundle" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
ReleaseInfo? release = await Read(releases);
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal("https://host/all.msixbundle", release.Package.Url.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Out_of_several_packages_the_one_for_this_machine_is_taken()
|
||||
{
|
||||
const string releases = """
|
||||
[
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "CursorLang-2.0.0.0-arm64.msix", "browser_download_url": "https://host/arm64.msix" },
|
||||
{ "name": "CursorLang-2.0.0.0-x64.msix", "browser_download_url": "https://host/x64.msix" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
string expected = RuntimeInformation.ProcessArchitecture == Architecture.Arm64
|
||||
? "https://host/arm64.msix"
|
||||
: "https://host/x64.msix";
|
||||
|
||||
ReleaseInfo? release = await Read(releases);
|
||||
|
||||
Assert.NotNull(release);
|
||||
Assert.Equal(expected, release.Package.Url.ToString());
|
||||
}
|
||||
|
||||
// Without the architecture in the name there is no telling which package is
|
||||
// for this machine — unless it is the only one there
|
||||
[Fact]
|
||||
public async Task A_package_without_an_architecture_is_taken_only_when_alone()
|
||||
{
|
||||
const string alone = """
|
||||
[
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "CursorLang.msix", "browser_download_url": "https://host/one.msix" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
const string ambiguous = """
|
||||
[
|
||||
{ "tag_name": "v2.0.0", "assets": [
|
||||
{ "name": "CursorLang.msix", "browser_download_url": "https://host/one.msix" },
|
||||
{ "name": "CursorLang-other.msix", "browser_download_url": "https://host/other.msix" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
Assert.NotNull(await Read(alone));
|
||||
Assert.Null(await Read(ambiguous));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_empty_list_of_releases_means_nothing_to_offer()
|
||||
{
|
||||
Assert.Null(await Read("[]"));
|
||||
}
|
||||
|
||||
// A server answering with something else is no reason to fail
|
||||
[Fact]
|
||||
public async Task An_answer_that_is_not_a_list_leaves_the_app_with_nothing()
|
||||
{
|
||||
Assert.Null(await Read("""{ "message": "Not Found" }"""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_refusal_of_the_server_is_raised()
|
||||
{
|
||||
var handler = FakeHttpHandler.Status(HttpStatusCode.Unauthorized);
|
||||
var feed = new GiteaReleaseFeed(handler.CreateClient(), Options());
|
||||
|
||||
await Assert.ThrowsAsync<HttpRequestException>(
|
||||
() => feed.GetLatestAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
private static string WithTag(string tag) => $$"""
|
||||
[
|
||||
{ "tag_name": "{{tag}}", "assets": [
|
||||
{ "name": "CursorLang.msixbundle", "browser_download_url": "https://host/a.msixbundle" } ] }
|
||||
]
|
||||
""";
|
||||
|
||||
private static UpdateOptions Options(string? token = null) => new()
|
||||
{
|
||||
ServiceUri = new Uri("https://git.alrakis.kz/"),
|
||||
Project = "alrakis/cursor-lang",
|
||||
AccessToken = token,
|
||||
};
|
||||
|
||||
private static Task<ReleaseInfo?> Read(string json) =>
|
||||
new GiteaReleaseFeed(FakeHttpHandler.Json(json).CreateClient(), Options())
|
||||
.GetLatestAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user