|
5 | 5 | package commands |
6 | 6 |
|
7 | 7 | import ( |
| 8 | + "context" |
| 9 | + "errors" |
8 | 10 | "testing" |
| 11 | + "time" |
9 | 12 |
|
10 | 13 | "github.com/Snuffy2/shellport/application/command" |
11 | 14 | "github.com/Snuffy2/shellport/application/configuration" |
12 | 15 | "github.com/Snuffy2/shellport/application/log" |
| 16 | + "golang.org/x/crypto/ssh" |
13 | 17 | ) |
14 | 18 |
|
15 | 19 | // TestSSHCommandKeepsBufferPoolScopedToSession verifies that SSH clients retain |
@@ -38,3 +42,77 @@ func TestSSHCommandKeepsBufferPoolScopedToSession(t *testing.T) { |
38 | 42 | ) |
39 | 43 | } |
40 | 44 | } |
| 45 | + |
| 46 | +// TestSSHCloseCancelsBeforeWaitingForRemote verifies Close can unblock remote |
| 47 | +// startup paths that only exit after the base context is cancelled. |
| 48 | +func TestSSHCloseCancelsBeforeWaitingForRemote(t *testing.T) { |
| 49 | + ctx, cancel := context.WithCancel(context.Background()) |
| 50 | + client := &sshClient{ |
| 51 | + baseCtx: ctx, |
| 52 | + baseCtxCancel: cancel, |
| 53 | + credentialReceive: make(chan []byte), |
| 54 | + fingerprintVerifyResultReceive: make(chan bool), |
| 55 | + remoteConnReceive: make(chan sshRemoteConn), |
| 56 | + credentialReceiveClosed: false, |
| 57 | + fingerprintVerifyResultReceiveClosed: false, |
| 58 | + } |
| 59 | + client.remoteCloseWait.Add(1) |
| 60 | + |
| 61 | + go func() { |
| 62 | + <-ctx.Done() |
| 63 | + close(client.remoteConnReceive) |
| 64 | + client.remoteCloseWait.Done() |
| 65 | + }() |
| 66 | + |
| 67 | + done := make(chan struct{}) |
| 68 | + go func() { |
| 69 | + _ = client.Close() |
| 70 | + close(done) |
| 71 | + }() |
| 72 | + |
| 73 | + select { |
| 74 | + case <-ctx.Done(): |
| 75 | + case <-time.After(100 * time.Millisecond): |
| 76 | + t.Fatal("Close did not cancel base context before waiting for remote") |
| 77 | + } |
| 78 | + |
| 79 | + select { |
| 80 | + case <-done: |
| 81 | + case <-time.After(100 * time.Millisecond): |
| 82 | + t.Fatal("Close did not return after remote shutdown") |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +type failingSSHWriter struct { |
| 87 | + err error |
| 88 | +} |
| 89 | + |
| 90 | +func (w failingSSHWriter) Write(_ []byte) (int, error) { |
| 91 | + return 0, w.err |
| 92 | +} |
| 93 | + |
| 94 | +// TestSSHLocalReturnsRemoteWriteErrors verifies stdin write failures surface to |
| 95 | +// the stream handler instead of leaving the UI in a misleading connected state. |
| 96 | +func TestSSHLocalReturnsRemoteWriteErrors(t *testing.T) { |
| 97 | + writeErr := errors.New("remote write failed") |
| 98 | + closed := false |
| 99 | + client := &sshClient{ |
| 100 | + l: log.NewDitch(), |
| 101 | + remoteConn: sshRemoteConn{ |
| 102 | + writer: failingSSHWriter{err: writeErr}, |
| 103 | + closer: func() error { closed = true; return nil }, |
| 104 | + session: &ssh.Session{}, |
| 105 | + }, |
| 106 | + } |
| 107 | + header := command.StreamHeader{} |
| 108 | + header.Set(SSHClientStdIn, 5) |
| 109 | + |
| 110 | + err := client.local(nil, newLimitedReader([]byte("hello")), header, make([]byte, 16)) |
| 111 | + |
| 112 | + if !errors.Is(err, writeErr) { |
| 113 | + t.Fatalf("expected remote write error, got %v", err) |
| 114 | + } |
| 115 | + if !closed { |
| 116 | + t.Fatal("expected remote closer to run after write failure") |
| 117 | + } |
| 118 | +} |
0 commit comments