v3.3.2 9697f9d

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()).WithRegistryForwarding(true)
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
65// TestHandler_DeniesRegistryForwardingWhenAuthDisabled proves a public
66// registry-domain request cannot reach the internal registry unless
67// forwarding was explicitly enabled (auth on).
68func TestHandler_DeniesRegistryForwardingWhenAuthDisabled(t *testing.T) {
69	proxySvc := inmocks.NewMockProxyService(t)
70	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{RegistryPort: 5000})
71	proxySvc.EXPECT().IsRegistryDomain("registry.example.com").Return(true)
72
73	handler := NewHandler(proxySvc, nil, testLogger())
74
75	req := httptest.NewRequest(http.MethodGet, "http://registry.example.com/v2/", nil)
76	req.Host = "registry.example.com"
77	w := httptest.NewRecorder()
78	handler.ServeHTTP(w, req)
79
80	assert.Equal(t, http.StatusNotFound, w.Code)
81	proxySvc.AssertNotCalled(t, "TrackRegistryRequest")
82}
83
84func TestHandler_NormalizesRequestHostForLookup(t *testing.T) {
85	tests := []struct {
86		name     string
87		host     string
88		wantHost string
89	}{
90		{name: "mixed case", host: "App.Example.com", wantHost: "app.example.com"},
91		{name: "explicit port", host: "App.Example.com:8080", wantHost: "app.example.com"},
92		{name: "trailing dot", host: "App.Example.com.", wantHost: "app.example.com"},
93	}
94
95	for _, tt := range tests {
96		t.Run(tt.name, func(t *testing.T) {
97			proxySvc := inmocks.NewMockProxyService(t)
98			proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
99			proxySvc.EXPECT().IsRegistryDomain(tt.wantHost).Return(false)
100			proxySvc.EXPECT().GetTarget(mock.Anything, tt.wantHost).Return(nil, domain.ErrNoTargetAvailable)
101
102			handler := NewHandler(proxySvc, nil, testLogger())
103			srv := httptest.NewServer(handler)
104			defer srv.Close()
105			req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
106			require.NoError(t, err)
107			req.Host = tt.host
108			resp, err := srv.Client().Do(req)
109			require.NoError(t, err)
110			defer resp.Body.Close()
111
112			assert.Equal(t, http.StatusNotFound, resp.StatusCode)
113		})
114	}
115}
116
117func TestHandler_RejectsInvalidRequestHost(t *testing.T) {
118	proxySvc := inmocks.NewMockProxyService(t)
119	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
120
121	handler := NewHandler(proxySvc, nil, testLogger())
122	srv := httptest.NewServer(handler)
123	defer srv.Close()
124	for _, host := range []string{"app.example.com:bad", "app.example.com:0", "app.example.com:99999"} {
125		req, err := http.NewRequest(http.MethodGet, srv.URL+"/", nil)
126		require.NoError(t, err)
127		req.Host = host
128		resp, err := srv.Client().Do(req)
129		require.NoError(t, err)
130		defer resp.Body.Close()
131
132		assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
133	}
134}
135
136func TestHandler_Returns404WhenNoTarget(t *testing.T) {
137	proxySvc := inmocks.NewMockProxyService(t)
138
139	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
140	proxySvc.EXPECT().IsRegistryDomain("unknown.example.com").Return(false)
141	proxySvc.EXPECT().GetTarget(mock.Anything, "unknown.example.com").Return(nil, domain.ErrNoTargetAvailable)
142
143	handler := NewHandler(proxySvc, nil, testLogger())
144
145	req := httptest.NewRequest(http.MethodGet, "http://unknown.example.com/", nil)
146	req.Host = "unknown.example.com"
147	w := httptest.NewRecorder()
148	handler.ServeHTTP(w, req)
149
150	assert.Equal(t, http.StatusNotFound, w.Code)
151}
152
153func TestHandler_ProxiesToTarget(t *testing.T) {
154	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
155		w.WriteHeader(http.StatusOK)
156		_, _ = w.Write([]byte("ok"))
157	}))
158	defer backend.Close()
159
160	proxySvc := inmocks.NewMockProxyService(t)
161
162	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
163	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
164
165	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
166
167	target := &domain.ProxyTarget{
168		Host:        "127.0.0.1",
169		Port:        backendPort,
170		ContainerID: "c-1",
171		Scheme:      "http",
172	}
173	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(target, nil)
174	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
175
176	handler := NewHandler(proxySvc, nil, testLogger())
177
178	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
179	req.Host = "app.example.com"
180	w := httptest.NewRecorder()
181	handler.ServeHTTP(w, req)
182
183	assert.Equal(t, http.StatusOK, w.Code)
184	assert.Equal(t, "ok", w.Body.String())
185}
186
187func TestHandler_ReadsMaxBodySizeConfig(t *testing.T) {
188	// Backend that reads the full request body, triggering MaxBytesReader.
189	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
190		_, _ = io.ReadAll(r.Body)
191		w.WriteHeader(http.StatusOK)
192	}))
193	defer backend.Close()
194
195	proxySvc := inmocks.NewMockProxyService(t)
196	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
197
198	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
199		MaxBodySize: 1024,
200	})
201	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
202	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
203		Host:        "127.0.0.1",
204		Port:        backendPort,
205		ContainerID: "c-1",
206		Scheme:      "http",
207	}, nil)
208	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
209
210	handler := NewHandler(proxySvc, nil, testLogger())
211
212	// Send a body larger than the 1024-byte limit.
213	largeBody := bytes.NewReader(make([]byte, 2048))
214	req := httptest.NewRequest(http.MethodPost, "http://app.example.com/test", largeBody)
215	req.Host = "app.example.com"
216	w := httptest.NewRecorder()
217	handler.ServeHTTP(w, req)
218
219	assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
220}
221
222func TestHandler_UpdatedConfigReflected(t *testing.T) {
223	proxySvc := inmocks.NewMockProxyService(t)
224
225	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
226		MaxConcurrentConns: 0,
227	}).Once()
228	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false).Once()
229	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable).Once()
230
231	handler := NewHandler(proxySvc, nil, testLogger())
232
233	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
234	req.Host = "app.example.com"
235	rec := httptest.NewRecorder()
236	handler.ServeHTTP(rec, req)
237	assert.Equal(t, http.StatusNotFound, rec.Code)
238
239	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
240		MaxConcurrentConns: 1,
241	}).Once()
242
243	handler.activeConns.Add(1)
244	defer handler.activeConns.Add(-1)
245
246	req2 := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
247	req2.Host = "app.example.com"
248	rec2 := httptest.NewRecorder()
249	handler.ServeHTTP(rec2, req2)
250	assert.Equal(t, http.StatusServiceUnavailable, rec2.Code)
251}
252
253func TestHandler_NoConcurrencyLimitWhenZero(t *testing.T) {
254	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
255		w.WriteHeader(http.StatusOK)
256	}))
257	defer backend.Close()
258
259	proxySvc := inmocks.NewMockProxyService(t)
260	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
261
262	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
263		MaxConcurrentConns: 0,
264	})
265	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
266	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
267		Host:        "127.0.0.1",
268		Port:        backendPort,
269		ContainerID: "c-1",
270		Scheme:      "http",
271	}, nil)
272	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
273
274	handler := NewHandler(proxySvc, nil, testLogger())
275
276	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
277	req.Host = "app.example.com"
278	w := httptest.NewRecorder()
279	handler.ServeHTTP(w, req)
280
281	assert.Equal(t, http.StatusOK, w.Code)
282}

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