using McpSsh.Server; using McpSsh.Server.Audit; using McpSsh.Server.Sftp; using McpSsh.Server.Ssh; using Renci.SshNet; namespace McpSsh.Tests; public sealed class SftpServiceTests { [Fact] public async Task PutAsync_ReturnsErrorWhenLocalFileIsOutsideWorkingDirectory() { var service = CreateService(); var result = await service.PutAsync("host", "user", "/tmp/file.txt", "/tmp/file.txt", null, null, null, null, null, CancellationToken.None); Assert.Equal("unsafe_local_path", result.Error); } [Fact] public async Task PutAsync_ReturnsErrorWhenLocalFileIsMissing() { var service = CreateService(); var result = await service.PutAsync("host", "user", "missing-file.txt", "/tmp/file.txt", null, null, null, null, null, CancellationToken.None); Assert.Equal("local_file_not_found", result.Error); } [Fact] public async Task GetAsync_ReturnsErrorWhenLocalFileExistsAndOverwriteIsFalse() { var path = Path.Combine("sftp-test-existing.txt"); await File.WriteAllTextAsync(path, "existing"); try { var service = CreateService(); var result = await service.GetAsync("host", "user", "/tmp/file.txt", path, null, null, null, overwrite: false, maxBytes: null, CancellationToken.None); Assert.Equal("local_file_exists", result.Error); } finally { File.Delete(path); } } private static SftpService CreateService() { return new SftpService(new ThrowingClientFactory(), new CapturingAuditLogger(), new FixedClock()); } private sealed class ThrowingClientFactory : ISshClientFactory { public SshClient CreateSshClient(SshConnectionRequest request) => throw new NotSupportedException(); public SftpClient CreateSftpClient(SshConnectionRequest request) => throw new NotSupportedException(); public ScpClient CreateScpClient(SshConnectionRequest request) => throw new NotSupportedException(); } private sealed class CapturingAuditLogger : IAuditLogger { public void Log(AuditEvent auditEvent) { } } private sealed class FixedClock : ISystemClock { public DateTimeOffset UtcNow => DateTimeOffset.Parse("2026-05-24T12:00:00Z"); } }