Served portait json:",omitempty" : sur un probe dont la MAC n'est pas réservée, false — qui est toute la réponse — disparaissait du fil, et un opérateur lisait un champ absent plutôt qu'une réponse négative. Trouvé en rejouant contre un vrai serveur les exemples de diagnostic écrits pour la documentation. Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
185 lines
4.7 KiB
Go
185 lines
4.7 KiB
Go
package dhcpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func subnet() Subnet {
|
|
return Subnet{
|
|
Network: "10.0.5.0/24",
|
|
InterfaceIP: "10.0.5.1",
|
|
VPCRoute: "10.0.0.0/16",
|
|
DefaultGateway: "10.0.5.254",
|
|
}
|
|
}
|
|
|
|
func hosts() []Host {
|
|
return []Host{
|
|
{MAC: "00:22:33:00:00:0a", IP: "10.0.5.10", VM: "vm-a", DefaultRoute: true},
|
|
{MAC: "00:22:33:00:00:0b", IP: "10.0.5.11", VM: "vm-b"},
|
|
}
|
|
}
|
|
|
|
func digestOf(t *testing.T, s State) string {
|
|
t.Helper()
|
|
d, err := Digest(s)
|
|
if err != nil {
|
|
t.Fatalf("Digest: %v", err)
|
|
}
|
|
return d
|
|
}
|
|
|
|
func TestDigest_IsStableAcrossHostOrder(t *testing.T) {
|
|
sub := subnet()
|
|
a := State{Subnet: &sub, Hosts: hosts()}
|
|
|
|
reversed := hosts()
|
|
reversed[0], reversed[1] = reversed[1], reversed[0]
|
|
b := State{Subnet: &sub, Hosts: reversed}
|
|
|
|
if digestOf(t, a) != digestOf(t, b) {
|
|
t.Error("host order must not change the digest: the watchdog would report a phantom drift")
|
|
}
|
|
}
|
|
|
|
func TestDigest_IsStableAcrossMACCase(t *testing.T) {
|
|
sub := subnet()
|
|
a := State{Subnet: &sub, Hosts: hosts()}
|
|
|
|
upper := hosts()
|
|
upper[0].MAC = "00:22:33:00:00:0A"
|
|
b := State{Subnet: &sub, Hosts: upper}
|
|
|
|
if digestOf(t, a) != digestOf(t, b) {
|
|
t.Error("mac case must not change the digest")
|
|
}
|
|
}
|
|
|
|
func TestDigest_IsStableAcrossIPv4InIPv6Notation(t *testing.T) {
|
|
sub := subnet()
|
|
a := State{Subnet: &sub, Hosts: hosts()}
|
|
|
|
mapped := subnet()
|
|
mapped.InterfaceIP = "::ffff:10.0.5.1"
|
|
b := State{Subnet: &mapped, Hosts: hosts()}
|
|
|
|
if digestOf(t, a) != digestOf(t, b) {
|
|
t.Error("the same address written in ipv4-mapped form must hash alike")
|
|
}
|
|
}
|
|
|
|
func TestDigest_ChangesWhenAHostIPChanges(t *testing.T) {
|
|
sub := subnet()
|
|
a := State{Subnet: &sub, Hosts: hosts()}
|
|
|
|
moved := hosts()
|
|
moved[0].IP = "10.0.5.99"
|
|
b := State{Subnet: &sub, Hosts: moved}
|
|
|
|
if digestOf(t, a) == digestOf(t, b) {
|
|
t.Error("a changed reservation must change the digest")
|
|
}
|
|
}
|
|
|
|
func TestDigest_ChangesWhenTheDefaultRouteFlagChanges(t *testing.T) {
|
|
sub := subnet()
|
|
a := State{Subnet: &sub, Hosts: hosts()}
|
|
|
|
flipped := hosts()
|
|
flipped[0].DefaultRoute = false
|
|
b := State{Subnet: &sub, Hosts: flipped}
|
|
|
|
if digestOf(t, a) == digestOf(t, b) {
|
|
t.Error("the default route flag is part of the served state")
|
|
}
|
|
}
|
|
|
|
func TestDigest_ChangesWhenTheSubnetChanges(t *testing.T) {
|
|
sub := subnet()
|
|
a := State{Subnet: &sub, Hosts: hosts()}
|
|
|
|
other := subnet()
|
|
other.DefaultGateway = "10.0.5.253"
|
|
b := State{Subnet: &other, Hosts: hosts()}
|
|
|
|
if digestOf(t, a) == digestOf(t, b) {
|
|
t.Error("the subnet configuration is part of the served state")
|
|
}
|
|
}
|
|
|
|
func TestDigest_DistinguishesNoSubnetFromAConfiguredOne(t *testing.T) {
|
|
sub := subnet()
|
|
configured := State{Subnet: &sub}
|
|
bare := State{}
|
|
|
|
if digestOf(t, configured) == digestOf(t, bare) {
|
|
t.Error("an unconfigured subnet must not hash like a configured one")
|
|
}
|
|
}
|
|
|
|
func TestDigest_EmptyAndNilHostsHashAlike(t *testing.T) {
|
|
if digestOf(t, State{Hosts: nil}) != digestOf(t, State{Hosts: []Host{}}) {
|
|
t.Error("nil and empty host lists describe the same state")
|
|
}
|
|
}
|
|
|
|
func TestDigest_RejectsAnInvalidMAC(t *testing.T) {
|
|
if _, err := Digest(State{Hosts: []Host{{MAC: "nope", IP: "10.0.5.10"}}}); err == nil {
|
|
t.Fatal("an invalid mac must be reported, not hashed")
|
|
}
|
|
}
|
|
|
|
func TestDigest_RejectsAnInvalidIP(t *testing.T) {
|
|
if _, err := Digest(State{Hosts: []Host{{MAC: "00:22:33:00:00:0a", IP: "10.0.5.300"}}}); err == nil {
|
|
t.Fatal("an invalid ip must be reported, not hashed")
|
|
}
|
|
}
|
|
|
|
func TestCanonical_RejectsADuplicateMAC(t *testing.T) {
|
|
dup := []Host{
|
|
{MAC: "00:22:33:00:00:0a", IP: "10.0.5.10"},
|
|
{MAC: "00:22:33:00:00:0A", IP: "10.0.5.11"},
|
|
}
|
|
if _, err := Canonical(State{Hosts: dup}); err == nil {
|
|
t.Fatal("the same mac twice is an inconsistent state, not something to hash")
|
|
}
|
|
}
|
|
|
|
func TestCanonical_NormalizesTheNetworkToItsBaseAddress(t *testing.T) {
|
|
sub := subnet()
|
|
sub.Network = "10.0.5.42/24"
|
|
|
|
got, err := CanonicalSubnet(sub)
|
|
if err != nil {
|
|
t.Fatalf("CanonicalSubnet: %v", err)
|
|
}
|
|
if got.Network != "10.0.5.0/24" {
|
|
t.Errorf("network = %s, want 10.0.5.0/24", got.Network)
|
|
}
|
|
}
|
|
|
|
func TestCanonical_SortsHostsByMAC(t *testing.T) {
|
|
unsorted := []Host{
|
|
{MAC: "00:22:33:00:00:0c", IP: "10.0.5.12"},
|
|
{MAC: "00:22:33:00:00:0a", IP: "10.0.5.10"},
|
|
}
|
|
got, err := Canonical(State{Hosts: unsorted})
|
|
if err != nil {
|
|
t.Fatalf("Canonical: %v", err)
|
|
}
|
|
if got.Hosts[0].MAC != "00:22:33:00:00:0a" {
|
|
t.Errorf("hosts = %v, want sorted by mac", got.Hosts)
|
|
}
|
|
}
|
|
|
|
func TestResponse_ServedIsAlwaysOnTheWire(t *testing.T) {
|
|
raw, err := json.Marshal(Response{OK: true, Served: false})
|
|
if err != nil {
|
|
t.Fatalf("Marshal: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"served":false`) {
|
|
t.Errorf("response = %s, want an explicit served:false — omitting it makes \"not served\" indistinguishable from a missing field when probing by hand", raw)
|
|
}
|
|
}
|