Bring an administratively down interface up in AddAddress before writing its addresses and routes. The kernel installs an address's connected route only while its link is up, so adding a gateway to a down interface was rejected as unreachable. A link raised this way is part of the pre-change state AddAddress restores, so it is returned to down on any path that does not complete. Also fix the rollback masking its own error: deleting the address it added takes the default route with it, since that route resolved its nexthop through the address's connected subnet. The following RouteDel then answered ESRCH and was reported in place of "aborted operation due to loss of internet".
275 lines
8.6 KiB
Go
275 lines
8.6 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Round-tripping an IPv4 address through ip2Uint/uint2IP must return the
|
|
// original four-byte value.
|
|
func TestIP2UintRoundTrip(t *testing.T) {
|
|
cases := []string{"0.0.0.0", "1.2.3.4", "192.168.1.255", "255.255.255.255"}
|
|
for _, c := range cases {
|
|
ip := net.ParseIP(c)
|
|
require.NotNil(t, ip, "could not parse %q", c)
|
|
got := uint2IP(ip2Uint(ip))
|
|
assert.True(t, got.Equal(ip), "round trip of %s: got %s", c, got)
|
|
// uint2IP should produce the canonical four-byte representation.
|
|
assert.Equal(t, net.IPv4len, len(got), "uint2IP(%s) length", c)
|
|
}
|
|
}
|
|
|
|
// ip2Uint must accept both the 16-byte and 4-byte representations of the same
|
|
// IPv4 address and yield an identical result.
|
|
func TestIP2UintAcceptsBothRepresentations(t *testing.T) {
|
|
ip16 := net.ParseIP("10.20.30.40")
|
|
ip4 := ip16.To4()
|
|
require.NotNil(t, ip4, "expected an IPv4 address")
|
|
assert.Equal(t, ip2Uint(ip16), ip2Uint(ip4),
|
|
"ip2Uint disagrees for 16-byte and 4-byte forms")
|
|
}
|
|
|
|
func TestAllFF(t *testing.T) {
|
|
cases := []struct {
|
|
in []byte
|
|
want bool
|
|
}{
|
|
{[]byte{}, true},
|
|
{[]byte{0xff}, true},
|
|
{[]byte{0xff, 0xff, 0xff, 0xff}, true},
|
|
{[]byte{0xff, 0x00}, false},
|
|
{[]byte{0x00}, false},
|
|
{[]byte{0xff, 0xff, 0xfe}, false},
|
|
}
|
|
for _, c := range cases {
|
|
assert.Equal(t, c.want, allFF(c.in), "allFF(%v)", c.in)
|
|
}
|
|
}
|
|
|
|
func TestGetBroadcast(t *testing.T) {
|
|
cases := []struct {
|
|
cidr string
|
|
want string
|
|
}{
|
|
{"192.168.1.0/24", "192.168.1.255"},
|
|
{"10.0.0.0/8", "10.255.255.255"},
|
|
{"172.16.5.0/30", "172.16.5.3"},
|
|
{"203.0.113.128/25", "203.0.113.255"},
|
|
{"fc00::/64", "fc00::ffff:ffff:ffff:ffff"},
|
|
}
|
|
for _, c := range cases {
|
|
_, network, err := net.ParseCIDR(c.cidr)
|
|
require.NoError(t, err, "ParseCIDR(%q)", c.cidr)
|
|
got := getBroadcast(network)
|
|
want := net.ParseIP(c.want)
|
|
assert.True(t, got.Equal(want), "getBroadcast(%s) = %s, want %s", c.cidr, got, c.want)
|
|
}
|
|
}
|
|
|
|
// getBroadcast must cope with an IPv4 address paired with an IPv4-in-IPv6 mask,
|
|
// exercising the family-normalisation branch.
|
|
func TestGetBroadcastMixedFamily(t *testing.T) {
|
|
network := &net.IPNet{
|
|
IP: net.ParseIP("192.168.1.0").To4(),
|
|
Mask: net.CIDRMask(96+24, 128), // /24 expressed as a 16-byte mask.
|
|
}
|
|
got := getBroadcast(network)
|
|
want := net.ParseIP("192.168.1.255")
|
|
assert.True(t, got.Equal(want), "getBroadcast mixed family = %s, want %s", got, want)
|
|
}
|
|
|
|
func TestSortableDirEntries(t *testing.T) {
|
|
dir := t.TempDir()
|
|
names := []string{"zebra", "alpha", "mike", "bravo"}
|
|
for _, n := range names {
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, n), nil, 0o644))
|
|
}
|
|
entries, err := os.ReadDir(dir)
|
|
require.NoError(t, err)
|
|
// Shuffle into a known unsorted order before sorting.
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Name() > entries[j].Name()
|
|
})
|
|
|
|
sortable := sortableDirEntries(entries)
|
|
assert.Equal(t, len(names), sortable.Len(), "Len()")
|
|
sort.Sort(sortable)
|
|
|
|
want := []string{"alpha", "bravo", "mike", "zebra"}
|
|
for i, w := range want {
|
|
assert.Equal(t, w, entries[i].Name(), "sorted[%d]", i)
|
|
}
|
|
}
|
|
|
|
func TestIntStringYAML(t *testing.T) {
|
|
t.Run("int", func(t *testing.T) {
|
|
var v intString
|
|
require.NoError(t, yaml.Unmarshal([]byte("42\n"), &v))
|
|
require.NotNil(t, v.Int, "Int = %v, want 42", v.Int)
|
|
assert.Equal(t, 42, *v.Int)
|
|
assert.Nil(t, v.String, "String should be nil")
|
|
out, err := yaml.Marshal(v)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "42\n", string(out), "marshal")
|
|
})
|
|
|
|
t.Run("string", func(t *testing.T) {
|
|
var v intString
|
|
require.NoError(t, yaml.Unmarshal([]byte("auto\n"), &v))
|
|
require.NotNil(t, v.String, "String = %v, want auto", v.String)
|
|
assert.Equal(t, "auto", *v.String)
|
|
assert.Nil(t, v.Int, "Int should be nil")
|
|
out, err := yaml.Marshal(v)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "auto\n", string(out), "marshal")
|
|
})
|
|
|
|
t.Run("empty marshals to null", func(t *testing.T) {
|
|
out, err := yaml.Marshal(intString{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "null\n", string(out), "marshal of empty")
|
|
})
|
|
}
|
|
|
|
func TestIntStringJSON(t *testing.T) {
|
|
t.Run("int", func(t *testing.T) {
|
|
var v intString
|
|
require.NoError(t, json.Unmarshal([]byte("42"), &v))
|
|
require.NotNil(t, v.Int, "Int = %v, want 42", v.Int)
|
|
assert.Equal(t, 42, *v.Int)
|
|
out, err := json.Marshal(v)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "42", string(out), "marshal")
|
|
})
|
|
|
|
t.Run("string", func(t *testing.T) {
|
|
var v intString
|
|
require.NoError(t, json.Unmarshal([]byte(`"auto"`), &v))
|
|
require.NotNil(t, v.String, "String = %v, want auto", v.String)
|
|
assert.Equal(t, "auto", *v.String)
|
|
out, err := json.Marshal(v)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, `"auto"`, string(out), "marshal")
|
|
})
|
|
|
|
t.Run("invalid", func(t *testing.T) {
|
|
var v intString
|
|
err := json.Unmarshal([]byte("{}"), &v)
|
|
assert.Error(t, err, "expected error for object input, got nil")
|
|
})
|
|
}
|
|
|
|
func TestRunCommand(t *testing.T) {
|
|
t.Run("stdout", func(t *testing.T) {
|
|
out, err := runCommand(context.Background(), "printf", "line1\nline2\n")
|
|
require.NoError(t, err, "unexpected error")
|
|
want := []string{"line1", "line2"}
|
|
assert.Equal(t, want, out, "out")
|
|
})
|
|
|
|
t.Run("nonzero exit", func(t *testing.T) {
|
|
_, err := runCommand(context.Background(), "false")
|
|
assert.Error(t, err, "expected error for failing command, got nil")
|
|
})
|
|
|
|
t.Run("missing binary", func(t *testing.T) {
|
|
_, err := runCommand(context.Background(), "this-binary-should-not-exist-12345")
|
|
assert.Error(t, err, "expected error for missing binary, got nil")
|
|
})
|
|
}
|
|
|
|
func TestTestInternet(t *testing.T) {
|
|
assert.True(t, testInternet(context.Background(), "test_success", 0),
|
|
"test_success sentinel should report success")
|
|
assert.False(t, testInternet(context.Background(), "test_fail", 0),
|
|
"test_fail sentinel should report failure")
|
|
}
|
|
|
|
func TestFileCopyAndMove(t *testing.T) {
|
|
dir := t.TempDir()
|
|
src := filepath.Join(dir, "src.txt")
|
|
content := []byte("hello world\n")
|
|
require.NoError(t, os.WriteFile(src, content, 0o640))
|
|
|
|
t.Run("fileCopy preserves contents and mode", func(t *testing.T) {
|
|
dst := filepath.Join(dir, "copy.txt")
|
|
require.NoError(t, fileCopy(src, dst))
|
|
got, err := os.ReadFile(dst)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, content, got, "copied contents")
|
|
fi, err := os.Stat(dst)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0o640), fi.Mode().Perm(), "copied mode")
|
|
})
|
|
|
|
t.Run("fileCopy missing source errors", func(t *testing.T) {
|
|
err := fileCopy(filepath.Join(dir, "nope"), filepath.Join(dir, "out"))
|
|
assert.Error(t, err, "expected error for missing source")
|
|
})
|
|
|
|
t.Run("fileMoveCopy removes source", func(t *testing.T) {
|
|
moveSrc := filepath.Join(dir, "movesrc.txt")
|
|
require.NoError(t, os.WriteFile(moveSrc, content, 0o644))
|
|
dst := filepath.Join(dir, "moved.txt")
|
|
require.NoError(t, fileMoveCopy(moveSrc, dst))
|
|
_, err := os.Stat(moveSrc)
|
|
assert.True(t, os.IsNotExist(err), "source should be gone, stat err = %v", err)
|
|
got, err := os.ReadFile(dst)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, content, got, "moved contents")
|
|
})
|
|
|
|
t.Run("fileMove same device renames", func(t *testing.T) {
|
|
moveSrc := filepath.Join(dir, "rename-src.txt")
|
|
require.NoError(t, os.WriteFile(moveSrc, content, 0o644))
|
|
dst := filepath.Join(dir, "renamed.txt")
|
|
require.NoError(t, fileMove(moveSrc, dst))
|
|
_, err := os.Stat(moveSrc)
|
|
assert.True(t, os.IsNotExist(err), "source should be gone after move, stat err = %v", err)
|
|
_, err = os.Stat(dst)
|
|
assert.NoError(t, err, "destination missing after move")
|
|
})
|
|
}
|
|
|
|
func TestNaturalLess(t *testing.T) {
|
|
cases := []struct {
|
|
a, b string
|
|
want bool
|
|
why string
|
|
}{
|
|
{"eth2", "eth10", true, "digit runs compare by value, not character"},
|
|
{"eth10", "eth2", false, "reversed"},
|
|
{"eth0", "eth1", true, "same width digits"},
|
|
{"enp0s3", "enp0s10", true, "embedded digit runs"},
|
|
{"eth0", "eth0", false, "equal strings are not less"},
|
|
{"eth", "eth0", true, "a prefix sorts before what extends it"},
|
|
{"eth0", "eth", false, "reversed prefix"},
|
|
{"eth007", "eth7", false, "leading zeros do not change the value"},
|
|
{"eth7", "eth007", false, "reversed equal values"},
|
|
{"eth1", "wlan0", true, "letters compare by byte"},
|
|
{"9", "10", true, "bare numbers"},
|
|
{"eth99999999999999999999", "eth999999999999999999999", true, "digit runs too long to parse"},
|
|
}
|
|
for _, c := range cases {
|
|
assert.Equal(t, c.want, naturalLess(c.a, c.b), "naturalLess(%q, %q): %s", c.a, c.b, c.why)
|
|
}
|
|
}
|
|
|
|
func TestLeadingDigits(t *testing.T) {
|
|
digits, rest := leadingDigits("10s3")
|
|
assert.Equal(t, "10", digits)
|
|
assert.Equal(t, "s3", rest)
|
|
|
|
digits, rest = leadingDigits("eth0")
|
|
assert.Empty(t, digits)
|
|
assert.Equal(t, "eth0", rest)
|
|
}
|