internal/cli/sshcmd_test.go
Ref: Size: 11.0 KiB History
package cli
import (
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
)
func testEnv() Env {
return Env{
URL: "http://s:8080", Gate: "gate.example:2222", Tenant: "default",
Key: "/home/u/.ssh/id_ed25519", KnownHosts: "/home/u/.ssh/eitri_known_hosts",
}
}
// The argv shape is load-bearing (two verified hops); pin it exactly.
func TestSSHArgvTwoVerifiedHops(t *testing.T) {
got := SSHArgv(testEnv(), "dev", nil)
wantProxy := "ssh -W %h:%p -o StrictHostKeyChecking=yes" +
" -o UserKnownHostsFile='/home/u/.ssh/eitri_known_hosts'" +
" -i '/home/u/.ssh/id_ed25519' -p 2222 ubuntu@gate.example"
want := []string{
"ssh",
"-o", "ProxyCommand=" + wantProxy,
"-o", "StrictHostKeyChecking=yes",
"-o", "UserKnownHostsFile=/home/u/.ssh/eitri_known_hosts",
"-i", "/home/u/.ssh/id_ed25519",
"ubuntu@default.dev",
}
if !slices.Equal(got, want) {
t.Errorf("argv:\n got %q\nwant %q", got, want)
}
}
// The wire name is ALWAYS the namespaced <tenant>.<vm> — a VM host cert's one
// principal — so SSHArgv never emits a bare target.
func TestSSHArgvAlwaysNamespaced(t *testing.T) {
got := SSHArgv(testEnv(), "dev", nil)
if want := "ubuntu@default.dev"; got[len(got)-1] != want {
t.Errorf("target = %q, want %q", got[len(got)-1], want)
}
}
// resolvePlane derives the tenant from the credential (via /me) when nothing
// pins it, so the namespaced connect name can be built without the user knowing
// their tenant.
func TestResolvePlaneTenantFromMe(t *testing.T) {
var gotAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
if r.URL.Path != "/api/v1/me" {
t.Errorf("path = %q, want /api/v1/me", r.URL.Path)
}
w.Write([]byte(`{"tenant":"acme","email":"me@acme.example"}`))
}))
defer srv.Close()
t.Setenv("EITRI_TOKEN", "tok123")
e := testEnv()
e.URL = srv.URL
e.Tenant = ""
got, err := resolvePlane(e)
if err != nil {
t.Fatal(err)
}
if got.Tenant != "acme" {
t.Errorf("tenant = %q, want acme", got.Tenant)
}
if gotAuth != "Bearer tok123" {
t.Errorf("auth = %q, want Bearer tok123", gotAuth)
}
}
// TestResolvePlaneGateOrder pins the gate's whole precedence chain in one place.
// A pinned gate (EITRI_GATE, or the config file — both reach here as a non-empty
// Env.Gate) outranks even a plane that names a different one; unpinned, the
// plane's own answer is taken; and a self-hosted plane that names none is an
// error rather than a hop through the hosted gate. The hosted rung below is
// pinned by TestGateFor, which needs no server to reach the hosted URL.
func TestResolvePlaneGateOrder(t *testing.T) {
t.Setenv("EITRI_TOKEN", "tok")
for _, tc := range []struct {
name string
pinned string // Env.Gate as the env or the config file left it
meBody string
want string
}{
{
name: "a pinned gate outranks the plane's own answer",
pinned: "gate.mine:2222",
meBody: `{"tenant":"acme","ssh_gate":"gate.acme.example:2222"}`,
want: "gate.mine:2222",
},
{
name: "unpinned, the plane names its own gate",
meBody: `{"tenant":"acme","ssh_gate":"gate.acme.example:2222"}`,
want: "gate.acme.example:2222",
},
} {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(tc.meBody))
}))
defer srv.Close()
e := testEnv()
e.URL, e.Gate, e.Tenant = srv.URL, tc.pinned, ""
got, err := resolvePlane(e)
if err != nil {
t.Fatal(err)
}
if got.Gate != tc.want {
t.Errorf("gate = %q, want %q", got.Gate, tc.want)
}
if got.Tenant != "acme" {
t.Errorf("tenant = %q, want acme", got.Tenant)
}
})
}
}
// TestGateFor pins the one rung that is scoped to a single plane. The hosted
// gate address is knowledge about eitri.sh, not about planes in general: applied
// to a self-hosted one it is not a weak guess but a wrong answer that looks
// right, and it fails later as a host-key refusal rather than as the missing
// setting it is. So it is offered for the hosted URL and refused elsewhere.
func TestGateFor(t *testing.T) {
for _, tc := range []struct {
name string
meGate, url string
want string
wantErr bool
}{
{
name: "the plane's own answer needs no default at all",
meGate: "gate.acme.example:2222", url: "https://eitri.acme.example",
want: "gate.acme.example:2222",
},
{
name: "the hosted plane naming none gets the hosted gate",
url: defaultURL, want: defaultGate,
},
{
name: "a trailing slash is the same hosted plane",
url: defaultURL + "/", want: defaultGate,
},
{
name: "a self-hosted plane naming none is an error, not a guess",
url: "https://eitri.acme.example", wantErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := gateFor(tc.meGate, tc.url)
if tc.wantErr {
if err == nil {
t.Fatalf("want an error, got gate %q", got)
}
for _, want := range []string{tc.url, "EITRI_GATE", "ssh_gate_domain"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("error must name %q: %v", want, err)
}
}
if strings.Contains(err.Error(), defaultGate) {
t.Errorf("the error must not still suggest the hosted gate: %v", err)
}
return
}
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("gate = %q, want %q", got, tc.want)
}
})
}
}
// Both values pinned is the offline path: nothing to ask, so nothing is asked.
func TestResolvePlaneFullyPinnedShortCircuits(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Error("a pinned tenant and gate must short-circuit — no /me call")
}))
defer srv.Close()
e := testEnv()
e.URL, e.Tenant = srv.URL, "team"
got, err := resolvePlane(e)
if err != nil {
t.Fatal(err)
}
if got.Tenant != "team" || got.Gate != "gate.example:2222" {
t.Errorf("resolved = %+v", got)
}
}
// A token is a credential, not an instruction to spend a round trip. Holding
// one must never cost a call that not holding one would have skipped: the
// tokenless path below resolves a pinned tenant on the hosted plane offline, so
// this one has to as well, or `eitri ssh` breaks on a plane the user cannot
// currently reach — and breaks it for exactly the laptops that are best set up.
func TestResolvePlaneATokenCostsNoNetworkCall(t *testing.T) {
t.Setenv("EITRI_TOKEN", "tok")
// There is no test server to point the hosted case at: the hosted rung is
// scoped to the hosted URL by name (see gateFor), so a /me here would leave
// the machine for console.eitri.sh. Which is what makes the assertion
// sound — that call answers 401 to this token, and finds no network at all
// in CI, so a regression fails here either way rather than passing quietly.
e := testEnv()
e.URL, e.Gate, e.Tenant = defaultURL, "", "team"
got, err := resolvePlane(e)
if err != nil {
t.Fatalf("a pinned tenant on the hosted plane must resolve without asking: %v", err)
}
if got.Tenant != "team" || got.Gate != defaultGate {
t.Errorf("resolved = %+v, want tenant team on %s", got, defaultGate)
}
// With the gate pinned too there IS something to prove it against: a live
// server that fails this test if a single request reaches it.
untouched := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Error("a tenant and gate that resolve here must reach the plane for nothing")
}))
defer untouched.Close()
e.URL, e.Gate = untouched.URL, "gate.example:2222"
if got, err = resolvePlane(e); err != nil || got.Gate != "gate.example:2222" {
t.Errorf("resolved = %+v, err = %v", got, err)
}
}
// A pinned tenant with no credential still needs a gate, and there is nobody to
// ask. Against the hosted plane the assumption is made without a call; against
// any other one there is nothing to assume, and the error says what to set.
func TestResolvePlaneNoTokenPinnedTenant(t *testing.T) {
t.Setenv("EITRI_TOKEN", "")
e := testEnv()
e.URL, e.Gate, e.Tenant = defaultURL, "", "team"
got, err := resolvePlane(e)
if err != nil {
t.Fatal(err)
}
if got.Gate != defaultGate {
t.Errorf("gate = %q, want %q", got.Gate, defaultGate)
}
e.URL = "https://eitri.acme.example"
if _, err := resolvePlane(e); err == nil || !strings.Contains(err.Error(), "names no SSH gate") {
t.Errorf("a self-hosted plane must not be given the hosted gate: %v", err)
}
}
// A credential that resolves to no tenant, or a failing /me, is a clear error.
func TestResolvePlaneMeFailures(t *testing.T) {
t.Setenv("EITRI_TOKEN", "tok")
// Empty tenant in the /me response.
empty := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"tenant":"","email":"x@y"}`))
}))
defer empty.Close()
e := testEnv()
e.URL, e.Tenant = empty.URL, ""
if _, err := resolvePlane(e); err == nil || !strings.Contains(err.Error(), "no tenant") {
t.Errorf("empty tenant: got %v", err)
}
// A non-2xx /me surfaces as an error naming the resolution step.
boom := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nope", http.StatusUnauthorized)
}))
defer boom.Close()
e.URL = boom.URL
if _, err := resolvePlane(e); err == nil || !strings.Contains(err.Error(), "resolving your tenant") {
t.Errorf("me error: got %v", err)
}
}
// With no tenant and no credential the connect name cannot be built; the error
// names every remedy before any network call.
func TestResolvePlaneNoTokenNoTenant(t *testing.T) {
t.Setenv("EITRI_TOKEN", "")
e := testEnv()
e.Tenant = ""
_, err := resolvePlane(e)
if err == nil || !strings.Contains(err.Error(), "eitri init") ||
!strings.Contains(err.Error(), "EITRI_TOKEN") || !strings.Contains(err.Error(), "EITRI_TENANT") {
t.Errorf("want error naming init, EITRI_TOKEN and EITRI_TENANT, got %v", err)
}
}
func TestSSHArgvGateDefaultPort(t *testing.T) {
e := testEnv()
e.Gate = "gate.example"
got := SSHArgv(e, "dev", nil)
wantProxy := "ssh -W %h:%p -o StrictHostKeyChecking=yes" +
" -o UserKnownHostsFile='/home/u/.ssh/eitri_known_hosts'" +
" -i '/home/u/.ssh/id_ed25519' -p 22 ubuntu@gate.example"
if got[2] != "ProxyCommand="+wantProxy {
t.Errorf("proxy = %q, want %q", got[2], "ProxyCommand="+wantProxy)
}
}
func TestSSHArgvIPv6Gate(t *testing.T) {
e := testEnv()
e.Gate = "[::1]:2222"
got := SSHArgv(e, "dev", nil)
if want := " -p 2222 ubuntu@::1"; !slices.ContainsFunc(got, func(s string) bool {
return len(s) > len(want) && s[len(s)-len(want):] == want
}) {
t.Errorf("ipv6 gate not split host/port correctly: %q", got[2])
}
}
func TestSSHArgvSpacedPathsAreQuoted(t *testing.T) {
e := testEnv()
e.Key = "/Users/My Name/.ssh/id_ed25519"
got := SSHArgv(e, "dev", nil)
if want := "-i '/Users/My Name/.ssh/id_ed25519'"; !slices.ContainsFunc(got, func(s string) bool {
return strings.Contains(s, want)
}) {
t.Errorf("spaced key path not quoted in proxy: %q", got[2])
}
}
func TestSSHArgvExtraArgsPassThrough(t *testing.T) {
got := SSHArgv(testEnv(), "dev", []string{"uptime", "-p"})
n := len(got)
if got[n-2] != "uptime" || got[n-1] != "-p" {
t.Errorf("extra args not trailing: %v", got[n-3:])
}
}