mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-21 00:49:38 +01:00
Extend the servertest harness with: - TestClient.Direct() accessor for advanced operations - TestClient.WaitForPeerCount and WaitForCondition helpers - TestHarness.ChangePolicy for ACL policy testing - AssertDERPMapPresent and AssertSelfHasAddresses New test suites: - content_test.go: self node, DERP map, peer properties, user profiles, update history monotonicity, and endpoint update propagation - policy_test.go: default allow-all, explicit policy, policy triggers updates on all nodes, multiple policy changes, multi-user mesh - ephemeral_test.go: ephemeral connect, cleanup after disconnect, mixed ephemeral/regular, reconnect prevents cleanup - routes_test.go: addresses in AllowedIPs, route advertise and approve, advertised routes via hostinfo, CGNAT range validation Also fix node_departs test to use WaitForCondition instead of assert.Eventually, and convert concurrent_join_and_leave to interleaved_join_and_leave with grace-period-tolerant assertions.
264 lines
6.1 KiB
Go
264 lines
6.1 KiB
Go
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.
|
|
}
|
|
}
|
|
}
|