mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
The CLI printed a fixed name in its examples and help text while already taking that name as a parameter, so `operator` and `rpc` now interpolate what they were handed, the way `cluster` already did. The config file's per-env URL block goes with it. Nothing read it: EnvURLs computes a service address from the environment's domain suffix, which is what every command actually calls, so the block was a second unasked answer to where a service lives. An older config still loads — an unknown key is ignored — and its env block is dropped on the next write. The default org is no longer a name. The Tasks queue is named for this repo rather than written out three times. Fixture hostnames follow the shape the operator emits, which one of those files was already using. crdt/privacy_fhe.go cited a PDF that is not in the tree; the two figures it backed are in the design doc it already points at. Co-authored-by: Hanzo Dev <[email protected]>
186 lines
4.6 KiB
Go
186 lines
4.6 KiB
Go
// Copyright (c) 2025, Hanzo Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package network
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakeResolver is a test Resolver: hostname → addresses map. Safe for
|
|
// concurrent use; Set rewrites the table atomically.
|
|
type fakeResolver struct {
|
|
mu sync.Mutex
|
|
table map[string][]string
|
|
}
|
|
|
|
func (f *fakeResolver) LookupHost(_ context.Context, host string) ([]string, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
addrs, ok := f.table[host]
|
|
if !ok {
|
|
return nil, &hostNotFound{host: host}
|
|
}
|
|
return append([]string(nil), addrs...), nil
|
|
}
|
|
|
|
func (f *fakeResolver) Set(host string, addrs []string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.table == nil {
|
|
f.table = map[string][]string{}
|
|
}
|
|
f.table[host] = addrs
|
|
}
|
|
|
|
type hostNotFound struct{ host string }
|
|
|
|
func (h *hostNotFound) Error() string { return "no such host: " + h.host }
|
|
|
|
// TestDNSMembershipSingleton: BASE_PEERS empty → Members() = [self].
|
|
func TestDNSMembershipSingleton(t *testing.T) {
|
|
m := newDNSMembership(context.Background(), "base-0", nil, &fakeResolver{}, time.Hour)
|
|
defer m.Close()
|
|
got := m.Members()
|
|
if len(got) != 1 || got[0] != "base-0" {
|
|
t.Errorf("singleton: Members() = %v, want [base-0]", got)
|
|
}
|
|
}
|
|
|
|
// TestDNSMembershipHeadless: one headless-Service-style seed resolves
|
|
// to multiple IPs → one member per IP.
|
|
func TestDNSMembershipHeadless(t *testing.T) {
|
|
r := &fakeResolver{}
|
|
r.Set("base.hanzo.svc.cluster.local", []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"})
|
|
m := newDNSMembership(
|
|
context.Background(),
|
|
"base-0",
|
|
[]string{"base.hanzo.svc.cluster.local:9999"},
|
|
r,
|
|
time.Hour,
|
|
)
|
|
defer m.Close()
|
|
|
|
got := m.Members()
|
|
if len(got) != 4 { // 3 IPs + self
|
|
t.Errorf("Members(): got %d, want 4", len(got))
|
|
}
|
|
// Port must be carried onto resolved addresses.
|
|
for _, id := range got {
|
|
if id == "base-0" {
|
|
continue
|
|
}
|
|
if !stringsHasSuffix(string(id), ":9999") {
|
|
t.Errorf("member %q missing :9999 port", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDNSMembershipCompose: three static compose service names, each
|
|
// resolving to one IP → 4 members (3 + self).
|
|
func TestDNSMembershipCompose(t *testing.T) {
|
|
r := &fakeResolver{}
|
|
r.Set("base-0", []string{"172.18.0.2"})
|
|
r.Set("base-1", []string{"172.18.0.3"})
|
|
r.Set("base-2", []string{"172.18.0.4"})
|
|
|
|
m := newDNSMembership(
|
|
context.Background(),
|
|
"base-0",
|
|
[]string{"base-0:9999", "base-1:9999", "base-2:9999"},
|
|
r,
|
|
time.Hour,
|
|
)
|
|
defer m.Close()
|
|
|
|
got := m.Members()
|
|
if len(got) != 4 {
|
|
t.Errorf("Members(): got %d, want 4 (3 peers + self)", len(got))
|
|
}
|
|
}
|
|
|
|
// TestDNSMembershipScaleUp: membership grows when DNS starts resolving
|
|
// new addresses. Simulates kubectl scale sts --replicas=3 landing.
|
|
func TestDNSMembershipScaleUp(t *testing.T) {
|
|
r := &fakeResolver{}
|
|
r.Set("base.svc", []string{"10.0.0.1"})
|
|
|
|
m := newDNSMembership(
|
|
context.Background(),
|
|
"base-0",
|
|
[]string{"base.svc:9999"},
|
|
r,
|
|
10*time.Millisecond,
|
|
)
|
|
defer m.Close()
|
|
|
|
initial := m.Members()
|
|
if len(initial) != 2 {
|
|
t.Fatalf("initial: got %d, want 2", len(initial))
|
|
}
|
|
|
|
watch := m.Watch(context.Background())
|
|
<-watch // drain initial snapshot
|
|
|
|
r.Set("base.svc", []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"})
|
|
|
|
select {
|
|
case snap := <-watch:
|
|
if len(snap) != 4 {
|
|
t.Errorf("after scale-up: got %d members, want 4", len(snap))
|
|
}
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("membership watcher did not fire on DNS change")
|
|
}
|
|
}
|
|
|
|
// TestDNSMembershipScaleDown: membership shrinks when DNS stops
|
|
// resolving an address. Simulates pod termination.
|
|
func TestDNSMembershipScaleDown(t *testing.T) {
|
|
r := &fakeResolver{}
|
|
r.Set("base.svc", []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"})
|
|
|
|
m := newDNSMembership(
|
|
context.Background(),
|
|
"base-0",
|
|
[]string{"base.svc:9999"},
|
|
r,
|
|
10*time.Millisecond,
|
|
)
|
|
defer m.Close()
|
|
|
|
if len(m.Members()) != 4 {
|
|
t.Fatalf("initial: got %d, want 4", len(m.Members()))
|
|
}
|
|
|
|
watch := m.Watch(context.Background())
|
|
<-watch // drain
|
|
|
|
r.Set("base.svc", []string{"10.0.0.1"}) // 2 pods terminated
|
|
|
|
select {
|
|
case snap := <-watch:
|
|
if len(snap) != 2 {
|
|
t.Errorf("after scale-down: got %d, want 2", len(snap))
|
|
}
|
|
case <-time.After(500 * time.Millisecond):
|
|
t.Fatal("membership watcher did not fire on DNS shrink")
|
|
}
|
|
}
|
|
|
|
// TestStaticMembership: in-process test-only impl, fixed set plus self.
|
|
func TestStaticMembership(t *testing.T) {
|
|
m := NewStaticMembership("a", []string{"b", "c"})
|
|
got := m.Members()
|
|
if len(got) != 3 {
|
|
t.Errorf("Members(): got %d, want 3", len(got))
|
|
}
|
|
}
|
|
|
|
func stringsHasSuffix(s, suffix string) bool {
|
|
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
|
|
}
|