// 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" ) // TestParseLBVIPSnapshot pins the parser for `show lb vips verbose` output. // The text below is a synthetic sample that mirrors format_lb_vip_detailed // in src/plugins/lb/lb.c: a header line per VIP optionally carrying the // src_ip_sticky token, followed by a protocol:/port: sub-line for non all- // port VIPs. If VPP changes this format the test will fail loudly — the // scrape is a temporary workaround until lb_vip_v2_dump exists. func TestParseLBVIPSnapshot(t *testing.T) { text := ` ip4-gre4 [1] 192.0.2.1/32 src_ip_sticky new_size:1024 protocol:6 port:80 counters: ip4-gre4 [2] 192.0.2.2/32 new_size:1024 protocol:17 port:53 ip6-gre6 [3] 2001:db8::1/128 src_ip_sticky new_size:1024 protocol:6 port:443 ip4-gre4 [4] 192.0.2.3/32 new_size:1024 ` got := parseLBVIPSnapshot(text) want := map[vipKey]lbVIPSnapshot{ {prefix: "192.0.2.1/32", protocol: 6, port: 80}: {index: 1, sticky: true}, {prefix: "192.0.2.2/32", protocol: 17, port: 53}: {index: 2, sticky: false}, {prefix: "2001:db8::1/128", protocol: 6, port: 443}: {index: 3, sticky: true}, {prefix: "192.0.2.3/32", protocol: 255, port: 0}: {index: 4, sticky: false}, // all-port VIP } if len(got) != len(want) { t.Errorf("got %d entries, want %d: %#v", len(got), len(want), got) } for k, v := range want { g, ok := got[k] if !ok { t.Errorf("missing key %+v", k) continue } if g != v { t.Errorf("key %+v: got %+v, want %+v", k, g, v) } } } // 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 in the VPP sync path: given a frontend with two pools, // the desired weights flip between pools based on which has any up backends. // This exercises vpp.desiredFromFrontend which wraps the pure helpers in // the health package; those helpers are unit-tested separately in health. 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)) } }) } }