master
go 460 lines 12.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package web
4
5 import (
6 "errors"
7 "net"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "os"
12 "testing"
13 "time"
14
15 "golang.org/x/net/http2"
16
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19
20 "github.com/netdata/netdata/go/plugins/pkg/confopt"
21 "github.com/netdata/netdata/go/plugins/pkg/tlscfg"
22 )
23
24 func TestNewHTTPClient(t *testing.T) {
25 tests := map[string]struct {
26 config ClientConfig
27 validate func(t *testing.T, client *http.Client, cfg ClientConfig)
28 wantErr bool
29 errMsg string
30 }{
31 "default config": {
32 config: ClientConfig{},
33 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
34 assert.Zero(t, client.Timeout)
35 assert.Nil(t, client.CheckRedirect)
36 assert.NotNil(t, client.Transport)
37
38 // Verify it's using http.Transport (not http2)
39 transport, ok := client.Transport.(*http.Transport)
40 assert.True(t, ok)
41 assert.NotNil(t, transport)
42 },
43 },
44 "with timeout": {
45 config: ClientConfig{
46 Timeout: confopt.Duration(time.Second * 5),
47 },
48 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
49 assert.Equal(t, time.Second*5, client.Timeout)
50
51 transport, ok := client.Transport.(*http.Transport)
52 require.True(t, ok)
53 assert.Equal(t, time.Second*5, transport.TLSHandshakeTimeout)
54 },
55 },
56 "not follow redirect": {
57 config: ClientConfig{
58 NotFollowRedirect: true,
59 },
60 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
61 assert.NotNil(t, client.CheckRedirect)
62
63 // Test the redirect function
64 err := client.CheckRedirect(nil, nil)
65 assert.Equal(t, ErrRedirectAttempted, err)
66 },
67 },
68 "with proxy URL": {
69 config: ClientConfig{
70 ProxyURL: "http://127.0.0.1:3128",
71 },
72 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
73 transport, ok := client.Transport.(*http.Transport)
74 require.True(t, ok)
75 assert.NotNil(t, transport.Proxy)
76
77 // Test proxy function
78 req := httptest.NewRequest("GET", "http://example.com", nil)
79 proxyURL, err := transport.Proxy(req)
80 assert.NoError(t, err)
81 assert.Equal(t, "http://127.0.0.1:3128", proxyURL.String())
82 },
83 },
84 "invalid proxy URL": {
85 config: ClientConfig{
86 ProxyURL: "://invalid-url",
87 },
88 wantErr: true,
89 errMsg: "error on parsing proxy URL",
90 },
91 "empty proxy URL uses environment": {
92 config: ClientConfig{
93 ProxyURL: "",
94 },
95 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
96 transport, ok := client.Transport.(*http.Transport)
97 require.True(t, ok)
98
99 // Set env var for testing
100 _ = os.Setenv("HTTP_PROXY", "http://env-proxy:8080")
101 defer func() { _ = os.Unsetenv("HTTP_PROXY") }()
102
103 req := httptest.NewRequest("GET", "http://example.com", nil)
104 proxyURL, err := transport.Proxy(req)
105 assert.NoError(t, err)
106 if proxyURL != nil {
107 assert.Equal(t, "http://env-proxy:8080", proxyURL.String())
108 }
109 },
110 },
111 "force HTTP2": {
112 config: ClientConfig{
113 ForceHTTP2: true,
114 },
115 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
116 // Verify it's using http2Transport
117 transport, ok := client.Transport.(*http2Transport)
118 assert.True(t, ok)
119 assert.NotNil(t, transport)
120 assert.NotNil(t, transport.t2)
121 assert.NotNil(t, transport.t2c)
122 },
123 },
124 "with TLS config": {
125 config: ClientConfig{
126 TLSConfig: tlscfg.TLSConfig{
127 InsecureSkipVerify: true,
128 },
129 },
130 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
131 transport, ok := client.Transport.(*http.Transport)
132 require.True(t, ok)
133 assert.NotNil(t, transport.TLSClientConfig)
134 assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
135 },
136 },
137 "invalid TLS config": {
138 config: ClientConfig{
139 TLSConfig: tlscfg.TLSConfig{
140 TLSCA: "/non/existent/ca.pem",
141 TLSCert: "/non/existent/cert.pem",
142 TLSKey: "/non/existent/key.pem",
143 },
144 },
145 wantErr: true,
146 errMsg: "error on creating TLS config",
147 },
148 "full config": {
149 config: ClientConfig{
150 Timeout: confopt.Duration(time.Second * 10),
151 NotFollowRedirect: true,
152 ProxyURL: "http://proxy:8080",
153 TLSConfig: tlscfg.TLSConfig{
154 InsecureSkipVerify: true,
155 },
156 },
157 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
158 assert.Equal(t, time.Second*10, client.Timeout)
159 assert.NotNil(t, client.CheckRedirect)
160
161 transport, ok := client.Transport.(*http.Transport)
162 require.True(t, ok)
163 assert.True(t, transport.TLSClientConfig.InsecureSkipVerify)
164 assert.NotNil(t, transport.Proxy)
165 },
166 },
167 "HTTP2 with TLS config": {
168 config: ClientConfig{
169 ForceHTTP2: true,
170 TLSConfig: tlscfg.TLSConfig{
171 InsecureSkipVerify: true,
172 },
173 },
174 validate: func(t *testing.T, client *http.Client, cfg ClientConfig) {
175 transport, ok := client.Transport.(*http2Transport)
176 require.True(t, ok)
177 assert.True(t, transport.t2.TLSClientConfig.InsecureSkipVerify)
178 assert.True(t, transport.t2c.TLSClientConfig.InsecureSkipVerify)
179 },
180 },
181 }
182
183 for name, test := range tests {
184 t.Run(name, func(t *testing.T) {
185 client, err := NewHTTPClient(test.config)
186
187 if test.wantErr {
188 assert.Error(t, err)
189 if test.errMsg != "" {
190 assert.Contains(t, err.Error(), test.errMsg)
191 }
192 assert.Nil(t, client)
193 return
194 }
195
196 require.NoError(t, err)
197 require.NotNil(t, client)
198
199 if test.validate != nil {
200 test.validate(t, client, test.config)
201 }
202 })
203 }
204 }
205
206 func TestHTTP2Transport_RoundTrip(t *testing.T) {
207 // Test that http2Transport can be created and has the expected structure
208 cfg := ClientConfig{
209 ForceHTTP2: true,
210 TLSConfig: tlscfg.TLSConfig{
211 InsecureSkipVerify: true,
212 },
213 }
214
215 client, err := NewHTTPClient(cfg)
216 require.NoError(t, err)
217
218 // Verify the transport is http2Transport
219 transport, ok := client.Transport.(*http2Transport)
220 require.True(t, ok)
221 assert.NotNil(t, transport.t2)
222 assert.NotNil(t, transport.t2c)
223
224 // Note: We can't easily test actual HTTP/2 communication without setting up
225 // proper HTTP/2 servers, which httptest doesn't support directly.
226 // The integration test with regular servers is sufficient for basic functionality.
227 }
228
229 func TestHTTP2Transport_CloseIdleConnections(t *testing.T) {
230 transport := &http2Transport{
231 t2: &http2.Transport{},
232 t2c: &http2.Transport{},
233 }
234
235 // This should not panic
236 assert.NotPanics(t, func() {
237 transport.CloseIdleConnections()
238 })
239 }
240
241 func TestProxyFunc(t *testing.T) {
242 tests := map[string]struct {
243 proxyURL string
244 envProxy string
245 validate func(t *testing.T, proxyFunc func(*http.Request) (*url.URL, error))
246 }{
247 "empty proxy URL uses environment": {
248 proxyURL: "",
249 envProxy: "http://env-proxy:8080",
250 validate: func(t *testing.T, proxyFunc func(*http.Request) (*url.URL, error)) {
251 _ = os.Setenv("HTTP_PROXY", "http://env-proxy:8080")
252 defer func() { _ = os.Unsetenv("HTTP_PROXY") }()
253
254 req := httptest.NewRequest("GET", "http://example.com", nil)
255 proxyURL, err := proxyFunc(req)
256 assert.NoError(t, err)
257 if proxyURL != nil {
258 assert.Equal(t, "http://env-proxy:8080", proxyURL.String())
259 }
260 },
261 },
262 "specific proxy URL": {
263 proxyURL: "http://specific-proxy:3128",
264 validate: func(t *testing.T, proxyFunc func(*http.Request) (*url.URL, error)) {
265 req := httptest.NewRequest("GET", "http://example.com", nil)
266 proxyURL, err := proxyFunc(req)
267 assert.NoError(t, err)
268 assert.Equal(t, "http://specific-proxy:3128", proxyURL.String())
269 },
270 },
271 "proxy URL with auth": {
272 proxyURL: "http://user:pass@proxy:3128",
273 validate: func(t *testing.T, proxyFunc func(*http.Request) (*url.URL, error)) {
274 req := httptest.NewRequest("GET", "http://example.com", nil)
275 proxyURL, err := proxyFunc(req)
276 assert.NoError(t, err)
277 assert.Equal(t, "http://user:pass@proxy:3128", proxyURL.String())
278 assert.Equal(t, "user", proxyURL.User.Username())
279 pass, _ := proxyURL.User.Password()
280 assert.Equal(t, "pass", pass)
281 },
282 },
283 "https proxy URL": {
284 proxyURL: "https://secure-proxy:443",
285 validate: func(t *testing.T, proxyFunc func(*http.Request) (*url.URL, error)) {
286 req := httptest.NewRequest("GET", "http://example.com", nil)
287 proxyURL, err := proxyFunc(req)
288 assert.NoError(t, err)
289 assert.Equal(t, "https://secure-proxy:443", proxyURL.String())
290 },
291 },
292 }
293
294 for name, test := range tests {
295 t.Run(name, func(t *testing.T) {
296 fn := proxyFunc(test.proxyURL)
297 assert.NotNil(t, fn)
298
299 if test.validate != nil {
300 test.validate(t, fn)
301 }
302 })
303 }
304 }
305
306 func TestRedirectFunc(t *testing.T) {
307 tests := map[string]struct {
308 notFollow bool
309 wantErr error
310 }{
311 "follow redirects": {
312 notFollow: false,
313 wantErr: nil,
314 },
315 "not follow redirects": {
316 notFollow: true,
317 wantErr: ErrRedirectAttempted,
318 },
319 }
320
321 for name, test := range tests {
322 t.Run(name, func(t *testing.T) {
323 fn := redirectFunc(test.notFollow)
324
325 if test.wantErr != nil {
326 assert.NotNil(t, fn)
327 err := fn(nil, nil)
328 assert.Equal(t, test.wantErr, err)
329 } else {
330 assert.Nil(t, fn)
331 }
332 })
333 }
334 }
335
336 func TestClientIntegration(t *testing.T) {
337 // Create a test server with various behaviors
338 redirectCount := 0
339 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
340 switch r.URL.Path {
341 case "/redirect":
342 redirectCount++
343 http.Redirect(w, r, "/final", http.StatusFound)
344 case "/final":
345 _, _ = w.Write([]byte("final destination"))
346 case "/timeout":
347 time.Sleep(time.Second * 2)
348 _, _ = w.Write([]byte("too late"))
349 default:
350 _, _ = w.Write([]byte("default response"))
351 }
352 }))
353 defer server.Close()
354
355 t.Run("follow redirects", func(t *testing.T) {
356 client, err := NewHTTPClient(ClientConfig{
357 NotFollowRedirect: false,
358 })
359 require.NoError(t, err)
360
361 resp, err := client.Get(server.URL + "/redirect")
362 require.NoError(t, err)
363 defer func() { _ = resp.Body.Close() }()
364
365 assert.Equal(t, "/final", resp.Request.URL.Path)
366 })
367
368 t.Run("not follow redirects", func(t *testing.T) {
369 client, err := NewHTTPClient(ClientConfig{
370 NotFollowRedirect: true,
371 })
372 require.NoError(t, err)
373
374 _, err = client.Get(server.URL + "/redirect")
375 assert.Error(t, err)
376
377 // Check if error contains redirect indication
378 urlErr, ok := err.(*url.Error)
379 if ok {
380 assert.Equal(t, ErrRedirectAttempted, urlErr.Err)
381 }
382 })
383
384 t.Run("timeout", func(t *testing.T) {
385 client, err := NewHTTPClient(ClientConfig{
386 Timeout: confopt.Duration(time.Millisecond * 500),
387 })
388 require.NoError(t, err)
389
390 _, err = client.Get(server.URL + "/timeout")
391 assert.Error(t, err)
392
393 var netErr net.Error
394 if errors.As(err, &netErr) {
395 assert.True(t, netErr.Timeout())
396 }
397 })
398 }
399
400 func TestTransportWithDifferentSchemes(t *testing.T) {
401 // Test that regular transport handles both http and https
402 client, err := NewHTTPClient(ClientConfig{
403 TLSConfig: tlscfg.TLSConfig{
404 InsecureSkipVerify: true,
405 },
406 })
407 require.NoError(t, err)
408
409 // HTTP server
410 httpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
411 _, _ = w.Write([]byte("http"))
412 }))
413 defer httpServer.Close()
414
415 // HTTPS server
416 httpsServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
417 _, _ = w.Write([]byte("https"))
418 }))
419 defer httpsServer.Close()
420
421 // Test HTTP
422 resp, err := client.Get(httpServer.URL)
423 require.NoError(t, err)
424 _ = resp.Body.Close()
425
426 // Test HTTPS
427 resp, err = client.Get(httpsServer.URL)
428 require.NoError(t, err)
429 _ = resp.Body.Close()
430 }
431
432 func TestHTTP2TransportStructure(t *testing.T) {
433 // Test the http2Transport structure and methods
434 transport := &http2Transport{
435 t2: &http2.Transport{},
436 t2c: &http2.Transport{AllowHTTP: true},
437 }
438
439 // Test HTTPS request routing
440 httpsReq := httptest.NewRequest("GET", "https://example.com", nil)
441 // Just verify it doesn't panic and routes to the correct transport
442 assert.NotPanics(t, func() {
443 // We can't actually execute the request without a server,
444 // but we can verify the routing logic
445 if httpsReq.URL.Scheme == "https" {
446 // Would use t2
447 assert.NotNil(t, transport.t2)
448 }
449 })
450
451 // Test HTTP request routing
452 httpReq := httptest.NewRequest("GET", "http://example.com", nil)
453 assert.NotPanics(t, func() {
454 if httpReq.URL.Scheme == "http" {
455 // Would use t2c
456 assert.NotNil(t, transport.t2c)
457 assert.True(t, transport.t2c.AllowHTTP)
458 }
459 })
460 }