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 ExecuteAsync(SshExecRequest request, CancellationToken cancellationToken) { Request = request; return Task.FromResult(_result); } } private sealed class BlockingExecutor : ISshCommandExecutor { public async Task 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 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"); } }