Build initial MCP SSH server
This commit is contained in:
110
tests/McpSsh.Tests/SshExecServiceTests.cs
Normal file
110
tests/McpSsh.Tests/SshExecServiceTests.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user