The published container image has no shell, so a session used to end at once with nothing printed for a command. It now explains that, and what to do about it, and exits with status 127. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
114 lines
2.7 KiB
Go
114 lines
2.7 KiB
Go
//go:build !windows
|
|
|
|
package sshd
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func session(t *testing.T) (*ssh.Session, func()) {
|
|
t.Helper()
|
|
good := newSigner(t)
|
|
addr, _, _ := start(t, good)
|
|
c, err := dial(addr, ssh.PublicKeys(good))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s, err := c.NewSession()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s, func() { s.Close(); c.Close() }
|
|
}
|
|
|
|
func TestExec(t *testing.T) {
|
|
s, done := session(t)
|
|
defer done()
|
|
out, err := s.Output("echo hello; echo oops >&2")
|
|
if err != nil || string(out) != "hello\n" {
|
|
t.Fatalf("got %q, %v", out, err)
|
|
}
|
|
}
|
|
|
|
func TestExecExitStatus(t *testing.T) {
|
|
s, done := session(t)
|
|
defer done()
|
|
err := s.Run("exit 3")
|
|
var ee *ssh.ExitError
|
|
if !errors.As(err, &ee) || ee.ExitStatus() != 3 {
|
|
t.Fatalf("want exit status 3, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecReadsStdin(t *testing.T) {
|
|
s, done := session(t)
|
|
defer done()
|
|
s.Stdin = strings.NewReader("from the client\n")
|
|
out, err := s.Output("cat")
|
|
if err != nil || string(out) != "from the client\n" {
|
|
t.Fatalf("got %q, %v", out, err)
|
|
}
|
|
}
|
|
|
|
func TestPtyExec(t *testing.T) {
|
|
s, done := session(t)
|
|
defer done()
|
|
if err := s.RequestPty("xterm", 24, 80, ssh.TerminalModes{ssh.ECHO: 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := s.CombinedOutput(`echo term=$TERM; test -t 0 && echo tty`)
|
|
if err != nil || !strings.Contains(string(out), "term=xterm") || !strings.Contains(string(out), "tty") {
|
|
t.Fatalf("got %q, %v", out, err)
|
|
}
|
|
}
|
|
|
|
func TestShell(t *testing.T) {
|
|
s, done := session(t)
|
|
defer done()
|
|
s.Stdin = strings.NewReader("echo from-shell\nexit 4\n")
|
|
var out strings.Builder
|
|
s.Stdout = &out
|
|
err := s.Shell()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = s.Wait()
|
|
var ee *ssh.ExitError
|
|
if !errors.As(err, &ee) || ee.ExitStatus() != 4 || !strings.Contains(out.String(), "from-shell") {
|
|
t.Fatalf("got %q, %v", out.String(), err)
|
|
}
|
|
}
|
|
|
|
// A machine with no shell (the published container image is one) must say so, not just hang up.
|
|
func TestNoShellIsExplained(t *testing.T) {
|
|
t.Setenv("SHELL", "/nonexistent/shell")
|
|
for name, usePty := range map[string]bool{"exec": false, "pty shell": true} {
|
|
s, done := session(t)
|
|
var stderr, stdout strings.Builder
|
|
s.Stderr, s.Stdout = &stderr, &stdout
|
|
var err error
|
|
if usePty {
|
|
if err = s.RequestPty("xterm", 24, 80, ssh.TerminalModes{}); err == nil {
|
|
err = s.Shell()
|
|
if err == nil {
|
|
err = s.Wait()
|
|
}
|
|
}
|
|
} else {
|
|
err = s.Run("true")
|
|
}
|
|
done()
|
|
var ee *ssh.ExitError
|
|
if !errors.As(err, &ee) || ee.ExitStatus() != 127 {
|
|
t.Errorf("%s: want exit status 127, got %v", name, err)
|
|
}
|
|
if !strings.Contains(stderr.String()+stdout.String(), "no shell to run") {
|
|
t.Errorf("%s: no explanation (stderr %q, stdout %q)", name, stderr.String(), stdout.String())
|
|
}
|
|
}
|
|
}
|