111 lines
3.6 KiB
C#
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");
|
|
}
|
|
}
|