v2.31.0-dev5 4162c86

accesslog_test.go RawHistoryFolder

1package middleware
2
3import (
4	"fmt"
5	"net/http"
6	"net/http/httptest"
7	"testing"
8
9	"github.com/stretchr/testify/assert"
10	"github.com/stretchr/testify/require"
11
12	out "github.com/bnema/gordon/internal/boundaries/out"
13)
14
15// mockAccessLogWriter records written entries and optionally returns an error.
16type mockAccessLogWriter struct {
17	entries []out.AccessLogEntry
18	err     error
19}
20
21func (m *mockAccessLogWriter) Write(entry out.AccessLogEntry) error {
22	m.entries = append(m.entries, entry)
23	return m.err
24}
25
26func TestAccessLogger_EmitsEntryPerRequest(t *testing.T) {
27	mock := &mockAccessLogWriter{}
28	log := testLogger()
29
30	handler := AccessLogger(mock, false, log, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31		w.WriteHeader(http.StatusOK)
32		_, _ = w.Write([]byte("hello"))
33	}))
34
35	req := httptest.NewRequest(http.MethodGet, "/some/path?foo=bar", nil)
36	req.RemoteAddr = "192.168.1.100:12345"
37	req.Header.Set("User-Agent", "test-agent")
38	req.Header.Set("Referer", "https://example.com")
39	req.RequestURI = "/some/path?foo=bar"
40	req.Host = "example.com"
41	req.Proto = "HTTP/1.1"
42	rw := httptest.NewRecorder()
43
44	handler.ServeHTTP(rw, req)
45
46	require.Len(t, mock.entries, 1)
47	e := mock.entries[0]
48	assert.Equal(t, http.MethodGet, e.Method)
49	assert.Equal(t, "/some/path", e.Path)
50	assert.Equal(t, "foo=bar", e.Query)
51	assert.Equal(t, "example.com", e.Host)
52	assert.Equal(t, http.StatusOK, e.Status)
53	assert.Equal(t, 5, e.BytesSent)
54	assert.Equal(t, "test-agent", e.UserAgent)
55	assert.Equal(t, "https://example.com", e.Referer)
56	assert.Equal(t, "HTTP/1.1", e.Proto)
57	assert.NotEmpty(t, e.RequestID)
58	assert.Equal(t, "192.168.1.100", e.ClientIP)
59}
60
61func TestAccessLogger_RequestIDReused(t *testing.T) {
62	mock := &mockAccessLogWriter{}
63	log := testLogger()
64
65	handler := AccessLogger(mock, false, log, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66		w.WriteHeader(http.StatusOK)
67	}))
68
69	req := httptest.NewRequest(http.MethodGet, "/", nil)
70	req.Header.Set("X-Request-ID", "fixed-id-123")
71	rw := httptest.NewRecorder()
72
73	handler.ServeHTTP(rw, req)
74
75	require.Len(t, mock.entries, 1)
76	assert.Equal(t, "fixed-id-123", mock.entries[0].RequestID)
77}
78
79func TestAccessLogger_ExcludeHealthCheckUA(t *testing.T) {
80	mock := &mockAccessLogWriter{}
81	log := testLogger()
82
83	handler := AccessLogger(mock, true, log, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84		w.WriteHeader(http.StatusOK)
85	}))
86
87	req := httptest.NewRequest(http.MethodGet, "/health", nil)
88	req.Header.Set("User-Agent", "Gordon-HealthCheck/1.0")
89	rw := httptest.NewRecorder()
90
91	handler.ServeHTTP(rw, req)
92
93	assert.Empty(t, mock.entries, "health-check request should not be logged")
94}
95
96func TestAccessLogger_ExcludeLoopbackIP(t *testing.T) {
97	mock := &mockAccessLogWriter{}
98	log := testLogger()
99
100	handler := AccessLogger(mock, true, log, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101		w.WriteHeader(http.StatusOK)
102	}))
103
104	req := httptest.NewRequest(http.MethodGet, "/health", nil)
105	req.RemoteAddr = "127.0.0.1:54321"
106	rw := httptest.NewRecorder()
107
108	handler.ServeHTTP(rw, req)
109
110	assert.Empty(t, mock.entries, "loopback request should not be logged")
111}
112
113func TestAccessLogger_NoExcludeWhenDisabled(t *testing.T) {
114	mock := &mockAccessLogWriter{}
115	log := testLogger()
116
117	handler := AccessLogger(mock, false, log, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118		w.WriteHeader(http.StatusOK)
119	}))
120
121	req := httptest.NewRequest(http.MethodGet, "/health", nil)
122	req.Header.Set("User-Agent", "Gordon-HealthCheck/1.0")
123	rw := httptest.NewRecorder()
124
125	handler.ServeHTTP(rw, req)
126
127	assert.Len(t, mock.entries, 1, "health-check request should be logged when excludeHealthChecks=false")
128}
129
130func TestAccessLogger_WriteFailureDoesNotAffectResponse(t *testing.T) {
131	mock := &mockAccessLogWriter{err: fmt.Errorf("disk full")}
132	log := testLogger()
133
134	handler := AccessLogger(mock, false, log, nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
135		w.WriteHeader(http.StatusOK)
136		_, _ = w.Write([]byte("ok"))
137	}))
138
139	req := httptest.NewRequest(http.MethodGet, "/", nil)
140	rw := httptest.NewRecorder()
141
142	// Should not panic or change the response.
143	handler.ServeHTTP(rw, req)
144
145	assert.Equal(t, http.StatusOK, rw.Code)
146	assert.Equal(t, "ok", rw.Body.String())
147}
148
149func TestAccessLogger_PanicRecoveryChain_Captures500(t *testing.T) {
150	mock := &mockAccessLogWriter{}
151	log := testLogger()
152
153	// Chain: AccessLogger (outer) → PanicRecovery → panicking handler
154	panicHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155		panic("test panic")
156	})
157	chain := AccessLogger(mock, false, log, nil)(PanicRecovery(log)(panicHandler))
158
159	req := httptest.NewRequest(http.MethodGet, "/crash", nil)
160	rw := httptest.NewRecorder()
161
162	chain.ServeHTTP(rw, req)
163
164	// PanicRecovery should have written 500.
165	assert.Equal(t, http.StatusInternalServerError, rw.Code)
166
167	// AccessLogger must have captured the 500.
168	require.Len(t, mock.entries, 1)
169	assert.Equal(t, http.StatusInternalServerError, mock.entries[0].Status)
170}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close