package querytee import ( "io" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" "github.com/go-kit/log" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/atomic" ) func Test_ProxyEndpoint_waitBackendResponseForDownstream(t *testing.T) { backendURL1, err := url.Parse("http://backend-1/") require.NoError(t, err) backendURL2, err := url.Parse("http://backend-2/") require.NoError(t, err) backendURL3, err := url.Parse("http://backend-3/") require.NoError(t, err) backendPref := NewProxyBackend("backend-1", backendURL1, time.Second, true) backendOther1 := NewProxyBackend("backend-2", backendURL2, time.Second, false) backendOther2 := NewProxyBackend("backend-3", backendURL3, time.Second, false) tests := map[string]struct { backends []*ProxyBackend responses []*backendResponse expected *ProxyBackend }{ "the preferred backend is the 1st response received": { backends: []*ProxyBackend{backendPref, backendOther1}, responses: []*backendResponse{ {backend: backendPref, status: 200}, }, expected: backendPref, }, "the preferred backend is the last response received": { backends: []*ProxyBackend{backendPref, backendOther1}, responses: []*backendResponse{ {backend: backendOther1, status: 200}, {backend: backendPref, status: 200}, }, expected: backendPref, }, "the preferred backend is the last response received but it's not successful": { backends: []*ProxyBackend{backendPref, backendOther1}, responses: []*backendResponse{ {backend: backendOther1, status: 200}, {backend: backendPref, status: 500}, }, expected: backendOther1, }, "the preferred backend is the 2nd response received but only the last one is successful": { backends: []*ProxyBackend{backendPref, backendOther1, backendOther2}, responses: []*backendResponse{ {backend: backendOther1, status: 500}, {backend: backendPref, status: 500}, {backend: backendOther2, status: 200}, }, expected: backendOther2, }, "there's no preferred backend configured and the 1st response is successful": { backends: []*ProxyBackend{backendOther1, backendOther2}, responses: []*backendResponse{ {backend: backendOther1, status: 200}, }, expected: backendOther1, }, "there's no preferred backend configured and the last response is successful": { backends: []*ProxyBackend{backendOther1, backendOther2}, responses: []*backendResponse{ {backend: backendOther1, status: 500}, {backend: backendOther2, status: 200}, }, expected: backendOther2, }, "no received response is successful": { backends: []*ProxyBackend{backendPref, backendOther1}, responses: []*backendResponse{ {backend: backendOther1, status: 500}, {backend: backendPref, status: 500}, }, expected: backendOther1, }, } for testName, testData := range tests { testData := testData t.Run(testName, func(t *testing.T) { endpoint := NewProxyEndpoint(testData.backends, "test", NewProxyMetrics(nil), log.NewNopLogger(), nil) // Send the responses from a dedicated goroutine. resCh := make(chan *backendResponse) go func() { for _, res := range testData.responses { resCh <- res } close(resCh) }() // Wait for the selected backend response. actual := endpoint.waitBackendResponseForDownstream(resCh) assert.Equal(t, testData.expected, actual.backend) }) } } func Test_ProxyEndpoint_Requests(t *testing.T) { var ( requestCount atomic.Uint64 wg sync.WaitGroup testHandler http.HandlerFunc ) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer wg.Done() defer requestCount.Add(1) testHandler(w, r) }) backend1 := httptest.NewServer(handler) defer backend1.Close() backendURL1, err := url.Parse(backend1.URL) require.NoError(t, err) backend2 := httptest.NewServer(handler) defer backend2.Close() backendURL2, err := url.Parse(backend2.URL) require.NoError(t, err) backends := []*ProxyBackend{ NewProxyBackend("backend-1", backendURL1, time.Second, true), NewProxyBackend("backend-2", backendURL2, time.Second, false), } endpoint := NewProxyEndpoint(backends, "test", NewProxyMetrics(nil), log.NewNopLogger(), nil) for _, tc := range []struct { name string request func(*testing.T) *http.Request handler func(*testing.T) http.HandlerFunc }{ { name: "GET-request", request: func(t *testing.T) *http.Request { r, err := http.NewRequest("GET", "http://test/api/v1/test", nil) r.Header["test-X"] = []string{"test-X-value"} require.NoError(t, err) return r }, handler: func(t *testing.T) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "test-X-value", r.Header.Get("test-X")) _, _ = w.Write([]byte("ok")) } }, }, { name: "GET-filter-accept-encoding", request: func(t *testing.T) *http.Request { r, err := http.NewRequest("GET", "http://test/api/v1/test", nil) r.Header.Set("Accept-Encoding", "gzip") require.NoError(t, err) return r }, handler: func(t *testing.T) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { require.Equal(t, 0, len(r.Header.Values("Accept-Encoding"))) _, _ = w.Write([]byte("ok")) } }, }, { name: "POST-request-with-body", request: func(t *testing.T) *http.Request { strings := strings.NewReader("this-is-some-payload") r, err := http.NewRequest("POST", "http://test/api/v1/test", strings) require.NoError(t, err) r.Header["test-X"] = []string{"test-X-value"} return r }, handler: func(t *testing.T) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) require.Equal(t, "this-is-some-payload", string(body)) require.NoError(t, err) require.Equal(t, "test-X-value", r.Header.Get("test-X")) _, _ = w.Write([]byte("ok")) } }, }, } { t.Run(tc.name, func(t *testing.T) { // reset request count requestCount.Store(0) wg.Add(2) if tc.handler == nil { testHandler = func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) } } else { testHandler = tc.handler(t) } w := httptest.NewRecorder() endpoint.ServeHTTP(w, tc.request(t)) require.Equal(t, "ok", w.Body.String()) require.Equal(t, 200, w.Code) wg.Wait() require.Equal(t, uint64(2), requestCount.Load()) }) } } func Test_backendResponse_succeeded(t *testing.T) { tests := map[string]struct { resStatus int resError error expected bool }{ "Error while executing request": { resStatus: 0, resError: errors.New("network error"), expected: false, }, "2xx response status code": { resStatus: 200, resError: nil, expected: true, }, "3xx response status code": { resStatus: 300, resError: nil, expected: false, }, "4xx response status code": { resStatus: 400, resError: nil, expected: true, }, "5xx response status code": { resStatus: 500, resError: nil, expected: false, }, } for testName, testData := range tests { t.Run(testName, func(t *testing.T) { res := &backendResponse{ status: testData.resStatus, err: testData.resError, } assert.Equal(t, testData.expected, res.succeeded()) }) } } func Test_backendResponse_statusCode(t *testing.T) { tests := map[string]struct { resStatus int resError error expected int }{ "Error while executing request": { resStatus: 0, resError: errors.New("network error"), expected: 500, }, "200 response status code": { resStatus: 200, resError: nil, expected: 200, }, "503 response status code": { resStatus: 503, resError: nil, expected: 503, }, } for testName, testData := range tests { t.Run(testName, func(t *testing.T) { res := &backendResponse{ status: testData.resStatus, err: testData.resError, } assert.Equal(t, testData.expected, res.statusCode()) }) } }