using System.Net; using System.Net.Http; using ClaudeDo.Installer.Core; namespace ClaudeDo.Installer.Tests; public sealed class ReleaseClientTests { private const string ApiBase = "https://git.example.test/api/v1/repos/releases/ClaudeDo"; [Fact] public async Task GetLatestReleaseAsync_ParsesTagAndAssets() { const string json = """ { "tag_name": "v0.2.0", "name": "v0.2.0", "assets": [ { "name": "ClaudeDo-0.2.0-win-x64.zip", "browser_download_url": "https://git.example.test/dl/zip", "size": 12345 }, { "name": "checksums.txt", "browser_download_url": "https://git.example.test/dl/checksums", "size": 128 } ] } """; var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, json); using var http = new HttpClient(handler); var client = new ReleaseClient(http, ApiBase); var release = await client.GetLatestReleaseAsync(CancellationToken.None); Assert.NotNull(release); Assert.Equal("v0.2.0", release!.TagName); Assert.Equal(2, release.Assets.Count); Assert.Equal("ClaudeDo-0.2.0-win-x64.zip", release.Assets[0].Name); Assert.Equal("https://git.example.test/dl/zip", release.Assets[0].BrowserDownloadUrl); Assert.Equal(12345, release.Assets[0].Size); } [Fact] public async Task GetLatestReleaseAsync_Returns_Null_On404() { var handler = new FakeHttpMessageHandler(HttpStatusCode.NotFound, ""); using var http = new HttpClient(handler); var client = new ReleaseClient(http, ApiBase); var release = await client.GetLatestReleaseAsync(CancellationToken.None); Assert.Null(release); } [Fact] public async Task GetLatestReleaseAsync_Returns_Null_OnNetworkError() { var handler = new FakeHttpMessageHandler(_ => throw new HttpRequestException("boom")); using var http = new HttpClient(handler); var client = new ReleaseClient(http, ApiBase); var release = await client.GetLatestReleaseAsync(CancellationToken.None); Assert.Null(release); } [Fact] public async Task GetLatestReleaseAsync_Hits_CorrectUrl() { var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, "{\"tag_name\":\"v0.1.0\",\"assets\":[]}"); using var http = new HttpClient(handler); var client = new ReleaseClient(http, ApiBase); _ = await client.GetLatestReleaseAsync(CancellationToken.None); Assert.Single(handler.Requests); Assert.Equal($"{ApiBase}/releases/latest", handler.Requests[0].RequestUri!.ToString()); } [Fact] public async Task DownloadAsync_WritesBytesToDisk() { var payload = new byte[] { 1, 2, 3, 4, 5 }; var handler = new FakeHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(payload) }); using var http = new HttpClient(handler); var client = new ReleaseClient(http, ApiBase); var tempPath = Path.Combine(Path.GetTempPath(), "ClaudeDoDlTest-" + Guid.NewGuid().ToString("N")); try { await client.DownloadAsync("https://example/foo", tempPath, new Progress(_ => { }), CancellationToken.None); Assert.True(File.Exists(tempPath)); Assert.Equal(payload, File.ReadAllBytes(tempPath)); } finally { if (File.Exists(tempPath)) File.Delete(tempPath); } } }