package queryrangebase import ( "bytes" "context" "io" "net/http" "strconv" "testing" "github.com/grafana/dskit/httpgrpc" "github.com/grafana/dskit/user" jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/loki/pkg/logproto" ) func TestRequest(t *testing.T) { // Create a Copy parsedRequest to assign the expected headers to the request without affecting other tests using the global. // The test below adds a Test-Header header to the request and expects it back once the encode/decode of request is done via PrometheusCodec parsedRequestWithHeaders := *parsedRequest parsedRequestWithHeaders.Headers = reqHeaders for i, tc := range []struct { url string expected Request expectedErr error }{ { url: query, expected: &parsedRequestWithHeaders, }, { url: "api/v1/query_range?start=foo", expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"start\"; cannot parse \"foo\" to a valid timestamp"), }, { url: "api/v1/query_range?start=123&end=bar", expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"end\"; cannot parse \"bar\" to a valid timestamp"), }, { url: "api/v1/query_range?start=123&end=0", expectedErr: errEndBeforeStart, }, { url: "api/v1/query_range?start=123&end=456&step=baz", expectedErr: httpgrpc.Errorf(http.StatusBadRequest, "invalid parameter \"step\"; cannot parse \"baz\" to a valid duration"), }, { url: "api/v1/query_range?start=123&end=456&step=-1", expectedErr: errNegativeStep, }, { url: "api/v1/query_range?start=0&end=11001&step=1", expectedErr: errStepTooSmall, }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { r, err := http.NewRequest("GET", tc.url, nil) require.NoError(t, err) r.Header.Add("Test-Header", "test") ctx := user.InjectOrgID(context.Background(), "1") // Get a deep copy of the request with Context changed to ctx r = r.Clone(ctx) req, err := PrometheusCodec.DecodeRequest(ctx, r, []string{"Test-Header"}) if err != nil { require.EqualValues(t, tc.expectedErr, err) return } require.EqualValues(t, tc.expected, req) rdash, err := PrometheusCodec.EncodeRequest(context.Background(), req) require.NoError(t, err) require.EqualValues(t, tc.url, rdash.RequestURI) }) } } func TestResponse(t *testing.T) { r := *parsedResponse r.Headers = respHeaders for i, tc := range []struct { body string expected *PrometheusResponse }{ { body: responseBody, expected: &r, }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { response := &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer([]byte(tc.body))), } resp, err := PrometheusCodec.DecodeResponse(context.Background(), response, nil) require.NoError(t, err) assert.Equal(t, tc.expected, resp) // Reset response, as the above call will have consumed the body reader. response = &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(bytes.NewBuffer([]byte(tc.body))), ContentLength: int64(len(tc.body)), } resp2, err := PrometheusCodec.EncodeResponse(context.Background(), nil, resp) require.NoError(t, err) assert.Equal(t, response, resp2) }) } } func TestMergeAPIResponses(t *testing.T) { for _, tc := range []struct { name string input []Response expected Response }{ { name: "No responses shouldn't panic and return a non-null result and result type.", input: []Response{}, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{}, }, }, }, { name: "A single empty response shouldn't panic.", input: []Response{ &PrometheusResponse{ Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{}, }, }, }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{}, }, }, }, { name: "Multiple empty responses shouldn't panic.", input: []Response{ &PrometheusResponse{ Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{}, }, }, &PrometheusResponse{ Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{}, }, }, }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{}, }, }, }, { name: "Basic merging of two responses.", input: []Response{ &PrometheusResponse{ Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{}, Samples: []logproto.LegacySample{ {Value: 0, TimestampMs: 0}, {Value: 1, TimestampMs: 1}, }, }, }, }, }, &PrometheusResponse{ Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{}, Samples: []logproto.LegacySample{ {Value: 2, TimestampMs: 2}, {Value: 3, TimestampMs: 3}, }, }, }, }, }, }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{}, Samples: []logproto.LegacySample{ {Value: 0, TimestampMs: 0}, {Value: 1, TimestampMs: 1}, {Value: 2, TimestampMs: 2}, {Value: 3, TimestampMs: 3}, }, }, }, }, }, }, { name: "Merging of responses when labels are in different order.", input: []Response{ mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[0,"0"],[1,"1"]]}]}}`), mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"]]}]}}`), }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}}, Samples: []logproto.LegacySample{ {Value: 0, TimestampMs: 0}, {Value: 1, TimestampMs: 1000}, {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 3000}, }, }, }, }, }, }, { name: "Merging of samples where there is single overlap.", input: []Response{ mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"]]}]}}`), mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"]]}]}}`), }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}}, Samples: []logproto.LegacySample{ {Value: 1, TimestampMs: 1000}, {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 3000}, }, }, }, }, }, }, { name: "Merging of samples where there is multiple partial overlaps.", input: []Response{ mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[1,"1"],[2,"2"],[3,"3"]]}]}}`), mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}]}}`), }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}}, Samples: []logproto.LegacySample{ {Value: 1, TimestampMs: 1000}, {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 3000}, {Value: 4, TimestampMs: 4000}, {Value: 5, TimestampMs: 5000}, }, }, }, }, }, }, { name: "Merging of samples where there is complete overlap.", input: []Response{ mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"a":"b","c":"d"},"values":[[2,"2"],[3,"3"]]}]}}`), mustParse(t, `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"c":"d","a":"b"},"values":[[2,"2"],[3,"3"],[4,"4"],[5,"5"]]}]}}`), }, expected: &PrometheusResponse{ Status: StatusSuccess, Data: PrometheusData{ ResultType: matrix, Result: []SampleStream{ { Labels: []logproto.LabelAdapter{{Name: "a", Value: "b"}, {Name: "c", Value: "d"}}, Samples: []logproto.LegacySample{ {Value: 2, TimestampMs: 2000}, {Value: 3, TimestampMs: 3000}, {Value: 4, TimestampMs: 4000}, {Value: 5, TimestampMs: 5000}, }, }, }, }, }, }} { t.Run(tc.name, func(t *testing.T) { output, err := PrometheusCodec.MergeResponse(tc.input...) require.NoError(t, err) require.Equal(t, tc.expected, output) }) } } func mustParse(t *testing.T, response string) Response { var resp PrometheusResponse // Needed as goimports automatically add a json import otherwise. json := jsoniter.ConfigCompatibleWithStandardLibrary require.NoError(t, json.Unmarshal([]byte(response), &resp)) return &resp }