headscale/hscontrol/servertest/lifecycle_test.go

102 lines
2.8 KiB
Go
Raw Normal View History

package servertest_test
import (
"fmt"
"testing"
"time"
"github.com/juanfont/headscale/hscontrol/servertest"
"github.com/stretchr/testify/assert"
"tailscale.com/types/netmap"
)
// TestConnectionLifecycle exercises the core node lifecycle:
// connecting, seeing peers, joining mid-session, departing, and
// reconnecting.
func TestConnectionLifecycle(t *testing.T) {
t.Parallel()
t.Run("single_node", func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 1)
nm := h.Client(0).Netmap()
assert.NotNil(t, nm, "single node should receive a netmap")
assert.Empty(t, nm.Peers, "single node should have no peers")
})
t.Run("new_node_joins_mesh", func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 3)
// Add a 4th client mid-test.
h.AddClient(t)
h.WaitForMeshComplete(t, 10*time.Second)
servertest.AssertMeshComplete(t, h.Clients())
servertest.AssertSymmetricVisibility(t, h.Clients())
})
t.Run("node_departs_peers_update", func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 3)
departingName := h.Client(2).Name
h.Client(2).Disconnect(t)
// The remaining clients should eventually see the departed
// node go offline or disappear. The grace period in poll.go
// is 10 seconds, so we need a generous timeout.
h.Client(0).WaitForCondition(t, "peer offline or gone", 60*time.Second,
func(nm *netmap.NetworkMap) bool {
for _, p := range nm.Peers {
hi := p.Hostinfo()
if hi.Valid() && hi.Hostname() == departingName {
isOnline, known := p.Online().GetOk()
// Peer is still present but offline is acceptable.
return known && !isOnline
}
}
// Peer gone entirely is also acceptable.
return true
})
})
t.Run("reconnect_restores_mesh", func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 2)
// Disconnect and reconnect.
h.Client(0).Disconnect(t)
h.Client(0).Reconnect(t)
// Mesh should recover.
h.WaitForMeshComplete(t, 15*time.Second)
servertest.AssertMeshComplete(t, h.Clients())
})
t.Run("session_replacement", func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, 2)
// Reconnect without explicitly waiting for the old session to
// fully drain. This tests that Headscale correctly replaces
// the old map session for the same node.
h.Client(0).Reconnect(t)
h.WaitForMeshComplete(t, 15*time.Second)
servertest.AssertMeshComplete(t, h.Clients())
})
t.Run("multiple_nodes_join_sequentially", func(t *testing.T) {
t.Parallel()
sizes := []int{2, 5, 10}
for _, n := range sizes {
t.Run(fmt.Sprintf("%d_nodes", n), func(t *testing.T) {
t.Parallel()
h := servertest.NewHarness(t, n)
servertest.AssertMeshComplete(t, h.Clients())
servertest.AssertSymmetricVisibility(t, h.Clients())
})
}
})
}