internal/smoke/exposure_test.go
Ref: Size: 7.1 KiB History
package smoke
import (
"context"
"errors"
"net"
"strings"
"testing"
"time"
"github.com/a73x/eitri/internal/server/api/client"
)
// TestProveExposureReadsTheBannerBack also pins where the dial target comes
// from: the grant names its own address and port, so the leg dials what the
// fleet published rather than an address assembled from anything else (the
// fake's host listing is deliberately absent — asking it would panic).
func TestProveExposureReadsTheBannerBack(t *testing.T) {
clock := &fakeClock{}
deleted := ""
api := &testAPI{
createExposureFunc: func(ctx context.Context, vmID string, guest, host int64, proto string) (client.Exposure, error) {
if vmID != "vm-1" {
t.Errorf("CreateExposure vmID = %q, want vm-1", vmID)
}
if guest != 22 {
t.Errorf("CreateExposure guestPort = %d, want 22", guest)
}
if host != 0 {
t.Errorf("CreateExposure hostPort = %d, want 0 — the fleet allocates the host port", host)
}
return client.Exposure{ID: "x-1", HostPort: 30080, HostAddr: "192.168.0.190"}, nil
},
deleteExposureFunc: func(ctx context.Context, id string) error { deleted = id; return nil },
}
dialed := ""
dial := func(ctx context.Context, addr string) (string, error) {
dialed = addr
return "SSH-2.0-OpenSSH_9.6\r\n", nil
}
if err := proveExposure(context.Background(), api, "vm-1", clock.now, clock.sleep, dial); err != nil {
t.Fatalf("proveExposure: %v", err)
}
if dialed != "192.168.0.190:30080" {
t.Errorf("dialed = %q, want 192.168.0.190:30080", dialed)
}
if deleted != "x-1" {
t.Errorf("revoked exposure = %q, want x-1 — the smoke leaves no exposure behind", deleted)
}
}
// TestProveExposureDialsAnIPv6Uplink pins the dial target's composition: a host
// that answers on an IPv6 address only resolves when the address is bracketed,
// which is what net.JoinHostPort does and plain concatenation does not.
func TestProveExposureDialsAnIPv6Uplink(t *testing.T) {
clock := &fakeClock{}
api := &testAPI{
createExposureFunc: func(context.Context, string, int64, int64, string) (client.Exposure, error) {
return client.Exposure{ID: "x-1", HostPort: 30080, HostAddr: "2001:db8::1"}, nil
},
deleteExposureFunc: func(context.Context, string) error { return nil },
}
dialed := ""
dial := func(ctx context.Context, addr string) (string, error) {
dialed = addr
return "SSH-2.0-OpenSSH_9.6\r\n", nil
}
if err := proveExposure(context.Background(), api, "vm-1", clock.now, clock.sleep, dial); err != nil {
t.Fatalf("proveExposure: %v", err)
}
if dialed != "[2001:db8::1]:30080" {
t.Errorf("dialed = %q, want [2001:db8::1]:30080", dialed)
}
}
func TestProveExposureRetriesUntilTheListenerConverges(t *testing.T) {
clock := &fakeClock{}
api := &testAPI{
createExposureFunc: func(context.Context, string, int64, int64, string) (client.Exposure, error) {
return client.Exposure{ID: "x-1", HostPort: 30080, HostAddr: "192.168.0.190"}, nil
},
deleteExposureFunc: func(context.Context, string) error { return nil },
}
calls := 0
dial := func(ctx context.Context, addr string) (string, error) {
calls++
if calls < 3 {
return "", errors.New("connection refused")
}
return "SSH-2.0-OpenSSH_9.6\r\n", nil
}
if err := proveExposure(context.Background(), api, "vm-1", clock.now, clock.sleep, dial); err != nil {
t.Fatalf("proveExposure: %v", err)
}
if calls != 3 {
t.Errorf("dial calls = %d, want 3", calls)
}
}
func TestProveExposureFailsOnTheWrongBanner(t *testing.T) {
clock := &fakeClock{}
api := &testAPI{
createExposureFunc: func(context.Context, string, int64, int64, string) (client.Exposure, error) {
return client.Exposure{ID: "x-1", HostPort: 30080, HostAddr: "192.168.0.190"}, nil
},
deleteExposureFunc: func(context.Context, string) error { return nil },
}
dial := func(ctx context.Context, addr string) (string, error) { return "HTTP/1.1 400\r\n", nil }
err := proveExposure(context.Background(), api, "vm-1", clock.now, clock.sleep, dial)
if err == nil {
t.Fatal("proveExposure: want error for a non-sshd answer, got nil")
}
if !strings.Contains(err.Error(), "SSH-2.0") {
t.Errorf("error = %q, want it to name the banner it wanted", err.Error())
}
}
// TestProveExposureFailsWhenTheGrantNamesNoAddress: an exposure whose host has
// not said where it answers is published nowhere, and the leg says so instead
// of dialing a port on an empty host.
func TestProveExposureFailsWhenTheGrantNamesNoAddress(t *testing.T) {
clock := &fakeClock{}
revoked := ""
api := &testAPI{
createExposureFunc: func(context.Context, string, int64, int64, string) (client.Exposure, error) {
return client.Exposure{ID: "x-1", HostPort: 30080}, nil
},
deleteExposureFunc: func(_ context.Context, id string) error { revoked = id; return nil },
}
err := proveExposure(context.Background(), api, "vm-1", clock.now, clock.sleep, nil)
if err == nil {
t.Fatal("proveExposure: want error when the exposure named no address, got nil")
}
if !strings.Contains(err.Error(), "host address") {
t.Errorf("error = %q, want it to mention the missing host address", err.Error())
}
if revoked != "x-1" {
t.Errorf("revoked exposure = %q, want the grant revoked even when it named nowhere to dial", revoked)
}
}
// bannerListener serves one connection, writing each of writes in turn with a
// pause between them, and returns the address to dial it on.
func bannerListener(t *testing.T, writes ...string) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
t.Cleanup(func() { ln.Close() })
go func() {
c, err := ln.Accept()
if err != nil {
return
}
defer c.Close()
for _, w := range writes {
if _, err := c.Write([]byte(w)); err != nil {
return
}
time.Sleep(20 * time.Millisecond)
}
time.Sleep(50 * time.Millisecond)
}()
return ln.Addr().String()
}
func TestReadBannerReadsWhatAListenerWrites(t *testing.T) {
got, err := readBanner(context.Background(), bannerListener(t, "SSH-2.0-Test\r\n"))
if err != nil {
t.Fatalf("readBanner: %v", err)
}
if !strings.HasPrefix(got, "SSH-2.0") {
t.Errorf("banner = %q, want an SSH-2.0 prefix", got)
}
}
// TestReadBannerReassemblesAFragmentedBanner pins the read-to-newline: a
// spliced connection can deliver the banner in segments, and a first segment
// shorter than the prefix must not fail a listener that is answering
// correctly.
func TestReadBannerReassemblesAFragmentedBanner(t *testing.T) {
got, err := readBanner(context.Background(), bannerListener(t, "SS", "H-2.0-Test\r\n"))
if err != nil {
t.Fatalf("readBanner: %v", err)
}
if !strings.HasPrefix(got, "SSH-2.0") {
t.Errorf("banner = %q, want the fragments reassembled into an SSH-2.0 prefix", got)
}
}
// TestReadBannerKeepsTheErrorWhenNothingArrives pins the retryable case: a
// listener that accepts and hangs up without a byte returns its error, which
// the leg treats as "not converged yet" rather than as a wrong answer.
func TestReadBannerKeepsTheErrorWhenNothingArrives(t *testing.T) {
got, err := readBanner(context.Background(), bannerListener(t))
if err == nil {
t.Fatalf("readBanner = %q, want the read error from a silent listener", got)
}
if got != "" {
t.Errorf("banner = %q, want empty", got)
}
}