1// Copyright 2017 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package v1
15
16import (
17	"context"
18	"errors"
19	"fmt"
20	"io/ioutil"
21	"math"
22	"net/http"
23	"net/http/httptest"
24	"net/url"
25	"reflect"
26	"strings"
27	"testing"
28	"time"
29
30	json "github.com/json-iterator/go"
31
32	"github.com/prometheus/common/model"
33)
34
35type apiTest struct {
36	do           func() (interface{}, Warnings, error)
37	inWarnings   []string
38	inErr        error
39	inStatusCode int
40	inRes        interface{}
41
42	reqPath   string
43	reqParam  url.Values
44	reqMethod string
45	res       interface{}
46	warnings  Warnings
47	err       error
48}
49
50type apiTestClient struct {
51	*testing.T
52	curTest apiTest
53}
54
55func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL {
56	path := ep
57	for k, v := range args {
58		path = strings.Replace(path, ":"+k, v, -1)
59	}
60	u := &url.URL{
61		Host: "test:9090",
62		Path: path,
63	}
64	return u
65}
66
67func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) {
68
69	test := c.curTest
70
71	if req.URL.Path != test.reqPath {
72		c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path)
73	}
74	if req.Method != test.reqMethod {
75		c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method)
76	}
77
78	b, err := json.Marshal(test.inRes)
79	if err != nil {
80		c.Fatal(err)
81	}
82
83	resp := &http.Response{}
84	if test.inStatusCode != 0 {
85		resp.StatusCode = test.inStatusCode
86	} else if test.inErr != nil {
87		resp.StatusCode = http.StatusUnprocessableEntity
88	} else {
89		resp.StatusCode = http.StatusOK
90	}
91
92	return resp, b, test.inWarnings, test.inErr
93}
94
95func (c *apiTestClient) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error) {
96	req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(args.Encode()))
97	if err != nil {
98		return nil, nil, nil, err
99	}
100	return c.Do(ctx, req)
101}
102
103func TestAPIs(t *testing.T) {
104
105	testTime := time.Now()
106
107	tc := &apiTestClient{
108		T: t,
109	}
110	promAPI := &httpAPI{
111		client: tc,
112	}
113
114	doAlertManagers := func() func() (interface{}, Warnings, error) {
115		return func() (interface{}, Warnings, error) {
116			v, err := promAPI.AlertManagers(context.Background())
117			return v, nil, err
118		}
119	}
120
121	doCleanTombstones := func() func() (interface{}, Warnings, error) {
122		return func() (interface{}, Warnings, error) {
123			return nil, nil, promAPI.CleanTombstones(context.Background())
124		}
125	}
126
127	doConfig := func() func() (interface{}, Warnings, error) {
128		return func() (interface{}, Warnings, error) {
129			v, err := promAPI.Config(context.Background())
130			return v, nil, err
131		}
132	}
133
134	doDeleteSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, Warnings, error) {
135		return func() (interface{}, Warnings, error) {
136			return nil, nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime)
137		}
138	}
139
140	doFlags := func() func() (interface{}, Warnings, error) {
141		return func() (interface{}, Warnings, error) {
142			v, err := promAPI.Flags(context.Background())
143			return v, nil, err
144		}
145	}
146
147	doBuildinfo := func() func() (interface{}, Warnings, error) {
148		return func() (interface{}, Warnings, error) {
149			v, err := promAPI.Buildinfo(context.Background())
150			return v, nil, err
151		}
152	}
153
154	doRuntimeinfo := func() func() (interface{}, Warnings, error) {
155		return func() (interface{}, Warnings, error) {
156			v, err := promAPI.Runtimeinfo(context.Background())
157			return v, nil, err
158		}
159	}
160
161	doLabelNames := func(matches []string) func() (interface{}, Warnings, error) {
162		return func() (interface{}, Warnings, error) {
163			return promAPI.LabelNames(context.Background(), matches, time.Now().Add(-100*time.Hour), time.Now())
164		}
165	}
166
167	doLabelValues := func(matches []string, label string) func() (interface{}, Warnings, error) {
168		return func() (interface{}, Warnings, error) {
169			return promAPI.LabelValues(context.Background(), label, matches, time.Now().Add(-100*time.Hour), time.Now())
170		}
171	}
172
173	doQuery := func(q string, ts time.Time) func() (interface{}, Warnings, error) {
174		return func() (interface{}, Warnings, error) {
175			return promAPI.Query(context.Background(), q, ts)
176		}
177	}
178
179	doQueryRange := func(q string, rng Range) func() (interface{}, Warnings, error) {
180		return func() (interface{}, Warnings, error) {
181			return promAPI.QueryRange(context.Background(), q, rng)
182		}
183	}
184
185	doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, Warnings, error) {
186		return func() (interface{}, Warnings, error) {
187			return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime)
188		}
189	}
190
191	doSnapshot := func(skipHead bool) func() (interface{}, Warnings, error) {
192		return func() (interface{}, Warnings, error) {
193			v, err := promAPI.Snapshot(context.Background(), skipHead)
194			return v, nil, err
195		}
196	}
197
198	doRules := func() func() (interface{}, Warnings, error) {
199		return func() (interface{}, Warnings, error) {
200			v, err := promAPI.Rules(context.Background())
201			return v, nil, err
202		}
203	}
204
205	doTargets := func() func() (interface{}, Warnings, error) {
206		return func() (interface{}, Warnings, error) {
207			v, err := promAPI.Targets(context.Background())
208			return v, nil, err
209		}
210	}
211
212	doTargetsMetadata := func(matchTarget string, metric string, limit string) func() (interface{}, Warnings, error) {
213		return func() (interface{}, Warnings, error) {
214			v, err := promAPI.TargetsMetadata(context.Background(), matchTarget, metric, limit)
215			return v, nil, err
216		}
217	}
218
219	doMetadata := func(metric string, limit string) func() (interface{}, Warnings, error) {
220		return func() (interface{}, Warnings, error) {
221			v, err := promAPI.Metadata(context.Background(), metric, limit)
222			return v, nil, err
223		}
224	}
225
226	doTSDB := func() func() (interface{}, Warnings, error) {
227		return func() (interface{}, Warnings, error) {
228			v, err := promAPI.TSDB(context.Background())
229			return v, nil, err
230		}
231	}
232
233	doQueryExemplars := func(query string, startTime time.Time, endTime time.Time) func() (interface{}, Warnings, error) {
234		return func() (interface{}, Warnings, error) {
235			v, err := promAPI.QueryExemplars(context.Background(), query, startTime, endTime)
236			return v, nil, err
237		}
238	}
239
240	queryTests := []apiTest{
241		{
242			do: doQuery("2", testTime),
243			inRes: &queryResult{
244				Type: model.ValScalar,
245				Result: &model.Scalar{
246					Value:     2,
247					Timestamp: model.TimeFromUnix(testTime.Unix()),
248				},
249			},
250
251			reqMethod: "POST",
252			reqPath:   "/api/v1/query",
253			reqParam: url.Values{
254				"query": []string{"2"},
255				"time":  []string{testTime.Format(time.RFC3339Nano)},
256			},
257			res: &model.Scalar{
258				Value:     2,
259				Timestamp: model.TimeFromUnix(testTime.Unix()),
260			},
261		},
262		{
263			do:    doQuery("2", testTime),
264			inErr: fmt.Errorf("some error"),
265
266			reqMethod: "POST",
267			reqPath:   "/api/v1/query",
268			reqParam: url.Values{
269				"query": []string{"2"},
270				"time":  []string{testTime.Format(time.RFC3339Nano)},
271			},
272			err: fmt.Errorf("some error"),
273		},
274		{
275			do:           doQuery("2", testTime),
276			inRes:        "some body",
277			inStatusCode: 500,
278			inErr: &Error{
279				Type:   ErrServer,
280				Msg:    "server error: 500",
281				Detail: "some body",
282			},
283
284			reqMethod: "POST",
285			reqPath:   "/api/v1/query",
286			reqParam: url.Values{
287				"query": []string{"2"},
288				"time":  []string{testTime.Format(time.RFC3339Nano)},
289			},
290			err: errors.New("server_error: server error: 500"),
291		},
292		{
293			do:           doQuery("2", testTime),
294			inRes:        "some body",
295			inStatusCode: 404,
296			inErr: &Error{
297				Type:   ErrClient,
298				Msg:    "client error: 404",
299				Detail: "some body",
300			},
301
302			reqMethod: "POST",
303			reqPath:   "/api/v1/query",
304			reqParam: url.Values{
305				"query": []string{"2"},
306				"time":  []string{testTime.Format(time.RFC3339Nano)},
307			},
308			err: errors.New("client_error: client error: 404"),
309		},
310		// Warning only.
311		{
312			do:         doQuery("2", testTime),
313			inWarnings: []string{"warning"},
314			inRes: &queryResult{
315				Type: model.ValScalar,
316				Result: &model.Scalar{
317					Value:     2,
318					Timestamp: model.TimeFromUnix(testTime.Unix()),
319				},
320			},
321
322			reqMethod: "POST",
323			reqPath:   "/api/v1/query",
324			reqParam: url.Values{
325				"query": []string{"2"},
326				"time":  []string{testTime.Format(time.RFC3339Nano)},
327			},
328			res: &model.Scalar{
329				Value:     2,
330				Timestamp: model.TimeFromUnix(testTime.Unix()),
331			},
332			warnings: []string{"warning"},
333		},
334		// Warning + error.
335		{
336			do:           doQuery("2", testTime),
337			inWarnings:   []string{"warning"},
338			inRes:        "some body",
339			inStatusCode: 404,
340			inErr: &Error{
341				Type:   ErrClient,
342				Msg:    "client error: 404",
343				Detail: "some body",
344			},
345
346			reqMethod: "POST",
347			reqPath:   "/api/v1/query",
348			reqParam: url.Values{
349				"query": []string{"2"},
350				"time":  []string{testTime.Format(time.RFC3339Nano)},
351			},
352			err:      errors.New("client_error: client error: 404"),
353			warnings: []string{"warning"},
354		},
355
356		{
357			do: doQueryRange("2", Range{
358				Start: testTime.Add(-time.Minute),
359				End:   testTime,
360				Step:  time.Minute,
361			}),
362			inErr: fmt.Errorf("some error"),
363
364			reqMethod: "POST",
365			reqPath:   "/api/v1/query_range",
366			reqParam: url.Values{
367				"query": []string{"2"},
368				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
369				"end":   []string{testTime.Format(time.RFC3339Nano)},
370				"step":  []string{time.Minute.String()},
371			},
372			err: fmt.Errorf("some error"),
373		},
374
375		{
376			do:        doLabelNames(nil),
377			inRes:     []string{"val1", "val2"},
378			reqMethod: "GET",
379			reqPath:   "/api/v1/labels",
380			res:       []string{"val1", "val2"},
381		},
382		{
383			do:         doLabelNames(nil),
384			inRes:      []string{"val1", "val2"},
385			inWarnings: []string{"a"},
386			reqMethod:  "GET",
387			reqPath:    "/api/v1/labels",
388			res:        []string{"val1", "val2"},
389			warnings:   []string{"a"},
390		},
391
392		{
393			do:        doLabelNames(nil),
394			inErr:     fmt.Errorf("some error"),
395			reqMethod: "GET",
396			reqPath:   "/api/v1/labels",
397			err:       fmt.Errorf("some error"),
398		},
399		{
400			do:         doLabelNames(nil),
401			inErr:      fmt.Errorf("some error"),
402			inWarnings: []string{"a"},
403			reqMethod:  "GET",
404			reqPath:    "/api/v1/labels",
405			err:        fmt.Errorf("some error"),
406			warnings:   []string{"a"},
407		},
408		{
409			do:        doLabelNames([]string{"up"}),
410			inRes:     []string{"val1", "val2"},
411			reqMethod: "GET",
412			reqPath:   "/api/v1/labels",
413			reqParam:  url.Values{"match[]": {"up"}},
414			res:       []string{"val1", "val2"},
415		},
416
417		{
418			do:        doLabelValues(nil, "mylabel"),
419			inRes:     []string{"val1", "val2"},
420			reqMethod: "GET",
421			reqPath:   "/api/v1/label/mylabel/values",
422			res:       model.LabelValues{"val1", "val2"},
423		},
424		{
425			do:         doLabelValues(nil, "mylabel"),
426			inRes:      []string{"val1", "val2"},
427			inWarnings: []string{"a"},
428			reqMethod:  "GET",
429			reqPath:    "/api/v1/label/mylabel/values",
430			res:        model.LabelValues{"val1", "val2"},
431			warnings:   []string{"a"},
432		},
433
434		{
435			do:        doLabelValues(nil, "mylabel"),
436			inErr:     fmt.Errorf("some error"),
437			reqMethod: "GET",
438			reqPath:   "/api/v1/label/mylabel/values",
439			err:       fmt.Errorf("some error"),
440		},
441		{
442			do:         doLabelValues(nil, "mylabel"),
443			inErr:      fmt.Errorf("some error"),
444			inWarnings: []string{"a"},
445			reqMethod:  "GET",
446			reqPath:    "/api/v1/label/mylabel/values",
447			err:        fmt.Errorf("some error"),
448			warnings:   []string{"a"},
449		},
450		{
451			do:        doLabelValues([]string{"up"}, "mylabel"),
452			inRes:     []string{"val1", "val2"},
453			reqMethod: "GET",
454			reqPath:   "/api/v1/label/mylabel/values",
455			reqParam:  url.Values{"match[]": {"up"}},
456			res:       model.LabelValues{"val1", "val2"},
457		},
458
459		{
460			do: doSeries("up", testTime.Add(-time.Minute), testTime),
461			inRes: []map[string]string{
462				{
463					"__name__": "up",
464					"job":      "prometheus",
465					"instance": "localhost:9090"},
466			},
467			reqMethod: "GET",
468			reqPath:   "/api/v1/series",
469			reqParam: url.Values{
470				"match": []string{"up"},
471				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
472				"end":   []string{testTime.Format(time.RFC3339Nano)},
473			},
474			res: []model.LabelSet{
475				{
476					"__name__": "up",
477					"job":      "prometheus",
478					"instance": "localhost:9090",
479				},
480			},
481		},
482		// Series with data + warning.
483		{
484			do: doSeries("up", testTime.Add(-time.Minute), testTime),
485			inRes: []map[string]string{
486				{
487					"__name__": "up",
488					"job":      "prometheus",
489					"instance": "localhost:9090"},
490			},
491			inWarnings: []string{"a"},
492			reqMethod:  "GET",
493			reqPath:    "/api/v1/series",
494			reqParam: url.Values{
495				"match": []string{"up"},
496				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
497				"end":   []string{testTime.Format(time.RFC3339Nano)},
498			},
499			res: []model.LabelSet{
500				{
501					"__name__": "up",
502					"job":      "prometheus",
503					"instance": "localhost:9090",
504				},
505			},
506			warnings: []string{"a"},
507		},
508
509		{
510			do:        doSeries("up", testTime.Add(-time.Minute), testTime),
511			inErr:     fmt.Errorf("some error"),
512			reqMethod: "GET",
513			reqPath:   "/api/v1/series",
514			reqParam: url.Values{
515				"match": []string{"up"},
516				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
517				"end":   []string{testTime.Format(time.RFC3339Nano)},
518			},
519			err: fmt.Errorf("some error"),
520		},
521		// Series with error and warning.
522		{
523			do:         doSeries("up", testTime.Add(-time.Minute), testTime),
524			inErr:      fmt.Errorf("some error"),
525			inWarnings: []string{"a"},
526			reqMethod:  "GET",
527			reqPath:    "/api/v1/series",
528			reqParam: url.Values{
529				"match": []string{"up"},
530				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
531				"end":   []string{testTime.Format(time.RFC3339Nano)},
532			},
533			err:      fmt.Errorf("some error"),
534			warnings: []string{"a"},
535		},
536
537		{
538			do: doSnapshot(true),
539			inRes: map[string]string{
540				"name": "20171210T211224Z-2be650b6d019eb54",
541			},
542			reqMethod: "POST",
543			reqPath:   "/api/v1/admin/tsdb/snapshot",
544			reqParam: url.Values{
545				"skip_head": []string{"true"},
546			},
547			res: SnapshotResult{
548				Name: "20171210T211224Z-2be650b6d019eb54",
549			},
550		},
551
552		{
553			do:        doSnapshot(true),
554			inErr:     fmt.Errorf("some error"),
555			reqMethod: "POST",
556			reqPath:   "/api/v1/admin/tsdb/snapshot",
557			err:       fmt.Errorf("some error"),
558		},
559
560		{
561			do:        doCleanTombstones(),
562			reqMethod: "POST",
563			reqPath:   "/api/v1/admin/tsdb/clean_tombstones",
564		},
565
566		{
567			do:        doCleanTombstones(),
568			inErr:     fmt.Errorf("some error"),
569			reqMethod: "POST",
570			reqPath:   "/api/v1/admin/tsdb/clean_tombstones",
571			err:       fmt.Errorf("some error"),
572		},
573
574		{
575			do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
576			inRes: []map[string]string{
577				{
578					"__name__": "up",
579					"job":      "prometheus",
580					"instance": "localhost:9090"},
581			},
582			reqMethod: "POST",
583			reqPath:   "/api/v1/admin/tsdb/delete_series",
584			reqParam: url.Values{
585				"match": []string{"up"},
586				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
587				"end":   []string{testTime.Format(time.RFC3339Nano)},
588			},
589		},
590
591		{
592			do:        doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
593			inErr:     fmt.Errorf("some error"),
594			reqMethod: "POST",
595			reqPath:   "/api/v1/admin/tsdb/delete_series",
596			reqParam: url.Values{
597				"match": []string{"up"},
598				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
599				"end":   []string{testTime.Format(time.RFC3339Nano)},
600			},
601			err: fmt.Errorf("some error"),
602		},
603
604		{
605			do:        doConfig(),
606			reqMethod: "GET",
607			reqPath:   "/api/v1/status/config",
608			inRes: map[string]string{
609				"yaml": "<content of the loaded config file in YAML>",
610			},
611			res: ConfigResult{
612				YAML: "<content of the loaded config file in YAML>",
613			},
614		},
615
616		{
617			do:        doConfig(),
618			reqMethod: "GET",
619			reqPath:   "/api/v1/status/config",
620			inErr:     fmt.Errorf("some error"),
621			err:       fmt.Errorf("some error"),
622		},
623
624		{
625			do:        doFlags(),
626			reqMethod: "GET",
627			reqPath:   "/api/v1/status/flags",
628			inRes: map[string]string{
629				"alertmanager.notification-queue-capacity": "10000",
630				"alertmanager.timeout":                     "10s",
631				"log.level":                                "info",
632				"query.lookback-delta":                     "5m",
633				"query.max-concurrency":                    "20",
634			},
635			res: FlagsResult{
636				"alertmanager.notification-queue-capacity": "10000",
637				"alertmanager.timeout":                     "10s",
638				"log.level":                                "info",
639				"query.lookback-delta":                     "5m",
640				"query.max-concurrency":                    "20",
641			},
642		},
643
644		{
645			do:        doFlags(),
646			reqMethod: "GET",
647			reqPath:   "/api/v1/status/flags",
648			inErr:     fmt.Errorf("some error"),
649			err:       fmt.Errorf("some error"),
650		},
651
652		{
653			do:        doBuildinfo(),
654			reqMethod: "GET",
655			reqPath:   "/api/v1/status/buildinfo",
656			inErr:     fmt.Errorf("some error"),
657			err:       fmt.Errorf("some error"),
658		},
659
660		{
661			do:        doBuildinfo(),
662			reqMethod: "GET",
663			reqPath:   "/api/v1/status/buildinfo",
664			inRes: map[string]interface{}{
665				"version":   "2.23.0",
666				"revision":  "26d89b4b0776fe4cd5a3656dfa520f119a375273",
667				"branch":    "HEAD",
668				"buildUser": "root@37609b3a0a21",
669				"buildDate": "20201126-10:56:17",
670				"goVersion": "go1.15.5",
671			},
672			res: BuildinfoResult{
673				Version:   "2.23.0",
674				Revision:  "26d89b4b0776fe4cd5a3656dfa520f119a375273",
675				Branch:    "HEAD",
676				BuildUser: "root@37609b3a0a21",
677				BuildDate: "20201126-10:56:17",
678				GoVersion: "go1.15.5",
679			},
680		},
681
682		{
683			do:        doRuntimeinfo(),
684			reqMethod: "GET",
685			reqPath:   "/api/v1/status/runtimeinfo",
686			inErr:     fmt.Errorf("some error"),
687			err:       fmt.Errorf("some error"),
688		},
689
690		{
691			do:        doRuntimeinfo(),
692			reqMethod: "GET",
693			reqPath:   "/api/v1/status/runtimeinfo",
694			inRes: map[string]interface{}{
695				"startTime":           "2020-05-18T15:52:53.4503113Z",
696				"CWD":                 "/prometheus",
697				"reloadConfigSuccess": true,
698				"lastConfigTime":      "2020-05-18T15:52:56Z",
699				"chunkCount":          72692,
700				"timeSeriesCount":     18476,
701				"corruptionCount":     0,
702				"goroutineCount":      217,
703				"GOMAXPROCS":          2,
704				"GOGC":                "100",
705				"GODEBUG":             "allocfreetrace",
706				"storageRetention":    "1d",
707			},
708			res: RuntimeinfoResult{
709				StartTime:           time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC),
710				CWD:                 "/prometheus",
711				ReloadConfigSuccess: true,
712				LastConfigTime:      time.Date(2020, 5, 18, 15, 52, 56, 0, time.UTC),
713				ChunkCount:          72692,
714				TimeSeriesCount:     18476,
715				CorruptionCount:     0,
716				GoroutineCount:      217,
717				GOMAXPROCS:          2,
718				GOGC:                "100",
719				GODEBUG:             "allocfreetrace",
720				StorageRetention:    "1d",
721			},
722		},
723
724		{
725			do:        doAlertManagers(),
726			reqMethod: "GET",
727			reqPath:   "/api/v1/alertmanagers",
728			inRes: map[string]interface{}{
729				"activeAlertManagers": []map[string]string{
730					{
731						"url": "http://127.0.0.1:9091/api/v1/alerts",
732					},
733				},
734				"droppedAlertManagers": []map[string]string{
735					{
736						"url": "http://127.0.0.1:9092/api/v1/alerts",
737					},
738				},
739			},
740			res: AlertManagersResult{
741				Active: []AlertManager{
742					{
743						URL: "http://127.0.0.1:9091/api/v1/alerts",
744					},
745				},
746				Dropped: []AlertManager{
747					{
748						URL: "http://127.0.0.1:9092/api/v1/alerts",
749					},
750				},
751			},
752		},
753
754		{
755			do:        doAlertManagers(),
756			reqMethod: "GET",
757			reqPath:   "/api/v1/alertmanagers",
758			inErr:     fmt.Errorf("some error"),
759			err:       fmt.Errorf("some error"),
760		},
761
762		{
763			do:        doRules(),
764			reqMethod: "GET",
765			reqPath:   "/api/v1/rules",
766			inRes: map[string]interface{}{
767				"groups": []map[string]interface{}{
768					{
769						"file":     "/rules.yaml",
770						"interval": 60,
771						"name":     "example",
772						"rules": []map[string]interface{}{
773							{
774								"alerts": []map[string]interface{}{
775									{
776										"activeAt": testTime.UTC().Format(time.RFC3339Nano),
777										"annotations": map[string]interface{}{
778											"summary": "High request latency",
779										},
780										"labels": map[string]interface{}{
781											"alertname": "HighRequestLatency",
782											"severity":  "page",
783										},
784										"state": "firing",
785										"value": "1e+00",
786									},
787								},
788								"annotations": map[string]interface{}{
789									"summary": "High request latency",
790								},
791								"duration": 600,
792								"health":   "ok",
793								"labels": map[string]interface{}{
794									"severity": "page",
795								},
796								"name":  "HighRequestLatency",
797								"query": "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
798								"type":  "alerting",
799							},
800							{
801								"health": "ok",
802								"name":   "job:http_inprogress_requests:sum",
803								"query":  "sum(http_inprogress_requests) by (job)",
804								"type":   "recording",
805							},
806						},
807					},
808				},
809			},
810			res: RulesResult{
811				Groups: []RuleGroup{
812					{
813						Name:     "example",
814						File:     "/rules.yaml",
815						Interval: 60,
816						Rules: []interface{}{
817							AlertingRule{
818								Alerts: []*Alert{
819									{
820										ActiveAt: testTime.UTC(),
821										Annotations: model.LabelSet{
822											"summary": "High request latency",
823										},
824										Labels: model.LabelSet{
825											"alertname": "HighRequestLatency",
826											"severity":  "page",
827										},
828										State: AlertStateFiring,
829										Value: "1e+00",
830									},
831								},
832								Annotations: model.LabelSet{
833									"summary": "High request latency",
834								},
835								Labels: model.LabelSet{
836									"severity": "page",
837								},
838								Duration:  600,
839								Health:    RuleHealthGood,
840								Name:      "HighRequestLatency",
841								Query:     "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
842								LastError: "",
843							},
844							RecordingRule{
845								Health:    RuleHealthGood,
846								Name:      "job:http_inprogress_requests:sum",
847								Query:     "sum(http_inprogress_requests) by (job)",
848								LastError: "",
849							},
850						},
851					},
852				},
853			},
854		},
855
856		// This has the newer API elements like lastEvaluation, evaluationTime, etc.
857		{
858			do:        doRules(),
859			reqMethod: "GET",
860			reqPath:   "/api/v1/rules",
861			inRes: map[string]interface{}{
862				"groups": []map[string]interface{}{
863					{
864						"file":     "/rules.yaml",
865						"interval": 60,
866						"name":     "example",
867						"rules": []map[string]interface{}{
868							{
869								"alerts": []map[string]interface{}{
870									{
871										"activeAt": testTime.UTC().Format(time.RFC3339Nano),
872										"annotations": map[string]interface{}{
873											"summary": "High request latency",
874										},
875										"labels": map[string]interface{}{
876											"alertname": "HighRequestLatency",
877											"severity":  "page",
878										},
879										"state": "firing",
880										"value": "1e+00",
881									},
882								},
883								"annotations": map[string]interface{}{
884									"summary": "High request latency",
885								},
886								"duration": 600,
887								"health":   "ok",
888								"labels": map[string]interface{}{
889									"severity": "page",
890								},
891								"name":           "HighRequestLatency",
892								"query":          "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
893								"type":           "alerting",
894								"evaluationTime": 0.5,
895								"lastEvaluation": "2020-05-18T15:52:53.4503113Z",
896								"state":          "firing",
897							},
898							{
899								"health":         "ok",
900								"name":           "job:http_inprogress_requests:sum",
901								"query":          "sum(http_inprogress_requests) by (job)",
902								"type":           "recording",
903								"evaluationTime": 0.3,
904								"lastEvaluation": "2020-05-18T15:52:53.4503113Z",
905							},
906						},
907					},
908				},
909			},
910			res: RulesResult{
911				Groups: []RuleGroup{
912					{
913						Name:     "example",
914						File:     "/rules.yaml",
915						Interval: 60,
916						Rules: []interface{}{
917							AlertingRule{
918								Alerts: []*Alert{
919									{
920										ActiveAt: testTime.UTC(),
921										Annotations: model.LabelSet{
922											"summary": "High request latency",
923										},
924										Labels: model.LabelSet{
925											"alertname": "HighRequestLatency",
926											"severity":  "page",
927										},
928										State: AlertStateFiring,
929										Value: "1e+00",
930									},
931								},
932								Annotations: model.LabelSet{
933									"summary": "High request latency",
934								},
935								Labels: model.LabelSet{
936									"severity": "page",
937								},
938								Duration:       600,
939								Health:         RuleHealthGood,
940								Name:           "HighRequestLatency",
941								Query:          "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5",
942								LastError:      "",
943								EvaluationTime: 0.5,
944								LastEvaluation: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC),
945								State:          "firing",
946							},
947							RecordingRule{
948								Health:         RuleHealthGood,
949								Name:           "job:http_inprogress_requests:sum",
950								Query:          "sum(http_inprogress_requests) by (job)",
951								LastError:      "",
952								EvaluationTime: 0.3,
953								LastEvaluation: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC),
954							},
955						},
956					},
957				},
958			},
959		},
960
961		{
962			do:        doRules(),
963			reqMethod: "GET",
964			reqPath:   "/api/v1/rules",
965			inErr:     fmt.Errorf("some error"),
966			err:       fmt.Errorf("some error"),
967		},
968
969		{
970			do:        doTargets(),
971			reqMethod: "GET",
972			reqPath:   "/api/v1/targets",
973			inRes: map[string]interface{}{
974				"activeTargets": []map[string]interface{}{
975					{
976						"discoveredLabels": map[string]string{
977							"__address__":      "127.0.0.1:9090",
978							"__metrics_path__": "/metrics",
979							"__scheme__":       "http",
980							"job":              "prometheus",
981						},
982						"labels": map[string]string{
983							"instance": "127.0.0.1:9090",
984							"job":      "prometheus",
985						},
986						"scrapePool":         "prometheus",
987						"scrapeUrl":          "http://127.0.0.1:9090",
988						"globalUrl":          "http://127.0.0.1:9090",
989						"lastError":          "error while scraping target",
990						"lastScrape":         testTime.UTC().Format(time.RFC3339Nano),
991						"lastScrapeDuration": 0.001146115,
992						"health":             "up",
993					},
994				},
995				"droppedTargets": []map[string]interface{}{
996					{
997						"discoveredLabels": map[string]string{
998							"__address__":      "127.0.0.1:9100",
999							"__metrics_path__": "/metrics",
1000							"__scheme__":       "http",
1001							"job":              "node",
1002						},
1003					},
1004				},
1005			},
1006			res: TargetsResult{
1007				Active: []ActiveTarget{
1008					{
1009						DiscoveredLabels: map[string]string{
1010							"__address__":      "127.0.0.1:9090",
1011							"__metrics_path__": "/metrics",
1012							"__scheme__":       "http",
1013							"job":              "prometheus",
1014						},
1015						Labels: model.LabelSet{
1016							"instance": "127.0.0.1:9090",
1017							"job":      "prometheus",
1018						},
1019						ScrapePool:         "prometheus",
1020						ScrapeURL:          "http://127.0.0.1:9090",
1021						GlobalURL:          "http://127.0.0.1:9090",
1022						LastError:          "error while scraping target",
1023						LastScrape:         testTime.UTC(),
1024						LastScrapeDuration: 0.001146115,
1025						Health:             HealthGood,
1026					},
1027				},
1028				Dropped: []DroppedTarget{
1029					{
1030						DiscoveredLabels: map[string]string{
1031							"__address__":      "127.0.0.1:9100",
1032							"__metrics_path__": "/metrics",
1033							"__scheme__":       "http",
1034							"job":              "node",
1035						},
1036					},
1037				},
1038			},
1039		},
1040
1041		{
1042			do:        doTargets(),
1043			reqMethod: "GET",
1044			reqPath:   "/api/v1/targets",
1045			inErr:     fmt.Errorf("some error"),
1046			err:       fmt.Errorf("some error"),
1047		},
1048
1049		{
1050			do: doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"),
1051			inRes: []map[string]interface{}{
1052				{
1053					"target": map[string]interface{}{
1054						"instance": "127.0.0.1:9090",
1055						"job":      "prometheus",
1056					},
1057					"type": "gauge",
1058					"help": "Number of goroutines that currently exist.",
1059					"unit": "",
1060				},
1061			},
1062			reqMethod: "GET",
1063			reqPath:   "/api/v1/targets/metadata",
1064			reqParam: url.Values{
1065				"match_target": []string{"{job=\"prometheus\"}"},
1066				"metric":       []string{"go_goroutines"},
1067				"limit":        []string{"1"},
1068			},
1069			res: []MetricMetadata{
1070				{
1071					Target: map[string]string{
1072						"instance": "127.0.0.1:9090",
1073						"job":      "prometheus",
1074					},
1075					Type: "gauge",
1076					Help: "Number of goroutines that currently exist.",
1077					Unit: "",
1078				},
1079			},
1080		},
1081
1082		{
1083			do:        doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"),
1084			inErr:     fmt.Errorf("some error"),
1085			reqMethod: "GET",
1086			reqPath:   "/api/v1/targets/metadata",
1087			reqParam: url.Values{
1088				"match_target": []string{"{job=\"prometheus\"}"},
1089				"metric":       []string{"go_goroutines"},
1090				"limit":        []string{"1"},
1091			},
1092			err: fmt.Errorf("some error"),
1093		},
1094
1095		{
1096			do: doMetadata("go_goroutines", "1"),
1097			inRes: map[string]interface{}{
1098				"go_goroutines": []map[string]interface{}{
1099					{
1100						"type": "gauge",
1101						"help": "Number of goroutines that currently exist.",
1102						"unit": "",
1103					},
1104				},
1105			},
1106			reqMethod: "GET",
1107			reqPath:   "/api/v1/metadata",
1108			reqParam: url.Values{
1109				"metric": []string{"go_goroutines"},
1110				"limit":  []string{"1"},
1111			},
1112			res: map[string][]Metadata{
1113				"go_goroutines": []Metadata{
1114					{
1115						Type: "gauge",
1116						Help: "Number of goroutines that currently exist.",
1117						Unit: "",
1118					},
1119				},
1120			},
1121		},
1122
1123		{
1124			do:        doMetadata("", "1"),
1125			inErr:     fmt.Errorf("some error"),
1126			reqMethod: "GET",
1127			reqPath:   "/api/v1/metadata",
1128			reqParam: url.Values{
1129				"metric": []string{""},
1130				"limit":  []string{"1"},
1131			},
1132			err: fmt.Errorf("some error"),
1133		},
1134
1135		{
1136			do:        doTSDB(),
1137			reqMethod: "GET",
1138			reqPath:   "/api/v1/status/tsdb",
1139			inErr:     fmt.Errorf("some error"),
1140			err:       fmt.Errorf("some error"),
1141		},
1142
1143		{
1144			do:        doTSDB(),
1145			reqMethod: "GET",
1146			reqPath:   "/api/v1/status/tsdb",
1147			inRes: map[string]interface{}{
1148				"seriesCountByMetricName": []interface{}{
1149					map[string]interface{}{
1150						"name":  "kubelet_http_requests_duration_seconds_bucket",
1151						"value": 1000,
1152					},
1153				},
1154				"labelValueCountByLabelName": []interface{}{
1155					map[string]interface{}{
1156						"name":  "__name__",
1157						"value": 200,
1158					},
1159				},
1160				"memoryInBytesByLabelName": []interface{}{
1161					map[string]interface{}{
1162						"name":  "id",
1163						"value": 4096,
1164					},
1165				},
1166				"seriesCountByLabelValuePair": []interface{}{
1167					map[string]interface{}{
1168						"name":  "job=kubelet",
1169						"value": 30000,
1170					},
1171				},
1172			},
1173			res: TSDBResult{
1174				SeriesCountByMetricName: []Stat{
1175					{
1176						Name:  "kubelet_http_requests_duration_seconds_bucket",
1177						Value: 1000,
1178					},
1179				},
1180				LabelValueCountByLabelName: []Stat{
1181					{
1182						Name:  "__name__",
1183						Value: 200,
1184					},
1185				},
1186				MemoryInBytesByLabelName: []Stat{
1187					{
1188						Name:  "id",
1189						Value: 4096,
1190					},
1191				},
1192				SeriesCountByLabelValuePair: []Stat{
1193					{
1194						Name:  "job=kubelet",
1195						Value: 30000,
1196					},
1197				},
1198			},
1199		},
1200
1201		{
1202			do:        doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime),
1203			reqMethod: "GET",
1204			reqPath:   "/api/v1/query_exemplars",
1205			inErr:     fmt.Errorf("some error"),
1206			err:       fmt.Errorf("some error"),
1207		},
1208
1209		{
1210			do:        doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime),
1211			reqMethod: "GET",
1212			reqPath:   "/api/v1/query_exemplars",
1213			inRes: []interface{}{
1214				map[string]interface{}{
1215					"seriesLabels": map[string]interface{}{
1216						"__name__": "tns_request_duration_seconds_bucket",
1217						"instance": "app:80",
1218						"job":      "tns/app",
1219					},
1220					"exemplars": []interface{}{
1221						map[string]interface{}{
1222							"labels": map[string]interface{}{
1223								"traceID": "19fd8c8a33975a23",
1224							},
1225							"value":     "0.003863295",
1226							"timestamp": model.TimeFromUnixNano(testTime.UnixNano()),
1227						},
1228						map[string]interface{}{
1229							"labels": map[string]interface{}{
1230								"traceID": "67f743f07cc786b0",
1231							},
1232							"value":     "0.001535405",
1233							"timestamp": model.TimeFromUnixNano(testTime.UnixNano()),
1234						},
1235					},
1236				},
1237			},
1238			res: []ExemplarQueryResult{
1239				{
1240					SeriesLabels: model.LabelSet{
1241						"__name__": "tns_request_duration_seconds_bucket",
1242						"instance": "app:80",
1243						"job":      "tns/app",
1244					},
1245					Exemplars: []Exemplar{
1246						{
1247							Labels:    model.LabelSet{"traceID": "19fd8c8a33975a23"},
1248							Value:     0.003863295,
1249							Timestamp: model.TimeFromUnixNano(testTime.UnixNano()),
1250						},
1251						{
1252							Labels:    model.LabelSet{"traceID": "67f743f07cc786b0"},
1253							Value:     0.001535405,
1254							Timestamp: model.TimeFromUnixNano(testTime.UnixNano()),
1255						},
1256					},
1257				},
1258			},
1259		},
1260	}
1261
1262	var tests []apiTest
1263	tests = append(tests, queryTests...)
1264
1265	for i, test := range tests {
1266		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
1267			tc.curTest = test
1268
1269			res, warnings, err := test.do()
1270
1271			if (test.inWarnings == nil) != (warnings == nil) && !reflect.DeepEqual(test.inWarnings, warnings) {
1272				t.Fatalf("mismatch in warnings expected=%v actual=%v", test.inWarnings, warnings)
1273			}
1274
1275			if test.err != nil {
1276				if err == nil {
1277					t.Fatalf("expected error %q but got none", test.err)
1278				}
1279				if err.Error() != test.err.Error() {
1280					t.Errorf("unexpected error: want %s, got %s", test.err, err)
1281				}
1282				if apiErr, ok := err.(*Error); ok {
1283					if apiErr.Detail != test.inRes {
1284						t.Errorf("%q should be %q", apiErr.Detail, test.inRes)
1285					}
1286				}
1287				return
1288			}
1289			if err != nil {
1290				t.Fatalf("unexpected error: %s", err)
1291			}
1292
1293			if !reflect.DeepEqual(res, test.res) {
1294				t.Errorf("unexpected result: want %v, got %v", test.res, res)
1295			}
1296		})
1297	}
1298}
1299
1300type testClient struct {
1301	*testing.T
1302
1303	ch  chan apiClientTest
1304	req *http.Request
1305}
1306
1307type apiClientTest struct {
1308	code             int
1309	response         interface{}
1310	expectedBody     string
1311	expectedErr      *Error
1312	expectedWarnings Warnings
1313}
1314
1315func (c *testClient) URL(ep string, args map[string]string) *url.URL {
1316	return nil
1317}
1318
1319func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
1320	if ctx == nil {
1321		c.Fatalf("context was not passed down")
1322	}
1323	if req != c.req {
1324		c.Fatalf("request was not passed down")
1325	}
1326
1327	test := <-c.ch
1328
1329	var b []byte
1330	var err error
1331
1332	switch v := test.response.(type) {
1333	case string:
1334		b = []byte(v)
1335	default:
1336		b, err = json.Marshal(v)
1337		if err != nil {
1338			c.Fatal(err)
1339		}
1340	}
1341
1342	resp := &http.Response{
1343		StatusCode: test.code,
1344	}
1345
1346	return resp, b, nil
1347}
1348
1349func TestAPIClientDo(t *testing.T) {
1350	tests := []apiClientTest{
1351		{
1352			code: http.StatusUnprocessableEntity,
1353			response: &apiResponse{
1354				Status:    "error",
1355				Data:      json.RawMessage(`null`),
1356				ErrorType: ErrBadData,
1357				Error:     "failed",
1358			},
1359			expectedErr: &Error{
1360				Type: ErrBadData,
1361				Msg:  "failed",
1362			},
1363			expectedBody: `null`,
1364		},
1365		{
1366			code: http.StatusUnprocessableEntity,
1367			response: &apiResponse{
1368				Status:    "error",
1369				Data:      json.RawMessage(`"test"`),
1370				ErrorType: ErrTimeout,
1371				Error:     "timed out",
1372			},
1373			expectedErr: &Error{
1374				Type: ErrTimeout,
1375				Msg:  "timed out",
1376			},
1377			expectedBody: `test`,
1378		},
1379		{
1380			code:     http.StatusInternalServerError,
1381			response: "500 error details",
1382			expectedErr: &Error{
1383				Type:   ErrServer,
1384				Msg:    "server error: 500",
1385				Detail: "500 error details",
1386			},
1387		},
1388		{
1389			code:     http.StatusNotFound,
1390			response: "404 error details",
1391			expectedErr: &Error{
1392				Type:   ErrClient,
1393				Msg:    "client error: 404",
1394				Detail: "404 error details",
1395			},
1396		},
1397		{
1398			code: http.StatusBadRequest,
1399			response: &apiResponse{
1400				Status:    "error",
1401				Data:      json.RawMessage(`null`),
1402				ErrorType: ErrBadData,
1403				Error:     "end timestamp must not be before start time",
1404			},
1405			expectedErr: &Error{
1406				Type: ErrBadData,
1407				Msg:  "end timestamp must not be before start time",
1408			},
1409		},
1410		{
1411			code:     http.StatusUnprocessableEntity,
1412			response: "bad json",
1413			expectedErr: &Error{
1414				Type: ErrBadResponse,
1415				Msg:  "readObjectStart: expect { or n, but found b, error found in #1 byte of ...|bad json|..., bigger context ...|bad json|...",
1416			},
1417		},
1418		{
1419			code: http.StatusUnprocessableEntity,
1420			response: &apiResponse{
1421				Status: "success",
1422				Data:   json.RawMessage(`"test"`),
1423			},
1424			expectedErr: &Error{
1425				Type: ErrBadResponse,
1426				Msg:  "inconsistent body for response code",
1427			},
1428		},
1429		{
1430			code: http.StatusUnprocessableEntity,
1431			response: &apiResponse{
1432				Status:    "success",
1433				Data:      json.RawMessage(`"test"`),
1434				ErrorType: ErrTimeout,
1435				Error:     "timed out",
1436			},
1437			expectedErr: &Error{
1438				Type: ErrBadResponse,
1439				Msg:  "inconsistent body for response code",
1440			},
1441		},
1442		{
1443			code: http.StatusOK,
1444			response: &apiResponse{
1445				Status:    "error",
1446				Data:      json.RawMessage(`"test"`),
1447				ErrorType: ErrTimeout,
1448				Error:     "timed out",
1449			},
1450			expectedErr: &Error{
1451				Type: ErrTimeout,
1452				Msg:  "timed out",
1453			},
1454		},
1455		{
1456			code: http.StatusOK,
1457			response: &apiResponse{
1458				Status:    "error",
1459				Data:      json.RawMessage(`"test"`),
1460				ErrorType: ErrTimeout,
1461				Error:     "timed out",
1462				Warnings:  []string{"a"},
1463			},
1464			expectedErr: &Error{
1465				Type: ErrTimeout,
1466				Msg:  "timed out",
1467			},
1468			expectedWarnings: []string{"a"},
1469		},
1470	}
1471
1472	tc := &testClient{
1473		T:   t,
1474		ch:  make(chan apiClientTest, 1),
1475		req: &http.Request{},
1476	}
1477	client := &apiClientImpl{
1478		client: tc,
1479	}
1480
1481	for i, test := range tests {
1482		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
1483
1484			tc.ch <- test
1485
1486			_, body, warnings, err := client.Do(context.Background(), tc.req)
1487
1488			if test.expectedWarnings != nil {
1489				if !reflect.DeepEqual(test.expectedWarnings, warnings) {
1490					t.Fatalf("mismatch in warnings expected=%v actual=%v", test.expectedWarnings, warnings)
1491				}
1492			} else {
1493				if warnings != nil {
1494					t.Fatalf("unexpexted warnings: %v", warnings)
1495				}
1496			}
1497
1498			if test.expectedErr != nil {
1499				if err == nil {
1500					t.Fatal("expected error, but got none")
1501				}
1502
1503				if test.expectedErr.Error() != err.Error() {
1504					t.Fatalf("expected error:%v, but got:%v", test.expectedErr.Error(), err.Error())
1505				}
1506
1507				if test.expectedErr.Detail != "" {
1508					apiErr := err.(*Error)
1509					if apiErr.Detail != test.expectedErr.Detail {
1510						t.Fatalf("expected error detail :%v, but got:%v", apiErr.Detail, test.expectedErr.Detail)
1511					}
1512				}
1513
1514				return
1515			}
1516
1517			if err != nil {
1518				t.Fatalf("unexpected error:%v", err)
1519			}
1520			if test.expectedBody != string(body) {
1521				t.Fatalf("expected body :%v, but got:%v", test.expectedBody, string(body))
1522			}
1523		})
1524
1525	}
1526}
1527
1528func TestSamplesJsonSerialization(t *testing.T) {
1529	tests := []struct {
1530		point    model.SamplePair
1531		expected string
1532	}{
1533		{
1534			point:    model.SamplePair{0, 0},
1535			expected: `[0,"0"]`,
1536		},
1537		{
1538			point:    model.SamplePair{1, 20},
1539			expected: `[0.001,"20"]`,
1540		},
1541		{
1542			point:    model.SamplePair{10, 20},
1543			expected: `[0.010,"20"]`,
1544		},
1545		{
1546			point:    model.SamplePair{100, 20},
1547			expected: `[0.100,"20"]`,
1548		},
1549		{
1550			point:    model.SamplePair{1001, 20},
1551			expected: `[1.001,"20"]`,
1552		},
1553		{
1554			point:    model.SamplePair{1010, 20},
1555			expected: `[1.010,"20"]`,
1556		},
1557		{
1558			point:    model.SamplePair{1100, 20},
1559			expected: `[1.100,"20"]`,
1560		},
1561		{
1562			point:    model.SamplePair{12345678123456555, 20},
1563			expected: `[12345678123456.555,"20"]`,
1564		},
1565		{
1566			point:    model.SamplePair{-1, 20},
1567			expected: `[-0.001,"20"]`,
1568		},
1569		{
1570			point:    model.SamplePair{0, model.SampleValue(math.NaN())},
1571			expected: `[0,"NaN"]`,
1572		},
1573		{
1574			point:    model.SamplePair{0, model.SampleValue(math.Inf(1))},
1575			expected: `[0,"+Inf"]`,
1576		},
1577		{
1578			point:    model.SamplePair{0, model.SampleValue(math.Inf(-1))},
1579			expected: `[0,"-Inf"]`,
1580		},
1581		{
1582			point:    model.SamplePair{0, model.SampleValue(1.2345678e6)},
1583			expected: `[0,"1234567.8"]`,
1584		},
1585		{
1586			point:    model.SamplePair{0, 1.2345678e-6},
1587			expected: `[0,"0.0000012345678"]`,
1588		},
1589		{
1590			point:    model.SamplePair{0, 1.2345678e-67},
1591			expected: `[0,"1.2345678e-67"]`,
1592		},
1593	}
1594
1595	for _, test := range tests {
1596		t.Run(test.expected, func(t *testing.T) {
1597			b, err := json.Marshal(test.point)
1598			if err != nil {
1599				t.Fatal(err)
1600			}
1601			if string(b) != test.expected {
1602				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
1603			}
1604
1605			// To test Unmarshal we will Unmarshal then re-Marshal this way we
1606			// can do a string compare, otherwise Nan values don't show equivalence
1607			// properly.
1608			var sp model.SamplePair
1609			if err = json.Unmarshal(b, &sp); err != nil {
1610				t.Fatal(err)
1611			}
1612
1613			b, err = json.Marshal(sp)
1614			if err != nil {
1615				t.Fatal(err)
1616			}
1617			if string(b) != test.expected {
1618				t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b))
1619			}
1620		})
1621	}
1622}
1623
1624type httpTestClient struct {
1625	client http.Client
1626}
1627
1628func (c *httpTestClient) URL(ep string, args map[string]string) *url.URL {
1629	return nil
1630}
1631
1632func (c *httpTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
1633	resp, err := c.client.Do(req)
1634	if err != nil {
1635		return nil, nil, err
1636	}
1637
1638	var body []byte
1639	done := make(chan struct{})
1640	go func() {
1641		body, err = ioutil.ReadAll(resp.Body)
1642		close(done)
1643	}()
1644
1645	select {
1646	case <-ctx.Done():
1647		<-done
1648		err = resp.Body.Close()
1649		if err == nil {
1650			err = ctx.Err()
1651		}
1652	case <-done:
1653	}
1654
1655	return resp, body, err
1656}
1657
1658func TestDoGetFallback(t *testing.T) {
1659	v := url.Values{"a": []string{"1", "2"}}
1660
1661	type testResponse struct {
1662		Values string
1663		Method string
1664	}
1665
1666	// Start a local HTTP server.
1667	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
1668		req.ParseForm()
1669		testResp, _ := json.Marshal(&testResponse{
1670			Values: req.Form.Encode(),
1671			Method: req.Method,
1672		})
1673
1674		apiResp := &apiResponse{
1675			Data: testResp,
1676		}
1677
1678		body, _ := json.Marshal(apiResp)
1679
1680		if req.Method == http.MethodPost {
1681			if req.URL.Path == "/blockPost405" {
1682				http.Error(w, string(body), http.StatusMethodNotAllowed)
1683				return
1684			}
1685		}
1686
1687		if req.Method == http.MethodPost {
1688			if req.URL.Path == "/blockPost501" {
1689				http.Error(w, string(body), http.StatusNotImplemented)
1690				return
1691			}
1692		}
1693
1694		w.Write(body)
1695	}))
1696	// Close the server when test finishes.
1697	defer server.Close()
1698
1699	u, err := url.Parse(server.URL)
1700	if err != nil {
1701		t.Fatal(err)
1702	}
1703	client := &httpTestClient{client: *(server.Client())}
1704	api := &apiClientImpl{
1705		client: client,
1706	}
1707
1708	// Do a post, and ensure that the post succeeds.
1709	_, b, _, err := api.DoGetFallback(context.TODO(), u, v)
1710	if err != nil {
1711		t.Fatalf("Error doing local request: %v", err)
1712	}
1713	resp := &testResponse{}
1714	if err := json.Unmarshal(b, resp); err != nil {
1715		t.Fatal(err)
1716	}
1717	if resp.Method != http.MethodPost {
1718		t.Fatalf("Mismatch method")
1719	}
1720	if resp.Values != v.Encode() {
1721		t.Fatalf("Mismatch in values")
1722	}
1723
1724	// Do a fallback to a get on 405.
1725	u.Path = "/blockPost405"
1726	_, b, _, err = api.DoGetFallback(context.TODO(), u, v)
1727	if err != nil {
1728		t.Fatalf("Error doing local request: %v", err)
1729	}
1730	if err := json.Unmarshal(b, resp); err != nil {
1731		t.Fatal(err)
1732	}
1733	if resp.Method != http.MethodGet {
1734		t.Fatalf("Mismatch method")
1735	}
1736	if resp.Values != v.Encode() {
1737		t.Fatalf("Mismatch in values")
1738	}
1739
1740	// Do a fallback to a get on 501.
1741	u.Path = "/blockPost501"
1742	_, b, _, err = api.DoGetFallback(context.TODO(), u, v)
1743	if err != nil {
1744		t.Fatalf("Error doing local request: %v", err)
1745	}
1746	if err := json.Unmarshal(b, resp); err != nil {
1747		t.Fatal(err)
1748	}
1749	if resp.Method != http.MethodGet {
1750		t.Fatalf("Mismatch method")
1751	}
1752	if resp.Values != v.Encode() {
1753		t.Fatalf("Mismatch in values")
1754	}
1755}
1756