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 }