hscontrol/servertest: add control plane lifecycle and consistency tests
Add three test files exercising the servertest harness:
- lifecycle_test.go: connection, disconnection, reconnection, session
replacement, and mesh formation at various sizes.
- consistency_test.go: symmetric visibility, consistent peer state,
address presence, concurrent join/leave convergence.
- weather_test.go: rapid reconnects, flapping stability, reconnect
with various delays, concurrent reconnects, and scale tests.
All tests use table-driven patterns with subtests.
2026-03-16 17:07:23 +00:00
|
|
|
package servertest_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/juanfont/headscale/hscontrol/servertest"
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
hscontrol/servertest: add policy, route, ephemeral, and content tests
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.
2026-03-16 19:09:10 +00:00
|
|
|
"tailscale.com/types/netmap"
|
hscontrol/servertest: add control plane lifecycle and consistency tests
Add three test files exercising the servertest harness:
- lifecycle_test.go: connection, disconnection, reconnection, session
replacement, and mesh formation at various sizes.
- consistency_test.go: symmetric visibility, consistent peer state,
address presence, concurrent join/leave convergence.
- weather_test.go: rapid reconnects, flapping stability, reconnect
with various delays, concurrent reconnects, and scale tests.
All tests use table-driven patterns with subtests.
2026-03-16 17:07:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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())
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-16 20:20:19 +00:00
|
|
|
t.Run("node_departs_peer_goes_offline", func(t *testing.T) {
|
hscontrol/servertest: add control plane lifecycle and consistency tests
Add three test files exercising the servertest harness:
- lifecycle_test.go: connection, disconnection, reconnection, session
replacement, and mesh formation at various sizes.
- consistency_test.go: symmetric visibility, consistent peer state,
address presence, concurrent join/leave convergence.
- weather_test.go: rapid reconnects, flapping stability, reconnect
with various delays, concurrent reconnects, and scale tests.
All tests use table-driven patterns with subtests.
2026-03-16 17:07:23 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
h := servertest.NewHarness(t, 3)
|
|
|
|
|
|
|
|
|
|
departingName := h.Client(2).Name
|
2026-03-16 20:20:19 +00:00
|
|
|
|
|
|
|
|
// First verify the departing node is online (may need a moment
|
|
|
|
|
// for Online status to propagate after mesh formation).
|
|
|
|
|
h.Client(0).WaitForCondition(t, "peer initially online", 15*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()
|
|
|
|
|
|
|
|
|
|
return known && isOnline
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
|
hscontrol/servertest: add control plane lifecycle and consistency tests
Add three test files exercising the servertest harness:
- lifecycle_test.go: connection, disconnection, reconnection, session
replacement, and mesh formation at various sizes.
- consistency_test.go: symmetric visibility, consistent peer state,
address presence, concurrent join/leave convergence.
- weather_test.go: rapid reconnects, flapping stability, reconnect
with various delays, concurrent reconnects, and scale tests.
All tests use table-driven patterns with subtests.
2026-03-16 17:07:23 +00:00
|
|
|
h.Client(2).Disconnect(t)
|
|
|
|
|
|
2026-03-16 20:20:19 +00:00
|
|
|
// After the 10-second grace period, the remaining clients
|
|
|
|
|
// should see the departed node as offline. The peer stays
|
|
|
|
|
// in the peer list (non-ephemeral nodes are not removed).
|
|
|
|
|
h.Client(0).WaitForCondition(t, "peer goes offline", 30*time.Second,
|
hscontrol/servertest: add policy, route, ephemeral, and content tests
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.
2026-03-16 19:09:10 +00:00
|
|
|
func(nm *netmap.NetworkMap) bool {
|
|
|
|
|
for _, p := range nm.Peers {
|
|
|
|
|
hi := p.Hostinfo()
|
|
|
|
|
if hi.Valid() && hi.Hostname() == departingName {
|
|
|
|
|
isOnline, known := p.Online().GetOk()
|
2026-03-16 20:20:19 +00:00
|
|
|
|
hscontrol/servertest: add policy, route, ephemeral, and content tests
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.
2026-03-16 19:09:10 +00:00
|
|
|
return known && !isOnline
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 20:20:19 +00:00
|
|
|
|
|
|
|
|
return false
|
hscontrol/servertest: add policy, route, ephemeral, and content tests
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.
2026-03-16 19:09:10 +00:00
|
|
|
})
|
hscontrol/servertest: add control plane lifecycle and consistency tests
Add three test files exercising the servertest harness:
- lifecycle_test.go: connection, disconnection, reconnection, session
replacement, and mesh formation at various sizes.
- consistency_test.go: symmetric visibility, consistent peer state,
address presence, concurrent join/leave convergence.
- weather_test.go: rapid reconnects, flapping stability, reconnect
with various delays, concurrent reconnects, and scale tests.
All tests use table-driven patterns with subtests.
2026-03-16 17:07:23 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|