Files
vpp-maglev/internal/grpcapi/server_test.go

448 lines
12 KiB
Go

// 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")
}
}