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
14// +build go1.7
15
16package v1
17
18import (
19	"context"
20	"encoding/json"
21	"errors"
22	"fmt"
23	"net/http"
24	"net/url"
25	"reflect"
26	"strings"
27	"testing"
28	"time"
29
30	"github.com/prometheus/common/model"
31)
32
33type apiTest struct {
34	do           func() (interface{}, error)
35	inErr        error
36	inStatusCode int
37	inRes        interface{}
38
39	reqPath   string
40	reqParam  url.Values
41	reqMethod string
42	res       interface{}
43	err       error
44}
45
46type apiTestClient struct {
47	*testing.T
48	curTest apiTest
49}
50
51func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL {
52	path := ep
53	for k, v := range args {
54		path = strings.Replace(path, ":"+k, v, -1)
55	}
56	u := &url.URL{
57		Host: "test:9090",
58		Path: path,
59	}
60	return u
61}
62
63func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
64
65	test := c.curTest
66
67	if req.URL.Path != test.reqPath {
68		c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path)
69	}
70	if req.Method != test.reqMethod {
71		c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method)
72	}
73
74	b, err := json.Marshal(test.inRes)
75	if err != nil {
76		c.Fatal(err)
77	}
78
79	resp := &http.Response{}
80	if test.inStatusCode != 0 {
81		resp.StatusCode = test.inStatusCode
82	} else if test.inErr != nil {
83		resp.StatusCode = statusAPIError
84	} else {
85		resp.StatusCode = http.StatusOK
86	}
87
88	return resp, b, test.inErr
89}
90
91func TestAPIs(t *testing.T) {
92
93	testTime := time.Now()
94
95	client := &apiTestClient{T: t}
96
97	promAPI := &httpAPI{
98		client: client,
99	}
100
101	doAlertManagers := func() func() (interface{}, error) {
102		return func() (interface{}, error) {
103			return promAPI.AlertManagers(context.Background())
104		}
105	}
106
107	doCleanTombstones := func() func() (interface{}, error) {
108		return func() (interface{}, error) {
109			return nil, promAPI.CleanTombstones(context.Background())
110		}
111	}
112
113	doConfig := func() func() (interface{}, error) {
114		return func() (interface{}, error) {
115			return promAPI.Config(context.Background())
116		}
117	}
118
119	doDeleteSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) {
120		return func() (interface{}, error) {
121			return nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime)
122		}
123	}
124
125	doFlags := func() func() (interface{}, error) {
126		return func() (interface{}, error) {
127			return promAPI.Flags(context.Background())
128		}
129	}
130
131	doLabelValues := func(label string) func() (interface{}, error) {
132		return func() (interface{}, error) {
133			return promAPI.LabelValues(context.Background(), label)
134		}
135	}
136
137	doQuery := func(q string, ts time.Time) func() (interface{}, error) {
138		return func() (interface{}, error) {
139			return promAPI.Query(context.Background(), q, ts)
140		}
141	}
142
143	doQueryRange := func(q string, rng Range) func() (interface{}, error) {
144		return func() (interface{}, error) {
145			return promAPI.QueryRange(context.Background(), q, rng)
146		}
147	}
148
149	doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, error) {
150		return func() (interface{}, error) {
151			return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime)
152		}
153	}
154
155	doSnapshot := func(skipHead bool) func() (interface{}, error) {
156		return func() (interface{}, error) {
157			return promAPI.Snapshot(context.Background(), skipHead)
158		}
159	}
160
161	doTargets := func() func() (interface{}, error) {
162		return func() (interface{}, error) {
163			return promAPI.Targets(context.Background())
164		}
165	}
166
167	queryTests := []apiTest{
168		{
169			do: doQuery("2", testTime),
170			inRes: &queryResult{
171				Type: model.ValScalar,
172				Result: &model.Scalar{
173					Value:     2,
174					Timestamp: model.TimeFromUnix(testTime.Unix()),
175				},
176			},
177
178			reqMethod: "GET",
179			reqPath:   "/api/v1/query",
180			reqParam: url.Values{
181				"query": []string{"2"},
182				"time":  []string{testTime.Format(time.RFC3339Nano)},
183			},
184			res: &model.Scalar{
185				Value:     2,
186				Timestamp: model.TimeFromUnix(testTime.Unix()),
187			},
188		},
189		{
190			do:    doQuery("2", testTime),
191			inErr: fmt.Errorf("some error"),
192
193			reqMethod: "GET",
194			reqPath:   "/api/v1/query",
195			reqParam: url.Values{
196				"query": []string{"2"},
197				"time":  []string{testTime.Format(time.RFC3339Nano)},
198			},
199			err: fmt.Errorf("some error"),
200		},
201		{
202			do:           doQuery("2", testTime),
203			inRes:        "some body",
204			inStatusCode: 500,
205			inErr: &Error{
206				Type:   ErrServer,
207				Msg:    "server error: 500",
208				Detail: "some body",
209			},
210
211			reqMethod: "GET",
212			reqPath:   "/api/v1/query",
213			reqParam: url.Values{
214				"query": []string{"2"},
215				"time":  []string{testTime.Format(time.RFC3339Nano)},
216			},
217			err: errors.New("server_error: server error: 500"),
218		},
219		{
220			do:           doQuery("2", testTime),
221			inRes:        "some body",
222			inStatusCode: 404,
223			inErr: &Error{
224				Type:   ErrClient,
225				Msg:    "client error: 404",
226				Detail: "some body",
227			},
228
229			reqMethod: "GET",
230			reqPath:   "/api/v1/query",
231			reqParam: url.Values{
232				"query": []string{"2"},
233				"time":  []string{testTime.Format(time.RFC3339Nano)},
234			},
235			err: errors.New("client_error: client error: 404"),
236		},
237
238		{
239			do: doQueryRange("2", Range{
240				Start: testTime.Add(-time.Minute),
241				End:   testTime,
242				Step:  time.Minute,
243			}),
244			inErr: fmt.Errorf("some error"),
245
246			reqMethod: "GET",
247			reqPath:   "/api/v1/query_range",
248			reqParam: url.Values{
249				"query": []string{"2"},
250				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
251				"end":   []string{testTime.Format(time.RFC3339Nano)},
252				"step":  []string{time.Minute.String()},
253			},
254			err: fmt.Errorf("some error"),
255		},
256
257		{
258			do:        doLabelValues("mylabel"),
259			inRes:     []string{"val1", "val2"},
260			reqMethod: "GET",
261			reqPath:   "/api/v1/label/mylabel/values",
262			res:       model.LabelValues{"val1", "val2"},
263		},
264
265		{
266			do:        doLabelValues("mylabel"),
267			inErr:     fmt.Errorf("some error"),
268			reqMethod: "GET",
269			reqPath:   "/api/v1/label/mylabel/values",
270			err:       fmt.Errorf("some error"),
271		},
272
273		{
274			do: doSeries("up", testTime.Add(-time.Minute), testTime),
275			inRes: []map[string]string{
276				{
277					"__name__": "up",
278					"job":      "prometheus",
279					"instance": "localhost:9090"},
280			},
281			reqMethod: "GET",
282			reqPath:   "/api/v1/series",
283			reqParam: url.Values{
284				"match": []string{"up"},
285				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
286				"end":   []string{testTime.Format(time.RFC3339Nano)},
287			},
288			res: []model.LabelSet{
289				model.LabelSet{
290					"__name__": "up",
291					"job":      "prometheus",
292					"instance": "localhost:9090",
293				},
294			},
295		},
296
297		{
298			do:        doSeries("up", testTime.Add(-time.Minute), testTime),
299			inErr:     fmt.Errorf("some error"),
300			reqMethod: "GET",
301			reqPath:   "/api/v1/series",
302			reqParam: url.Values{
303				"match": []string{"up"},
304				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
305				"end":   []string{testTime.Format(time.RFC3339Nano)},
306			},
307			err: fmt.Errorf("some error"),
308		},
309
310		{
311			do: doSnapshot(true),
312			inRes: map[string]string{
313				"name": "20171210T211224Z-2be650b6d019eb54",
314			},
315			reqMethod: "POST",
316			reqPath:   "/api/v1/admin/tsdb/snapshot",
317			reqParam: url.Values{
318				"skip_head": []string{"true"},
319			},
320			res: SnapshotResult{
321				Name: "20171210T211224Z-2be650b6d019eb54",
322			},
323		},
324
325		{
326			do:        doSnapshot(true),
327			inErr:     fmt.Errorf("some error"),
328			reqMethod: "POST",
329			reqPath:   "/api/v1/admin/tsdb/snapshot",
330			err:       fmt.Errorf("some error"),
331		},
332
333		{
334			do:        doCleanTombstones(),
335			reqMethod: "POST",
336			reqPath:   "/api/v1/admin/tsdb/clean_tombstones",
337		},
338
339		{
340			do:        doCleanTombstones(),
341			inErr:     fmt.Errorf("some error"),
342			reqMethod: "POST",
343			reqPath:   "/api/v1/admin/tsdb/clean_tombstones",
344			err:       fmt.Errorf("some error"),
345		},
346
347		{
348			do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
349			inRes: []map[string]string{
350				{
351					"__name__": "up",
352					"job":      "prometheus",
353					"instance": "localhost:9090"},
354			},
355			reqMethod: "POST",
356			reqPath:   "/api/v1/admin/tsdb/delete_series",
357			reqParam: url.Values{
358				"match": []string{"up"},
359				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
360				"end":   []string{testTime.Format(time.RFC3339Nano)},
361			},
362		},
363
364		{
365			do:        doDeleteSeries("up", testTime.Add(-time.Minute), testTime),
366			inErr:     fmt.Errorf("some error"),
367			reqMethod: "POST",
368			reqPath:   "/api/v1/admin/tsdb/delete_series",
369			reqParam: url.Values{
370				"match": []string{"up"},
371				"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
372				"end":   []string{testTime.Format(time.RFC3339Nano)},
373			},
374			err: fmt.Errorf("some error"),
375		},
376
377		{
378			do:        doConfig(),
379			reqMethod: "GET",
380			reqPath:   "/api/v1/status/config",
381			inRes: map[string]string{
382				"yaml": "<content of the loaded config file in YAML>",
383			},
384			res: ConfigResult{
385				YAML: "<content of the loaded config file in YAML>",
386			},
387		},
388
389		{
390			do:        doConfig(),
391			reqMethod: "GET",
392			reqPath:   "/api/v1/status/config",
393			inErr:     fmt.Errorf("some error"),
394			err:       fmt.Errorf("some error"),
395		},
396
397		{
398			do:        doFlags(),
399			reqMethod: "GET",
400			reqPath:   "/api/v1/status/flags",
401			inRes: map[string]string{
402				"alertmanager.notification-queue-capacity": "10000",
403				"alertmanager.timeout":                     "10s",
404				"log.level":                                "info",
405				"query.lookback-delta":                     "5m",
406				"query.max-concurrency":                    "20",
407			},
408			res: FlagsResult{
409				"alertmanager.notification-queue-capacity": "10000",
410				"alertmanager.timeout":                     "10s",
411				"log.level":                                "info",
412				"query.lookback-delta":                     "5m",
413				"query.max-concurrency":                    "20",
414			},
415		},
416
417		{
418			do:        doFlags(),
419			reqMethod: "GET",
420			reqPath:   "/api/v1/status/flags",
421			inErr:     fmt.Errorf("some error"),
422			err:       fmt.Errorf("some error"),
423		},
424
425		{
426			do:        doAlertManagers(),
427			reqMethod: "GET",
428			reqPath:   "/api/v1/alertmanagers",
429			inRes: map[string]interface{}{
430				"activeAlertManagers": []map[string]string{
431					{
432						"url": "http://127.0.0.1:9091/api/v1/alerts",
433					},
434				},
435				"droppedAlertManagers": []map[string]string{
436					{
437						"url": "http://127.0.0.1:9092/api/v1/alerts",
438					},
439				},
440			},
441			res: AlertManagersResult{
442				Active: []AlertManager{
443					{
444						URL: "http://127.0.0.1:9091/api/v1/alerts",
445					},
446				},
447				Dropped: []AlertManager{
448					{
449						URL: "http://127.0.0.1:9092/api/v1/alerts",
450					},
451				},
452			},
453		},
454
455		{
456			do:        doAlertManagers(),
457			reqMethod: "GET",
458			reqPath:   "/api/v1/alertmanagers",
459			inErr:     fmt.Errorf("some error"),
460			err:       fmt.Errorf("some error"),
461		},
462
463		{
464			do:        doTargets(),
465			reqMethod: "GET",
466			reqPath:   "/api/v1/targets",
467			inRes: map[string]interface{}{
468				"activeTargets": []map[string]interface{}{
469					{
470						"discoveredLabels": map[string]string{
471							"__address__":      "127.0.0.1:9090",
472							"__metrics_path__": "/metrics",
473							"__scheme__":       "http",
474							"job":              "prometheus",
475						},
476						"labels": map[string]string{
477							"instance": "127.0.0.1:9090",
478							"job":      "prometheus",
479						},
480						"scrapeUrl":  "http://127.0.0.1:9090",
481						"lastError":  "error while scraping target",
482						"lastScrape": testTime.UTC().Format(time.RFC3339Nano),
483						"health":     "up",
484					},
485				},
486				"droppedTargets": []map[string]interface{}{
487					{
488						"discoveredLabels": map[string]string{
489							"__address__":      "127.0.0.1:9100",
490							"__metrics_path__": "/metrics",
491							"__scheme__":       "http",
492							"job":              "node",
493						},
494					},
495				},
496			},
497			res: TargetsResult{
498				Active: []ActiveTarget{
499					{
500						DiscoveredLabels: model.LabelSet{
501							"__address__":      "127.0.0.1:9090",
502							"__metrics_path__": "/metrics",
503							"__scheme__":       "http",
504							"job":              "prometheus",
505						},
506						Labels: model.LabelSet{
507							"instance": "127.0.0.1:9090",
508							"job":      "prometheus",
509						},
510						ScrapeURL:  "http://127.0.0.1:9090",
511						LastError:  "error while scraping target",
512						LastScrape: testTime.UTC(),
513						Health:     HealthGood,
514					},
515				},
516				Dropped: []DroppedTarget{
517					{
518						DiscoveredLabels: model.LabelSet{
519							"__address__":      "127.0.0.1:9100",
520							"__metrics_path__": "/metrics",
521							"__scheme__":       "http",
522							"job":              "node",
523						},
524					},
525				},
526			},
527		},
528
529		{
530			do:        doTargets(),
531			reqMethod: "GET",
532			reqPath:   "/api/v1/targets",
533			inErr:     fmt.Errorf("some error"),
534			err:       fmt.Errorf("some error"),
535		},
536	}
537
538	var tests []apiTest
539	tests = append(tests, queryTests...)
540
541	for i, test := range tests {
542		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
543			client.curTest = test
544
545			res, err := test.do()
546
547			if test.err != nil {
548				if err == nil {
549					t.Fatalf("expected error %q but got none", test.err)
550				}
551				if err.Error() != test.err.Error() {
552					t.Errorf("unexpected error: want %s, got %s", test.err, err)
553				}
554				if apiErr, ok := err.(*Error); ok {
555					if apiErr.Detail != test.inRes {
556						t.Errorf("%q should be %q", apiErr.Detail, test.inRes)
557					}
558				}
559				return
560			}
561			if err != nil {
562				t.Fatalf("unexpected error: %s", err)
563			}
564
565			if !reflect.DeepEqual(res, test.res) {
566				t.Errorf("unexpected result: want %v, got %v", test.res, res)
567			}
568		})
569	}
570}
571
572type testClient struct {
573	*testing.T
574
575	ch  chan apiClientTest
576	req *http.Request
577}
578
579type apiClientTest struct {
580	code         int
581	response     interface{}
582	expectedBody string
583	expectedErr  *Error
584}
585
586func (c *testClient) URL(ep string, args map[string]string) *url.URL {
587	return nil
588}
589
590func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
591	if ctx == nil {
592		c.Fatalf("context was not passed down")
593	}
594	if req != c.req {
595		c.Fatalf("request was not passed down")
596	}
597
598	test := <-c.ch
599
600	var b []byte
601	var err error
602
603	switch v := test.response.(type) {
604	case string:
605		b = []byte(v)
606	default:
607		b, err = json.Marshal(v)
608		if err != nil {
609			c.Fatal(err)
610		}
611	}
612
613	resp := &http.Response{
614		StatusCode: test.code,
615	}
616
617	return resp, b, nil
618}
619
620func TestAPIClientDo(t *testing.T) {
621	tests := []apiClientTest{
622		{
623			code: statusAPIError,
624			response: &apiResponse{
625				Status:    "error",
626				Data:      json.RawMessage(`null`),
627				ErrorType: ErrBadData,
628				Error:     "failed",
629			},
630			expectedErr: &Error{
631				Type: ErrBadData,
632				Msg:  "failed",
633			},
634			expectedBody: `null`,
635		},
636		{
637			code: statusAPIError,
638			response: &apiResponse{
639				Status:    "error",
640				Data:      json.RawMessage(`"test"`),
641				ErrorType: ErrTimeout,
642				Error:     "timed out",
643			},
644			expectedErr: &Error{
645				Type: ErrTimeout,
646				Msg:  "timed out",
647			},
648			expectedBody: `test`,
649		},
650		{
651			code:     http.StatusInternalServerError,
652			response: "500 error details",
653			expectedErr: &Error{
654				Type:   ErrServer,
655				Msg:    "server error: 500",
656				Detail: "500 error details",
657			},
658		},
659		{
660			code:     http.StatusNotFound,
661			response: "404 error details",
662			expectedErr: &Error{
663				Type:   ErrClient,
664				Msg:    "client error: 404",
665				Detail: "404 error details",
666			},
667		},
668		{
669			code: http.StatusBadRequest,
670			response: &apiResponse{
671				Status:    "error",
672				Data:      json.RawMessage(`null`),
673				ErrorType: ErrBadData,
674				Error:     "end timestamp must not be before start time",
675			},
676			expectedErr: &Error{
677				Type: ErrBadData,
678				Msg:  "end timestamp must not be before start time",
679			},
680		},
681		{
682			code:     statusAPIError,
683			response: "bad json",
684			expectedErr: &Error{
685				Type: ErrBadResponse,
686				Msg:  "invalid character 'b' looking for beginning of value",
687			},
688		},
689		{
690			code: statusAPIError,
691			response: &apiResponse{
692				Status: "success",
693				Data:   json.RawMessage(`"test"`),
694			},
695			expectedErr: &Error{
696				Type: ErrBadResponse,
697				Msg:  "inconsistent body for response code",
698			},
699		},
700		{
701			code: statusAPIError,
702			response: &apiResponse{
703				Status:    "success",
704				Data:      json.RawMessage(`"test"`),
705				ErrorType: ErrTimeout,
706				Error:     "timed out",
707			},
708			expectedErr: &Error{
709				Type: ErrBadResponse,
710				Msg:  "inconsistent body for response code",
711			},
712		},
713		{
714			code: http.StatusOK,
715			response: &apiResponse{
716				Status:    "error",
717				Data:      json.RawMessage(`"test"`),
718				ErrorType: ErrTimeout,
719				Error:     "timed out",
720			},
721			expectedErr: &Error{
722				Type: ErrBadResponse,
723				Msg:  "inconsistent body for response code",
724			},
725		},
726	}
727
728	tc := &testClient{
729		T:   t,
730		ch:  make(chan apiClientTest, 1),
731		req: &http.Request{},
732	}
733	client := &apiClient{tc}
734
735	for i, test := range tests {
736		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
737
738			tc.ch <- test
739
740			_, body, err := client.Do(context.Background(), tc.req)
741
742			if test.expectedErr != nil {
743				if err == nil {
744					t.Fatalf("expected error %q but got none", test.expectedErr)
745				}
746				if test.expectedErr.Error() != err.Error() {
747					t.Errorf("unexpected error: want %q, got %q", test.expectedErr, err)
748				}
749				if test.expectedErr.Detail != "" {
750					apiErr := err.(*Error)
751					if apiErr.Detail != test.expectedErr.Detail {
752						t.Errorf("unexpected error details: want %q, got %q", test.expectedErr.Detail, apiErr.Detail)
753					}
754				}
755				return
756			}
757			if err != nil {
758				t.Fatalf("unexpeceted error %s", err)
759			}
760
761			want, got := test.expectedBody, string(body)
762			if want != got {
763				t.Errorf("unexpected body: want %q, got %q", want, got)
764			}
765		})
766
767	}
768}
769