package servertest import ( "net/netip" "testing" "time" ) // AssertMeshComplete verifies that every client in the slice sees // exactly (len(clients) - 1) peers, i.e. a fully connected mesh. func AssertMeshComplete(tb testing.TB, clients []*TestClient) { tb.Helper() expected := len(clients) - 1 for _, c := range clients { nm := c.Netmap() if nm == nil { tb.Errorf("AssertMeshComplete: %s has no netmap", c.Name) continue } if got := len(nm.Peers); got != expected { tb.Errorf("AssertMeshComplete: %s has %d peers, want %d (peers: %v)", c.Name, got, expected, c.PeerNames()) } } } // AssertSymmetricVisibility checks that peer visibility is symmetric: // if client A sees client B, then client B must also see client A. func AssertSymmetricVisibility(tb testing.TB, clients []*TestClient) { tb.Helper() for _, a := range clients { for _, b := range clients { if a == b { continue } _, aSeesB := a.PeerByName(b.Name) _, bSeesA := b.PeerByName(a.Name) if aSeesB != bSeesA { tb.Errorf("AssertSymmetricVisibility: %s sees %s = %v, but %s sees %s = %v", a.Name, b.Name, aSeesB, b.Name, a.Name, bSeesA) } } } } // AssertPeerOnline checks that the observer sees peerName as online. func AssertPeerOnline(tb testing.TB, observer *TestClient, peerName string) { tb.Helper() peer, ok := observer.PeerByName(peerName) if !ok { tb.Errorf("AssertPeerOnline: %s does not see peer %s", observer.Name, peerName) return } isOnline, known := peer.Online().GetOk() if !known || !isOnline { tb.Errorf("AssertPeerOnline: %s sees peer %s but Online=%v (known=%v), want true", observer.Name, peerName, isOnline, known) } } // AssertPeerOffline checks that the observer sees peerName as offline. func AssertPeerOffline(tb testing.TB, observer *TestClient, peerName string) { tb.Helper() peer, ok := observer.PeerByName(peerName) if !ok { // Peer gone entirely counts as "offline" for this assertion. return } isOnline, known := peer.Online().GetOk() if known && isOnline { tb.Errorf("AssertPeerOffline: %s sees peer %s as online, want offline", observer.Name, peerName) } } // AssertPeerGone checks that the observer does NOT have peerName in // its peer list at all. func AssertPeerGone(tb testing.TB, observer *TestClient, peerName string) { tb.Helper() _, ok := observer.PeerByName(peerName) if ok { tb.Errorf("AssertPeerGone: %s still sees peer %s", observer.Name, peerName) } } // AssertPeerHasAllowedIPs checks that a peer has the expected // AllowedIPs prefixes. func AssertPeerHasAllowedIPs(tb testing.TB, observer *TestClient, peerName string, want []netip.Prefix) { tb.Helper() peer, ok := observer.PeerByName(peerName) if !ok { tb.Errorf("AssertPeerHasAllowedIPs: %s does not see peer %s", observer.Name, peerName) return } got := make([]netip.Prefix, 0, peer.AllowedIPs().Len()) for i := range peer.AllowedIPs().Len() { got = append(got, peer.AllowedIPs().At(i)) } if len(got) != len(want) { tb.Errorf("AssertPeerHasAllowedIPs: %s sees %s with AllowedIPs %v, want %v", observer.Name, peerName, got, want) return } // Build a set for comparison. wantSet := make(map[netip.Prefix]bool, len(want)) for _, p := range want { wantSet[p] = true } for _, p := range got { if !wantSet[p] { tb.Errorf("AssertPeerHasAllowedIPs: %s sees %s with unexpected AllowedIP %v (want %v)", observer.Name, peerName, p, want) } } } // AssertConsistentState checks that all clients agree on peer // properties: every connected client should see the same set of // peer hostnames. func AssertConsistentState(tb testing.TB, clients []*TestClient) { tb.Helper() for _, c := range clients { nm := c.Netmap() if nm == nil { continue } peerNames := make(map[string]bool, len(nm.Peers)) for _, p := range nm.Peers { hi := p.Hostinfo() if hi.Valid() { peerNames[hi.Hostname()] = true } } // Check that c sees all other connected clients. for _, other := range clients { if other == c || other.Netmap() == nil { continue } if !peerNames[other.Name] { tb.Errorf("AssertConsistentState: %s does not see %s (peers: %v)", c.Name, other.Name, c.PeerNames()) } } } } // AssertDERPMapPresent checks that the netmap contains a DERP map. func AssertDERPMapPresent(tb testing.TB, client *TestClient) { tb.Helper() nm := client.Netmap() if nm == nil { tb.Errorf("AssertDERPMapPresent: %s has no netmap", client.Name) return } if nm.DERPMap == nil { tb.Errorf("AssertDERPMapPresent: %s has nil DERPMap", client.Name) return } if len(nm.DERPMap.Regions) == 0 { tb.Errorf("AssertDERPMapPresent: %s has empty DERPMap regions", client.Name) } } // AssertSelfHasAddresses checks that the self node has at least one address. func AssertSelfHasAddresses(tb testing.TB, client *TestClient) { tb.Helper() nm := client.Netmap() if nm == nil { tb.Errorf("AssertSelfHasAddresses: %s has no netmap", client.Name) return } if !nm.SelfNode.Valid() { tb.Errorf("AssertSelfHasAddresses: %s self node is invalid", client.Name) return } if nm.SelfNode.Addresses().Len() == 0 { tb.Errorf("AssertSelfHasAddresses: %s self node has no addresses", client.Name) } } // EventuallyAssertMeshComplete retries AssertMeshComplete up to // timeout, useful when waiting for state to propagate. func EventuallyAssertMeshComplete(tb testing.TB, clients []*TestClient, timeout time.Duration) { tb.Helper() expected := len(clients) - 1 deadline := time.After(timeout) for { allGood := true for _, c := range clients { nm := c.Netmap() if nm == nil || len(nm.Peers) < expected { allGood = false break } } if allGood { // Final strict check. AssertMeshComplete(tb, clients) return } select { case <-deadline: // Report the failure with details. for _, c := range clients { nm := c.Netmap() got := 0 if nm != nil { got = len(nm.Peers) } if got != expected { tb.Errorf("EventuallyAssertMeshComplete: %s has %d peers, want %d (timeout %v)", c.Name, got, expected, timeout) } } return case <-time.After(100 * time.Millisecond): // Poll again. } } }