Files
netbird/management/internals/modules/agentnetwork/labelgen/labelgen_test.go
Brad Ison ebfdf7d7b8 [management] Rework Agent Network endpoint identity and settings bootstrap (#7085)
Store the per-account gateway endpoint as {domain, proxy_address} with a
global unique index on the full hostname; dedicated = (domain ==
proxy_address). Bootstrap becomes an explicit POST carrying exactly one
of proxy_address (server allocates an adjective-noun label beneath it)
or endpoint (claimed verbatim, address-first); provider create loses its
bootstrap side effect. PUT is a full replace with every field required —
the immutable identity fields must be echoed unchanged and a mismatch is
rejected with 422. A guarded DELETE releases the endpoint: refused with
412 while providers exist or a proxy is actively serving the endpoint
hostname (matched case-insensitively); re-creating bootstraps fresh. A
self-addressed pin excludes its address from the account's cluster allow
list, and the live mapping update path now addresses the serving proxy
from the synthesized service. Existing rows are migrated on all three
store engines.
2026-08-10 19:06:55 +02:00

181 lines
6.2 KiB
Go

package labelgen
import (
"math/rand"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPickUnique_DeterministicWithSeededRng locks the property the
// caller relies on: same seed + same taken set → same pick. Without
// that, the bootstrap flow can't reproduce a label across retries.
func TestPickUnique_DeterministicWithSeededRng(t *testing.T) {
taken := map[string]struct{}{}
rngA := rand.New(rand.NewSource(42))
rngB := rand.New(rand.NewSource(42))
a := PickUnique(rngA, taken, "abcd")
b := PickUnique(rngB, taken, "abcd")
assert.Equal(t, a, b, "Same seed and taken set must produce identical pick")
}
// TestPickUnique_AvoidsTakenWordsWhenMostAreReserved seeds taken with
// every word in the pool except a handful and confirms PickUnique
// finds one of the remaining free entries instead of returning the
// fallback form.
func TestPickUnique_AvoidsTakenWordsWhenMostAreReserved(t *testing.T) {
pool := uniqueWords()
require.NotEmpty(t, pool, "wordlist must be populated for the test to mean anything")
free := map[string]struct{}{
pool[0]: {},
pool[len(pool)/2]: {},
pool[len(pool)-1]: {},
}
taken := make(map[string]struct{}, len(pool))
for _, w := range pool {
if _, ok := free[w]; ok {
continue
}
taken[w] = struct{}{}
}
rng := rand.New(rand.NewSource(7))
got := PickUnique(rng, taken, "abcd")
_, isFree := free[got]
assert.True(t, isFree, "PickUnique must return one of the free words; got %q", got)
assert.NotContains(t, got, "-", "Free pick must not be the suffix fallback form")
}
// TestPickUnique_FallsBackWhenAllReserved exhausts the pool and
// confirms PickUnique appends the supplied suffix instead of
// returning a duplicate.
func TestPickUnique_FallsBackWhenAllReserved(t *testing.T) {
pool := uniqueWords()
taken := make(map[string]struct{}, len(pool))
for _, w := range pool {
taken[w] = struct{}{}
}
rng := rand.New(rand.NewSource(99))
got := PickUnique(rng, taken, "abcd")
assert.True(t, strings.HasSuffix(got, "-abcd"), "Exhausted pool must produce <word>-<suffix>; got %q", got)
prefix := strings.TrimSuffix(got, "-abcd")
found := false
for _, w := range pool {
if w == prefix {
found = true
break
}
}
assert.True(t, found, "Fallback prefix must be drawn from the wordlist; got %q", prefix)
}
// TestUniqueWords_DropsDuplicates guards against authoring slips in
// words.go: every entry must be unique and DNS-safe.
func TestUniqueWords_DropsDuplicates(t *testing.T) {
pool := uniqueWords()
seen := make(map[string]struct{}, len(pool))
for _, w := range pool {
_, dup := seen[w]
assert.False(t, dup, "Duplicate entry %q in deduplicated pool", w)
seen[w] = struct{}{}
assert.GreaterOrEqual(t, len(w), 4, "Word %q is shorter than 4 chars", w)
assert.LessOrEqual(t, len(w), 12, "Word %q is longer than 12 chars", w)
for _, r := range w {
ok := r >= 'a' && r <= 'z'
assert.True(t, ok, "Word %q contains non-lowercase-ASCII rune %q", w, r)
}
}
assert.GreaterOrEqual(t, len(pool), 500, "Pool must contain at least 500 unique words")
}
// TestPickTuple_ShapeAndPoolMembership locks the wire-visible shape: an
// adjective and a noun, each from its own pool, joined by a single hyphen so
// the result stays one DNS label.
func TestPickTuple_ShapeAndPoolMembership(t *testing.T) {
nouns := uniqueWords()
inNouns := make(map[string]struct{}, len(nouns))
for _, w := range nouns {
inNouns[w] = struct{}{}
}
inAdjectives := make(map[string]struct{}, len(adjectives))
for _, a := range adjectives {
inAdjectives[a] = struct{}{}
}
rng := rand.New(rand.NewSource(7))
for i := 0; i < 200; i++ {
got := PickTuple(rng)
parts := strings.Split(got, "-")
require.Len(t, parts, 2, "PickTuple must produce exactly two hyphen-joined words; got %q", got)
_, adjOK := inAdjectives[parts[0]]
assert.True(t, adjOK, "First half must be an adjective; %q not in adjectives (from %q)", parts[0], got)
_, nounOK := inNouns[parts[1]]
assert.True(t, nounOK, "Second half must be a noun; %q not in words (from %q)", parts[1], got)
assert.LessOrEqual(t, len(got), 63, "Label must fit a DNS label; got %q (%d chars)", got, len(got))
}
}
// TestAdjectives_AreDisjointFromNouns keeps the namespace a clean product and
// prevents nonsense like "azure-azure": a handful of the noun pool's entries
// are adjectival, and any overlap would let the same word land on both sides.
func TestAdjectives_AreDisjointFromNouns(t *testing.T) {
nouns := make(map[string]struct{}, len(uniqueWords()))
for _, w := range uniqueWords() {
nouns[w] = struct{}{}
}
for _, a := range adjectives {
_, clash := nouns[a]
assert.False(t, clash, "Adjective %q also appears in the noun pool; remove it from one list", a)
}
}
// TestAdjectives_AreDNSSafeAndDeduplicated mirrors the curation contract stated
// in words.go: lowercase ASCII, 4-12 chars, no digits or hyphens, no repeats.
func TestAdjectives_AreDNSSafeAndDeduplicated(t *testing.T) {
seen := make(map[string]struct{}, len(adjectives))
for _, a := range adjectives {
_, dup := seen[a]
assert.False(t, dup, "Duplicate adjective %q", a)
seen[a] = struct{}{}
assert.Regexp(t, `^[a-z]{4,12}$`, a, "Adjective %q must be 4-12 lowercase ASCII letters", a)
}
assert.Greater(t, len(adjectives), 150, "Adjective pool too small to give a useful namespace")
}
// TestPickTuple_DeterministicWithSeededRng documents that generation is a pure
// function of the rng, which is what makes allocation retries reproducible in tests.
func TestPickTuple_DeterministicWithSeededRng(t *testing.T) {
a := PickTuple(rand.New(rand.NewSource(42)))
b := PickTuple(rand.New(rand.NewSource(42)))
assert.Equal(t, a, b, "Same seed must yield the same tuple")
}
// TestPickTuple_SpansALargeNamespace guards the reason we moved to tuples: a
// single-word pool caps the GLOBAL namespace at 857. Drawing many tuples must
// yield overwhelmingly distinct values.
func TestPickTuple_SpansALargeNamespace(t *testing.T) {
rng := rand.New(rand.NewSource(11))
seen := make(map[string]struct{}, 2000)
for i := 0; i < 2000; i++ {
seen[PickTuple(rng)] = struct{}{}
}
assert.Greater(t, len(seen), 1900,
"2000 draws should be nearly all distinct across a ~200k namespace; got %d unique", len(seen))
}