Kristoffer Dalby 4cca63155d all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.

Pattern applied across the tree:
  In-package         [Foo], [Foo.Bar]
  Cross-package      [pkg.Foo], [pkg.Foo.Bar]
  Stdlib             [netip.Prefix], [errors.Is], [context.Context]
  Tailscale          [tailcfg.MapResponse], [tailcfg.Node.CapMap],
                     [tailcfg.NodeAttrSuggestExitNode]

Skip rules:
  - File:line refs left as plain text
  - HuJSON wire keys inside backtick raw strings untouched
  - ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
    symbols, left as plain text
  - JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
    link tags, decorative dividers — all left as-is
2026-05-19 09:55:22 +02: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 [TestClient] instances,
// 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 [ServerOption] values through to the underlying
// [TestServer].
func WithServerOptions(opts ...ServerOption) HarnessOption {
return func(c *harnessConfig) { c.serverOpts = append(c.serverOpts, opts...) }
}
// WithDefaultClientOptions applies [ClientOption] values 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 [TestHarness.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 [netmap.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)
}