github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prometheus/handleroptions/fetch_options_test.go (about)

     1  // Copyright (c) 2019 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package handleroptions
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"fmt"
    27  	"math"
    28  	"mime/multipart"
    29  	"net/http"
    30  	"net/http/httptest"
    31  	"net/url"
    32  	"regexp"
    33  	"testing"
    34  	"time"
    35  
    36  	"github.com/m3db/m3/src/dbnode/encoding"
    37  	"github.com/m3db/m3/src/dbnode/topology"
    38  	"github.com/m3db/m3/src/metrics/policy"
    39  	"github.com/m3db/m3/src/query/models"
    40  	"github.com/m3db/m3/src/query/storage"
    41  	"github.com/m3db/m3/src/query/storage/m3/storagemetadata"
    42  	xerrors "github.com/m3db/m3/src/x/errors"
    43  	"github.com/m3db/m3/src/x/headers"
    44  	xhttp "github.com/m3db/m3/src/x/net/http"
    45  
    46  	"github.com/stretchr/testify/assert"
    47  	"github.com/stretchr/testify/require"
    48  )
    49  
    50  func TestFetchOptionsBuilder(t *testing.T) {
    51  	type expectedLookback struct {
    52  		value time.Duration
    53  	}
    54  
    55  	tests := []struct {
    56  		name                                  string
    57  		defaultLimit                          int
    58  		defaultRangeLimit                     time.Duration
    59  		defaultRestrictByTag                  *storage.RestrictByTag
    60  		headers                               map[string]string
    61  		query                                 string
    62  		expectedLimit                         int
    63  		expectedRangeLimit                    time.Duration
    64  		expectedRestrict                      *storage.RestrictQueryOptions
    65  		expectedLookback                      *expectedLookback
    66  		expectedReadConsistencyLevel          *topology.ReadConsistencyLevel
    67  		expectedIterateEqualTimestampStrategy *encoding.IterateEqualTimestampStrategy
    68  		expectedErr                           bool
    69  	}{
    70  		{
    71  			name:          "default limit with no headers",
    72  			defaultLimit:  42,
    73  			headers:       map[string]string{},
    74  			expectedLimit: 42,
    75  		},
    76  		{
    77  			name:         "limit with header",
    78  			defaultLimit: 42,
    79  			headers: map[string]string{
    80  				headers.LimitMaxSeriesHeader: "4242",
    81  			},
    82  			expectedLimit: 4242,
    83  		},
    84  		{
    85  			name:         "bad limit header",
    86  			defaultLimit: 42,
    87  			headers: map[string]string{
    88  				headers.LimitMaxSeriesHeader: "not_a_number",
    89  			},
    90  			expectedErr: true,
    91  		},
    92  		{
    93  			name:               "default range limit with no headers",
    94  			defaultRangeLimit:  42 * time.Hour,
    95  			headers:            map[string]string{},
    96  			expectedRangeLimit: 42 * time.Hour,
    97  		},
    98  		{
    99  			name:              "range limit with header",
   100  			defaultRangeLimit: 42 * time.Hour,
   101  			headers: map[string]string{
   102  				headers.LimitMaxRangeHeader: "84h",
   103  			},
   104  			expectedRangeLimit: 84 * time.Hour,
   105  		},
   106  		{
   107  			name:              "bad range limit header",
   108  			defaultRangeLimit: 42 * time.Hour,
   109  			headers: map[string]string{
   110  				// Not a parseable time range string.
   111  				headers.LimitMaxRangeHeader: "4242",
   112  			},
   113  			expectedErr: true,
   114  		},
   115  		{
   116  			name: "unaggregated metrics type",
   117  			headers: map[string]string{
   118  				headers.MetricsTypeHeader: storagemetadata.UnaggregatedMetricsType.String(),
   119  			},
   120  			expectedRestrict: &storage.RestrictQueryOptions{
   121  				RestrictByType: &storage.RestrictByType{
   122  					MetricsType: storagemetadata.UnaggregatedMetricsType,
   123  				},
   124  			},
   125  		},
   126  		{
   127  			name: "aggregated metrics type",
   128  			headers: map[string]string{
   129  				headers.MetricsTypeHeader:          storagemetadata.AggregatedMetricsType.String(),
   130  				headers.MetricsStoragePolicyHeader: "1m:14d",
   131  			},
   132  			expectedRestrict: &storage.RestrictQueryOptions{
   133  				RestrictByType: &storage.RestrictByType{
   134  					MetricsType:   storagemetadata.AggregatedMetricsType,
   135  					StoragePolicy: policy.MustParseStoragePolicy("1m:14d"),
   136  				},
   137  			},
   138  		},
   139  		{
   140  			name: "unaggregated metrics type with storage policy",
   141  			headers: map[string]string{
   142  				headers.MetricsTypeHeader:          storagemetadata.UnaggregatedMetricsType.String(),
   143  				headers.MetricsStoragePolicyHeader: "1m:14d",
   144  			},
   145  			expectedErr: true,
   146  		},
   147  		{
   148  			name: "aggregated metrics type without storage policy",
   149  			headers: map[string]string{
   150  				headers.MetricsTypeHeader: storagemetadata.AggregatedMetricsType.String(),
   151  			},
   152  			expectedErr: true,
   153  		},
   154  		{
   155  			name: "unrecognized metrics type",
   156  			headers: map[string]string{
   157  				headers.MetricsTypeHeader: "foo",
   158  			},
   159  			expectedErr: true,
   160  		},
   161  		{
   162  			name:  "can set lookback duration",
   163  			query: "lookback=10s",
   164  			expectedLookback: &expectedLookback{
   165  				value: 10 * time.Second,
   166  			},
   167  		},
   168  		{
   169  			name:  "can set lookback duration based on step",
   170  			query: "lookback=step&step=10s",
   171  			expectedLookback: &expectedLookback{
   172  				value: 10 * time.Second,
   173  			},
   174  		},
   175  		{
   176  			name:        "bad lookback returns error",
   177  			query:       "lookback=foo",
   178  			expectedErr: true,
   179  		},
   180  		{
   181  			name:        "loookback step but step is bad",
   182  			query:       "lookback=step&step=invalid",
   183  			expectedErr: true,
   184  		},
   185  		{
   186  			name:        "lookback step but step is negative",
   187  			query:       "lookback=step&step=-1",
   188  			expectedErr: true,
   189  		},
   190  		{
   191  			name: "restrict by tags json header",
   192  			headers: map[string]string{
   193  				headers.RestrictByTagsJSONHeader: stripSpace(`{
   194  					"match":[{"name":"foo", "value":"bar", "type":"EQUAL"}],
   195  					"strip":["foo"]
   196  				}`),
   197  			},
   198  			expectedRestrict: &storage.RestrictQueryOptions{
   199  				RestrictByTag: &storage.RestrictByTag{
   200  					Restrict: models.Matchers{
   201  						mustMatcher("foo", "bar", models.MatchEqual),
   202  					},
   203  					Strip: toStrip("foo"),
   204  				},
   205  			},
   206  		},
   207  		{
   208  			name: "restrict by tags json defaults",
   209  			defaultRestrictByTag: &storage.RestrictByTag{
   210  				Restrict: models.Matchers{
   211  					mustMatcher("foo", "bar", models.MatchEqual),
   212  				},
   213  				Strip: toStrip("foo"),
   214  			},
   215  			expectedRestrict: &storage.RestrictQueryOptions{
   216  				RestrictByTag: &storage.RestrictByTag{
   217  					Restrict: models.Matchers{
   218  						mustMatcher("foo", "bar", models.MatchEqual),
   219  					},
   220  					Strip: toStrip("foo"),
   221  				},
   222  			},
   223  		},
   224  		{
   225  			name: "restrict by tags json default override by header",
   226  			defaultRestrictByTag: &storage.RestrictByTag{
   227  				Restrict: models.Matchers{
   228  					mustMatcher("foo", "bar", models.MatchEqual),
   229  				},
   230  				Strip: toStrip("foo"),
   231  			},
   232  			headers: map[string]string{
   233  				headers.RestrictByTagsJSONHeader: stripSpace(`{
   234  					"match":[{"name":"qux", "value":"qaz", "type":"EQUAL"}],
   235  					"strip":["qux"]
   236  				}`),
   237  			},
   238  			expectedRestrict: &storage.RestrictQueryOptions{
   239  				RestrictByTag: &storage.RestrictByTag{
   240  					Restrict: models.Matchers{
   241  						mustMatcher("qux", "qaz", models.MatchEqual),
   242  					},
   243  					Strip: toStrip("qux"),
   244  				},
   245  			},
   246  		},
   247  		{
   248  			name: "restrict by policies with metrics type",
   249  			headers: map[string]string{
   250  				headers.MetricsTypeHeader:                      "aggregated",
   251  				headers.MetricsRestrictByStoragePoliciesHeader: "10m:60d",
   252  			},
   253  			expectedErr: true,
   254  		},
   255  		{
   256  			name: "restrict by policies with storage policy",
   257  			headers: map[string]string{
   258  				headers.MetricsStoragePolicyHeader:             "10m:60d",
   259  				headers.MetricsRestrictByStoragePoliciesHeader: "10m:60d",
   260  			},
   261  			expectedErr: true,
   262  		},
   263  		{
   264  			name: "restrict by policies - invalid policy",
   265  			headers: map[string]string{
   266  				headers.MetricsRestrictByStoragePoliciesHeader: "10m",
   267  			},
   268  			expectedErr: true,
   269  		},
   270  		{
   271  			name: "restrict by policies - invalid delimiter",
   272  			headers: map[string]string{
   273  				headers.MetricsRestrictByStoragePoliciesHeader: "10m:60d,5m:30d",
   274  			},
   275  			expectedErr: true,
   276  		},
   277  		{
   278  			name: "restrict by policies - single policy",
   279  			headers: map[string]string{
   280  				headers.MetricsRestrictByStoragePoliciesHeader: "10m:60d",
   281  			},
   282  			expectedRestrict: &storage.RestrictQueryOptions{
   283  				RestrictByTypes: []*storage.RestrictByType{
   284  					{
   285  						MetricsType:   storagemetadata.AggregatedMetricsType,
   286  						StoragePolicy: policy.MustParseStoragePolicy("10m:60d"),
   287  					},
   288  				},
   289  			},
   290  		},
   291  		{
   292  			name: "restrict by policies - multiple policy",
   293  			headers: map[string]string{
   294  				headers.MetricsRestrictByStoragePoliciesHeader: "10m:60d;5m:30d",
   295  			},
   296  			expectedRestrict: &storage.RestrictQueryOptions{
   297  				RestrictByTypes: []*storage.RestrictByType{
   298  					{
   299  						MetricsType:   storagemetadata.AggregatedMetricsType,
   300  						StoragePolicy: policy.MustParseStoragePolicy("10m:60d"),
   301  					},
   302  					{
   303  						MetricsType:   storagemetadata.AggregatedMetricsType,
   304  						StoragePolicy: policy.MustParseStoragePolicy("5m:30d"),
   305  					},
   306  				},
   307  			},
   308  		},
   309  		{
   310  			name: "read overrides",
   311  			headers: map[string]string{
   312  				headers.ReadConsistencyLevelHeader:          "all",
   313  				headers.IterateEqualTimestampStrategyHeader: "iterate_lowest_value",
   314  			},
   315  			expectedReadConsistencyLevel:          &topology.ValidReadConsistencyLevels()[5],
   316  			expectedIterateEqualTimestampStrategy: &encoding.ValidIterateEqualTimestampStrategies()[2],
   317  		},
   318  	}
   319  
   320  	for _, test := range tests {
   321  		t.Run(test.name, func(t *testing.T) {
   322  			builder, err := NewFetchOptionsBuilder(FetchOptionsBuilderOptions{
   323  				Limits: FetchOptionsBuilderLimitsOptions{
   324  					SeriesLimit: test.defaultLimit,
   325  				},
   326  				RestrictByTag: test.defaultRestrictByTag,
   327  				Timeout:       10 * time.Second,
   328  			})
   329  			require.NoError(t, err)
   330  
   331  			url := "/foo"
   332  			if test.query != "" {
   333  				url += "?" + test.query
   334  			}
   335  			req := httptest.NewRequest("GET", url, nil)
   336  			for k, v := range test.headers {
   337  				req.Header.Add(k, v)
   338  			}
   339  
   340  			ctx, opts, err := builder.NewFetchOptions(context.Background(), req)
   341  			if !test.expectedErr {
   342  				require.NoError(t, err)
   343  				require.Equal(t, test.expectedLimit, opts.SeriesLimit)
   344  				if test.expectedRestrict == nil {
   345  					require.Nil(t, opts.RestrictQueryOptions)
   346  				} else {
   347  					require.NotNil(t, opts.RestrictQueryOptions)
   348  					require.Equal(t, *test.expectedRestrict, *opts.RestrictQueryOptions)
   349  				}
   350  				if test.expectedLookback == nil {
   351  					require.Nil(t, opts.LookbackDuration)
   352  				} else {
   353  					require.NotNil(t, opts.LookbackDuration)
   354  					require.Equal(t, test.expectedLookback.value, *opts.LookbackDuration)
   355  				}
   356  				if test.expectedReadConsistencyLevel == nil {
   357  					require.Nil(t, opts.ReadConsistencyLevel)
   358  				} else {
   359  					require.NotNil(t, opts.ReadConsistencyLevel)
   360  					require.Equal(t, *test.expectedReadConsistencyLevel, *opts.ReadConsistencyLevel)
   361  				}
   362  				if test.expectedIterateEqualTimestampStrategy == nil {
   363  					require.Nil(t, opts.IterateEqualTimestampStrategy)
   364  				} else {
   365  					require.NotNil(t, opts.IterateEqualTimestampStrategy)
   366  					require.Equal(t, *test.expectedIterateEqualTimestampStrategy, *opts.IterateEqualTimestampStrategy)
   367  				}
   368  				require.Equal(t, 10*time.Second, opts.Timeout)
   369  				// Check context has deadline and headers from
   370  				// the request.
   371  				_, ok := ctx.Deadline()
   372  				require.True(t, ok)
   373  				headers := ctx.Value(RequestHeaderKey)
   374  				require.NotNil(t, headers)
   375  				_, ok = headers.(http.Header)
   376  				require.True(t, ok)
   377  			} else {
   378  				require.Error(t, err)
   379  			}
   380  		})
   381  	}
   382  }
   383  
   384  func TestInvalidStep(t *testing.T) {
   385  	req := httptest.NewRequest("GET", "/foo", nil)
   386  	vals := make(url.Values)
   387  	vals.Del(StepParam)
   388  	vals.Add(StepParam, "-10.50s")
   389  	req.URL.RawQuery = vals.Encode()
   390  	_, _, err := ParseStep(req)
   391  	require.NotNil(t, err, "unable to parse request")
   392  }
   393  
   394  func TestParseLookbackDuration(t *testing.T) {
   395  	r := httptest.NewRequest(http.MethodGet, "/foo", nil)
   396  	_, ok, err := ParseLookbackDuration(r)
   397  	require.NoError(t, err)
   398  	require.False(t, ok)
   399  
   400  	r = httptest.NewRequest(http.MethodGet, "/foo?step=60s&lookback=step", nil)
   401  	v, ok, err := ParseLookbackDuration(r)
   402  	require.NoError(t, err)
   403  	require.True(t, ok)
   404  	assert.Equal(t, time.Minute, v)
   405  
   406  	r = httptest.NewRequest(http.MethodGet, "/foo?step=60s&lookback=120s", nil)
   407  	v, ok, err = ParseLookbackDuration(r)
   408  	require.NoError(t, err)
   409  	require.True(t, ok)
   410  	assert.Equal(t, 2*time.Minute, v)
   411  
   412  	r = httptest.NewRequest(http.MethodGet, "/foo?step=60s&lookback=foobar", nil)
   413  	_, _, err = ParseLookbackDuration(r)
   414  	require.Error(t, err)
   415  }
   416  
   417  func TestParseDuration(t *testing.T) {
   418  	r, err := http.NewRequest(http.MethodGet, "/foo?step=10s", nil)
   419  	require.NoError(t, err)
   420  	v, err := ParseDuration(r, StepParam)
   421  	require.NoError(t, err)
   422  	assert.Equal(t, 10*time.Second, v)
   423  }
   424  
   425  func TestParseFloatDuration(t *testing.T) {
   426  	r, err := http.NewRequest(http.MethodGet, "/foo?step=10.50m", nil)
   427  	require.NoError(t, err)
   428  	v, err := ParseDuration(r, StepParam)
   429  	require.NoError(t, err)
   430  	assert.Equal(t, 10*time.Minute+30*time.Second, v)
   431  
   432  	r = httptest.NewRequest(http.MethodGet, "/foo?step=10.00m", nil)
   433  	require.NoError(t, err)
   434  	v, err = ParseDuration(r, StepParam)
   435  	require.NoError(t, err)
   436  	assert.Equal(t, 10*time.Minute, v)
   437  }
   438  
   439  func TestParseDurationParsesIntAsSeconds(t *testing.T) {
   440  	r, err := http.NewRequest(http.MethodGet, "/foo?step=30", nil)
   441  	require.NoError(t, err)
   442  	v, err := ParseDuration(r, StepParam)
   443  	require.NoError(t, err)
   444  	assert.Equal(t, 30*time.Second, v)
   445  }
   446  
   447  func TestParseDurationParsesFloatAsSeconds(t *testing.T) {
   448  	r, err := http.NewRequest(http.MethodGet, "/foo?step=30.00", nil)
   449  	require.NoError(t, err)
   450  	v, err := ParseDuration(r, StepParam)
   451  	require.NoError(t, err)
   452  	assert.Equal(t, 30*time.Second, v)
   453  }
   454  
   455  func TestParseDurationError(t *testing.T) {
   456  	r, err := http.NewRequest(http.MethodGet, "/foo?step=bar10", nil)
   457  	require.NoError(t, err)
   458  	_, err = ParseDuration(r, StepParam)
   459  	assert.Error(t, err)
   460  }
   461  
   462  func TestParseDurationOverflowError(t *testing.T) {
   463  	r, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/foo?step=%f", float64(math.MaxInt64)), nil)
   464  	require.NoError(t, err)
   465  	_, err = ParseDuration(r, StepParam)
   466  	assert.Error(t, err)
   467  }
   468  
   469  func TestFetchOptionsWithHeader(t *testing.T) {
   470  	type expectedLookback struct {
   471  		value time.Duration
   472  	}
   473  
   474  	headers := map[string]string{
   475  		headers.MetricsTypeHeader:          storagemetadata.AggregatedMetricsType.String(),
   476  		headers.MetricsStoragePolicyHeader: "1m:14d",
   477  		headers.RestrictByTagsJSONHeader: `{
   478  			"match":[
   479  				{"name":"a", "value":"b", "type":"EQUAL"},
   480  				{"name":"c", "value":"d", "type":"NOTEQUAL"},
   481  				{"name":"e", "value":"f", "type":"REGEXP"},
   482  				{"name":"g", "value":"h", "type":"NOTREGEXP"},
   483  				{"name":"i", "value":"j", "type":"EXISTS"},
   484  				{"name":"k", "value":"l", "type":"NOTEXISTS"}
   485  			],
   486  			"strip":["foo"]
   487  		}`,
   488  		headers.ReadConsistencyLevelHeader:          "all",
   489  		headers.IterateEqualTimestampStrategyHeader: "iterate_lowest_value",
   490  	}
   491  
   492  	builder, err := NewFetchOptionsBuilder(FetchOptionsBuilderOptions{
   493  		Limits: FetchOptionsBuilderLimitsOptions{
   494  			SeriesLimit: 5,
   495  		},
   496  		Timeout: 10 * time.Second,
   497  	})
   498  	require.NoError(t, err)
   499  
   500  	req := httptest.NewRequest("GET", "/", nil)
   501  	for k, v := range headers {
   502  		req.Header.Add(k, v)
   503  	}
   504  
   505  	_, opts, err := builder.NewFetchOptions(context.Background(), req)
   506  	require.NoError(t, err)
   507  	require.NotNil(t, opts.RestrictQueryOptions)
   508  	ex := &storage.RestrictQueryOptions{
   509  		RestrictByType: &storage.RestrictByType{
   510  			MetricsType:   storagemetadata.AggregatedMetricsType,
   511  			StoragePolicy: policy.MustParseStoragePolicy("1m:14d"),
   512  		},
   513  		RestrictByTag: &storage.RestrictByTag{
   514  			Restrict: models.Matchers{
   515  				mustMatcher("a", "b", models.MatchEqual),
   516  				mustMatcher("c", "d", models.MatchNotEqual),
   517  				mustMatcher("e", "f", models.MatchRegexp),
   518  				mustMatcher("g", "h", models.MatchNotRegexp),
   519  				mustMatcher("i", "j", models.MatchField),
   520  				mustMatcher("k", "l", models.MatchNotField),
   521  			},
   522  			Strip: toStrip("foo"),
   523  		},
   524  	}
   525  
   526  	require.Equal(t, ex, opts.RestrictQueryOptions)
   527  	require.Equal(t, topology.ReadConsistencyLevelAll, *opts.ReadConsistencyLevel)
   528  	require.Equal(t, encoding.IterateLowestValue, *opts.IterateEqualTimestampStrategy)
   529  }
   530  
   531  func stripSpace(str string) string {
   532  	return regexp.MustCompile(`\s+`).ReplaceAllString(str, "")
   533  }
   534  
   535  func TestParseRequestTimeout(t *testing.T) {
   536  	req := httptest.NewRequest("GET", "/read?timeout=2m", nil)
   537  	dur, err := ParseRequestTimeout(req, time.Second)
   538  	require.NoError(t, err)
   539  	assert.Equal(t, 2*time.Minute, dur)
   540  }
   541  
   542  func TestParseRelatedQueryOptions(t *testing.T) {
   543  	t.Parallel()
   544  
   545  	tests := map[string]struct {
   546  		expectErr      bool
   547  		expectedOk     bool
   548  		headers        []string
   549  		expectedResult *storage.RelatedQueryOptions
   550  	}{
   551  		"simple": {
   552  			headers:    []string{"1635160222:1635166222"},
   553  			expectErr:  false,
   554  			expectedOk: true,
   555  			expectedResult: &storage.RelatedQueryOptions{
   556  				Timespans: []storage.QueryTimespan{
   557  					{Start: 1635160222000000000, End: 1635166222000000000},
   558  				},
   559  			},
   560  		},
   561  		"multiple queries (second header ignored)": {
   562  			headers:    []string{"1635160222:1635166222", "1635161222:1635165222"},
   563  			expectErr:  false,
   564  			expectedOk: true,
   565  			expectedResult: &storage.RelatedQueryOptions{
   566  				Timespans: []storage.QueryTimespan{
   567  					{Start: 1635160222000000000, End: 1635166222000000000},
   568  				},
   569  			},
   570  		},
   571  		"multiple queries same header.": {
   572  			headers:    []string{"1635160222:1635166222;1635161222:1635165222"},
   573  			expectErr:  false,
   574  			expectedOk: true,
   575  			expectedResult: &storage.RelatedQueryOptions{
   576  				Timespans: []storage.QueryTimespan{
   577  					{Start: 1635160222000000000, End: 1635166222000000000},
   578  					{Start: 1635161222000000000, End: 1635165222000000000},
   579  				},
   580  			},
   581  		},
   582  		"no related_queries": {
   583  			headers:        []string{},
   584  			expectErr:      false,
   585  			expectedOk:     false,
   586  			expectedResult: nil,
   587  		},
   588  		"incomplete pair": {
   589  			headers:        []string{"1635160222"},
   590  			expectErr:      true,
   591  			expectedOk:     false,
   592  			expectedResult: nil,
   593  		},
   594  		"invalid pair (start time)": {
   595  			headers:        []string{"2m:6m"},
   596  			expectErr:      true,
   597  			expectedOk:     false,
   598  			expectedResult: nil,
   599  		},
   600  		"invalid pair (end time)": {
   601  			headers:        []string{"1635160222:6m"},
   602  			expectErr:      true,
   603  			expectedOk:     false,
   604  			expectedResult: nil,
   605  		},
   606  		"invalid pair (end time after start time)": {
   607  			headers:        []string{"1635166222:1635160222"},
   608  			expectErr:      true,
   609  			expectedOk:     false,
   610  			expectedResult: nil,
   611  		},
   612  	}
   613  
   614  	for name, tc := range tests {
   615  		name, tc := name, tc
   616  		t.Run(name, func(t *testing.T) {
   617  			t.Parallel()
   618  
   619  			req := httptest.NewRequest("GET", "/read", nil)
   620  			for _, header := range tc.headers {
   621  				req.Header.Add(headers.RelatedQueriesHeader, header)
   622  			}
   623  			options, ok, err := ParseRelatedQueryOptions(req)
   624  			assert.Equal(t, tc.expectedOk, ok,
   625  				"Expected result of ok to be %v got %v", tc.expectedOk, ok)
   626  
   627  			if tc.expectErr {
   628  				assert.Error(t, err)
   629  			} else {
   630  				assert.NoError(t, err)
   631  			}
   632  
   633  			if tc.expectedResult == nil {
   634  				assert.Nil(t, options)
   635  			} else {
   636  				assert.NotNil(t, options)
   637  				assert.Equal(t, tc.expectedResult, options)
   638  			}
   639  		})
   640  	}
   641  }
   642  
   643  func TestTimeoutParseWithHeader(t *testing.T) {
   644  	req := httptest.NewRequest("POST", "/dummy", nil)
   645  	req.Header.Add("timeout", "1ms")
   646  
   647  	timeout, err := ParseRequestTimeout(req, time.Second)
   648  	assert.NoError(t, err)
   649  	assert.Equal(t, timeout, time.Millisecond)
   650  
   651  	req.Header.Add(headers.TimeoutHeader, "1s")
   652  	timeout, err = ParseRequestTimeout(req, time.Second)
   653  	assert.NoError(t, err)
   654  	assert.Equal(t, timeout, time.Second)
   655  
   656  	req.Header.Del("timeout")
   657  	req.Header.Del(headers.TimeoutHeader)
   658  	timeout, err = ParseRequestTimeout(req, 2*time.Minute)
   659  	assert.NoError(t, err)
   660  	assert.Equal(t, timeout, 2*time.Minute)
   661  
   662  	req.Header.Add("timeout", "invalid")
   663  	_, err = ParseRequestTimeout(req, 15*time.Second)
   664  	assert.Error(t, err)
   665  	assert.True(t, xerrors.IsInvalidParams(err))
   666  }
   667  
   668  func TestTimeoutParseWithPostRequestParam(t *testing.T) {
   669  	params := url.Values{}
   670  	params.Add("timeout", "1ms")
   671  
   672  	buff := bytes.NewBuffer(nil)
   673  	form := multipart.NewWriter(buff)
   674  	form.WriteField("timeout", "1ms")
   675  	require.NoError(t, form.Close())
   676  
   677  	req := httptest.NewRequest("POST", "/dummy", buff)
   678  	req.Header.Set(xhttp.HeaderContentType, form.FormDataContentType())
   679  
   680  	timeout, err := ParseRequestTimeout(req, time.Second)
   681  	assert.NoError(t, err)
   682  	assert.Equal(t, timeout, time.Millisecond)
   683  }
   684  
   685  func TestTimeoutParseWithGetRequestParam(t *testing.T) {
   686  	params := url.Values{}
   687  	params.Add("timeout", "1ms")
   688  
   689  	req := httptest.NewRequest("GET", "/dummy?"+params.Encode(), nil)
   690  
   691  	timeout, err := ParseRequestTimeout(req, time.Second)
   692  	assert.NoError(t, err)
   693  	assert.Equal(t, timeout, time.Millisecond)
   694  }
   695  
   696  func TestInstanceMultiple(t *testing.T) {
   697  	req := httptest.NewRequest("GET", "/", nil)
   698  	m, err := ParseInstanceMultiple(req, 2.0)
   699  	require.NoError(t, err)
   700  	require.Equal(t, float32(2.0), m)
   701  
   702  	req.Header.Set(headers.LimitInstanceMultipleHeader, "3.0")
   703  	m, err = ParseInstanceMultiple(req, 2.0)
   704  	require.NoError(t, err)
   705  	require.Equal(t, float32(3.0), m)
   706  
   707  	req.Header.Set(headers.LimitInstanceMultipleHeader, "blah")
   708  	_, err = ParseInstanceMultiple(req, 2.0)
   709  	require.Error(t, err)
   710  	require.Contains(t, err.Error(), "could not parse instance multiple")
   711  }