test(proxy): migrate HTTP handler tests to adapter layer, add h2c integration tests
Moves concurrency/body-size tests from usecase to adapter where the logic now lives. Adds end-to-end h2c test confirming HTTP/2 prior- knowledge transport works. Adds usecase tests for new boundary methods (IsRegistryDomain, TrackInFlight, ProxyConfig).
- commit
- 01ffd59ae913a98f4f9bd6751ba36c4a8d19c43b
- author
- brice · · 2026-03-15 22:43 UTC
- parent
- ec22770
- signature
- unsigned
3 files · +509 -115
A internal/adapters/in/http/proxy/forwarder_test.go +201 -0
| @@ -0,0 +1,201 @@ | |||
| 1 | + | package proxy | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "net" | |
| 6 | + | "net/http" | |
| 7 | + | "net/http/httptest" | |
| 8 | + | "strconv" | |
| 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 | + | ||
| 21 | + | func TestForwardToTarget_H2CEndToEnd(t *testing.T) { | |
| 22 | + | var h2cProtos http.Protocols | |
| 23 | + | h2cProtos.SetUnencryptedHTTP2(true) | |
| 24 | + | ||
| 25 | + | h2cBackend := &http.Server{ | |
| 26 | + | Protocols: &h2cProtos, | |
| 27 | + | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 28 | + | w.Header().Set("X-Proto", r.Proto) | |
| 29 | + | w.WriteHeader(http.StatusOK) | |
| 30 | + | _, _ = fmt.Fprintf(w, "proto=%s", r.Proto) | |
| 31 | + | }), | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | ln, err := net.Listen("tcp", "127.0.0.1:0") | |
| 35 | + | require.NoError(t, err) | |
| 36 | + | defer ln.Close() | |
| 37 | + | ||
| 38 | + | go func() { | |
| 39 | + | _ = h2cBackend.Serve(ln) | |
| 40 | + | }() | |
| 41 | + | defer h2cBackend.Close() | |
| 42 | + | ||
| 43 | + | _, portStr, splitErr := net.SplitHostPort(ln.Addr().String()) | |
| 44 | + | require.NoError(t, splitErr) | |
| 45 | + | port, atoiErr := strconv.Atoi(portStr) | |
| 46 | + | require.NoError(t, atoiErr) | |
| 47 | + | ||
| 48 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 49 | + | ||
| 50 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 51 | + | proxySvc.EXPECT().IsRegistryDomain("grpc.example.com").Return(false) | |
| 52 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "grpc.example.com").Return(&domain.ProxyTarget{ | |
| 53 | + | Host: "127.0.0.1", | |
| 54 | + | Port: port, | |
| 55 | + | ContainerID: "grpc-1", | |
| 56 | + | Scheme: "http", | |
| 57 | + | Protocol: "h2c", | |
| 58 | + | }, nil) | |
| 59 | + | proxySvc.EXPECT().TrackInFlight("grpc-1").Return(func() {}) | |
| 60 | + | ||
| 61 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |
| 62 | + | ||
| 63 | + | req := httptest.NewRequest(http.MethodGet, "http://grpc.example.com/test", nil) | |
| 64 | + | req.Host = "grpc.example.com" | |
| 65 | + | rec := httptest.NewRecorder() | |
| 66 | + | ||
| 67 | + | handler.ServeHTTP(rec, req) | |
| 68 | + | ||
| 69 | + | assert.Equal(t, http.StatusOK, rec.Code) | |
| 70 | + | assert.Contains(t, rec.Body.String(), "HTTP/2") | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | func TestForwardToTarget_HTTP1StillWorks(t *testing.T) { | |
| 74 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 75 | + | _, _ = fmt.Fprintf(w, "proto=%s", r.Proto) | |
| 76 | + | })) | |
| 77 | + | defer backend.Close() | |
| 78 | + | ||
| 79 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |
| 80 | + | ||
| 81 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 82 | + | ||
| 83 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 84 | + | proxySvc.EXPECT().IsRegistryDomain("web.example.com").Return(false) | |
| 85 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "web.example.com").Return(&domain.ProxyTarget{ | |
| 86 | + | Host: "127.0.0.1", | |
| 87 | + | Port: backendPort, | |
| 88 | + | ContainerID: "web-1", | |
| 89 | + | Scheme: "http", | |
| 90 | + | }, nil) | |
| 91 | + | proxySvc.EXPECT().TrackInFlight("web-1").Return(func() {}) | |
| 92 | + | ||
| 93 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |
| 94 | + | ||
| 95 | + | req := httptest.NewRequest(http.MethodGet, "http://web.example.com/", nil) | |
| 96 | + | req.Host = "web.example.com" | |
| 97 | + | rec := httptest.NewRecorder() | |
| 98 | + | ||
| 99 | + | handler.ServeHTTP(rec, req) | |
| 100 | + | ||
| 101 | + | assert.Equal(t, http.StatusOK, rec.Code) | |
| 102 | + | assert.Contains(t, rec.Body.String(), "HTTP/1.1") | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func TestTransportForTarget_SelectsCorrectTransport(t *testing.T) { | |
| 106 | + | handler := NewHandler(inmocks.NewMockProxyService(t), zerowrap.Default()) | |
| 107 | + | ||
| 108 | + | h2cTarget := &domain.ProxyTarget{Protocol: "h2c"} | |
| 109 | + | httpTarget := &domain.ProxyTarget{Protocol: ""} | |
| 110 | + | ||
| 111 | + | assert.Equal(t, handler.h2cTransport, handler.transportForTarget(h2cTarget)) | |
| 112 | + | assert.Equal(t, handler.appTransport, handler.transportForTarget(httpTarget)) | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | func TestForwardToTarget_OriginalHostPreserved(t *testing.T) { | |
| 116 | + | var receivedHost string | |
| 117 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 118 | + | receivedHost = r.Host | |
| 119 | + | w.WriteHeader(http.StatusOK) | |
| 120 | + | })) | |
| 121 | + | defer backend.Close() | |
| 122 | + | ||
| 123 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |
| 124 | + | ||
| 125 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 126 | + | ||
| 127 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 128 | + | proxySvc.EXPECT().IsRegistryDomain("external.example.com").Return(false) | |
| 129 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "external.example.com").Return(&domain.ProxyTarget{ | |
| 130 | + | Host: "127.0.0.1", | |
| 131 | + | Port: backendPort, | |
| 132 | + | ContainerID: "", | |
| 133 | + | Scheme: "http", | |
| 134 | + | OriginalHost: "upstream.example.com", | |
| 135 | + | }, nil) | |
| 136 | + | proxySvc.EXPECT().TrackInFlight("").Return(func() {}) | |
| 137 | + | ||
| 138 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |
| 139 | + | ||
| 140 | + | req := httptest.NewRequest(http.MethodGet, "http://external.example.com/", nil) | |
| 141 | + | req.Host = "external.example.com" | |
| 142 | + | w := httptest.NewRecorder() | |
| 143 | + | handler.ServeHTTP(w, req) | |
| 144 | + | ||
| 145 | + | assert.Equal(t, http.StatusOK, w.Code) | |
| 146 | + | assert.Contains(t, receivedHost, "upstream.example.com") | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func TestForwardToTarget_BackendDown_Returns503(t *testing.T) { | |
| 150 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 151 | + | ||
| 152 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 153 | + | proxySvc.EXPECT().IsRegistryDomain("down.example.com").Return(false) | |
| 154 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "down.example.com").Return(&domain.ProxyTarget{ | |
| 155 | + | Host: "127.0.0.1", | |
| 156 | + | Port: 1, | |
| 157 | + | ContainerID: "c-down", | |
| 158 | + | Scheme: "http", | |
| 159 | + | }, nil) | |
| 160 | + | proxySvc.EXPECT().TrackInFlight("c-down").Return(func() {}) | |
| 161 | + | ||
| 162 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |
| 163 | + | ||
| 164 | + | req := httptest.NewRequest(http.MethodGet, "http://down.example.com/", nil) | |
| 165 | + | req.Host = "down.example.com" | |
| 166 | + | w := httptest.NewRecorder() | |
| 167 | + | handler.ServeHTTP(w, req) | |
| 168 | + | ||
| 169 | + | assert.Equal(t, http.StatusServiceUnavailable, w.Code) | |
| 170 | + | } | |
| 171 | + | ||
| 172 | + | func TestForwardToTarget_ProxyHeaderSet(t *testing.T) { | |
| 173 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 174 | + | w.WriteHeader(http.StatusOK) | |
| 175 | + | })) | |
| 176 | + | defer backend.Close() | |
| 177 | + | ||
| 178 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |
| 179 | + | ||
| 180 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 181 | + | ||
| 182 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 183 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |
| 184 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{ | |
| 185 | + | Host: "127.0.0.1", | |
| 186 | + | Port: backendPort, | |
| 187 | + | ContainerID: "c-1", | |
| 188 | + | Scheme: "http", | |
| 189 | + | }, nil) | |
| 190 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |
| 191 | + | ||
| 192 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |
| 193 | + | ||
| 194 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |
| 195 | + | req.Host = "app.example.com" | |
| 196 | + | w := httptest.NewRecorder() | |
| 197 | + | handler.ServeHTTP(w, req) | |
| 198 | + | ||
| 199 | + | assert.Equal(t, http.StatusOK, w.Code) | |
| 200 | + | assert.Equal(t, "Gordon", w.Header().Get("X-Proxied-By")) | |
| 201 | + | } | |
| @@ -0,0 +1,201 @@ | |||||
| 1 | + | package proxy | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "net" | |||
| 6 | + | "net/http" | |||
| 7 | + | "net/http/httptest" | |||
| 8 | + | "strconv" | |||
| 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 | + | ||||
| 21 | + | func TestForwardToTarget_H2CEndToEnd(t *testing.T) { | |||
| 22 | + | var h2cProtos http.Protocols | |||
| 23 | + | h2cProtos.SetUnencryptedHTTP2(true) | |||
| 24 | + | ||||
| 25 | + | h2cBackend := &http.Server{ | |||
| 26 | + | Protocols: &h2cProtos, | |||
| 27 | + | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 28 | + | w.Header().Set("X-Proto", r.Proto) | |||
| 29 | + | w.WriteHeader(http.StatusOK) | |||
| 30 | + | _, _ = fmt.Fprintf(w, "proto=%s", r.Proto) | |||
| 31 | + | }), | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | ln, err := net.Listen("tcp", "127.0.0.1:0") | |||
| 35 | + | require.NoError(t, err) | |||
| 36 | + | defer ln.Close() | |||
| 37 | + | ||||
| 38 | + | go func() { | |||
| 39 | + | _ = h2cBackend.Serve(ln) | |||
| 40 | + | }() | |||
| 41 | + | defer h2cBackend.Close() | |||
| 42 | + | ||||
| 43 | + | _, portStr, splitErr := net.SplitHostPort(ln.Addr().String()) | |||
| 44 | + | require.NoError(t, splitErr) | |||
| 45 | + | port, atoiErr := strconv.Atoi(portStr) | |||
| 46 | + | require.NoError(t, atoiErr) | |||
| 47 | + | ||||
| 48 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 49 | + | ||||
| 50 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 51 | + | proxySvc.EXPECT().IsRegistryDomain("grpc.example.com").Return(false) | |||
| 52 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "grpc.example.com").Return(&domain.ProxyTarget{ | |||
| 53 | + | Host: "127.0.0.1", | |||
| 54 | + | Port: port, | |||
| 55 | + | ContainerID: "grpc-1", | |||
| 56 | + | Scheme: "http", | |||
| 57 | + | Protocol: "h2c", | |||
| 58 | + | }, nil) | |||
| 59 | + | proxySvc.EXPECT().TrackInFlight("grpc-1").Return(func() {}) | |||
| 60 | + | ||||
| 61 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |||
| 62 | + | ||||
| 63 | + | req := httptest.NewRequest(http.MethodGet, "http://grpc.example.com/test", nil) | |||
| 64 | + | req.Host = "grpc.example.com" | |||
| 65 | + | rec := httptest.NewRecorder() | |||
| 66 | + | ||||
| 67 | + | handler.ServeHTTP(rec, req) | |||
| 68 | + | ||||
| 69 | + | assert.Equal(t, http.StatusOK, rec.Code) | |||
| 70 | + | assert.Contains(t, rec.Body.String(), "HTTP/2") | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | func TestForwardToTarget_HTTP1StillWorks(t *testing.T) { | |||
| 74 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 75 | + | _, _ = fmt.Fprintf(w, "proto=%s", r.Proto) | |||
| 76 | + | })) | |||
| 77 | + | defer backend.Close() | |||
| 78 | + | ||||
| 79 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |||
| 80 | + | ||||
| 81 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 82 | + | ||||
| 83 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 84 | + | proxySvc.EXPECT().IsRegistryDomain("web.example.com").Return(false) | |||
| 85 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "web.example.com").Return(&domain.ProxyTarget{ | |||
| 86 | + | Host: "127.0.0.1", | |||
| 87 | + | Port: backendPort, | |||
| 88 | + | ContainerID: "web-1", | |||
| 89 | + | Scheme: "http", | |||
| 90 | + | }, nil) | |||
| 91 | + | proxySvc.EXPECT().TrackInFlight("web-1").Return(func() {}) | |||
| 92 | + | ||||
| 93 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |||
| 94 | + | ||||
| 95 | + | req := httptest.NewRequest(http.MethodGet, "http://web.example.com/", nil) | |||
| 96 | + | req.Host = "web.example.com" | |||
| 97 | + | rec := httptest.NewRecorder() | |||
| 98 | + | ||||
| 99 | + | handler.ServeHTTP(rec, req) | |||
| 100 | + | ||||
| 101 | + | assert.Equal(t, http.StatusOK, rec.Code) | |||
| 102 | + | assert.Contains(t, rec.Body.String(), "HTTP/1.1") | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func TestTransportForTarget_SelectsCorrectTransport(t *testing.T) { | |||
| 106 | + | handler := NewHandler(inmocks.NewMockProxyService(t), zerowrap.Default()) | |||
| 107 | + | ||||
| 108 | + | h2cTarget := &domain.ProxyTarget{Protocol: "h2c"} | |||
| 109 | + | httpTarget := &domain.ProxyTarget{Protocol: ""} | |||
| 110 | + | ||||
| 111 | + | assert.Equal(t, handler.h2cTransport, handler.transportForTarget(h2cTarget)) | |||
| 112 | + | assert.Equal(t, handler.appTransport, handler.transportForTarget(httpTarget)) | |||
| 113 | + | } | |||
| 114 | + | ||||
| 115 | + | func TestForwardToTarget_OriginalHostPreserved(t *testing.T) { | |||
| 116 | + | var receivedHost string | |||
| 117 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 118 | + | receivedHost = r.Host | |||
| 119 | + | w.WriteHeader(http.StatusOK) | |||
| 120 | + | })) | |||
| 121 | + | defer backend.Close() | |||
| 122 | + | ||||
| 123 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |||
| 124 | + | ||||
| 125 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 126 | + | ||||
| 127 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 128 | + | proxySvc.EXPECT().IsRegistryDomain("external.example.com").Return(false) | |||
| 129 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "external.example.com").Return(&domain.ProxyTarget{ | |||
| 130 | + | Host: "127.0.0.1", | |||
| 131 | + | Port: backendPort, | |||
| 132 | + | ContainerID: "", | |||
| 133 | + | Scheme: "http", | |||
| 134 | + | OriginalHost: "upstream.example.com", | |||
| 135 | + | }, nil) | |||
| 136 | + | proxySvc.EXPECT().TrackInFlight("").Return(func() {}) | |||
| 137 | + | ||||
| 138 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |||
| 139 | + | ||||
| 140 | + | req := httptest.NewRequest(http.MethodGet, "http://external.example.com/", nil) | |||
| 141 | + | req.Host = "external.example.com" | |||
| 142 | + | w := httptest.NewRecorder() | |||
| 143 | + | handler.ServeHTTP(w, req) | |||
| 144 | + | ||||
| 145 | + | assert.Equal(t, http.StatusOK, w.Code) | |||
| 146 | + | assert.Contains(t, receivedHost, "upstream.example.com") | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func TestForwardToTarget_BackendDown_Returns503(t *testing.T) { | |||
| 150 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 151 | + | ||||
| 152 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 153 | + | proxySvc.EXPECT().IsRegistryDomain("down.example.com").Return(false) | |||
| 154 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "down.example.com").Return(&domain.ProxyTarget{ | |||
| 155 | + | Host: "127.0.0.1", | |||
| 156 | + | Port: 1, | |||
| 157 | + | ContainerID: "c-down", | |||
| 158 | + | Scheme: "http", | |||
| 159 | + | }, nil) | |||
| 160 | + | proxySvc.EXPECT().TrackInFlight("c-down").Return(func() {}) | |||
| 161 | + | ||||
| 162 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |||
| 163 | + | ||||
| 164 | + | req := httptest.NewRequest(http.MethodGet, "http://down.example.com/", nil) | |||
| 165 | + | req.Host = "down.example.com" | |||
| 166 | + | w := httptest.NewRecorder() | |||
| 167 | + | handler.ServeHTTP(w, req) | |||
| 168 | + | ||||
| 169 | + | assert.Equal(t, http.StatusServiceUnavailable, w.Code) | |||
| 170 | + | } | |||
| 171 | + | ||||
| 172 | + | func TestForwardToTarget_ProxyHeaderSet(t *testing.T) { | |||
| 173 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 174 | + | w.WriteHeader(http.StatusOK) | |||
| 175 | + | })) | |||
| 176 | + | defer backend.Close() | |||
| 177 | + | ||||
| 178 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |||
| 179 | + | ||||
| 180 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 181 | + | ||||
| 182 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 183 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |||
| 184 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{ | |||
| 185 | + | Host: "127.0.0.1", | |||
| 186 | + | Port: backendPort, | |||
| 187 | + | ContainerID: "c-1", | |||
| 188 | + | Scheme: "http", | |||
| 189 | + | }, nil) | |||
| 190 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |||
| 191 | + | ||||
| 192 | + | handler := NewHandler(proxySvc, zerowrap.Default()) | |||
| 193 | + | ||||
| 194 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |||
| 195 | + | req.Host = "app.example.com" | |||
| 196 | + | w := httptest.NewRecorder() | |||
| 197 | + | handler.ServeHTTP(w, req) | |||
| 198 | + | ||||
| 199 | + | assert.Equal(t, http.StatusOK, w.Code) | |||
| 200 | + | assert.Equal(t, "Gordon", w.Header().Get("X-Proxied-By")) | |||
| 201 | + | } | |||
A internal/adapters/in/http/proxy/handler_test.go +192 -0
| @@ -0,0 +1,192 @@ | |||
| 1 | + | package proxy | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "net" | |
| 5 | + | "net/http" | |
| 6 | + | "net/http/httptest" | |
| 7 | + | "testing" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/zerowrap" | |
| 10 | + | "github.com/stretchr/testify/assert" | |
| 11 | + | "github.com/stretchr/testify/mock" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/gordon/internal/boundaries/in" | |
| 14 | + | inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks" | |
| 15 | + | "github.com/bnema/gordon/internal/domain" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | func testLogger() zerowrap.Logger { | |
| 19 | + | return zerowrap.Default() | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func TestHandler_ConcurrentConnectionLimit_503WhenFull(t *testing.T) { | |
| 23 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 24 | + | ||
| 25 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |
| 26 | + | MaxConcurrentConns: 1, | |
| 27 | + | }) | |
| 28 | + | ||
| 29 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 30 | + | ||
| 31 | + | // Simulate an in-flight connection. | |
| 32 | + | handler.activeConns.Add(1) | |
| 33 | + | defer handler.activeConns.Add(-1) | |
| 34 | + | ||
| 35 | + | req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil) | |
| 36 | + | w := httptest.NewRecorder() | |
| 37 | + | handler.ServeHTTP(w, req) | |
| 38 | + | ||
| 39 | + | assert.Equal(t, http.StatusServiceUnavailable, w.Code) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestHandler_RoutesToRegistry(t *testing.T) { | |
| 43 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 44 | + | ||
| 45 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |
| 46 | + | RegistryPort: 5000, | |
| 47 | + | }) | |
| 48 | + | proxySvc.EXPECT().IsRegistryDomain("registry.example.com").Return(true) | |
| 49 | + | proxySvc.EXPECT().TrackRegistryRequest().Return() | |
| 50 | + | proxySvc.EXPECT().ReleaseRegistryRequest().Return() | |
| 51 | + | ||
| 52 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 53 | + | ||
| 54 | + | req := httptest.NewRequest(http.MethodGet, "http://registry.example.com/v2/", nil) | |
| 55 | + | req.Host = "registry.example.com" | |
| 56 | + | w := httptest.NewRecorder() | |
| 57 | + | handler.ServeHTTP(w, req) | |
| 58 | + | ||
| 59 | + | assert.True(t, w.Code == http.StatusBadGateway || w.Code == http.StatusServiceUnavailable) | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func TestHandler_Returns404WhenNoTarget(t *testing.T) { | |
| 63 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 64 | + | ||
| 65 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 66 | + | proxySvc.EXPECT().IsRegistryDomain("unknown.example.com").Return(false) | |
| 67 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "unknown.example.com").Return(nil, domain.ErrNoTargetAvailable) | |
| 68 | + | ||
| 69 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 70 | + | ||
| 71 | + | req := httptest.NewRequest(http.MethodGet, "http://unknown.example.com/", nil) | |
| 72 | + | req.Host = "unknown.example.com" | |
| 73 | + | w := httptest.NewRecorder() | |
| 74 | + | handler.ServeHTTP(w, req) | |
| 75 | + | ||
| 76 | + | assert.Equal(t, http.StatusNotFound, w.Code) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | func TestHandler_ProxiesToTarget(t *testing.T) { | |
| 80 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 81 | + | w.WriteHeader(http.StatusOK) | |
| 82 | + | _, _ = w.Write([]byte("ok")) | |
| 83 | + | })) | |
| 84 | + | defer backend.Close() | |
| 85 | + | ||
| 86 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 87 | + | ||
| 88 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |
| 89 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |
| 90 | + | ||
| 91 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |
| 92 | + | ||
| 93 | + | target := &domain.ProxyTarget{ | |
| 94 | + | Host: "127.0.0.1", | |
| 95 | + | Port: backendPort, | |
| 96 | + | ContainerID: "c-1", | |
| 97 | + | Scheme: "http", | |
| 98 | + | } | |
| 99 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(target, nil) | |
| 100 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |
| 101 | + | ||
| 102 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 103 | + | ||
| 104 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |
| 105 | + | req.Host = "app.example.com" | |
| 106 | + | w := httptest.NewRecorder() | |
| 107 | + | handler.ServeHTTP(w, req) | |
| 108 | + | ||
| 109 | + | assert.Equal(t, http.StatusOK, w.Code) | |
| 110 | + | assert.Equal(t, "ok", w.Body.String()) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func TestHandler_MaxBodySize_Applied(t *testing.T) { | |
| 114 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 115 | + | ||
| 116 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |
| 117 | + | MaxBodySize: 1024, | |
| 118 | + | }) | |
| 119 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |
| 120 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable) | |
| 121 | + | ||
| 122 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 123 | + | ||
| 124 | + | req := httptest.NewRequest(http.MethodPost, "http://app.example.com/test", nil) | |
| 125 | + | req.Host = "app.example.com" | |
| 126 | + | w := httptest.NewRecorder() | |
| 127 | + | handler.ServeHTTP(w, req) | |
| 128 | + | ||
| 129 | + | assert.Equal(t, http.StatusNotFound, w.Code) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func TestHandler_UpdatedConfigReflected(t *testing.T) { | |
| 133 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 134 | + | ||
| 135 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |
| 136 | + | MaxConcurrentConns: 0, | |
| 137 | + | }).Once() | |
| 138 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false).Once() | |
| 139 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable).Once() | |
| 140 | + | ||
| 141 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 142 | + | ||
| 143 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |
| 144 | + | req.Host = "app.example.com" | |
| 145 | + | rec := httptest.NewRecorder() | |
| 146 | + | handler.ServeHTTP(rec, req) | |
| 147 | + | assert.Equal(t, http.StatusNotFound, rec.Code) | |
| 148 | + | ||
| 149 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |
| 150 | + | MaxConcurrentConns: 1, | |
| 151 | + | }).Once() | |
| 152 | + | ||
| 153 | + | handler.activeConns.Add(1) | |
| 154 | + | defer handler.activeConns.Add(-1) | |
| 155 | + | ||
| 156 | + | req2 := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |
| 157 | + | req2.Host = "app.example.com" | |
| 158 | + | rec2 := httptest.NewRecorder() | |
| 159 | + | handler.ServeHTTP(rec2, req2) | |
| 160 | + | assert.Equal(t, http.StatusServiceUnavailable, rec2.Code) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func TestHandler_NoConcurrencyLimitWhenZero(t *testing.T) { | |
| 164 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| 165 | + | w.WriteHeader(http.StatusOK) | |
| 166 | + | })) | |
| 167 | + | defer backend.Close() | |
| 168 | + | ||
| 169 | + | proxySvc := inmocks.NewMockProxyService(t) | |
| 170 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |
| 171 | + | ||
| 172 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |
| 173 | + | MaxConcurrentConns: 0, | |
| 174 | + | }) | |
| 175 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |
| 176 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{ | |
| 177 | + | Host: "127.0.0.1", | |
| 178 | + | Port: backendPort, | |
| 179 | + | ContainerID: "c-1", | |
| 180 | + | Scheme: "http", | |
| 181 | + | }, nil) | |
| 182 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |
| 183 | + | ||
| 184 | + | handler := NewHandler(proxySvc, testLogger()) | |
| 185 | + | ||
| 186 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |
| 187 | + | req.Host = "app.example.com" | |
| 188 | + | w := httptest.NewRecorder() | |
| 189 | + | handler.ServeHTTP(w, req) | |
| 190 | + | ||
| 191 | + | assert.Equal(t, http.StatusOK, w.Code) | |
| 192 | + | } | |
| @@ -0,0 +1,192 @@ | |||||
| 1 | + | package proxy | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "net" | |||
| 5 | + | "net/http" | |||
| 6 | + | "net/http/httptest" | |||
| 7 | + | "testing" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/zerowrap" | |||
| 10 | + | "github.com/stretchr/testify/assert" | |||
| 11 | + | "github.com/stretchr/testify/mock" | |||
| 12 | + | ||||
| 13 | + | "github.com/bnema/gordon/internal/boundaries/in" | |||
| 14 | + | inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks" | |||
| 15 | + | "github.com/bnema/gordon/internal/domain" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | func testLogger() zerowrap.Logger { | |||
| 19 | + | return zerowrap.Default() | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func TestHandler_ConcurrentConnectionLimit_503WhenFull(t *testing.T) { | |||
| 23 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 24 | + | ||||
| 25 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |||
| 26 | + | MaxConcurrentConns: 1, | |||
| 27 | + | }) | |||
| 28 | + | ||||
| 29 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 30 | + | ||||
| 31 | + | // Simulate an in-flight connection. | |||
| 32 | + | handler.activeConns.Add(1) | |||
| 33 | + | defer handler.activeConns.Add(-1) | |||
| 34 | + | ||||
| 35 | + | req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil) | |||
| 36 | + | w := httptest.NewRecorder() | |||
| 37 | + | handler.ServeHTTP(w, req) | |||
| 38 | + | ||||
| 39 | + | assert.Equal(t, http.StatusServiceUnavailable, w.Code) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestHandler_RoutesToRegistry(t *testing.T) { | |||
| 43 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 44 | + | ||||
| 45 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |||
| 46 | + | RegistryPort: 5000, | |||
| 47 | + | }) | |||
| 48 | + | proxySvc.EXPECT().IsRegistryDomain("registry.example.com").Return(true) | |||
| 49 | + | proxySvc.EXPECT().TrackRegistryRequest().Return() | |||
| 50 | + | proxySvc.EXPECT().ReleaseRegistryRequest().Return() | |||
| 51 | + | ||||
| 52 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 53 | + | ||||
| 54 | + | req := httptest.NewRequest(http.MethodGet, "http://registry.example.com/v2/", nil) | |||
| 55 | + | req.Host = "registry.example.com" | |||
| 56 | + | w := httptest.NewRecorder() | |||
| 57 | + | handler.ServeHTTP(w, req) | |||
| 58 | + | ||||
| 59 | + | assert.True(t, w.Code == http.StatusBadGateway || w.Code == http.StatusServiceUnavailable) | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func TestHandler_Returns404WhenNoTarget(t *testing.T) { | |||
| 63 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 64 | + | ||||
| 65 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 66 | + | proxySvc.EXPECT().IsRegistryDomain("unknown.example.com").Return(false) | |||
| 67 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "unknown.example.com").Return(nil, domain.ErrNoTargetAvailable) | |||
| 68 | + | ||||
| 69 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 70 | + | ||||
| 71 | + | req := httptest.NewRequest(http.MethodGet, "http://unknown.example.com/", nil) | |||
| 72 | + | req.Host = "unknown.example.com" | |||
| 73 | + | w := httptest.NewRecorder() | |||
| 74 | + | handler.ServeHTTP(w, req) | |||
| 75 | + | ||||
| 76 | + | assert.Equal(t, http.StatusNotFound, w.Code) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | func TestHandler_ProxiesToTarget(t *testing.T) { | |||
| 80 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 81 | + | w.WriteHeader(http.StatusOK) | |||
| 82 | + | _, _ = w.Write([]byte("ok")) | |||
| 83 | + | })) | |||
| 84 | + | defer backend.Close() | |||
| 85 | + | ||||
| 86 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 87 | + | ||||
| 88 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{}) | |||
| 89 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |||
| 90 | + | ||||
| 91 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |||
| 92 | + | ||||
| 93 | + | target := &domain.ProxyTarget{ | |||
| 94 | + | Host: "127.0.0.1", | |||
| 95 | + | Port: backendPort, | |||
| 96 | + | ContainerID: "c-1", | |||
| 97 | + | Scheme: "http", | |||
| 98 | + | } | |||
| 99 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(target, nil) | |||
| 100 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |||
| 101 | + | ||||
| 102 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 103 | + | ||||
| 104 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |||
| 105 | + | req.Host = "app.example.com" | |||
| 106 | + | w := httptest.NewRecorder() | |||
| 107 | + | handler.ServeHTTP(w, req) | |||
| 108 | + | ||||
| 109 | + | assert.Equal(t, http.StatusOK, w.Code) | |||
| 110 | + | assert.Equal(t, "ok", w.Body.String()) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func TestHandler_MaxBodySize_Applied(t *testing.T) { | |||
| 114 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 115 | + | ||||
| 116 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |||
| 117 | + | MaxBodySize: 1024, | |||
| 118 | + | }) | |||
| 119 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |||
| 120 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable) | |||
| 121 | + | ||||
| 122 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 123 | + | ||||
| 124 | + | req := httptest.NewRequest(http.MethodPost, "http://app.example.com/test", nil) | |||
| 125 | + | req.Host = "app.example.com" | |||
| 126 | + | w := httptest.NewRecorder() | |||
| 127 | + | handler.ServeHTTP(w, req) | |||
| 128 | + | ||||
| 129 | + | assert.Equal(t, http.StatusNotFound, w.Code) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | func TestHandler_UpdatedConfigReflected(t *testing.T) { | |||
| 133 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 134 | + | ||||
| 135 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |||
| 136 | + | MaxConcurrentConns: 0, | |||
| 137 | + | }).Once() | |||
| 138 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false).Once() | |||
| 139 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(nil, domain.ErrNoTargetAvailable).Once() | |||
| 140 | + | ||||
| 141 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 142 | + | ||||
| 143 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |||
| 144 | + | req.Host = "app.example.com" | |||
| 145 | + | rec := httptest.NewRecorder() | |||
| 146 | + | handler.ServeHTTP(rec, req) | |||
| 147 | + | assert.Equal(t, http.StatusNotFound, rec.Code) | |||
| 148 | + | ||||
| 149 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |||
| 150 | + | MaxConcurrentConns: 1, | |||
| 151 | + | }).Once() | |||
| 152 | + | ||||
| 153 | + | handler.activeConns.Add(1) | |||
| 154 | + | defer handler.activeConns.Add(-1) | |||
| 155 | + | ||||
| 156 | + | req2 := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |||
| 157 | + | req2.Host = "app.example.com" | |||
| 158 | + | rec2 := httptest.NewRecorder() | |||
| 159 | + | handler.ServeHTTP(rec2, req2) | |||
| 160 | + | assert.Equal(t, http.StatusServiceUnavailable, rec2.Code) | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func TestHandler_NoConcurrencyLimitWhenZero(t *testing.T) { | |||
| 164 | + | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |||
| 165 | + | w.WriteHeader(http.StatusOK) | |||
| 166 | + | })) | |||
| 167 | + | defer backend.Close() | |||
| 168 | + | ||||
| 169 | + | proxySvc := inmocks.NewMockProxyService(t) | |||
| 170 | + | backendPort := backend.Listener.Addr().(*net.TCPAddr).Port | |||
| 171 | + | ||||
| 172 | + | proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{ | |||
| 173 | + | MaxConcurrentConns: 0, | |||
| 174 | + | }) | |||
| 175 | + | proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false) | |||
| 176 | + | proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{ | |||
| 177 | + | Host: "127.0.0.1", | |||
| 178 | + | Port: backendPort, | |||
| 179 | + | ContainerID: "c-1", | |||
| 180 | + | Scheme: "http", | |||
| 181 | + | }, nil) | |||
| 182 | + | proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {}) | |||
| 183 | + | ||||
| 184 | + | handler := NewHandler(proxySvc, testLogger()) | |||
| 185 | + | ||||
| 186 | + | req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil) | |||
| 187 | + | req.Host = "app.example.com" | |||
| 188 | + | w := httptest.NewRecorder() | |||
| 189 | + | handler.ServeHTTP(w, req) | |||
| 190 | + | ||||
| 191 | + | assert.Equal(t, http.StatusOK, w.Code) | |||
| 192 | + | } | |||
M internal/usecase/proxy/service_test.go +116 -115
| @@ -2,7 +2,6 @@ package proxy | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "net/http/httptest" | |
| 6 | 5 | "testing" | |
| 7 | 6 | "time" | |
| 8 | 7 | ||
| @@ -398,120 +397,6 @@ func TestContainerDeployedHandler_Handle_NoDomain(t *testing.T) { | |||
| 398 | 397 | assert.NoError(t, err) | |
| 399 | 398 | } | |
| 400 | 399 | ||
| 401 | - | func TestService_ConcurrentConnectionLimit(t *testing.T) { | |
| 402 | - | runtime := outmocks.NewMockContainerRuntime(t) | |
| 403 | - | containerSvc := inmocks.NewMockContainerService(t) | |
| 404 | - | configSvc := inmocks.NewMockConfigService(t) | |
| 405 | - | ||
| 406 | - | // Allow only 1 concurrent connection | |
| 407 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |
| 408 | - | MaxConcurrentConns: 1, | |
| 409 | - | }) | |
| 410 | - | ||
| 411 | - | assert.Equal(t, 1, svc.config.MaxConcurrentConns) | |
| 412 | - | assert.Equal(t, int64(0), svc.activeConns.Load(), "active connections should start at 0") | |
| 413 | - | } | |
| 414 | - | ||
| 415 | - | func TestService_ConcurrentConnectionLimit_Disabled(t *testing.T) { | |
| 416 | - | runtime := outmocks.NewMockContainerRuntime(t) | |
| 417 | - | containerSvc := inmocks.NewMockContainerService(t) | |
| 418 | - | configSvc := inmocks.NewMockConfigService(t) | |
| 419 | - | ||
| 420 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |
| 421 | - | MaxConcurrentConns: 0, | |
| 422 | - | }) | |
| 423 | - | ||
| 424 | - | assert.Equal(t, 0, svc.config.MaxConcurrentConns, "0 means no limit") | |
| 425 | - | } | |
| 426 | - | ||
| 427 | - | func TestService_ConcurrentConnectionLimit_503WhenFull(t *testing.T) { | |
| 428 | - | runtime := outmocks.NewMockContainerRuntime(t) | |
| 429 | - | containerSvc := inmocks.NewMockContainerService(t) | |
| 430 | - | configSvc := inmocks.NewMockConfigService(t) | |
| 431 | - | ||
| 432 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |
| 433 | - | MaxConcurrentConns: 1, | |
| 434 | - | }) | |
| 435 | - | ||
| 436 | - | // Simulate an in-flight connection by incrementing the atomic counter | |
| 437 | - | svc.activeConns.Add(1) | |
| 438 | - | defer svc.activeConns.Add(-1) | |
| 439 | - | ||
| 440 | - | // Next request should get 503 | |
| 441 | - | req := httptest.NewRequest("GET", "http://example.com/test", nil) | |
| 442 | - | w := httptest.NewRecorder() | |
| 443 | - | svc.ServeHTTP(w, req) | |
| 444 | - | ||
| 445 | - | assert.Equal(t, 503, w.Code) | |
| 446 | - | } | |
| 447 | - | ||
| 448 | - | func TestServeHTTP_MaxBodySize(t *testing.T) { | |
| 449 | - | tests := []struct { | |
| 450 | - | name string | |
| 451 | - | maxBodySize int64 | |
| 452 | - | bodySize int | |
| 453 | - | expect413 bool | |
| 454 | - | }{ | |
| 455 | - | { | |
| 456 | - | name: "body within limit", | |
| 457 | - | maxBodySize: 1024, | |
| 458 | - | bodySize: 512, | |
| 459 | - | expect413: false, | |
| 460 | - | }, | |
| 461 | - | { | |
| 462 | - | name: "body exceeds limit returns 413", | |
| 463 | - | maxBodySize: 1024, | |
| 464 | - | bodySize: 2048, | |
| 465 | - | expect413: true, | |
| 466 | - | }, | |
| 467 | - | { | |
| 468 | - | name: "no limit configured allows any size", | |
| 469 | - | maxBodySize: 0, | |
| 470 | - | bodySize: 10240, | |
| 471 | - | expect413: false, | |
| 472 | - | }, | |
| 473 | - | } | |
| 474 | - | ||
| 475 | - | for _, tt := range tests { | |
| 476 | - | t.Run(tt.name, func(t *testing.T) { | |
| 477 | - | // Create fresh mocks for each test | |
| 478 | - | runtime := outmocks.NewMockContainerRuntime(t) | |
| 479 | - | containerSvc := inmocks.NewMockContainerService(t) | |
| 480 | - | configSvc := inmocks.NewMockConfigService(t) | |
| 481 | - | ||
| 482 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |
| 483 | - | MaxBodySize: tt.maxBodySize, | |
| 484 | - | }) | |
| 485 | - | ||
| 486 | - | // Mock GetExternalRoutes to return empty map (no external routes) | |
| 487 | - | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |
| 488 | - | ||
| 489 | - | // Mock container service Get to return not found (404) | |
| 490 | - | containerSvc.EXPECT().Get(mock.Anything, "example.com").Return(nil, false) | |
| 491 | - | ||
| 492 | - | // Create a test request with a body of specified size | |
| 493 | - | body := make([]byte, tt.bodySize) | |
| 494 | - | for i := range body { | |
| 495 | - | body[i] = 'a' | |
| 496 | - | } | |
| 497 | - | ||
| 498 | - | req := httptest.NewRequest("POST", "http://example.com/test", nil) | |
| 499 | - | req.Header.Set("Host", "example.com") | |
| 500 | - | w := httptest.NewRecorder() | |
| 501 | - | ||
| 502 | - | // Note: We're testing ServeHTTP which applies the MaxBytesReader wrapper | |
| 503 | - | // The actual limit enforcement happens during request body reading by httputil.ReverseProxy | |
| 504 | - | // So we verify the MaxBytesReader is applied correctly | |
| 505 | - | svc.ServeHTTP(w, req) | |
| 506 | - | ||
| 507 | - | // The MaxBytesReader is applied, but the actual error happens when ReverseProxy reads the body | |
| 508 | - | // We can't easily test the full flow without a real backend, so we verify the wrapper is applied | |
| 509 | - | // by checking that MaxBodySize is properly stored in the service config | |
| 510 | - | assert.Equal(t, tt.maxBodySize, svc.config.MaxBodySize) | |
| 511 | - | }) | |
| 512 | - | } | |
| 513 | - | } | |
| 514 | - | ||
| 515 | 400 | func TestRegistryInFlightTracking(t *testing.T) { | |
| 516 | 401 | svc := &Service{ | |
| 517 | 402 | inFlight: make(map[string]int), | |
| @@ -572,6 +457,122 @@ func TestDrainRegistryInFlightTimeout(t *testing.T) { | |||
| 572 | 457 | } | |
| 573 | 458 | } | |
| 574 | 459 | ||
| 460 | + | func TestService_ProxyConfig_ReflectsUpdates(t *testing.T) { | |
| 461 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 462 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 463 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 464 | + | ||
| 465 | + | svc := NewService(runtime, containerSvc, configSvc, Config{ | |
| 466 | + | RegistryDomain: "old.registry.com", | |
| 467 | + | RegistryPort: 5000, | |
| 468 | + | MaxBodySize: 1024, | |
| 469 | + | MaxResponseSize: 2048, | |
| 470 | + | MaxConcurrentConns: 10, | |
| 471 | + | }) | |
| 472 | + | ||
| 473 | + | cfg := svc.ProxyConfig() | |
| 474 | + | assert.Equal(t, "old.registry.com", cfg.RegistryDomain) | |
| 475 | + | assert.Equal(t, 5000, cfg.RegistryPort) | |
| 476 | + | assert.Equal(t, int64(1024), cfg.MaxBodySize) | |
| 477 | + | assert.Equal(t, int64(2048), cfg.MaxResponseSize) | |
| 478 | + | assert.Equal(t, 10, cfg.MaxConcurrentConns) | |
| 479 | + | ||
| 480 | + | svc.UpdateConfig(Config{ | |
| 481 | + | RegistryDomain: "new.registry.com", | |
| 482 | + | RegistryPort: 5001, | |
| 483 | + | MaxBodySize: 4096, | |
| 484 | + | MaxResponseSize: 8192, | |
| 485 | + | MaxConcurrentConns: 50, | |
| 486 | + | }) | |
| 487 | + | ||
| 488 | + | cfg = svc.ProxyConfig() | |
| 489 | + | assert.Equal(t, "new.registry.com", cfg.RegistryDomain) | |
| 490 | + | assert.Equal(t, 5001, cfg.RegistryPort) | |
| 491 | + | assert.Equal(t, int64(4096), cfg.MaxBodySize) | |
| 492 | + | assert.Equal(t, int64(8192), cfg.MaxResponseSize) | |
| 493 | + | assert.Equal(t, 50, cfg.MaxConcurrentConns) | |
| 494 | + | } | |
| 495 | + | ||
| 496 | + | func TestService_IsRegistryDomain(t *testing.T) { | |
| 497 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 498 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 499 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 500 | + | ||
| 501 | + | svc := NewService(runtime, containerSvc, configSvc, Config{ | |
| 502 | + | RegistryDomain: "registry.example.com", | |
| 503 | + | }) | |
| 504 | + | ||
| 505 | + | assert.True(t, svc.IsRegistryDomain("registry.example.com")) | |
| 506 | + | assert.False(t, svc.IsRegistryDomain("other.example.com")) | |
| 507 | + | assert.False(t, svc.IsRegistryDomain("")) | |
| 508 | + | } | |
| 509 | + | ||
| 510 | + | func TestService_IsRegistryDomain_EmptyConfig(t *testing.T) { | |
| 511 | + | runtime := outmocks.NewMockContainerRuntime(t) | |
| 512 | + | containerSvc := inmocks.NewMockContainerService(t) | |
| 513 | + | configSvc := inmocks.NewMockConfigService(t) | |
| 514 | + | ||
| 515 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |
| 516 | + | ||
| 517 | + | assert.False(t, svc.IsRegistryDomain("registry.example.com")) | |
| 518 | + | assert.False(t, svc.IsRegistryDomain("")) | |
| 519 | + | } | |
| 520 | + | ||
| 521 | + | func TestService_TrackInFlight(t *testing.T) { | |
| 522 | + | svc := &Service{ | |
| 523 | + | inFlight: make(map[string]int), | |
| 524 | + | } | |
| 525 | + | ||
| 526 | + | // Empty container ID returns noop | |
| 527 | + | release := svc.TrackInFlight("") | |
| 528 | + | release() // should not panic | |
| 529 | + | ||
| 530 | + | // Track a container | |
| 531 | + | release1 := svc.TrackInFlight("c-1") | |
| 532 | + | svc.inFlightMu.Lock() | |
| 533 | + | assert.Equal(t, 1, svc.inFlight["c-1"]) | |
| 534 | + | svc.inFlightMu.Unlock() | |
| 535 | + | ||
| 536 | + | // Track same container again | |
| 537 | + | release2 := svc.TrackInFlight("c-1") | |
| 538 | + | svc.inFlightMu.Lock() | |
| 539 | + | assert.Equal(t, 2, svc.inFlight["c-1"]) | |
| 540 | + | svc.inFlightMu.Unlock() | |
| 541 | + | ||
| 542 | + | // Release one | |
| 543 | + | release2() | |
| 544 | + | svc.inFlightMu.Lock() | |
| 545 | + | assert.Equal(t, 1, svc.inFlight["c-1"]) | |
| 546 | + | svc.inFlightMu.Unlock() | |
| 547 | + | ||
| 548 | + | // Release last — should delete key | |
| 549 | + | release1() | |
| 550 | + | svc.inFlightMu.Lock() | |
| 551 | + | _, exists := svc.inFlight["c-1"] | |
| 552 | + | assert.False(t, exists, "container should be removed from inFlight map when count reaches 0") | |
| 553 | + | svc.inFlightMu.Unlock() | |
| 554 | + | } | |
| 555 | + | ||
| 556 | + | func TestService_TrackRegistryRequest(t *testing.T) { | |
| 557 | + | svc := &Service{ | |
| 558 | + | inFlight: make(map[string]int), | |
| 559 | + | } | |
| 560 | + | ||
| 561 | + | assert.Equal(t, int64(0), svc.RegistryInFlight()) | |
| 562 | + | ||
| 563 | + | svc.TrackRegistryRequest() | |
| 564 | + | assert.Equal(t, int64(1), svc.RegistryInFlight()) | |
| 565 | + | ||
| 566 | + | svc.TrackRegistryRequest() | |
| 567 | + | assert.Equal(t, int64(2), svc.RegistryInFlight()) | |
| 568 | + | ||
| 569 | + | svc.ReleaseRegistryRequest() | |
| 570 | + | assert.Equal(t, int64(1), svc.RegistryInFlight()) | |
| 571 | + | ||
| 572 | + | svc.ReleaseRegistryRequest() | |
| 573 | + | assert.Equal(t, int64(0), svc.RegistryInFlight()) | |
| 574 | + | } | |
| 575 | + | ||
| 575 | 576 | func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | |
| 576 | 577 | runtime := outmocks.NewMockContainerRuntime(t) | |
| 577 | 578 | containerSvc := inmocks.NewMockContainerService(t) | |
| @@ -2,7 +2,6 @@ package proxy | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "net/http/httptest" | |||
| 6 | "testing" | 5 | "testing" | ||
| 7 | "time" | 6 | "time" | ||
| 8 | 7 | ||||
| @@ -398,120 +397,6 @@ func TestContainerDeployedHandler_Handle_NoDomain(t *testing.T) { | |||||
| 398 | assert.NoError(t, err) | 397 | assert.NoError(t, err) | ||
| 399 | } | 398 | } | ||
| 400 | 399 | ||||
| 401 | - | func TestService_ConcurrentConnectionLimit(t *testing.T) { | |||
| 402 | - | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 403 | - | containerSvc := inmocks.NewMockContainerService(t) | |||
| 404 | - | configSvc := inmocks.NewMockConfigService(t) | |||
| 405 | - | ||||
| 406 | - | // Allow only 1 concurrent connection | |||
| 407 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |||
| 408 | - | MaxConcurrentConns: 1, | |||
| 409 | - | }) | |||
| 410 | - | ||||
| 411 | - | assert.Equal(t, 1, svc.config.MaxConcurrentConns) | |||
| 412 | - | assert.Equal(t, int64(0), svc.activeConns.Load(), "active connections should start at 0") | |||
| 413 | - | } | |||
| 414 | - | ||||
| 415 | - | func TestService_ConcurrentConnectionLimit_Disabled(t *testing.T) { | |||
| 416 | - | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 417 | - | containerSvc := inmocks.NewMockContainerService(t) | |||
| 418 | - | configSvc := inmocks.NewMockConfigService(t) | |||
| 419 | - | ||||
| 420 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |||
| 421 | - | MaxConcurrentConns: 0, | |||
| 422 | - | }) | |||
| 423 | - | ||||
| 424 | - | assert.Equal(t, 0, svc.config.MaxConcurrentConns, "0 means no limit") | |||
| 425 | - | } | |||
| 426 | - | ||||
| 427 | - | func TestService_ConcurrentConnectionLimit_503WhenFull(t *testing.T) { | |||
| 428 | - | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 429 | - | containerSvc := inmocks.NewMockContainerService(t) | |||
| 430 | - | configSvc := inmocks.NewMockConfigService(t) | |||
| 431 | - | ||||
| 432 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |||
| 433 | - | MaxConcurrentConns: 1, | |||
| 434 | - | }) | |||
| 435 | - | ||||
| 436 | - | // Simulate an in-flight connection by incrementing the atomic counter | |||
| 437 | - | svc.activeConns.Add(1) | |||
| 438 | - | defer svc.activeConns.Add(-1) | |||
| 439 | - | ||||
| 440 | - | // Next request should get 503 | |||
| 441 | - | req := httptest.NewRequest("GET", "http://example.com/test", nil) | |||
| 442 | - | w := httptest.NewRecorder() | |||
| 443 | - | svc.ServeHTTP(w, req) | |||
| 444 | - | ||||
| 445 | - | assert.Equal(t, 503, w.Code) | |||
| 446 | - | } | |||
| 447 | - | ||||
| 448 | - | func TestServeHTTP_MaxBodySize(t *testing.T) { | |||
| 449 | - | tests := []struct { | |||
| 450 | - | name string | |||
| 451 | - | maxBodySize int64 | |||
| 452 | - | bodySize int | |||
| 453 | - | expect413 bool | |||
| 454 | - | }{ | |||
| 455 | - | { | |||
| 456 | - | name: "body within limit", | |||
| 457 | - | maxBodySize: 1024, | |||
| 458 | - | bodySize: 512, | |||
| 459 | - | expect413: false, | |||
| 460 | - | }, | |||
| 461 | - | { | |||
| 462 | - | name: "body exceeds limit returns 413", | |||
| 463 | - | maxBodySize: 1024, | |||
| 464 | - | bodySize: 2048, | |||
| 465 | - | expect413: true, | |||
| 466 | - | }, | |||
| 467 | - | { | |||
| 468 | - | name: "no limit configured allows any size", | |||
| 469 | - | maxBodySize: 0, | |||
| 470 | - | bodySize: 10240, | |||
| 471 | - | expect413: false, | |||
| 472 | - | }, | |||
| 473 | - | } | |||
| 474 | - | ||||
| 475 | - | for _, tt := range tests { | |||
| 476 | - | t.Run(tt.name, func(t *testing.T) { | |||
| 477 | - | // Create fresh mocks for each test | |||
| 478 | - | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 479 | - | containerSvc := inmocks.NewMockContainerService(t) | |||
| 480 | - | configSvc := inmocks.NewMockConfigService(t) | |||
| 481 | - | ||||
| 482 | - | svc := NewService(runtime, containerSvc, configSvc, Config{ | |||
| 483 | - | MaxBodySize: tt.maxBodySize, | |||
| 484 | - | }) | |||
| 485 | - | ||||
| 486 | - | // Mock GetExternalRoutes to return empty map (no external routes) | |||
| 487 | - | configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{}) | |||
| 488 | - | ||||
| 489 | - | // Mock container service Get to return not found (404) | |||
| 490 | - | containerSvc.EXPECT().Get(mock.Anything, "example.com").Return(nil, false) | |||
| 491 | - | ||||
| 492 | - | // Create a test request with a body of specified size | |||
| 493 | - | body := make([]byte, tt.bodySize) | |||
| 494 | - | for i := range body { | |||
| 495 | - | body[i] = 'a' | |||
| 496 | - | } | |||
| 497 | - | ||||
| 498 | - | req := httptest.NewRequest("POST", "http://example.com/test", nil) | |||
| 499 | - | req.Header.Set("Host", "example.com") | |||
| 500 | - | w := httptest.NewRecorder() | |||
| 501 | - | ||||
| 502 | - | // Note: We're testing ServeHTTP which applies the MaxBytesReader wrapper | |||
| 503 | - | // The actual limit enforcement happens during request body reading by httputil.ReverseProxy | |||
| 504 | - | // So we verify the MaxBytesReader is applied correctly | |||
| 505 | - | svc.ServeHTTP(w, req) | |||
| 506 | - | ||||
| 507 | - | // The MaxBytesReader is applied, but the actual error happens when ReverseProxy reads the body | |||
| 508 | - | // We can't easily test the full flow without a real backend, so we verify the wrapper is applied | |||
| 509 | - | // by checking that MaxBodySize is properly stored in the service config | |||
| 510 | - | assert.Equal(t, tt.maxBodySize, svc.config.MaxBodySize) | |||
| 511 | - | }) | |||
| 512 | - | } | |||
| 513 | - | } | |||
| 514 | - | ||||
| 515 | func TestRegistryInFlightTracking(t *testing.T) { | 400 | func TestRegistryInFlightTracking(t *testing.T) { | ||
| 516 | svc := &Service{ | 401 | svc := &Service{ | ||
| 517 | inFlight: make(map[string]int), | 402 | inFlight: make(map[string]int), | ||
| @@ -572,6 +457,122 @@ func TestDrainRegistryInFlightTimeout(t *testing.T) { | |||||
| 572 | } | 457 | } | ||
| 573 | } | 458 | } | ||
| 574 | 459 | ||||
| 460 | + | func TestService_ProxyConfig_ReflectsUpdates(t *testing.T) { | |||
| 461 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 462 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 463 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 464 | + | ||||
| 465 | + | svc := NewService(runtime, containerSvc, configSvc, Config{ | |||
| 466 | + | RegistryDomain: "old.registry.com", | |||
| 467 | + | RegistryPort: 5000, | |||
| 468 | + | MaxBodySize: 1024, | |||
| 469 | + | MaxResponseSize: 2048, | |||
| 470 | + | MaxConcurrentConns: 10, | |||
| 471 | + | }) | |||
| 472 | + | ||||
| 473 | + | cfg := svc.ProxyConfig() | |||
| 474 | + | assert.Equal(t, "old.registry.com", cfg.RegistryDomain) | |||
| 475 | + | assert.Equal(t, 5000, cfg.RegistryPort) | |||
| 476 | + | assert.Equal(t, int64(1024), cfg.MaxBodySize) | |||
| 477 | + | assert.Equal(t, int64(2048), cfg.MaxResponseSize) | |||
| 478 | + | assert.Equal(t, 10, cfg.MaxConcurrentConns) | |||
| 479 | + | ||||
| 480 | + | svc.UpdateConfig(Config{ | |||
| 481 | + | RegistryDomain: "new.registry.com", | |||
| 482 | + | RegistryPort: 5001, | |||
| 483 | + | MaxBodySize: 4096, | |||
| 484 | + | MaxResponseSize: 8192, | |||
| 485 | + | MaxConcurrentConns: 50, | |||
| 486 | + | }) | |||
| 487 | + | ||||
| 488 | + | cfg = svc.ProxyConfig() | |||
| 489 | + | assert.Equal(t, "new.registry.com", cfg.RegistryDomain) | |||
| 490 | + | assert.Equal(t, 5001, cfg.RegistryPort) | |||
| 491 | + | assert.Equal(t, int64(4096), cfg.MaxBodySize) | |||
| 492 | + | assert.Equal(t, int64(8192), cfg.MaxResponseSize) | |||
| 493 | + | assert.Equal(t, 50, cfg.MaxConcurrentConns) | |||
| 494 | + | } | |||
| 495 | + | ||||
| 496 | + | func TestService_IsRegistryDomain(t *testing.T) { | |||
| 497 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 498 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 499 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 500 | + | ||||
| 501 | + | svc := NewService(runtime, containerSvc, configSvc, Config{ | |||
| 502 | + | RegistryDomain: "registry.example.com", | |||
| 503 | + | }) | |||
| 504 | + | ||||
| 505 | + | assert.True(t, svc.IsRegistryDomain("registry.example.com")) | |||
| 506 | + | assert.False(t, svc.IsRegistryDomain("other.example.com")) | |||
| 507 | + | assert.False(t, svc.IsRegistryDomain("")) | |||
| 508 | + | } | |||
| 509 | + | ||||
| 510 | + | func TestService_IsRegistryDomain_EmptyConfig(t *testing.T) { | |||
| 511 | + | runtime := outmocks.NewMockContainerRuntime(t) | |||
| 512 | + | containerSvc := inmocks.NewMockContainerService(t) | |||
| 513 | + | configSvc := inmocks.NewMockConfigService(t) | |||
| 514 | + | ||||
| 515 | + | svc := NewService(runtime, containerSvc, configSvc, Config{}) | |||
| 516 | + | ||||
| 517 | + | assert.False(t, svc.IsRegistryDomain("registry.example.com")) | |||
| 518 | + | assert.False(t, svc.IsRegistryDomain("")) | |||
| 519 | + | } | |||
| 520 | + | ||||
| 521 | + | func TestService_TrackInFlight(t *testing.T) { | |||
| 522 | + | svc := &Service{ | |||
| 523 | + | inFlight: make(map[string]int), | |||
| 524 | + | } | |||
| 525 | + | ||||
| 526 | + | // Empty container ID returns noop | |||
| 527 | + | release := svc.TrackInFlight("") | |||
| 528 | + | release() // should not panic | |||
| 529 | + | ||||
| 530 | + | // Track a container | |||
| 531 | + | release1 := svc.TrackInFlight("c-1") | |||
| 532 | + | svc.inFlightMu.Lock() | |||
| 533 | + | assert.Equal(t, 1, svc.inFlight["c-1"]) | |||
| 534 | + | svc.inFlightMu.Unlock() | |||
| 535 | + | ||||
| 536 | + | // Track same container again | |||
| 537 | + | release2 := svc.TrackInFlight("c-1") | |||
| 538 | + | svc.inFlightMu.Lock() | |||
| 539 | + | assert.Equal(t, 2, svc.inFlight["c-1"]) | |||
| 540 | + | svc.inFlightMu.Unlock() | |||
| 541 | + | ||||
| 542 | + | // Release one | |||
| 543 | + | release2() | |||
| 544 | + | svc.inFlightMu.Lock() | |||
| 545 | + | assert.Equal(t, 1, svc.inFlight["c-1"]) | |||
| 546 | + | svc.inFlightMu.Unlock() | |||
| 547 | + | ||||
| 548 | + | // Release last — should delete key | |||
| 549 | + | release1() | |||
| 550 | + | svc.inFlightMu.Lock() | |||
| 551 | + | _, exists := svc.inFlight["c-1"] | |||
| 552 | + | assert.False(t, exists, "container should be removed from inFlight map when count reaches 0") | |||
| 553 | + | svc.inFlightMu.Unlock() | |||
| 554 | + | } | |||
| 555 | + | ||||
| 556 | + | func TestService_TrackRegistryRequest(t *testing.T) { | |||
| 557 | + | svc := &Service{ | |||
| 558 | + | inFlight: make(map[string]int), | |||
| 559 | + | } | |||
| 560 | + | ||||
| 561 | + | assert.Equal(t, int64(0), svc.RegistryInFlight()) | |||
| 562 | + | ||||
| 563 | + | svc.TrackRegistryRequest() | |||
| 564 | + | assert.Equal(t, int64(1), svc.RegistryInFlight()) | |||
| 565 | + | ||||
| 566 | + | svc.TrackRegistryRequest() | |||
| 567 | + | assert.Equal(t, int64(2), svc.RegistryInFlight()) | |||
| 568 | + | ||||
| 569 | + | svc.ReleaseRegistryRequest() | |||
| 570 | + | assert.Equal(t, int64(1), svc.RegistryInFlight()) | |||
| 571 | + | ||||
| 572 | + | svc.ReleaseRegistryRequest() | |||
| 573 | + | assert.Equal(t, int64(0), svc.RegistryInFlight()) | |||
| 574 | + | } | |||
| 575 | + | ||||
| 575 | func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | 576 | func TestService_GetTarget_HostMode_UsesProxyPortLabel(t *testing.T) { | ||
| 576 | runtime := outmocks.NewMockContainerRuntime(t) | 577 | runtime := outmocks.NewMockContainerRuntime(t) | ||
| 577 | containerSvc := inmocks.NewMockContainerService(t) | 578 | containerSvc := inmocks.NewMockContainerService(t) | ||