v2.31.0-dev9 062dcae

handler_test.go RawHistoryFolder

1package proxy
2
3import (
4	"bytes"
5	"io"
6	"net"
7	"net/http"
8	"net/http/httptest"
9	"testing"
10
11	"github.com/bnema/zerowrap"
12	"github.com/stretchr/testify/assert"
13	"github.com/stretchr/testify/mock"
14	"github.com/stretchr/testify/require"
15
16	"github.com/bnema/gordon/internal/boundaries/in"
17	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
18	"github.com/bnema/gordon/internal/domain"
19)
20
21func testLogger() zerowrap.Logger {
22	return zerowrap.Default()
23}
24
25func TestHandler_ConcurrentConnectionLimit_503WhenFull(t *testing.T) {
26	proxySvc := inmocks.NewMockProxyService(t)
27
28	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
29		MaxConcurrentConns: 1,
30	})
31
32	handler := NewHandler(proxySvc, nil, testLogger())
33
34	// Simulate an in-flight connection.
35	handler.activeConns.Add(1)
36	defer handler.activeConns.Add(-1)
37
38	req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil)
39	w := httptest.NewRecorder()
40	handler.ServeHTTP(w, req)
41
42	assert.Equal(t, http.StatusServiceUnavailable, w.Code)
43}
44
45func TestHandler_RoutesToRegistry(t *testing.T) {
46	proxySvc := inmocks.NewMockProxyService(t)
47
48	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
49		RegistryPort: 5000,
50	})
51	proxySvc.EXPECT().IsRegistryDomain("registry.example.com").Return(true)
52	proxySvc.EXPECT().TrackRegistryRequest().Return()
53	proxySvc.EXPECT().ReleaseRegistryRequest().Return()
54
55	handler := NewHandler(proxySvc, nil, testLogger())
56
57	req := httptest.NewRequest(http.MethodGet, "http://registry.example.com/v2/", nil)
58	req.Host = "registry.example.com"
59	w := httptest.NewRecorder()
60	handler.ServeHTTP(w, req)
61
62	assert.True(t, w.Code == http.StatusBadGateway || w.Code == http.StatusServiceUnavailable)
63}
64
65func TestHandler_NormalizesRequestHostForLookup(t *testing.T) {
66	tests := []struct {
67		name     string
68		host     string
69		wantHost string
70	}{
71		{name: "mixed case", host: "App.Example.com", wantHost: "app.example.com"},
72		{name: "explicit port", host: "App.Example.com:8080", wantHost: "app.example.com"},
73		{name: "trailing dot", host: "App.Example.com.", wantHost: "app.example.com"},
74	}
75
76	for _, tt := range tests {
77		t.Run(tt.name, func(t *testing.T) {
78			proxySvc := inmocks.NewMockProxyService(t)
79			proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
80			proxySvc.EXPECT().IsRegistryDomain(tt.wantHost).Return(false)
81			proxySvc.EXPECT().GetTarget(mock.Anything, tt.wantHost).Return(nil, domain.ErrNoTargetAvailable)
82
83			handler := NewHandler(proxySvc, nil, testLogger())
84			srv := httptest.NewServer(handler)
85			defer srv.Close()
86			req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
87			require.NoError(t, err)
88			req.Host = tt.host
89			resp, err := srv.Client().Do(req)
90			require.NoError(t, err)
91			defer resp.Body.Close()
92
93			assert.Equal(t, http.StatusNotFound, resp.StatusCode)
94		})
95	}
96}
97
98func TestHandler_RejectsInvalidRequestHost(t *testing.T) {
99	proxySvc := inmocks.NewMockProxyService(t)
100	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
101
102	handler := NewHandler(proxySvc, nil, testLogger())
103	srv := httptest.NewServer(handler)
104	defer srv.Close()
105	for _, host := range []string{"app.example.com:bad", "app.example.com:0", "app.example.com:99999"} {
106		req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
107		require.NoError(t, err)
108		req.Host = host
109		resp, err := srv.Client().Do(req)
110		require.NoError(t, err)
111		defer resp.Body.Close()
112
113		assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
114	}
115}
116
117func TestHandler_Returns404WhenNoTarget(t *testing.T) {
118	proxySvc := inmocks.NewMockProxyService(t)
119
120	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
121	proxySvc.EXPECT().IsRegistryDomain("unknown.example.com").Return(false)
122	proxySvc.EXPECT().GetTarget(mock.Anything, "unknown.example.com").Return(nil, domain.ErrNoTargetAvailable)
123
124	handler := NewHandler(proxySvc, nil, testLogger())
125
126	req := httptest.NewRequest(http.MethodGet, "http://unknown.example.com/", nil)
127	req.Host = "unknown.example.com"
128	w := httptest.NewRecorder()
129	handler.ServeHTTP(w, req)
130
131	assert.Equal(t, http.StatusNotFound, w.Code)
132}
133
134func TestHandler_ProxiesToTarget(t *testing.T) {
135	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
136		w.WriteHeader(http.StatusOK)
137		_, _ = w.Write([]byte("ok"))
138	}))
139	defer backend.Close()
140
141	proxySvc := inmocks.NewMockProxyService(t)
142
143	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
144	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
145
146	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
147
148	target := &domain.ProxyTarget{
149		Host:        "127.0.0.1",
150		Port:        backendPort,
151		ContainerID: "c-1",
152		Scheme:      "http",
153	}
154	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(target, nil)
155	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
156
157	handler := NewHandler(proxySvc, nil, testLogger())
158
159	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
160	req.Host = "app.example.com"
161	w := httptest.NewRecorder()
162	handler.ServeHTTP(w, req)
163
164	assert.Equal(t, http.StatusOK, w.Code)
165	assert.Equal(t, "ok", w.Body.String())
166}
167
168func TestHandler_ReadsMaxBodySizeConfig(t *testing.T) {
169	// Backend that reads the full request body, triggering MaxBytesReader.
170	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
171		_, _ = io.ReadAll(r.Body)
172		w.WriteHeader(http.StatusOK)
173	}))
174	defer backend.Close()
175
176	proxySvc := inmocks.NewMockProxyService(t)
177	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
178
179	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
180		MaxBodySize: 1024,
181	})
182	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
183	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
184		Host:        "127.0.0.1",
185		Port:        backendPort,
186		ContainerID: "c-1",
187		Scheme:      "http",
188	}, nil)
189	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
190
191	handler := NewHandler(proxySvc, nil, testLogger())
192
193	// Send a body larger than the 1024-byte limit.
194	largeBody := bytes.NewReader(make([]byte, 2048))
195	req := httptest.NewRequest(http.MethodPost, "http://app.example.com/test", largeBody)
196	req.Host = "app.example.com"
197	w := httptest.NewRecorder()
198	handler.ServeHTTP(w, req)
199
200	assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
201}
202
203func TestHandler_UpdatedConfigReflected(t *testing.T) {
204	proxySvc := inmocks.NewMockProxyService(t)
205
206	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
207		MaxConcurrentConns: 0,
208	}).Once()
209	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false).Once()
210	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable).Once()
211
212	handler := NewHandler(proxySvc, nil, testLogger())
213
214	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
215	req.Host = "app.example.com"
216	rec := httptest.NewRecorder()
217	handler.ServeHTTP(rec, req)
218	assert.Equal(t, http.StatusNotFound, rec.Code)
219
220	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
221		MaxConcurrentConns: 1,
222	}).Once()
223
224	handler.activeConns.Add(1)
225	defer handler.activeConns.Add(-1)
226
227	req2 := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
228	req2.Host = "app.example.com"
229	rec2 := httptest.NewRecorder()
230	handler.ServeHTTP(rec2, req2)
231	assert.Equal(t, http.StatusServiceUnavailable, rec2.Code)
232}
233
234func TestHandler_NoConcurrencyLimitWhenZero(t *testing.T) {
235	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
236		w.WriteHeader(http.StatusOK)
237	}))
238	defer backend.Close()
239
240	proxySvc := inmocks.NewMockProxyService(t)
241	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
242
243	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
244		MaxConcurrentConns: 0,
245	})
246	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
247	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
248		Host:        "127.0.0.1",
249		Port:        backendPort,
250		ContainerID: "c-1",
251		Scheme:      "http",
252	}, nil)
253	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
254
255	handler := NewHandler(proxySvc, nil, testLogger())
256
257	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
258	req.Host = "app.example.com"
259	w := httptest.NewRecorder()
260	handler.ServeHTTP(w, req)
261
262	assert.Equal(t, http.StatusOK, w.Code)
263}

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