Files
headscale/hscontrol/servertest/harness.go
Kristoffer Dalby f87b08676d 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-19 07:05:58 +01:00

183 lines
4.7 KiB
Go

package servertest
import (
"fmt"
"testing"
"time"
"github.com/juanfont/headscale/hscontrol/types"
)
// TestHarness orchestrates a TestServer with multiple TestClients,
// providing a convenient setup for multi-node control plane tests.
type TestHarness struct {
Server *TestServer
clients []*TestClient
// Default user shared by all clients unless overridden.
defaultUser *types.User
}
// HarnessOption configures a TestHarness.
type HarnessOption func(*harnessConfig)
type harnessConfig struct {
serverOpts []ServerOption
clientOpts []ClientOption
convergenceMax time.Duration
}
func defaultHarnessConfig() *harnessConfig {
return &harnessConfig{
convergenceMax: 30 * time.Second,
}
}
// WithServerOptions passes ServerOptions through to the underlying
// TestServer.
func WithServerOptions(opts ...ServerOption) HarnessOption {
return func(c *harnessConfig) { c.serverOpts = append(c.serverOpts, opts...) }
}
// WithDefaultClientOptions applies ClientOptions to every client
// created by NewHarness.
func WithDefaultClientOptions(opts ...ClientOption) HarnessOption {
return func(c *harnessConfig) { c.clientOpts = append(c.clientOpts, opts...) }
}
// WithConvergenceTimeout sets how long WaitForMeshComplete waits.
func WithConvergenceTimeout(d time.Duration) HarnessOption {
return func(c *harnessConfig) { c.convergenceMax = d }
}
// NewHarness creates a TestServer and numClients connected clients.
// All clients share a default user and are registered with reusable
// pre-auth keys. The harness waits for all clients to form a
// complete mesh before returning.
func NewHarness(tb testing.TB, numClients int, opts ...HarnessOption) *TestHarness {
tb.Helper()
hc := defaultHarnessConfig()
for _, o := range opts {
o(hc)
}
server := NewServer(tb, hc.serverOpts...)
// Create a shared default user.
user := server.CreateUser(tb, "harness-default")
h := &TestHarness{
Server: server,
defaultUser: user,
}
// Create and connect clients.
for i := range numClients {
name := clientName(i)
copts := append([]ClientOption{WithUser(user)}, hc.clientOpts...)
c := NewClient(tb, server, name, copts...)
h.clients = append(h.clients, c)
}
// Wait for the mesh to converge.
if numClients > 1 {
h.WaitForMeshComplete(tb, hc.convergenceMax)
} else if numClients == 1 {
// Single node: just wait for the first netmap.
h.clients[0].WaitForUpdate(tb, hc.convergenceMax)
}
return h
}
// Client returns the i-th client (0-indexed).
func (h *TestHarness) Client(i int) *TestClient {
return h.clients[i]
}
// Clients returns all clients.
func (h *TestHarness) Clients() []*TestClient {
return h.clients
}
// ConnectedClients returns clients that currently have an active
// long-poll session (pollDone channel is still open).
func (h *TestHarness) ConnectedClients() []*TestClient {
var out []*TestClient
for _, c := range h.clients {
select {
case <-c.pollDone:
// Poll has ended, client is disconnected.
default:
out = append(out, c)
}
}
return out
}
// AddClient creates and connects a new client to the existing mesh.
func (h *TestHarness) AddClient(tb testing.TB, opts ...ClientOption) *TestClient {
tb.Helper()
name := clientName(len(h.clients))
copts := append([]ClientOption{WithUser(h.defaultUser)}, opts...)
c := NewClient(tb, h.Server, name, copts...)
h.clients = append(h.clients, c)
return c
}
// WaitForMeshComplete blocks until every connected client sees
// (connectedCount - 1) peers.
func (h *TestHarness) WaitForMeshComplete(tb testing.TB, timeout time.Duration) {
tb.Helper()
connected := h.ConnectedClients()
expectedPeers := max(len(connected)-1, 0)
for _, c := range connected {
c.WaitForPeers(tb, expectedPeers, timeout)
}
}
// WaitForConvergence waits until all connected clients have a
// non-nil NetworkMap and their peer counts have stabilised.
func (h *TestHarness) WaitForConvergence(tb testing.TB, timeout time.Duration) {
tb.Helper()
h.WaitForMeshComplete(tb, timeout)
}
// ChangePolicy sets an ACL policy on the server and propagates changes
// to all connected nodes. The policy should be a valid HuJSON policy document.
func (h *TestHarness) ChangePolicy(tb testing.TB, policy []byte) {
tb.Helper()
changed, err := h.Server.State().SetPolicy(policy)
if err != nil {
tb.Fatalf("servertest: ChangePolicy: %v", err)
}
if changed {
changes, err := h.Server.State().ReloadPolicy()
if err != nil {
tb.Fatalf("servertest: ReloadPolicy: %v", err)
}
h.Server.App.Change(changes...)
}
}
// DefaultUser returns the shared user for adding more clients.
func (h *TestHarness) DefaultUser() *types.User {
return h.defaultUser
}
func clientName(index int) string {
return fmt.Sprintf("node-%d", index)
}