package chislog_test import ( "bytes" "encoding/json" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "gitea.dwysokinski.me/twhelp/core/internal/chislog" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestLogger(t *testing.T) { t.Parallel() tests := []struct { name string register func(r chi.Router) options []chislog.Option req func(t *testing.T) *http.Request assertEntry func(t *testing.T, entry map[string]any) excluded bool }{ { name: "log level should be Info when status code >= 200 and < 400", register: func(r chi.Router) { r.Get("/info", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) }, req: func(t *testing.T) *http.Request { t.Helper() return httptest.NewRequest(http.MethodGet, "/info?test=true", nil) }, assertEntry: func(t *testing.T, entry map[string]any) { t.Helper() assert.Equal(t, slog.LevelInfo.String(), entry[slog.LevelKey]) assert.Equal(t, "192.0.2.1", entry["httpRequest.ip"]) // default ip address set by httptest.NewRequest }, }, { name: "log level should be Warn when status code >= 400 and < 500", register: func(r chi.Router) { r.Get("/warn", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) }) }, req: func(t *testing.T) *http.Request { t.Helper() return httptest.NewRequest(http.MethodGet, "/warn?test=true", nil) }, assertEntry: func(t *testing.T, entry map[string]any) { t.Helper() assert.Equal(t, slog.LevelWarn.String(), entry[slog.LevelKey]) }, }, { name: "log level should be Error when status code >= 500", register: func(r chi.Router) { r.Get("/error", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) }) }, req: func(t *testing.T) *http.Request { t.Helper() return httptest.NewRequest(http.MethodGet, "/error?test=true", nil) }, assertEntry: func(t *testing.T, entry map[string]any) { t.Helper() assert.Equal(t, slog.LevelError.String(), entry[slog.LevelKey]) }, }, { name: "read IP from header X-Forwarded-For", options: []chislog.Option{ chislog.WithIPExtractor(func(r *http.Request) string { return r.Header.Get("X-Forwarded-For") }), }, register: func(r chi.Router) { r.Post("/x-forwarded-for", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) }, req: func(t *testing.T) *http.Request { t.Helper() req := httptest.NewRequest(http.MethodPost, "/x-forwarded-for", nil) req.Header.Set("X-Forwarded-For", "111.111.111.111") return req }, assertEntry: func(t *testing.T, entry map[string]any) { t.Helper() assert.Equal(t, slog.LevelInfo.String(), entry[slog.LevelKey]) assert.Equal(t, "111.111.111.111", entry["httpRequest.ip"]) }, }, { name: "log entry should be skipped", options: []chislog.Option{ chislog.WithFilter(func(r *http.Request) bool { return !strings.HasPrefix(r.URL.Path, "/meta") }), }, register: func(r chi.Router) { r.Get("/meta/test", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) }, req: func(t *testing.T) *http.Request { t.Helper() return httptest.NewRequest(http.MethodGet, "/meta/test", nil) }, excluded: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() buf := bytes.NewBuffer(nil) logger := slog.New(slog.NewJSONHandler(buf, nil)) r := chi.NewRouter() r.Use(chislog.Logger(logger, tt.options...)) tt.register(r) rr := httptest.NewRecorder() req := tt.req(t) r.ServeHTTP(rr, req) if tt.excluded { assert.Zero(t, buf.Bytes()) return } var entry map[string]any require.NoError(t, json.Unmarshal(buf.Bytes(), &entry)) assert.NotEmpty(t, entry["time"]) assert.Equal(t, req.Method, entry["httpRequest.method"]) assert.Equal(t, req.RequestURI, entry["httpRequest.uri"]) assert.Equal(t, req.Referer(), entry["httpRequest.referer"]) assert.Equal(t, req.UserAgent(), entry["httpRequest.userAgent"]) assert.Equal(t, req.Proto, entry["httpRequest.proto"]) assert.InDelta(t, float64(rr.Code), entry["httpResponse.status"], 0.01) assert.GreaterOrEqual(t, entry["httpResponse.bytes"], 0.0) assert.GreaterOrEqual(t, entry["httpResponse.duration"], 0.0) if tt.assertEntry != nil { tt.assertEntry(t, entry) } }) } }