// Copyright (c) 2026, Pim van Pelt package vpp import ( "net" "testing" "git.ipng.ch/ipng/vpp-maglev/internal/config" "git.ipng.ch/ipng/vpp-maglev/internal/health" ) // TestAsFromBackend locks down the state → (weight, flush) truth table. // This is the single source of truth for how maglevd decides what to // program into VPP for each backend state. If this test needs updating // the behavior has deliberately changed. func TestAsFromBackend(t *testing.T) { cases := []struct { name string poolIdx int activePool int state health.State cfgWeight int wantWeight uint8 wantFlush bool }{ // up in active pool → configured weight, no flush {"up active w100", 0, 0, health.StateUp, 100, 100, false}, {"up active w50", 0, 0, health.StateUp, 50, 50, false}, {"up active w0", 0, 0, health.StateUp, 0, 0, false}, {"up active clamp-high", 0, 0, health.StateUp, 150, 100, false}, {"up active clamp-low", 0, 0, health.StateUp, -5, 0, false}, // up in non-active pool → standby (weight 0), no flush {"up standby pool0 active=1", 0, 1, health.StateUp, 100, 0, false}, {"up standby pool1 active=0", 1, 0, health.StateUp, 100, 0, false}, {"up standby pool2 active=0", 2, 0, health.StateUp, 100, 0, false}, // up in secondary, promoted because pool[1] is now active {"up failover pool1 active=1", 1, 1, health.StateUp, 100, 100, false}, // unknown → off, drain {"unknown pool0 active=0", 0, 0, health.StateUnknown, 100, 0, false}, {"unknown pool1 active=0", 1, 0, health.StateUnknown, 100, 0, false}, // down → off, drain (probe might be wrong) {"down pool0 active=0", 0, 0, health.StateDown, 100, 0, false}, {"down pool1 active=1", 1, 1, health.StateDown, 100, 0, false}, // paused → off, drain (graceful maintenance) {"paused pool0 active=0", 0, 0, health.StatePaused, 100, 0, false}, // disabled → off, flush (hard stop) {"disabled pool0 active=0", 0, 0, health.StateDisabled, 100, 0, true}, {"disabled pool1 active=1", 1, 1, health.StateDisabled, 100, 0, true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { w, f := asFromBackend(tc.poolIdx, tc.activePool, tc.state, tc.cfgWeight) if w != tc.wantWeight { t.Errorf("weight: got %d, want %d", w, tc.wantWeight) } if f != tc.wantFlush { t.Errorf("flush: got %v, want %v", f, tc.wantFlush) } }) } } // TestActivePoolIndex locks down the priority-failover selector: the first // pool containing at least one up backend is the active pool. Default 0. func TestActivePoolIndex(t *testing.T) { mkFE := func(pools ...[]string) config.Frontend { out := make([]config.Pool, len(pools)) for i, p := range pools { out[i] = config.Pool{Name: "p", Backends: map[string]config.PoolBackend{}} for _, name := range p { out[i].Backends[name] = config.PoolBackend{Weight: 100} } } return config.Frontend{Pools: out} } cases := []struct { name string fe config.Frontend states map[string]health.State want int }{ { name: "pool0 has up, pool1 standby", fe: mkFE([]string{"a", "b"}, []string{"c", "d"}), states: map[string]health.State{"a": health.StateUp, "b": health.StateDown, "c": health.StateUp, "d": health.StateUp}, want: 0, }, { name: "pool0 all down, pool1 has up → failover", fe: mkFE([]string{"a", "b"}, []string{"c", "d"}), states: map[string]health.State{"a": health.StateDown, "b": health.StateDown, "c": health.StateUp, "d": health.StateUp}, want: 1, }, { name: "pool0 all disabled, pool1 has up → failover", fe: mkFE([]string{"a", "b"}, []string{"c"}), states: map[string]health.State{"a": health.StateDisabled, "b": health.StateDisabled, "c": health.StateUp}, want: 1, }, { name: "pool0 all paused, pool1 has up → failover", fe: mkFE([]string{"a"}, []string{"c"}), states: map[string]health.State{"a": health.StatePaused, "c": health.StateUp}, want: 1, }, { name: "pool0 all unknown (startup), pool1 up → pool1", fe: mkFE([]string{"a"}, []string{"c"}), states: map[string]health.State{"a": health.StateUnknown, "c": health.StateUp}, want: 1, }, { name: "nothing up anywhere → default 0", fe: mkFE([]string{"a"}, []string{"c"}), states: map[string]health.State{"a": health.StateDown, "c": health.StateDown}, want: 0, }, { name: "1 up in pool0 is enough", fe: mkFE([]string{"a", "b", "c"}, []string{"d"}), states: map[string]health.State{"a": health.StateDown, "b": health.StateDown, "c": health.StateUp, "d": health.StateUp}, want: 0, }, { name: "three tiers, pool0 and pool1 both empty → pool2", fe: mkFE([]string{"a"}, []string{"b"}, []string{"c"}), states: map[string]health.State{"a": health.StateDown, "b": health.StateDown, "c": health.StateUp}, want: 2, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := activePoolIndex(tc.fe, tc.states) if got != tc.want { t.Errorf("got pool %d, want pool %d", got, tc.want) } }) } } // fakeStateSource implements StateSource from a static map. type fakeStateSource struct { cfg *config.Config states map[string]health.State } func (f *fakeStateSource) Config() *config.Config { return f.cfg } func (f *fakeStateSource) BackendState(name string) (health.State, bool) { s, ok := f.states[name] return s, ok } // TestDesiredFromFrontendFailover is the end-to-end integration test for // priority-failover: given a frontend with two pools, the desired weights // flip between pools based on which has any up backends. func TestDesiredFromFrontendFailover(t *testing.T) { ip := func(s string) net.IP { return net.ParseIP(s).To4() } cfg := &config.Config{ Backends: map[string]config.Backend{ "p1": {Address: ip("10.0.0.1"), Enabled: true}, "p2": {Address: ip("10.0.0.2"), Enabled: true}, "s1": {Address: ip("10.0.0.11"), Enabled: true}, "s2": {Address: ip("10.0.0.12"), Enabled: true}, }, } fe := config.Frontend{ Address: ip("192.0.2.1"), Protocol: "tcp", Port: 80, Pools: []config.Pool{ {Name: "primary", Backends: map[string]config.PoolBackend{ "p1": {Weight: 100}, "p2": {Weight: 100}, }}, {Name: "fallback", Backends: map[string]config.PoolBackend{ "s1": {Weight: 100}, "s2": {Weight: 100}, }}, }, } tests := []struct { name string states map[string]health.State want map[string]uint8 // backend IP → expected weight }{ { name: "primary all up → primary serves, secondary standby", states: map[string]health.State{ "p1": health.StateUp, "p2": health.StateUp, "s1": health.StateUp, "s2": health.StateUp, }, want: map[string]uint8{ "10.0.0.1": 100, "10.0.0.2": 100, "10.0.0.11": 0, "10.0.0.12": 0, }, }, { name: "primary 1 up → primary still serves", states: map[string]health.State{ "p1": health.StateDown, "p2": health.StateUp, "s1": health.StateUp, "s2": health.StateUp, }, want: map[string]uint8{ "10.0.0.1": 0, "10.0.0.2": 100, "10.0.0.11": 0, "10.0.0.12": 0, }, }, { name: "primary all down → failover to secondary", states: map[string]health.State{ "p1": health.StateDown, "p2": health.StateDown, "s1": health.StateUp, "s2": health.StateUp, }, want: map[string]uint8{ "10.0.0.1": 0, "10.0.0.2": 0, "10.0.0.11": 100, "10.0.0.12": 100, }, }, { name: "primary all disabled → failover", states: map[string]health.State{ "p1": health.StateDisabled, "p2": health.StateDisabled, "s1": health.StateUp, "s2": health.StateUp, }, want: map[string]uint8{ "10.0.0.1": 0, "10.0.0.2": 0, "10.0.0.11": 100, "10.0.0.12": 100, }, }, { name: "everything down → all zero, no serving", states: map[string]health.State{ "p1": health.StateDown, "p2": health.StateDown, "s1": health.StateDown, "s2": health.StateDown, }, want: map[string]uint8{ "10.0.0.1": 0, "10.0.0.2": 0, "10.0.0.11": 0, "10.0.0.12": 0, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { src := &fakeStateSource{cfg: cfg, states: tc.states} d := desiredFromFrontend(cfg, fe, src) for addr, wantW := range tc.want { got, ok := d.ASes[addr] if !ok { t.Errorf("%s: missing from desired set", addr) continue } if got.Weight != wantW { t.Errorf("%s: weight got %d, want %d", addr, got.Weight, wantW) } } if len(d.ASes) != len(tc.want) { t.Errorf("got %d ASes, want %d", len(d.ASes), len(tc.want)) } }) } }