All checks were successful
check / check (push) Successful in 2m30s
keyfunc ssh install appends the public line on a host through the system ssh, only when absent, feeding the line on standard input; keyfunc ssh to serves the derived key from an in-process agent on a private socket and runs the system ssh with it, the private key never on disk. Two review rounds; the second passed with no findings. Model: opus-5 (implementation and review); fable-5-1 (landing)
256 lines
6.6 KiB
Go
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")
|
|
}
|