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
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 [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)
|
|
}
|