Merge subtask
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
using ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
internal sealed class FakeProcessRunner : IProcessRunner
|
||||
{
|
||||
private readonly Queue<(int ExitCode, string Output)> _responses;
|
||||
|
||||
public FakeProcessRunner(params (int ExitCode, string Output)[] responses)
|
||||
{
|
||||
_responses = new Queue<(int, string)>(responses);
|
||||
}
|
||||
|
||||
public List<(string FileName, string Arguments)> Calls { get; } = new();
|
||||
|
||||
public Task<(int ExitCode, string Output)> RunAsync(string fileName, string arguments, string? workingDirectory, CancellationToken ct)
|
||||
{
|
||||
Calls.Add((fileName, arguments));
|
||||
var response = _responses.Count > 0 ? _responses.Dequeue() : (0, "");
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class GitCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public GitCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdgitcheck_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
private string MakeGitExe()
|
||||
{
|
||||
var path = Path.Combine(_dir, "git.exe");
|
||||
File.WriteAllText(path, "");
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Git_present_with_parseable_version_reports_ok()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((0, "git version 2.43.0.windows.1\n"));
|
||||
var check = new GitCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
Assert.Contains("2.43.0", result.Message);
|
||||
Assert.Contains(_dir, result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Git_not_found_on_path_reports_failed()
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new GitCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Equal(CheckSeverity.Error, result.Severity);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Git_nonzero_exit_reports_failed()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((1, "some error"));
|
||||
var check = new GitCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class GitIdentityCheckTests : IDisposable
|
||||
{
|
||||
private readonly string _dir;
|
||||
|
||||
public GitIdentityCheckTests()
|
||||
{
|
||||
_dir = Path.Combine(Path.GetTempPath(), $"cdgitid_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_dir);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { Directory.Delete(_dir, recursive: true); } catch { }
|
||||
}
|
||||
|
||||
private void MakeGitExe() => File.WriteAllText(Path.Combine(_dir, "git.exe"), "");
|
||||
|
||||
[Fact]
|
||||
public async Task Both_set_reports_ok()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((0, "Mika Kuns\n"), (0, "mika@example.com\n"));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Only_name_set_reports_failed_naming_email()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((0, "Mika Kuns\n"), (1, ""));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Contains("user.email", result.Message);
|
||||
Assert.DoesNotContain("user.name", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Only_email_set_reports_failed_naming_name()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((1, ""), (0, "mika@example.com\n"));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Contains("user.name", result.Message);
|
||||
Assert.DoesNotContain("user.email", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Neither_set_reports_failed_naming_both()
|
||||
{
|
||||
MakeGitExe();
|
||||
var runner = new FakeProcessRunner((1, ""), (1, ""));
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Contains("user.name", result.Message);
|
||||
Assert.Contains("user.email", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Git_missing_reports_unknown_not_failed()
|
||||
{
|
||||
var runner = new FakeProcessRunner();
|
||||
var check = new GitIdentityCheck(runner, pathOverride: _dir, pathExtOverride: ".exe");
|
||||
|
||||
var result = await check.RunAsync(new InstallContext(), CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Unknown, result.Status);
|
||||
Assert.Empty(runner.Calls);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Checks.Interfaces;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class PortCheckTests
|
||||
{
|
||||
private sealed class FakeOwnerResolver : IPortOwnerResolver
|
||||
{
|
||||
private readonly string? _owner;
|
||||
public FakeOwnerResolver(string? owner) => _owner = owner;
|
||||
public Task<string?> FindOwningProcessNameAsync(int port, CancellationToken ct) => Task.FromResult(_owner);
|
||||
}
|
||||
|
||||
private static int GetFreePort()
|
||||
{
|
||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||
listener.Start();
|
||||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||
listener.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Both_ports_free_reports_ok()
|
||||
{
|
||||
var ctx = new InstallContext { SignalRPort = GetFreePort(), ExternalMcpPort = GetFreePort() };
|
||||
var check = new PortCheck(new FakeOwnerResolver(null));
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Port_in_use_by_other_process_reports_failed()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var occupying = new TcpListener(IPAddress.Loopback, port);
|
||||
occupying.Start();
|
||||
var ctx = new InstallContext { SignalRPort = port, ExternalMcpPort = GetFreePort() };
|
||||
var check = new PortCheck(new FakeOwnerResolver("SomeOtherApp"));
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Equal(CheckSeverity.Warning, result.Severity);
|
||||
Assert.Contains("SomeOtherApp", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Port_in_use_by_own_worker_reports_ok()
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var occupying = new TcpListener(IPAddress.Loopback, port);
|
||||
occupying.Start();
|
||||
var ctx = new InstallContext { SignalRPort = port, ExternalMcpPort = GetFreePort() };
|
||||
var check = new PortCheck(new FakeOwnerResolver("ClaudeDo.Worker"));
|
||||
|
||||
var result = await check.RunAsync(ctx, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
Assert.Contains("ClaudeDo Worker", result.Message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using ClaudeDo.Installer.Checks;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Tests.Checks;
|
||||
|
||||
public sealed class WriteAccessCheckTests : IDisposable
|
||||
{
|
||||
private readonly List<string> _cleanupDirs = new();
|
||||
|
||||
private string TempDir()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"cdwriteaccess_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
_cleanupDirs.Add(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var dir in _cleanupDirs)
|
||||
{
|
||||
try { Directory.Delete(dir, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Writable_directory_reports_ok()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var check = new WriteAccessCheck();
|
||||
|
||||
var result = await check.RunAsync(new InstallContext { InstallDirectory = dir }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NonExistent_path_under_writable_parent_reports_ok()
|
||||
{
|
||||
var parent = TempDir();
|
||||
var target = Path.Combine(parent, "does", "not", "exist");
|
||||
var check = new WriteAccessCheck();
|
||||
|
||||
var result = await check.RunAsync(new InstallContext { InstallDirectory = target }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Ok, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unwritable_directory_reports_failed_with_no_leftover_file()
|
||||
{
|
||||
var dir = TempDir();
|
||||
var dirInfo = new DirectoryInfo(dir);
|
||||
var identity = WindowsIdentity.GetCurrent().User!;
|
||||
var denyRule = new FileSystemAccessRule(identity, FileSystemRights.CreateFiles | FileSystemRights.WriteData, AccessControlType.Deny);
|
||||
|
||||
var acl = dirInfo.GetAccessControl();
|
||||
acl.AddAccessRule(denyRule);
|
||||
dirInfo.SetAccessControl(acl);
|
||||
|
||||
try
|
||||
{
|
||||
var check = new WriteAccessCheck();
|
||||
|
||||
var result = await check.RunAsync(new InstallContext { InstallDirectory = dir }, CancellationToken.None);
|
||||
|
||||
Assert.Equal(CheckStatus.Failed, result.Status);
|
||||
Assert.Equal(CheckSeverity.Error, result.Severity);
|
||||
Assert.Contains(dir, result.Message);
|
||||
Assert.Empty(Directory.GetFiles(dir));
|
||||
}
|
||||
finally
|
||||
{
|
||||
acl.RemoveAccessRule(denyRule);
|
||||
dirInfo.SetAccessControl(acl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.5.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user