test(releases): port ReleaseClient + ChecksumVerifier tests to new project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,106 +0,0 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests;
|
||||
|
||||
public sealed class ChecksumVerifierTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
|
||||
public ChecksumVerifierTests()
|
||||
{
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDoChecksum-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_tempDir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_KnownVector_EmptyFile()
|
||||
{
|
||||
var path = Path.Combine(_tempDir, "empty.bin");
|
||||
File.WriteAllBytes(path, Array.Empty<byte>());
|
||||
|
||||
var hash = ChecksumVerifier.ComputeSha256(path);
|
||||
|
||||
Assert.Equal("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSha256_KnownVector_Hello()
|
||||
{
|
||||
var path = Path.Combine(_tempDir, "hello.bin");
|
||||
File.WriteAllText(path, "hello");
|
||||
|
||||
var hash = ChecksumVerifier.ComputeSha256(path);
|
||||
|
||||
Assert.Equal("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", hash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_ReturnsTrue_WhenHashMatches()
|
||||
{
|
||||
var path = Path.Combine(_tempDir, "x.bin");
|
||||
File.WriteAllText(path, "hello");
|
||||
|
||||
Assert.True(ChecksumVerifier.Verify(path,
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_IsCaseInsensitive()
|
||||
{
|
||||
var path = Path.Combine(_tempDir, "x.bin");
|
||||
File.WriteAllText(path, "hello");
|
||||
|
||||
Assert.True(ChecksumVerifier.Verify(path,
|
||||
"2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_ReturnsFalse_OnMismatch()
|
||||
{
|
||||
var path = Path.Combine(_tempDir, "x.bin");
|
||||
File.WriteAllText(path, "hello");
|
||||
|
||||
Assert.False(ChecksumVerifier.Verify(path, new string('0', 64)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseChecksumsFile_ReadsTwoLines()
|
||||
{
|
||||
var content = """
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ClaudeDo-0.2.0-win-x64.zip
|
||||
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 ClaudeDo.Installer-0.2.0.exe
|
||||
""";
|
||||
|
||||
var map = ChecksumVerifier.ParseChecksumsFile(content);
|
||||
|
||||
Assert.Equal(2, map.Count);
|
||||
Assert.Equal(
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
map["ClaudeDo-0.2.0-win-x64.zip"]);
|
||||
Assert.Equal(
|
||||
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
||||
map["ClaudeDo.Installer-0.2.0.exe"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseChecksumsFile_SkipsBlankAndMalformedLines()
|
||||
{
|
||||
var content = """
|
||||
|
||||
not a line
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 file.zip
|
||||
|
||||
""";
|
||||
|
||||
var map = ChecksumVerifier.ParseChecksumsFile(content);
|
||||
|
||||
Assert.Single(map);
|
||||
Assert.True(map.ContainsKey("file.zip"));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Installer.Steps;
|
||||
using ClaudeDo.Releases;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests;
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests;
|
||||
|
||||
internal sealed class FakeHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;
|
||||
private readonly object _lock = new();
|
||||
private readonly List<HttpRequestMessage> _requests = new();
|
||||
|
||||
public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public FakeHttpMessageHandler(HttpStatusCode status, string body)
|
||||
: this(_ => new HttpResponseMessage(status) { Content = new StringContent(body) })
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<HttpRequestMessage> Requests
|
||||
{
|
||||
get { lock (_lock) return _requests.ToArray(); }
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_lock) _requests.Add(request);
|
||||
return Task.FromResult(_handler(request));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using ClaudeDo.Installer.Core;
|
||||
using ClaudeDo.Releases;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests;
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
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<long>(_ => { }), CancellationToken.None);
|
||||
|
||||
Assert.True(File.Exists(tempPath));
|
||||
Assert.Equal(payload, File.ReadAllBytes(tempPath));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath)) File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user