package grpc import ( "context" "log/slog" "sync" "testing" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) type recordingHandler struct { mu sync.Mutex levels []slog.Level } func (h *recordingHandler) Enabled(context.Context, slog.Level) bool { return true } func (h *recordingHandler) Handle(ctx context.Context, record slog.Record) error { h.mu.Lock() defer h.mu.Unlock() h.levels = append(h.levels, record.Level) return nil } func (h *recordingHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return h } func (h *recordingHandler) WithGroup(name string) slog.Handler { return h } func TestLoggingUnaryInterceptor(t *testing.T) { tests := []struct { name string handlerErr error wantLevel slog.Level }{ {name: "nil error logs info", wantLevel: slog.LevelInfo}, {name: "not found logs warn", handlerErr: status.Error(codes.NotFound, "missing"), wantLevel: slog.LevelWarn}, {name: "internal logs error", handlerErr: status.Error(codes.Internal, "boom"), wantLevel: slog.LevelError}, {name: "unimplemented logs warn", handlerErr: status.Error(codes.Unimplemented, "stub"), wantLevel: slog.LevelWarn}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := &recordingHandler{} interceptor := LoggingUnaryInterceptor(slog.New(h)) _, err := interceptor( context.Background(), "struct{}{}", &grpc.UnaryServerInfo{FullMethod: "/ha.v1.LightService/TurnOn"}, func(ctx context.Context, req any) (any, error) { return "ok", tt.handlerErr }, ) if status.Code(err) != status.Code(tt.handlerErr) { t.Fatalf("handler error = %v, want %v", err, tt.handlerErr) } h.mu.Lock() defer h.mu.Unlock() if len(h.levels) == 0 { t.Fatal("no log records captured") } got := h.levels[len(h.levels)-1] if got != tt.wantLevel { t.Fatalf("log level = %v, want %v", got, tt.wantLevel) } }) } }