package mcproxy import ( "context" "io" "log/slog" "net" "path/filepath" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/test/bufconn" pb "git.wntrmute.dev/kyle/mc-proxy/gen/mc_proxy/v1" "git.wntrmute.dev/kyle/mc-proxy/internal/config" "git.wntrmute.dev/kyle/mc-proxy/internal/db" "git.wntrmute.dev/kyle/mc-proxy/internal/firewall" "git.wntrmute.dev/kyle/mc-proxy/internal/grpcserver" "git.wntrmute.dev/kyle/mc-proxy/internal/server" ) func setupTestClient(t *testing.T) *Client { t.Helper() // Database in temp dir. dbPath := filepath.Join(t.TempDir(), "test.db") store, err := db.Open(dbPath) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { store.Close() }) if err := store.Migrate(); err != nil { t.Fatalf("migrate: %v", err) } // Seed with one listener and one route. listeners := []config.Listener{ { Addr: ":443", Routes: []config.Route{ {Hostname: "example.test", Backend: "127.0.0.1:8443"}, }, }, } fw := config.Firewall{ BlockedIPs: []string{"10.0.0.1"}, } if err := store.Seed(listeners, fw); err != nil { t.Fatalf("seed: %v", err) } // Build server with matching in-memory state. fwObj, err := firewall.New("", []string{"10.0.0.1"}, nil, nil, 0, 0) if err != nil { t.Fatalf("firewall: %v", err) } cfg := &config.Config{ Proxy: config.Proxy{ ConnectTimeout: config.Duration{Duration: 5 * time.Second}, IdleTimeout: config.Duration{Duration: 30 * time.Second}, ShutdownTimeout: config.Duration{Duration: 5 * time.Second}, }, } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) // Load listener data from DB to get correct IDs. dbListeners, err := store.ListListeners() if err != nil { t.Fatalf("list listeners: %v", err) } var listenerData []server.ListenerData for _, l := range dbListeners { dbRoutes, err := store.ListRoutes(l.ID) if err != nil { t.Fatalf("list routes: %v", err) } routes := make(map[string]server.RouteInfo, len(dbRoutes)) for _, r := range dbRoutes { routes[r.Hostname] = server.RouteInfo{ Backend: r.Backend, Mode: r.Mode, } } listenerData = append(listenerData, server.ListenerData{ ID: l.ID, Addr: l.Addr, ProxyProtocol: l.ProxyProtocol, Routes: routes, }) } srv := server.New(cfg, fwObj, listenerData, logger, "test-version") // Set up bufconn gRPC server. lis := bufconn.Listen(1024 * 1024) grpcSrv := grpc.NewServer() pb.RegisterProxyAdminServiceServer(grpcSrv, &testAdminServer{ srv: srv, store: store, logger: logger, }) // Register health service. healthServer := health.NewServer() healthServer.SetServingStatus("", healthpb.HealthCheckResponse_SERVING) healthpb.RegisterHealthServer(grpcSrv, healthServer) go func() { if err := grpcSrv.Serve(lis); err != nil { t.Logf("grpc serve: %v", err) } }() t.Cleanup(grpcSrv.Stop) conn, err := grpc.NewClient("passthrough://bufconn", grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { t.Fatalf("dial bufconn: %v", err) } t.Cleanup(func() { conn.Close() }) return &Client{ conn: conn, admin: pb.NewProxyAdminServiceClient(conn), health: healthpb.NewHealthClient(conn), } } // testAdminServer is a minimal implementation for testing. // It delegates to the real grpcserver.AdminServer logic. type testAdminServer struct { pb.UnimplementedProxyAdminServiceServer srv *server.Server store *db.Store logger *slog.Logger } func (s *testAdminServer) GetStatus(ctx context.Context, req *pb.GetStatusRequest) (*pb.GetStatusResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).GetStatus(ctx, req) } func (s *testAdminServer) ListRoutes(ctx context.Context, req *pb.ListRoutesRequest) (*pb.ListRoutesResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).ListRoutes(ctx, req) } func (s *testAdminServer) AddRoute(ctx context.Context, req *pb.AddRouteRequest) (*pb.AddRouteResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).AddRoute(ctx, req) } func (s *testAdminServer) RemoveRoute(ctx context.Context, req *pb.RemoveRouteRequest) (*pb.RemoveRouteResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).RemoveRoute(ctx, req) } func (s *testAdminServer) GetFirewallRules(ctx context.Context, req *pb.GetFirewallRulesRequest) (*pb.GetFirewallRulesResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).GetFirewallRules(ctx, req) } func (s *testAdminServer) AddFirewallRule(ctx context.Context, req *pb.AddFirewallRuleRequest) (*pb.AddFirewallRuleResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).AddFirewallRule(ctx, req) } func (s *testAdminServer) RemoveFirewallRule(ctx context.Context, req *pb.RemoveFirewallRuleRequest) (*pb.RemoveFirewallRuleResponse, error) { return grpcserver.NewAdminServer(s.srv, s.store, s.logger).RemoveFirewallRule(ctx, req) } func TestClientGetStatus(t *testing.T) { client := setupTestClient(t) ctx := context.Background() status, err := client.GetStatus(ctx) if err != nil { t.Fatalf("GetStatus: %v", err) } if status.Version != "test-version" { t.Errorf("got version %q, want %q", status.Version, "test-version") } if len(status.Listeners) != 1 { t.Errorf("got %d listeners, want 1", len(status.Listeners)) } if status.Listeners[0].Addr != ":443" { t.Errorf("got listener addr %q, want %q", status.Listeners[0].Addr, ":443") } } func TestClientListRoutes(t *testing.T) { client := setupTestClient(t) ctx := context.Background() routes, err := client.ListRoutes(ctx, ":443") if err != nil { t.Fatalf("ListRoutes: %v", err) } if len(routes) != 1 { t.Fatalf("got %d routes, want 1", len(routes)) } if routes[0].Hostname != "example.test" { t.Errorf("got hostname %q, want %q", routes[0].Hostname, "example.test") } if routes[0].Backend != "127.0.0.1:8443" { t.Errorf("got backend %q, want %q", routes[0].Backend, "127.0.0.1:8443") } } func TestClientAddRemoveRoute(t *testing.T) { client := setupTestClient(t) ctx := context.Background() // Add a new route. err := client.AddRoute(ctx, ":443", Route{Hostname: "new.test", Backend: "127.0.0.1:9443"}) if err != nil { t.Fatalf("AddRoute: %v", err) } // Verify it was added. routes, err := client.ListRoutes(ctx, ":443") if err != nil { t.Fatalf("ListRoutes: %v", err) } if len(routes) != 2 { t.Fatalf("got %d routes after add, want 2", len(routes)) } // Remove the route. err = client.RemoveRoute(ctx, ":443", "new.test") if err != nil { t.Fatalf("RemoveRoute: %v", err) } // Verify it was removed. routes, err = client.ListRoutes(ctx, ":443") if err != nil { t.Fatalf("ListRoutes: %v", err) } if len(routes) != 1 { t.Fatalf("got %d routes after remove, want 1", len(routes)) } } func TestClientGetFirewallRules(t *testing.T) { client := setupTestClient(t) ctx := context.Background() rules, err := client.GetFirewallRules(ctx) if err != nil { t.Fatalf("GetFirewallRules: %v", err) } if len(rules) != 1 { t.Fatalf("got %d rules, want 1", len(rules)) } if rules[0].Type != FirewallRuleIP { t.Errorf("got type %q, want %q", rules[0].Type, FirewallRuleIP) } if rules[0].Value != "10.0.0.1" { t.Errorf("got value %q, want %q", rules[0].Value, "10.0.0.1") } } func TestClientAddRemoveFirewallRule(t *testing.T) { client := setupTestClient(t) ctx := context.Background() // Add a CIDR rule. err := client.AddFirewallRule(ctx, FirewallRuleCIDR, "192.168.0.0/16") if err != nil { t.Fatalf("AddFirewallRule: %v", err) } // Verify it was added. rules, err := client.GetFirewallRules(ctx) if err != nil { t.Fatalf("GetFirewallRules: %v", err) } if len(rules) != 2 { t.Fatalf("got %d rules after add, want 2", len(rules)) } // Remove the rule. err = client.RemoveFirewallRule(ctx, FirewallRuleCIDR, "192.168.0.0/16") if err != nil { t.Fatalf("RemoveFirewallRule: %v", err) } // Verify it was removed. rules, err = client.GetFirewallRules(ctx) if err != nil { t.Fatalf("GetFirewallRules: %v", err) } if len(rules) != 1 { t.Fatalf("got %d rules after remove, want 1", len(rules)) } } func TestClientCheckHealth(t *testing.T) { client := setupTestClient(t) ctx := context.Background() status, err := client.CheckHealth(ctx) if err != nil { t.Fatalf("CheckHealth: %v", err) } if status != HealthServing { t.Errorf("got health status %v, want %v", status, HealthServing) } } func TestHealthStatusString(t *testing.T) { tests := []struct { status HealthStatus want string }{ {HealthUnknown, "UNKNOWN"}, {HealthServing, "SERVING"}, {HealthNotServing, "NOT_SERVING"}, } for _, tt := range tests { if got := tt.status.String(); got != tt.want { t.Errorf("HealthStatus(%d).String() = %q, want %q", tt.status, got, tt.want) } } }