mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-27 03:41:23 +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.
183 lines
4.7 KiB
Go
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)
|
|
}
|