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
15 "github.com/bnema/gordon/internal/boundaries/in"
16 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
17 "github.com/bnema/gordon/internal/domain"
18)
19
20func testLogger() zerowrap.Logger {
21 return zerowrap.Default()
22}
23
24func TestHandler_ConcurrentConnectionLimit_503WhenFull(t *testing.T) {
25 proxySvc := inmocks.NewMockProxyService(t)
26
27 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
28 MaxConcurrentConns: 1,
29 })
30
31 handler := NewHandler(proxySvc, nil, testLogger())
32
33 // Simulate an in-flight connection.
34 handler.activeConns.Add(1)
35 defer handler.activeConns.Add(-1)
36
37 req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil)
38 w := httptest.NewRecorder()
39 handler.ServeHTTP(w, req)
40
41 assert.Equal(t, http.StatusServiceUnavailable, w.Code)
42}
43
44func TestHandler_RoutesToRegistry(t *testing.T) {
45 proxySvc := inmocks.NewMockProxyService(t)
46
47 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
48 RegistryPort: 5000,
49 })
50 proxySvc.EXPECT().IsRegistryDomain("registry.example.com").Return(true)
51 proxySvc.EXPECT().TrackRegistryRequest().Return()
52 proxySvc.EXPECT().ReleaseRegistryRequest().Return()
53
54 handler := NewHandler(proxySvc, nil, testLogger())
55
56 req := httptest.NewRequest(http.MethodGet, "http://registry.example.com/v2/", nil)
57 req.Host = "registry.example.com"
58 w := httptest.NewRecorder()
59 handler.ServeHTTP(w, req)
60
61 assert.True(t, w.Code == http.StatusBadGateway || w.Code == http.StatusServiceUnavailable)
62}
63
64func TestHandler_Returns404WhenNoTarget(t *testing.T) {
65 proxySvc := inmocks.NewMockProxyService(t)
66
67 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
68 proxySvc.EXPECT().IsRegistryDomain("unknown.example.com").Return(false)
69 proxySvc.EXPECT().GetTarget(mock.Anything, "unknown.example.com").Return(nil, domain.ErrNoTargetAvailable)
70
71 handler := NewHandler(proxySvc, nil, testLogger())
72
73 req := httptest.NewRequest(http.MethodGet, "http://unknown.example.com/", nil)
74 req.Host = "unknown.example.com"
75 w := httptest.NewRecorder()
76 handler.ServeHTTP(w, req)
77
78 assert.Equal(t, http.StatusNotFound, w.Code)
79}
80
81func TestHandler_ProxiesToTarget(t *testing.T) {
82 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
83 w.WriteHeader(http.StatusOK)
84 _, _ = w.Write([]byte("ok"))
85 }))
86 defer backend.Close()
87
88 proxySvc := inmocks.NewMockProxyService(t)
89
90 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
91 proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
92
93 backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
94
95 target := &domain.ProxyTarget{
96 Host: "127.0.0.1",
97 Port: backendPort,
98 ContainerID: "c-1",
99 Scheme: "http",
100 }
101 proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(target, nil)
102 proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
103
104 handler := NewHandler(proxySvc, nil, testLogger())
105
106 req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
107 req.Host = "app.example.com"
108 w := httptest.NewRecorder()
109 handler.ServeHTTP(w, req)
110
111 assert.Equal(t, http.StatusOK, w.Code)
112 assert.Equal(t, "ok", w.Body.String())
113}
114
115func TestHandler_ReadsMaxBodySizeConfig(t *testing.T) {
116 // Backend that reads the full request body, triggering MaxBytesReader.
117 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 _, _ = io.ReadAll(r.Body)
119 w.WriteHeader(http.StatusOK)
120 }))
121 defer backend.Close()
122
123 proxySvc := inmocks.NewMockProxyService(t)
124 backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
125
126 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
127 MaxBodySize: 1024,
128 })
129 proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
130 proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
131 Host: "127.0.0.1",
132 Port: backendPort,
133 ContainerID: "c-1",
134 Scheme: "http",
135 }, nil)
136 proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
137
138 handler := NewHandler(proxySvc, nil, testLogger())
139
140 // Send a body larger than the 1024-byte limit.
141 largeBody := bytes.NewReader(make([]byte, 2048))
142 req := httptest.NewRequest(http.MethodPost, "http://app.example.com/test", largeBody)
143 req.Host = "app.example.com"
144 w := httptest.NewRecorder()
145 handler.ServeHTTP(w, req)
146
147 assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
148}
149
150func TestHandler_UpdatedConfigReflected(t *testing.T) {
151 proxySvc := inmocks.NewMockProxyService(t)
152
153 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
154 MaxConcurrentConns: 0,
155 }).Once()
156 proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false).Once()
157 proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable).Once()
158
159 handler := NewHandler(proxySvc, nil, testLogger())
160
161 req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
162 req.Host = "app.example.com"
163 rec := httptest.NewRecorder()
164 handler.ServeHTTP(rec, req)
165 assert.Equal(t, http.StatusNotFound, rec.Code)
166
167 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
168 MaxConcurrentConns: 1,
169 }).Once()
170
171 handler.activeConns.Add(1)
172 defer handler.activeConns.Add(-1)
173
174 req2 := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
175 req2.Host = "app.example.com"
176 rec2 := httptest.NewRecorder()
177 handler.ServeHTTP(rec2, req2)
178 assert.Equal(t, http.StatusServiceUnavailable, rec2.Code)
179}
180
181func TestHandler_NoConcurrencyLimitWhenZero(t *testing.T) {
182 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
183 w.WriteHeader(http.StatusOK)
184 }))
185 defer backend.Close()
186
187 proxySvc := inmocks.NewMockProxyService(t)
188 backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
189
190 proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{
191 MaxConcurrentConns: 0,
192 })
193 proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
194 proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
195 Host: "127.0.0.1",
196 Port: backendPort,
197 ContainerID: "c-1",
198 Scheme: "http",
199 }, nil)
200 proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
201
202 handler := NewHandler(proxySvc, nil, testLogger())
203
204 req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
205 req.Host = "app.example.com"
206 w := httptest.NewRecorder()
207 handler.ServeHTTP(w, req)
208
209 assert.Equal(t, http.StatusOK, w.Code)
210}