// SPDX-License-Identifier: Apache-2.0 package grpcapi import ( "context" "net" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "git.ipng.ch/ipng/vpp-maglev/internal/checker" "git.ipng.ch/ipng/vpp-maglev/internal/config" "git.ipng.ch/ipng/vpp-maglev/internal/health" ) func makeTestChecker(ctx context.Context) *checker.Checker { cfg := &config.Config{ HealthChecker: config.HealthCheckerConfig{TransitionHistory: 5}, HealthChecks: map[string]config.HealthCheck{ "icmp": { Type: "icmp", Interval: time.Hour, // long interval: probes won't fire during tests Timeout: time.Second, Fall: 3, Rise: 2, }, }, Backends: map[string]config.Backend{ "be0": { Address: net.ParseIP("10.0.0.2"), HealthCheck: "icmp", Enabled: true, }, }, Frontends: map[string]config.Frontend{ "web": { Address: net.ParseIP("192.0.2.1"), Protocol: "tcp", Port: 80, Pools: []config.Pool{ {Name: "primary", Backends: map[string]config.PoolBackend{ "be0": {Weight: 100}, }}, }, }, }, } c := checker.New(cfg) go c.Run(ctx) //nolint:errcheck // Allow the Run goroutine to initialize workers. time.Sleep(10 * time.Millisecond) return c } func startTestServer(t *testing.T, ctx context.Context, c *checker.Checker) (MaglevClient, func()) { t.Helper() lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := grpc.NewServer() RegisterMaglevServer(srv, NewServer(ctx, c, nil, "", nil)) go srv.Serve(lis) //nolint:errcheck conn, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { t.Fatalf("dial: %v", err) } return NewMaglevClient(conn), func() { _ = conn.Close() srv.Stop() } } func TestListFrontends(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() resp, err := client.ListFrontends(ctx, &ListFrontendsRequest{}) if err != nil { t.Fatalf("ListFrontends: %v", err) } if len(resp.FrontendNames) != 1 || resp.FrontendNames[0] != "web" { t.Errorf("ListFrontends: got %v, want [web]", resp.FrontendNames) } } func TestGetFrontend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() info, err := client.GetFrontend(ctx, &GetFrontendRequest{Name: "web"}) if err != nil { t.Fatalf("GetFrontend: %v", err) } if info.Address != "192.0.2.1" { t.Errorf("GetFrontend address: got %q, want 192.0.2.1", info.Address) } if info.Port != 80 { t.Errorf("GetFrontend port: got %d, want 80", info.Port) } if len(info.Pools) != 1 || info.Pools[0].Name != "primary" { t.Errorf("GetFrontend pools: got %v, want [{primary [be0]}]", info.Pools) } if len(info.Pools[0].Backends) != 1 || info.Pools[0].Backends[0].Name != "be0" { t.Errorf("GetFrontend pools[0].backends: got %v, want [{be0 100}]", info.Pools[0].Backends) } if info.Pools[0].Backends[0].Weight != 100 { t.Errorf("GetFrontend pools[0].backends[0].weight: got %d, want 100", info.Pools[0].Backends[0].Weight) } } func TestGetFrontendNotFound(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() _, err := client.GetFrontend(ctx, &GetFrontendRequest{Name: "nope"}) if err == nil { t.Error("expected error for unknown frontend") } } func TestListBackends(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() resp, err := client.ListBackends(ctx, &ListBackendsRequest{}) if err != nil { t.Fatalf("ListBackends: %v", err) } if len(resp.BackendNames) != 1 || resp.BackendNames[0] != "be0" { t.Errorf("ListBackends: got %v, want [be0]", resp.BackendNames) } } func TestGetBackend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() info, err := client.GetBackend(ctx, &GetBackendRequest{Name: "be0"}) if err != nil { t.Fatalf("GetBackend: %v", err) } if info.State != health.StateUnknown.String() { t.Errorf("initial state: got %q, want unknown", info.State) } if !info.Enabled { t.Error("expected enabled=true") } if info.Healthcheck != "icmp" { t.Errorf("healthcheck: got %q, want icmp", info.Healthcheck) } } func TestGetBackendNotFound(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() _, err := client.GetBackend(ctx, &GetBackendRequest{Name: "nope"}) if err == nil { t.Error("expected error for unknown backend") } } func TestPauseResumeBackend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() info, err := client.PauseBackend(ctx, &BackendRequest{Name: "be0"}) if err != nil { t.Fatalf("PauseBackend: %v", err) } if info.State != health.StatePaused.String() { t.Errorf("after pause: got %q, want paused", info.State) } info, err = client.ResumeBackend(ctx, &BackendRequest{Name: "be0"}) if err != nil { t.Fatalf("ResumeBackend: %v", err) } if info.State != health.StateUnknown.String() { t.Errorf("after resume: got %q, want unknown", info.State) } } func TestSetFrontendPoolBackendWeight(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() info, err := client.SetFrontendPoolBackendWeight(ctx, &SetWeightRequest{ Frontend: "web", Pool: "primary", Backend: "be0", Weight: 42, }) if err != nil { t.Fatalf("SetFrontendPoolBackendWeight: %v", err) } if len(info.Pools) == 0 || len(info.Pools[0].Backends) == 0 { t.Fatal("response missing pools/backends") } if info.Pools[0].Backends[0].Weight != 42 { t.Errorf("weight: got %d, want 42", info.Pools[0].Backends[0].Weight) } // Invalid weight. _, err = client.SetFrontendPoolBackendWeight(ctx, &SetWeightRequest{ Frontend: "web", Pool: "primary", Backend: "be0", Weight: 101, }) if err == nil { t.Error("expected error for weight 101") } // Unknown frontend. _, err = client.SetFrontendPoolBackendWeight(ctx, &SetWeightRequest{ Frontend: "nope", Pool: "primary", Backend: "be0", Weight: 50, }) if err == nil { t.Error("expected error for unknown frontend") } } func TestEnableDisableBackend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() info, err := client.DisableBackend(ctx, &BackendRequest{Name: "be0"}) if err != nil { t.Fatalf("DisableBackend: %v", err) } if info.State != "disabled" { t.Errorf("after disable: got %q, want disabled", info.State) } if info.Enabled { t.Error("after disable: Enabled should be false") } info, err = client.EnableBackend(ctx, &BackendRequest{Name: "be0"}) if err != nil { t.Fatalf("EnableBackend: %v", err) } if info.State != "unknown" { t.Errorf("after enable: got %q, want unknown", info.State) } if !info.Enabled { t.Error("after enable: Enabled should be true") } } func TestListHealthChecks(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() resp, err := client.ListHealthChecks(ctx, &ListHealthChecksRequest{}) if err != nil { t.Fatalf("ListHealthChecks: %v", err) } if len(resp.Names) != 1 || resp.Names[0] != "icmp" { t.Errorf("ListHealthChecks: got %v, want [icmp]", resp.Names) } } func TestGetHealthCheck(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() info, err := client.GetHealthCheck(ctx, &GetHealthCheckRequest{Name: "icmp"}) if err != nil { t.Fatalf("GetHealthCheck: %v", err) } if info.Type != "icmp" { t.Errorf("type: got %q, want icmp", info.Type) } if info.Fall != 3 || info.Rise != 2 { t.Errorf("fall/rise: got %d/%d, want 3/2", info.Fall, info.Rise) } } func TestGetHealthCheckNotFound(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() _, err := client.GetHealthCheck(ctx, &GetHealthCheckRequest{Name: "nope"}) if err == nil { t.Error("expected error for unknown healthcheck") } } func TestWatchEventsServerShutdown(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) // Use a separate server context so we can cancel it independently. srvCtx, srvCancel := context.WithCancel(ctx) client, cleanup := startTestServer(t, srvCtx, c) defer cleanup() stream, err := client.WatchEvents(ctx, &WatchRequest{}) if err != nil { t.Fatalf("WatchEvents: %v", err) } // Drain the initial synthetic snapshots (one per backend, one per frontend). for i := 0; i < 2; i++ { if _, err := stream.Recv(); err != nil { t.Fatalf("initial Recv %d: %v", i, err) } } // Cancel the server context; the stream must terminate. srvCancel() _, err = stream.Recv() if err == nil { t.Fatal("expected stream to close after server shutdown, got nil error") } } func TestWatchEventsBackend(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() stream, err := client.WatchEvents(ctx, &WatchRequest{}) if err != nil { t.Fatalf("WatchEvents: %v", err) } // Should receive the current state for be0 immediately as a BackendEvent. ev, err := stream.Recv() if err != nil { t.Fatalf("Recv: %v", err) } be, ok := ev.Event.(*Event_Backend) if !ok { t.Fatalf("expected BackendEvent, got %T", ev.Event) } if be.Backend.BackendName != "be0" { t.Errorf("initial event: backend=%q, want be0", be.Backend.BackendName) } } func TestWatchEventsLogOnly(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() f := false stream, err := client.WatchEvents(ctx, &WatchRequest{Backend: &f, Frontend: &f}) if err != nil { t.Fatalf("WatchEvents: %v", err) } // No initial snapshot should arrive (backend disabled). Verify by checking // that the stream has no immediately-readable event. recvCh := make(chan *Event, 1) go func() { ev, _ := stream.Recv() recvCh <- ev }() select { case ev := <-recvCh: if _, isLog := ev.Event.(*Event_Log); !isLog { t.Errorf("expected only LogEvents, got %T", ev.Event) } case <-time.After(50 * time.Millisecond): // expected: no backend snapshot arrived } } func TestWatchEventsInvalidLogLevel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() c := makeTestChecker(ctx) client, cleanup := startTestServer(t, ctx, c) defer cleanup() // For streaming RPCs the server error arrives on the first Recv, not on the // initial call. stream, err := client.WatchEvents(ctx, &WatchRequest{LogLevel: "verbose"}) if err != nil { t.Fatalf("WatchEvents: %v", err) } _, err = stream.Recv() if err == nil { t.Fatal("expected error for invalid log_level") } }