Add SFTP tools with SCP fallback
This commit is contained in:
75
tests/McpSsh.Tests/SftpServiceTests.cs
Normal file
75
tests/McpSsh.Tests/SftpServiceTests.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ namespace McpSsh.Tests;
|
||||
public sealed class TerminalSessionManagerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task StartAsync_CreatesSessionAndExecsRequestedShell()
|
||||
public async Task StartAsync_CreatesSessionWithoutWritingShellSetup()
|
||||
{
|
||||
using var manager = CreateManager(out var factory, out _);
|
||||
|
||||
var result = await manager.StartAsync(" prod-api ", " deploy ", "bash", null, null, null, "/keys/id", "secret", null, CancellationToken.None);
|
||||
var result = await manager.StartAsync(" prod-api ", " deploy ", null, null, null, "/keys/id", "secret", null, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Error);
|
||||
Assert.StartsWith("term_", result.SessionId);
|
||||
@@ -21,14 +21,14 @@ public sealed class TerminalSessionManagerTests
|
||||
Assert.Equal("deploy", factory.Request.Username);
|
||||
Assert.Equal("/keys/id", factory.Request.KeyPath);
|
||||
Assert.Equal("secret", factory.Request.KeyPassphrase);
|
||||
Assert.Contains("exec bash\n", factory.Connection.Writes);
|
||||
Assert.Empty(factory.Connection.Writes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Write_SendsInputToActiveSession()
|
||||
{
|
||||
using var manager = CreateManager(out var factory, out _);
|
||||
var start = await manager.StartAsync("prod-api", "deploy", null, null, null, null, null, null, null, CancellationToken.None);
|
||||
var start = await manager.StartAsync("prod-api", "deploy", null, null, null, null, null, null, CancellationToken.None);
|
||||
|
||||
var result = manager.Write(start.SessionId!, "uptime\n");
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class TerminalSessionManagerTests
|
||||
public async Task Read_DrainsBufferedOutputAndReportsTruncation()
|
||||
{
|
||||
using var manager = CreateManager(out var factory, out _);
|
||||
var start = await manager.StartAsync("prod-api", "deploy", null, null, null, null, null, null, null, CancellationToken.None);
|
||||
var start = await manager.StartAsync("prod-api", "deploy", null, null, null, null, null, null, CancellationToken.None);
|
||||
factory.Connection.QueueOutput("abcdef");
|
||||
|
||||
var first = await ReadUntilOutputAsync(manager, start.SessionId!, 3);
|
||||
@@ -67,7 +67,7 @@ public sealed class TerminalSessionManagerTests
|
||||
public async Task Stop_DisposesAndRemovesSession()
|
||||
{
|
||||
using var manager = CreateManager(out var factory, out _);
|
||||
var start = await manager.StartAsync("prod-api", "deploy", null, null, null, null, null, null, null, CancellationToken.None);
|
||||
var start = await manager.StartAsync("prod-api", "deploy", null, null, null, null, null, null, CancellationToken.None);
|
||||
|
||||
var result = manager.Stop(start.SessionId!);
|
||||
var writeAfterStop = manager.Write(start.SessionId!, "pwd\n");
|
||||
|
||||
Reference in New Issue
Block a user