github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/tools/querytee/proxy_test.go (about)

     1  package querytee
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/go-kit/log"
    14  	"github.com/gorilla/mux"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  var testRoutes = []Route{
    20  	{Path: "/api/v1/query", RouteName: "api_v1_query", Methods: []string{"GET"}, ResponseComparator: &testComparator{}},
    21  }
    22  
    23  type testComparator struct{}
    24  
    25  func (testComparator) Compare(expected, actual []byte) error { return nil }
    26  
    27  func Test_NewProxy(t *testing.T) {
    28  	cfg := ProxyConfig{}
    29  
    30  	p, err := NewProxy(cfg, log.NewNopLogger(), testRoutes, nil)
    31  	assert.Equal(t, errMinBackends, err)
    32  	assert.Nil(t, p)
    33  }
    34  
    35  func Test_Proxy_RequestsForwarding(t *testing.T) {
    36  	const (
    37  		querySingleMetric1 = `{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"cortex_build_info"},"value":[1583320883,"1"]}]}}`
    38  		querySingleMetric2 = `{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"cortex_build_info"},"value":[1583320883,"2"]}]}}`
    39  	)
    40  
    41  	type mockedBackend struct {
    42  		pathPrefix string
    43  		handler    http.HandlerFunc
    44  	}
    45  
    46  	tests := map[string]struct {
    47  		backends            []mockedBackend
    48  		preferredBackendIdx int
    49  		expectedStatus      int
    50  		expectedRes         string
    51  	}{
    52  		"one backend returning 2xx": {
    53  			backends: []mockedBackend{
    54  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
    55  			},
    56  			expectedStatus: 200,
    57  			expectedRes:    querySingleMetric1,
    58  		},
    59  		"one backend returning 5xx": {
    60  			backends: []mockedBackend{
    61  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
    62  			},
    63  			expectedStatus: 500,
    64  			expectedRes:    "",
    65  		},
    66  		"two backends without path prefix": {
    67  			backends: []mockedBackend{
    68  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
    69  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric2)},
    70  			},
    71  			preferredBackendIdx: 0,
    72  			expectedStatus:      200,
    73  			expectedRes:         querySingleMetric1,
    74  		},
    75  		"two backends with the same path prefix": {
    76  			backends: []mockedBackend{
    77  				{
    78  					pathPrefix: "/api/prom",
    79  					handler:    mockQueryResponse("/api/prom/api/v1/query", 200, querySingleMetric1),
    80  				},
    81  				{
    82  					pathPrefix: "/api/prom",
    83  					handler:    mockQueryResponse("/api/prom/api/v1/query", 200, querySingleMetric2),
    84  				},
    85  			},
    86  			preferredBackendIdx: 0,
    87  			expectedStatus:      200,
    88  			expectedRes:         querySingleMetric1,
    89  		},
    90  		"two backends with different path prefix": {
    91  			backends: []mockedBackend{
    92  				{
    93  					pathPrefix: "/prefix-1",
    94  					handler:    mockQueryResponse("/prefix-1/api/v1/query", 200, querySingleMetric1),
    95  				},
    96  				{
    97  					pathPrefix: "/prefix-2",
    98  					handler:    mockQueryResponse("/prefix-2/api/v1/query", 200, querySingleMetric2),
    99  				},
   100  			},
   101  			preferredBackendIdx: 0,
   102  			expectedStatus:      200,
   103  			expectedRes:         querySingleMetric1,
   104  		},
   105  		"preferred backend returns 4xx": {
   106  			backends: []mockedBackend{
   107  				{handler: mockQueryResponse("/api/v1/query", 400, "")},
   108  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
   109  			},
   110  			preferredBackendIdx: 0,
   111  			expectedStatus:      400,
   112  			expectedRes:         "",
   113  		},
   114  		"preferred backend returns 5xx": {
   115  			backends: []mockedBackend{
   116  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   117  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
   118  			},
   119  			preferredBackendIdx: 0,
   120  			expectedStatus:      200,
   121  			expectedRes:         querySingleMetric1,
   122  		},
   123  		"non-preferred backend returns 5xx": {
   124  			backends: []mockedBackend{
   125  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
   126  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   127  			},
   128  			preferredBackendIdx: 0,
   129  			expectedStatus:      200,
   130  			expectedRes:         querySingleMetric1,
   131  		},
   132  		"all backends returns 5xx": {
   133  			backends: []mockedBackend{
   134  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   135  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   136  			},
   137  			preferredBackendIdx: 0,
   138  			expectedStatus:      500,
   139  			expectedRes:         "",
   140  		},
   141  	}
   142  
   143  	for testName, testData := range tests {
   144  		t.Run(testName, func(t *testing.T) {
   145  			backendURLs := []string{}
   146  
   147  			// Start backend servers.
   148  			for _, b := range testData.backends {
   149  				s := httptest.NewServer(b.handler)
   150  				defer s.Close()
   151  
   152  				backendURLs = append(backendURLs, s.URL+b.pathPrefix)
   153  			}
   154  
   155  			// Start the proxy.
   156  			cfg := ProxyConfig{
   157  				BackendEndpoints:   strings.Join(backendURLs, ","),
   158  				PreferredBackend:   strconv.Itoa(testData.preferredBackendIdx),
   159  				ServerServicePort:  0,
   160  				BackendReadTimeout: time.Second,
   161  			}
   162  
   163  			if len(backendURLs) == 2 {
   164  				cfg.CompareResponses = true
   165  			}
   166  
   167  			p, err := NewProxy(cfg, log.NewNopLogger(), testRoutes, nil)
   168  			require.NoError(t, err)
   169  			require.NotNil(t, p)
   170  			defer p.Stop() //nolint:errcheck
   171  
   172  			require.NoError(t, p.Start())
   173  
   174  			// Send a query request to the proxy.
   175  			res, err := http.Get(fmt.Sprintf("http://%s/api/v1/query", p.Endpoint()))
   176  			require.NoError(t, err)
   177  
   178  			defer res.Body.Close()
   179  			body, err := ioutil.ReadAll(res.Body)
   180  			require.NoError(t, err)
   181  
   182  			assert.Equal(t, testData.expectedStatus, res.StatusCode)
   183  			assert.Equal(t, testData.expectedRes, string(body))
   184  		})
   185  	}
   186  }
   187  
   188  func TestProxy_Passthrough(t *testing.T) {
   189  	type route struct {
   190  		path, response string
   191  	}
   192  
   193  	type mockedBackend struct {
   194  		routes []route
   195  	}
   196  
   197  	type query struct {
   198  		path               string
   199  		expectedRes        string
   200  		expectedStatusCode int
   201  	}
   202  
   203  	const (
   204  		pathCommon = "/common" // common path implemented by both backends
   205  
   206  		pathZero = "/zero" // only implemented by backend at index 0
   207  		pathOne  = "/one"  // only implemented by backend at index 1
   208  
   209  		// responses by backend at index 0
   210  		responseCommon0 = "common-0"
   211  		responseZero    = "zero"
   212  
   213  		// responses by backend at index 1
   214  		responseCommon1 = "common-1"
   215  		responseOne     = "one"
   216  	)
   217  
   218  	backends := []mockedBackend{
   219  		{
   220  			routes: []route{
   221  				{
   222  					path:     pathCommon,
   223  					response: responseCommon0,
   224  				},
   225  				{
   226  					path:     pathZero,
   227  					response: responseZero,
   228  				},
   229  			},
   230  		},
   231  		{
   232  			routes: []route{
   233  				{
   234  					path:     pathCommon,
   235  					response: responseCommon1,
   236  				},
   237  				{
   238  					path:     pathOne,
   239  					response: responseOne,
   240  				},
   241  			},
   242  		},
   243  	}
   244  
   245  	tests := map[string]struct {
   246  		preferredBackendIdx int
   247  		queries             []query
   248  	}{
   249  		"first backend preferred": {
   250  			preferredBackendIdx: 0,
   251  			queries: []query{
   252  				{
   253  					path:               pathCommon,
   254  					expectedRes:        responseCommon0,
   255  					expectedStatusCode: 200,
   256  				},
   257  				{
   258  					path:               pathZero,
   259  					expectedRes:        responseZero,
   260  					expectedStatusCode: 200,
   261  				},
   262  				{
   263  					path:               pathOne,
   264  					expectedRes:        "404 page not found\n",
   265  					expectedStatusCode: 404,
   266  				},
   267  			},
   268  		},
   269  		"second backend preferred": {
   270  			preferredBackendIdx: 1,
   271  			queries: []query{
   272  				{
   273  					path:               pathCommon,
   274  					expectedRes:        responseCommon1,
   275  					expectedStatusCode: 200,
   276  				},
   277  				{
   278  					path:               pathOne,
   279  					expectedRes:        responseOne,
   280  					expectedStatusCode: 200,
   281  				},
   282  				{
   283  					path:               pathZero,
   284  					expectedRes:        "404 page not found\n",
   285  					expectedStatusCode: 404,
   286  				},
   287  			},
   288  		},
   289  	}
   290  
   291  	for testName, testData := range tests {
   292  		t.Run(testName, func(t *testing.T) {
   293  			backendURLs := []string{}
   294  
   295  			// Start backend servers.
   296  			for _, b := range backends {
   297  				router := mux.NewRouter()
   298  				for _, route := range b.routes {
   299  					router.Handle(route.path, mockQueryResponse(route.path, 200, route.response))
   300  				}
   301  				s := httptest.NewServer(router)
   302  				defer s.Close()
   303  
   304  				backendURLs = append(backendURLs, s.URL)
   305  			}
   306  
   307  			// Start the proxy.
   308  			cfg := ProxyConfig{
   309  				BackendEndpoints:               strings.Join(backendURLs, ","),
   310  				PreferredBackend:               strconv.Itoa(testData.preferredBackendIdx),
   311  				ServerServicePort:              0,
   312  				BackendReadTimeout:             time.Second,
   313  				PassThroughNonRegisteredRoutes: true,
   314  			}
   315  
   316  			p, err := NewProxy(cfg, log.NewNopLogger(), testRoutes, nil)
   317  			require.NoError(t, err)
   318  			require.NotNil(t, p)
   319  			defer p.Stop() //nolint:errcheck
   320  
   321  			require.NoError(t, p.Start())
   322  
   323  			for _, query := range testData.queries {
   324  
   325  				// Send a query request to the proxy.
   326  				res, err := http.Get(fmt.Sprintf("http://%s%s", p.Endpoint(), query.path))
   327  				require.NoError(t, err)
   328  
   329  				defer res.Body.Close()
   330  				body, err := ioutil.ReadAll(res.Body)
   331  				require.NoError(t, err)
   332  
   333  				assert.Equal(t, query.expectedStatusCode, res.StatusCode)
   334  				assert.Equal(t, query.expectedRes, string(body))
   335  			}
   336  		})
   337  	}
   338  }
   339  
   340  func mockQueryResponse(path string, status int, res string) http.HandlerFunc {
   341  	return func(w http.ResponseWriter, r *http.Request) {
   342  		// Ensure the path is the expected one.
   343  		if r.URL.Path != path {
   344  			w.WriteHeader(http.StatusNotFound)
   345  			return
   346  		}
   347  
   348  		// Send back the mocked response.
   349  		w.WriteHeader(status)
   350  		if status == http.StatusOK {
   351  			_, _ = w.Write([]byte(res))
   352  		}
   353  	}
   354  }