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}