diff --git a/cmd/server/main.go b/cmd/server/main.go index 7d22723f..efa51356 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -239,22 +239,8 @@ func run() int { ) } - srvOpts := []server.Option{ - server.WithEnableServices(cfg.EnableServices), - server.WithLogger(logger), - server.WithGRPCServerOptions(extraOpts...), - server.WithMaxRecvMsgBytes(cfg.GRPCMaxRecvMsgBytes), - server.WithMaxSendMsgBytes(cfg.GRPCMaxSendMsgBytes), - } - if cfg.InsecureListen { - srvOpts = append(srvOpts, server.WithInsecure()) - } else { - srvOpts = append(srvOpts, server.WithTLS(serverTLS)) - } - if rlInterceptor != nil { - srvOpts = append(srvOpts, server.WithRateLimiter(rlInterceptor)) - } - srv, err := server.New(cfg.GRPCPort, authInterceptor, srvOpts...) + srvBuild := buildServerOptions(cfg, logger, extraOpts, serverTLS, rlInterceptor) + srv, err := server.New(cfg.GRPCPort, authInterceptor, srvBuild.Opts...) if err != nil { logger.ErrorContext(ctx, "failed to create server", "error", err) return 1 @@ -346,18 +332,8 @@ func run() int { } } - gwOpts := []server.GatewayOption{ - server.WithGatewayLogger(logger), - server.WithOpenAPISpec(openAPISpec), - server.WithGatewayMaxRecvMsgBytes(cfg.GRPCMaxRecvMsgBytes), - server.WithGatewayMaxSendMsgBytes(cfg.GRPCMaxSendMsgBytes), - } - if cfg.InsecureListen { - gwOpts = append(gwOpts, server.WithGatewayInsecure()) - } else { - gwOpts = append(gwOpts, server.WithGatewayTLS(gwTLS)) - } - gw, err = server.NewGateway(ctx, cfg.HTTPPort, fmt.Sprintf("localhost:%s", cfg.GRPCPort), gwOpts...) + gwBuild := buildGatewayOptions(cfg, logger, openAPISpec, gwTLS) + gw, err = server.NewGateway(ctx, cfg.HTTPPort, fmt.Sprintf("localhost:%s", cfg.GRPCPort), gwBuild.Opts...) if err != nil { logger.ErrorContext(ctx, "failed to create HTTP gateway", "error", err) return 1 diff --git a/cmd/server/options.go b/cmd/server/options.go new file mode 100644 index 00000000..48ec05fc --- /dev/null +++ b/cmd/server/options.go @@ -0,0 +1,81 @@ +package main + +import ( + "log/slog" + + "google.golang.org/grpc" + + "github.com/opendecree/decree/internal/ratelimit" + "github.com/opendecree/decree/internal/server" +) + +// serverOptionsBuild captures the option slice handed to server.New plus the +// boolean decisions that drove it. Tests assert on the decisions so a flag +// silently dropped (read but never wired into an option) is caught. +type serverOptionsBuild struct { + Opts []server.Option + UseTLS bool + UseInsecure bool + HasRateLimiter bool +} + +func buildServerOptions( + cfg serverConfig, + logger *slog.Logger, + extraGRPCOpts []grpc.ServerOption, + serverTLS *server.TLSConfig, + rl *ratelimit.Interceptor, +) serverOptionsBuild { + out := serverOptionsBuild{ + Opts: []server.Option{ + server.WithEnableServices(cfg.EnableServices), + server.WithLogger(logger), + server.WithGRPCServerOptions(extraGRPCOpts...), + server.WithMaxRecvMsgBytes(cfg.GRPCMaxRecvMsgBytes), + server.WithMaxSendMsgBytes(cfg.GRPCMaxSendMsgBytes), + }, + } + if cfg.InsecureListen { + out.Opts = append(out.Opts, server.WithInsecure()) + out.UseInsecure = true + } else { + out.Opts = append(out.Opts, server.WithTLS(serverTLS)) + out.UseTLS = true + } + if rl != nil { + out.Opts = append(out.Opts, server.WithRateLimiter(rl)) + out.HasRateLimiter = true + } + return out +} + +// gatewayOptionsBuild mirrors serverOptionsBuild for the HTTP gateway. +type gatewayOptionsBuild struct { + Opts []server.GatewayOption + UseTLS bool + UseInsecure bool +} + +func buildGatewayOptions( + cfg serverConfig, + logger *slog.Logger, + openAPISpec []byte, + gwTLS *server.GatewayTLSConfig, +) gatewayOptionsBuild { + out := gatewayOptionsBuild{ + Opts: []server.GatewayOption{ + server.WithGatewayLogger(logger), + server.WithOpenAPISpec(openAPISpec), + server.WithGatewayMaxRecvMsgBytes(cfg.GRPCMaxRecvMsgBytes), + server.WithGatewayMaxSendMsgBytes(cfg.GRPCMaxSendMsgBytes), + }, + } + if cfg.InsecureListen { + out.Opts = append(out.Opts, server.WithGatewayInsecure()) + out.UseInsecure = true + } else { + out.Opts = append(out.Opts, server.WithGatewayTLS(gwTLS)) + out.UseTLS = true + } + return out +} diff --git a/cmd/server/options_test.go b/cmd/server/options_test.go new file mode 100644 index 00000000..1fdc6169 --- /dev/null +++ b/cmd/server/options_test.go @@ -0,0 +1,108 @@ +package main + +import ( + "io" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/time/rate" + "google.golang.org/grpc" + + "github.com/opendecree/decree/internal/ratelimit" + "github.com/opendecree/decree/internal/server" +) + +func discardLogger() *slog.Logger { + return slog.New(slog.NewTextHandler(io.Discard, nil)) +} + +func baseServerCfg() serverConfig { + return serverConfig{ + EnableServices: []string{"schema", "config", "audit"}, + GRPCMaxRecvMsgBytes: 4 << 20, + GRPCMaxSendMsgBytes: 4 << 20, + } +} + +func newTestRateLimiter() *ratelimit.Interceptor { + lim := ratelimit.NewInProcess(rate.Limit(1), 1) + return ratelimit.New(ratelimit.Config{Authenticated: lim}) +} + +func TestBuildServerOptions_TLS(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = false + tlsCfg := &server.TLSConfig{CertFile: "cert.pem", KeyFile: "key.pem"} + + got := buildServerOptions(cfg, discardLogger(), nil, tlsCfg, nil) + + assert.True(t, got.UseTLS, "expected TLS branch") + assert.False(t, got.UseInsecure, "expected insecure branch off") + assert.False(t, got.HasRateLimiter, "rate limiter should not be wired when nil") + assert.Len(t, got.Opts, 6, "5 base options + TLS option") +} + +func TestBuildServerOptions_Insecure(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = true + + got := buildServerOptions(cfg, discardLogger(), nil, nil, nil) + + assert.False(t, got.UseTLS) + assert.True(t, got.UseInsecure) + assert.Len(t, got.Opts, 6, "5 base options + Insecure option") +} + +func TestBuildServerOptions_RateLimiterWired(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = true + + got := buildServerOptions(cfg, discardLogger(), nil, nil, newTestRateLimiter()) + + assert.True(t, got.HasRateLimiter, "non-nil rate limiter must be wired into the option slice") + assert.Len(t, got.Opts, 7, "5 base options + Insecure + RateLimiter") +} + +func TestBuildServerOptions_RateLimiterAbsent(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = true + + got := buildServerOptions(cfg, discardLogger(), nil, nil, nil) + + assert.False(t, got.HasRateLimiter, "nil rate limiter must not produce a WithRateLimiter option") + assert.Len(t, got.Opts, 6) +} + +func TestBuildServerOptions_ExtraGRPCOpts(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = true + extra := []grpc.ServerOption{grpc.MaxConcurrentStreams(42)} + + got := buildServerOptions(cfg, discardLogger(), extra, nil, nil) + + assert.Len(t, got.Opts, 6, "extra grpc opts go inside WithGRPCServerOptions, not as separate options") +} + +func TestBuildGatewayOptions_TLS(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = false + gwTLS := &server.GatewayTLSConfig{CAFile: "ca.pem"} + + got := buildGatewayOptions(cfg, discardLogger(), []byte(`{}`), gwTLS) + + assert.True(t, got.UseTLS) + assert.False(t, got.UseInsecure) + assert.Len(t, got.Opts, 5, "4 base options + TLS option") +} + +func TestBuildGatewayOptions_Insecure(t *testing.T) { + cfg := baseServerCfg() + cfg.InsecureListen = true + + got := buildGatewayOptions(cfg, discardLogger(), []byte(`{}`), nil) + + assert.False(t, got.UseTLS) + assert.True(t, got.UseInsecure) + assert.Len(t, got.Opts, 5, "4 base options + Insecure option") +}