Files
keyfunc/internal/cli/ssh_test.go
sneak 4b8373ed1f
All checks were successful
check / check (push) Successful in 19s
The ssh install and ssh to commands (closes #2)
install runs the system ssh and hands the host a short shell script.
The public key line reaches it on standard input, never on a command
line others on the host could read. The script makes ~/.ssh and
authorized_keys if missing, adds the line unless it is already there,
and says which of the two it did.

to serves the key from an agent inside the tool, on a unix socket in a
temporary directory only its owner can enter, and points ssh at it with
-o IdentityAgent. Socket and directory go when the command ends and the
private key is never written to disk. Only this command ends with the
status ssh ended with rather than status 1.

Model: opus-5
2026-09-07 16:22:32 +00:00

256 lines
6.6 KiB
Go

package cli_test
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"git.eeqj.de/sneak/keyfunc/internal/cli"
"git.eeqj.de/sneak/keyfunc/internal/cli/ssh"
"git.eeqj.de/sneak/keyfunc/internal/mnemonic"
"github.com/stretchr/testify/require"
)
// The modes the host is supposed to end up with, and the mode the
// stand-in ssh needs so that it can be run at all.
const (
directoryMode = 0o700
fileMode = 0o600
standInMode = 0o755
)
// failingStatus is the status the stand-in ssh ends with when a test
// wants to see a status handed on.
const failingStatus = 7
// The host, and where on it the key ends up.
const (
host = "someone@example.com"
keptUnder = ".ssh"
keptIn = "authorized_keys"
)
// installer is a stand-in for the system ssh for the install command.
// It writes down what it was given and then runs the command meant for
// the host right here, with the home directory pointed at a directory
// standing in for the host's, so that what keyfunc sends can be
// watched doing its work.
const installer = `
while [ $# -gt 1 ]; do
printf '%s\n' "$1" >> "$KEYFUNC_TEST_ARGUMENTS"
shift
done
printf '%s' "$1" > "$KEYFUNC_TEST_COMMAND"
HOME="$KEYFUNC_TEST_HOME"
export HOME
eval "$1"
`
// caller is a stand-in for the system ssh for the to command. It
// writes down the arguments it was given, notes the agent socket if
// there really is one at the path it was handed, and ends with the
// status the test asked for.
const caller = `
for argument in "$@"; do
printf '%s\n' "$argument" >> "$KEYFUNC_TEST_ARGUMENTS"
done
socket=${2#IdentityAgent=}
if [ -S "$socket" ]; then
printf '%s\n' "$socket" > "$KEYFUNC_TEST_SOCKET"
fi
exit "$KEYFUNC_TEST_STATUS"
`
// pretended is where a stand-in ssh writes down what it was asked to
// do.
type pretended struct {
// home stands in for the home directory on the host.
home string
// arguments holds what ssh was given before the command, one per
// line.
arguments string
// command holds what ssh was told to run on the host.
command string
}
func TestTheKeyIsAddedToTheHostAndThenLeftAlone(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
pretend := pretendHost(t)
require.Equal(t, "added\n", run(t, "ssh", "install", host))
directory, err := os.Stat(filepath.Join(pretend.home, keptUnder))
require.NoError(t, err)
require.Equal(t,
os.FileMode(directoryMode), directory.Mode().Perm(),
)
path := filepath.Join(pretend.home, keptUnder, keptIn)
file, err := os.Stat(path)
require.NoError(t, err)
require.Equal(t, os.FileMode(fileMode), file.Mode().Perm())
added := read(t, path)
require.Equal(t, vectorZero+" keyfunc/ssh/0\n", added)
require.Equal(t, "already present\n", run(t, "ssh", "install", host))
require.Equal(t, added, read(t, path))
}
func TestTheKeyDoesNotRunIntoALineWithNoNewlineAtItsEnd(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
pretend := pretendHost(t)
already := "ssh-ed25519 AAAAsomebodyelse somebody@else"
require.NoError(t,
os.Mkdir(filepath.Join(pretend.home, keptUnder), directoryMode),
)
path := filepath.Join(pretend.home, keptUnder, keptIn)
require.NoError(t, os.WriteFile(path, []byte(already), fileMode))
require.Equal(t, "added\n", run(t, "ssh", "install", host))
require.Equal(t,
already+"\n"+vectorZero+" keyfunc/ssh/0\n",
read(t, path),
)
}
func TestTheKeyLineIsNotOnTheCommandLine(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
pretend := pretendHost(t)
run(t, "ssh", "install", host)
require.NotContains(t, read(t, pretend.arguments), "ssh-ed25519")
require.NotContains(t, read(t, pretend.command), "ssh-ed25519")
}
func TestWhatComesAfterTheDashesIsGivenToSSH(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
pretend := pretendHost(t)
run(t, "ssh", "install", host, "--", "-p", "2222")
require.Equal(t,
[]string{"-p", "2222", host},
recorded(t, pretend.arguments),
)
}
func TestSSHIsPointedAtTheAgentAndItsStatusIsHandedOn(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
arguments, noted := pretendCall(t)
_, err := execute(t, "ssh", "to", host, "uptime")
var passed ssh.StatusError
require.ErrorAs(t, err, &passed)
require.Equal(t, failingStatus, passed.Status)
given := recorded(t, arguments)
require.Equal(t, "-o", given[0])
require.Equal(t, []string{host, "uptime"}, given[2:])
// The stand-in wrote the path down only because there really was
// a socket there while it ran.
socket := strings.TrimSpace(read(t, noted))
require.Equal(t, "IdentityAgent="+socket, given[1])
require.NoDirExists(t, filepath.Dir(socket))
}
func TestTheToolEndsWithTheStatusSSHEndedWith(t *testing.T) {
t.Setenv(mnemonic.Variable, example())
pretendCall(t)
given := os.Args
t.Cleanup(func() { os.Args = given })
os.Args = []string{"keyfunc", "ssh", "to", host, "uptime"}
require.Equal(t, failingStatus, cli.Main())
}
// pretendHost puts the install stand-in on the path and gives back the
// places it writes to.
func pretendHost(t *testing.T) pretended {
t.Helper()
pretend := pretended{
home: t.TempDir(),
arguments: filepath.Join(t.TempDir(), "arguments"),
command: filepath.Join(t.TempDir(), "command"),
}
t.Setenv("KEYFUNC_TEST_HOME", pretend.home)
t.Setenv("KEYFUNC_TEST_ARGUMENTS", pretend.arguments)
t.Setenv("KEYFUNC_TEST_COMMAND", pretend.command)
standIn(t, installer)
return pretend
}
// pretendCall puts the to stand-in on the path and gives back the file
// the arguments are written down in and the file the agent socket is
// noted in.
func pretendCall(t *testing.T) (string, string) {
t.Helper()
arguments := filepath.Join(t.TempDir(), "arguments")
noted := filepath.Join(t.TempDir(), "socket")
t.Setenv("KEYFUNC_TEST_ARGUMENTS", arguments)
t.Setenv("KEYFUNC_TEST_SOCKET", noted)
t.Setenv("KEYFUNC_TEST_STATUS", strconv.Itoa(failingStatus))
standIn(t, caller)
return arguments, noted
}
// standIn writes a stand-in for the system ssh and puts it first on
// the path, so that the tool finds it instead of the real one.
func standIn(t *testing.T, body string) {
t.Helper()
directory := t.TempDir()
err := os.WriteFile(
filepath.Join(directory, "ssh"),
[]byte("#!/bin/sh\n"+body), standInMode,
)
require.NoError(t, err)
t.Setenv("PATH",
directory+string(os.PathListSeparator)+os.Getenv("PATH"),
)
}
// read returns what is in a file.
func read(t *testing.T, path string) string {
t.Helper()
//nolint:gosec // the path is a temporary file of the test's own
content, err := os.ReadFile(path)
require.NoError(t, err)
return string(content)
}
// recorded returns the arguments a stand-in wrote down, one per line.
func recorded(t *testing.T, path string) []string {
t.Helper()
return strings.Split(strings.TrimSuffix(read(t, path), "\n"), "\n")
}