Files
mcp-ssh/tests/McpSsh.Tests/SshExecServiceTests.cs
2026-05-24 20:45:12 +00:00

111 lines
3.6 KiB
C#

using McpSsh.Server;
using McpSsh.Server.Audit;
using McpSsh.Server.Ssh;
namespace McpSsh.Tests;
public sealed class SshExecServiceTests
{
[Fact]
public async Task ExecuteAsync_PassesValidatedRequestToExecutor()
{
var executor = new CapturingExecutor(new SshExecResult(0, "ok", "", 12, false, null, null));
var auditLogger = new CapturingAuditLogger();
var service = CreateService(executor, auditLogger: auditLogger);
var result = await service.ExecuteAsync(" prod-api ", " deploy ", "uptime", null, null, " /keys/deploy ", "secret", null, CancellationToken.None);
Assert.Equal(0, result.ExitCode);
Assert.NotNull(executor.Request);
Assert.Equal("prod-api", executor.Request.Host);
Assert.Equal("deploy", executor.Request.Username);
Assert.Equal(22, executor.Request.Port);
Assert.Equal(30, executor.Request.TimeoutSeconds);
Assert.Equal("/keys/deploy", executor.Request.KeyPath);
Assert.Equal("secret", executor.Request.KeyPassphrase);
Assert.Single(auditLogger.Events);
Assert.True(auditLogger.Events[0].Success);
}
[Fact]
public async Task ExecuteAsync_AllowsDestructiveCommands()
{
var executor = new CapturingExecutor();
var service = CreateService(executor);
var result = await service.ExecuteAsync("prod-api", "deploy", "rm -rf /", null, null, null, null, null, CancellationToken.None);
Assert.Equal(0, result.ExitCode);
Assert.Equal("rm -rf /", executor.Request?.Command);
}
[Fact]
public async Task ExecuteAsync_ReturnsTimeoutWhenExecutorObservesCancellation()
{
var executor = new BlockingExecutor();
var service = CreateService(executor);
var result = await service.ExecuteAsync("prod-api", "deploy", "sleep 5", null, null, null, null, 1, CancellationToken.None);
Assert.True(result.TimedOut);
Assert.Equal("ssh_timeout", result.Error);
}
private static SshExecService CreateService(
ISshCommandExecutor executor,
IAuditLogger? auditLogger = null)
{
return new SshExecService(
executor,
auditLogger ?? new CapturingAuditLogger(),
new FixedClock());
}
private sealed class CapturingExecutor : ISshCommandExecutor
{
private readonly SshExecResult _result;
public CapturingExecutor()
: this(new SshExecResult(0, "", "", 1, false, null, null))
{
}
public CapturingExecutor(SshExecResult result)
{
_result = result;
}
public SshExecRequest? Request { get; private set; }
public Task<SshExecResult> ExecuteAsync(SshExecRequest request, CancellationToken cancellationToken)
{
Request = request;
return Task.FromResult(_result);
}
}
private sealed class BlockingExecutor : ISshCommandExecutor
{
public async Task<SshExecResult> ExecuteAsync(SshExecRequest request, CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
return new SshExecResult(0, "", "", 0, false, null, null);
}
}
private sealed class CapturingAuditLogger : IAuditLogger
{
public List<AuditEvent> Events { get; } = [];
public void Log(AuditEvent auditEvent)
{
Events.Add(auditEvent);
}
}
private sealed class FixedClock : ISystemClock
{
public DateTimeOffset UtcNow => DateTimeOffset.Parse("2026-05-24T12:00:00Z");
}
}