Files
ssh-netbox-wrapper/internal/resolver/chain_test.go
T
Sebastian UnterschützandClaude Fable 5.2 4287b7b650 fix: harden config loading and ssh addresses, stop resolver on API errors
- read netssh.yaml from the user config directory only, not from the
  working directory
- refuse addresses from NetBox or the cache that are neither an IP
  address nor a hostname (ssh read "-oProxyCommand=…" as an option)
- resolver chain stops at an API failure; only "no address" falls
  through to the next strategy
- netssh --help and -h show netssh's help
- start at most one background cache refresh per 5 minutes
- validate the legacy netbox.url
- page through all IP addresses of a host
- CI: run format check, vet, and tests on pull requests and main
- changelog for v0.0.25

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-15 23:25:28 +01:00

170 lines
4.6 KiB
Go

package resolver
import (
"context"
"errors"
"testing"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/config"
"git.zb-server.de/Sebi/ssh-netbox-wrapper/internal/netbox"
)
// stubStrategy is a test double for Strategy.
type stubStrategy struct {
name string
ip string
err error
}
func (s *stubStrategy) Name() string { return s.name }
func (s *stubStrategy) Resolve(_ context.Context, _ *netbox.HostEntry, _ *netbox.Client) (string, error) {
return s.ip, s.err
}
func TestChain_FirstStrategySucceeds(t *testing.T) {
c := &Chain{strategies: []Strategy{
&stubStrategy{name: "first", ip: "10.0.0.1"},
&stubStrategy{name: "second", ip: "10.0.0.2"},
}}
ip, err := c.Resolve(context.Background(), &netbox.HostEntry{Name: "host"}, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ip != "10.0.0.1" {
t.Errorf("got %q, want first strategy's IP %q", ip, "10.0.0.1")
}
}
func TestChain_FallsBackToNextStrategy(t *testing.T) {
c := &Chain{strategies: []Strategy{
&stubStrategy{name: "first", err: ErrNoIP},
&stubStrategy{name: "second", ip: "10.0.0.2"},
}}
ip, err := c.Resolve(context.Background(), &netbox.HostEntry{Name: "host"}, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ip != "10.0.0.2" {
t.Errorf("got %q, want second strategy's IP %q", ip, "10.0.0.2")
}
}
func TestChain_AllStrategiesFail(t *testing.T) {
c := &Chain{strategies: []Strategy{
&stubStrategy{name: "a", err: ErrNoIP},
&stubStrategy{name: "b", err: errors.New("api error")},
}}
_, err := c.Resolve(context.Background(), &netbox.HostEntry{Name: "host"}, nil)
if err == nil {
t.Error("expected error when all strategies fail")
}
}
func TestChain_EmptyStrategies(t *testing.T) {
c := &Chain{}
_, err := c.Resolve(context.Background(), &netbox.HostEntry{Name: "host"}, nil)
if err == nil {
t.Error("empty chain should return an error")
}
}
func TestNew_PrimaryIP(t *testing.T) {
cfg := config.ResolverConfig{Strategies: []string{"primary_ip"}}
c, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if len(c.strategies) != 1 {
t.Errorf("got %d strategies, want 1", len(c.strategies))
}
if c.strategies[0].Name() != "primary_ip" {
t.Errorf("strategy name: got %q, want %q", c.strategies[0].Name(), "primary_ip")
}
}
func TestNew_ManagementSubnet(t *testing.T) {
cfg := config.ResolverConfig{
Strategies: []string{"management_subnet"},
ManagementSubnets: []string{"10.0.0.0/8"},
}
c, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if c.strategies[0].Name() != "management_subnet" {
t.Errorf("strategy name: got %q, want %q", c.strategies[0].Name(), "management_subnet")
}
}
func TestNew_ManagementSubnet_InvalidCIDR(t *testing.T) {
cfg := config.ResolverConfig{
Strategies: []string{"management_subnet"},
ManagementSubnets: []string{"not-a-cidr"},
}
_, err := New(cfg)
if err == nil {
t.Error("invalid CIDR should return an error")
}
}
func TestNew_InterfaceName(t *testing.T) {
cfg := config.ResolverConfig{
Strategies: []string{"interface_name"},
InterfaceName: "mgmt0",
}
c, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if c.strategies[0].Name() != "interface_name" {
t.Errorf("strategy name: got %q", c.strategies[0].Name())
}
}
func TestNew_InterfaceName_MissingConfig(t *testing.T) {
cfg := config.ResolverConfig{
Strategies: []string{"interface_name"},
InterfaceName: "", // not set
}
_, err := New(cfg)
if err == nil {
t.Error("interface_name without config should return an error")
}
}
func TestNew_UnknownStrategy(t *testing.T) {
cfg := config.ResolverConfig{Strategies: []string{"nonexistent"}}
_, err := New(cfg)
if err == nil {
t.Error("unknown strategy should return an error")
}
}
func TestNew_MultipleStrategies(t *testing.T) {
cfg := config.ResolverConfig{
Strategies: []string{"management_subnet", "primary_ip"},
ManagementSubnets: []string{"10.0.0.0/8"},
}
c, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if len(c.strategies) != 2 {
t.Errorf("got %d strategies, want 2", len(c.strategies))
}
}
// An API failure must not fall through: the next strategy would connect to
// and cache a different address than the configured order asks for.
func TestChain_APIErrorStopsTheChain(t *testing.T) {
apiErr := errors.New("connection refused")
c := &Chain{strategies: []Strategy{
&stubStrategy{name: "management_subnet", err: apiErr},
&stubStrategy{name: "primary_ip", ip: "10.0.0.2"},
}}
ip, err := c.Resolve(context.Background(), &netbox.HostEntry{Name: "host"}, nil)
if !errors.Is(err, apiErr) || ip != "" {
t.Fatalf("got %q, %v; want the API error and no address", ip, err)
}
}