package middleware import ( "bytes" "encoding/json" "net/http/httptest" "strings" "testing" logpkg "Noooste/garage-ui/pkg/logger" "github.com/gofiber/fiber/v3" "github.com/rs/zerolog" ) // newLoggingTestApp installs RequestID + Logging middleware with a provided // zerolog.Logger writing to buf so tests can assert on the JSON output. func newLoggingTestApp(t *testing.T, buf *bytes.Buffer, handler fiber.Handler) *fiber.App { t.Helper() base := zerolog.New(buf) app := fiber.New() app.Use(RequestID()) app.Use(Logging(base)) app.Get("/ping", handler) app.Get("/health", handler) return app } func parseLines(t *testing.T, buf *bytes.Buffer) []map[string]any { t.Helper() out := []map[string]any{} for _, line := range strings.Split(buf.String(), "\n") { if strings.TrimSpace(line) == "" { continue } var m map[string]any if err := json.Unmarshal([]byte(line), &m); err != nil { t.Fatalf("not JSON: %v — %s", err, line) } out = append(out, m) } return out } func TestLogging_InjectsLoggerIntoContext(t *testing.T) { var buf bytes.Buffer app := newLoggingTestApp(t, &buf, func(c fiber.Ctx) error { logpkg.FromCtx(c.Context()).Info().Str("stage", "handler").Msg("handled") return c.SendString("ok") }) req := httptest.NewRequest("GET", "/ping", nil) if _, err := app.Test(req); err != nil { t.Fatalf("app.Test: %v", err) } lines := parseLines(t, &buf) if len(lines) < 2 { t.Fatalf("expected >=2 log lines (handler + access), got %d: %q", len(lines), buf.String()) } h := lines[0] if h["stage"] != "handler" { t.Errorf("handler line stage = %v", h["stage"]) } if _, ok := h["request_id"].(string); !ok || h["request_id"] == "" { t.Errorf("handler line missing request_id: %v", h) } if h["method"] != "GET" || h["path"] != "/ping" { t.Errorf("handler line missing method/path: %v", h) } } func TestLogging_EmitsAccessLogLine(t *testing.T) { var buf bytes.Buffer app := newLoggingTestApp(t, &buf, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/ping", nil) if _, err := app.Test(req); err != nil { t.Fatalf("app.Test: %v", err) } lines := parseLines(t, &buf) var access map[string]any for i := len(lines) - 1; i >= 0; i-- { if _, ok := lines[i]["status"]; ok { access = lines[i] break } } if access == nil { t.Fatalf("no access-log line found: %s", buf.String()) } if access["method"] != "GET" || access["path"] != "/ping" { t.Errorf("access line method/path wrong: %v", access) } if got, _ := access["status"].(float64); got != 200 { t.Errorf("access line status = %v, want 200", access["status"]) } if _, ok := access["duration_ms"].(float64); !ok { t.Errorf("access line missing duration_ms: %v", access) } if access["level"] != "info" { t.Errorf("200 should log at info; got level %v", access["level"]) } } func TestLogging_SkipsHealthEndpoint(t *testing.T) { var buf bytes.Buffer app := newLoggingTestApp(t, &buf, func(c fiber.Ctx) error { return c.SendString("ok") }) req := httptest.NewRequest("GET", "/health", nil) if _, err := app.Test(req); err != nil { t.Fatalf("app.Test: %v", err) } for _, line := range parseLines(t, &buf) { if _, hasStatus := line["status"]; hasStatus { if line["path"] == "/health" { t.Errorf("/health should not emit an access log line: %v", line) } } } } func TestLogging_LevelByStatus(t *testing.T) { cases := []struct { status int wantLevel string }{ {200, "info"}, {404, "warn"}, {500, "error"}, } for _, tc := range cases { tc := tc t.Run(tc.wantLevel, func(t *testing.T) { var buf bytes.Buffer app := newLoggingTestApp(t, &buf, func(c fiber.Ctx) error { return c.Status(tc.status).SendString("x") }) req := httptest.NewRequest("GET", "/ping", nil) if _, err := app.Test(req); err != nil { t.Fatalf("app.Test: %v", err) } var access map[string]any for _, line := range parseLines(t, &buf) { if _, ok := line["status"]; ok { access = line } } if access == nil { t.Fatalf("no access line") } if access["level"] != tc.wantLevel { t.Errorf("status %d → level %v, want %v", tc.status, access["level"], tc.wantLevel) } }) } }