809 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			809 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Copyright 2016 The Prometheus Authors
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package v1
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"net/url"
 | |
| 	"reflect"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/gogo/protobuf/proto"
 | |
| 	"github.com/golang/snappy"
 | |
| 	"github.com/prometheus/common/model"
 | |
| 	"github.com/prometheus/common/route"
 | |
| 
 | |
| 	"github.com/prometheus/prometheus/config"
 | |
| 	"github.com/prometheus/prometheus/pkg/labels"
 | |
| 	"github.com/prometheus/prometheus/pkg/timestamp"
 | |
| 	"github.com/prometheus/prometheus/prompb"
 | |
| 	"github.com/prometheus/prometheus/promql"
 | |
| 	"github.com/prometheus/prometheus/retrieval"
 | |
| 	"github.com/prometheus/prometheus/storage/remote"
 | |
| )
 | |
| 
 | |
| type targetRetrieverFunc func() []*retrieval.Target
 | |
| 
 | |
| func (f targetRetrieverFunc) Targets() []*retrieval.Target {
 | |
| 	return f()
 | |
| }
 | |
| 
 | |
| type alertmanagerRetrieverFunc func() []*url.URL
 | |
| 
 | |
| func (f alertmanagerRetrieverFunc) Alertmanagers() []*url.URL {
 | |
| 	return f()
 | |
| }
 | |
| 
 | |
| var samplePrometheusCfg = config.Config{
 | |
| 	GlobalConfig:       config.GlobalConfig{},
 | |
| 	AlertingConfig:     config.AlertingConfig{},
 | |
| 	RuleFiles:          []string{},
 | |
| 	ScrapeConfigs:      []*config.ScrapeConfig{},
 | |
| 	RemoteWriteConfigs: []*config.RemoteWriteConfig{},
 | |
| 	RemoteReadConfigs:  []*config.RemoteReadConfig{},
 | |
| }
 | |
| 
 | |
| func TestEndpoints(t *testing.T) {
 | |
| 	suite, err := promql.NewTest(t, `
 | |
| 		load 1m
 | |
| 			test_metric1{foo="bar"} 0+100x100
 | |
| 			test_metric1{foo="boo"} 1+0x100
 | |
| 			test_metric2{foo="boo"} 1+0x100
 | |
| 	`)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer suite.Close()
 | |
| 
 | |
| 	if err := suite.Run(); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	now := time.Now()
 | |
| 
 | |
| 	tr := targetRetrieverFunc(func() []*retrieval.Target {
 | |
| 		return []*retrieval.Target{
 | |
| 			retrieval.NewTarget(
 | |
| 				labels.FromMap(map[string]string{
 | |
| 					model.SchemeLabel:      "http",
 | |
| 					model.AddressLabel:     "example.com:8080",
 | |
| 					model.MetricsPathLabel: "/metrics",
 | |
| 				}),
 | |
| 				nil,
 | |
| 				url.Values{},
 | |
| 			),
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	ar := alertmanagerRetrieverFunc(func() []*url.URL {
 | |
| 		return []*url.URL{{
 | |
| 			Scheme: "http",
 | |
| 			Host:   "alertmanager.example.com:8080",
 | |
| 			Path:   "/api/v1/alerts",
 | |
| 		}}
 | |
| 	})
 | |
| 
 | |
| 	api := &API{
 | |
| 		Queryable:             suite.Storage(),
 | |
| 		QueryEngine:           suite.QueryEngine(),
 | |
| 		targetRetriever:       tr,
 | |
| 		alertmanagerRetriever: ar,
 | |
| 		now:    func() time.Time { return now },
 | |
| 		config: func() config.Config { return samplePrometheusCfg },
 | |
| 		ready:  func(f http.HandlerFunc) http.HandlerFunc { return f },
 | |
| 	}
 | |
| 
 | |
| 	start := time.Unix(0, 0)
 | |
| 
 | |
| 	var tests = []struct {
 | |
| 		endpoint apiFunc
 | |
| 		params   map[string]string
 | |
| 		query    url.Values
 | |
| 		response interface{}
 | |
| 		errType  errorType
 | |
| 	}{
 | |
| 		{
 | |
| 			endpoint: api.query,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"2"},
 | |
| 				"time":  []string{"123.4"},
 | |
| 			},
 | |
| 			response: &queryData{
 | |
| 				ResultType: promql.ValueTypeScalar,
 | |
| 				Result: promql.Scalar{
 | |
| 					V: 2,
 | |
| 					T: timestamp.FromTime(start.Add(123*time.Second + 400*time.Millisecond)),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.query,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"0.333"},
 | |
| 				"time":  []string{"1970-01-01T00:02:03Z"},
 | |
| 			},
 | |
| 			response: &queryData{
 | |
| 				ResultType: promql.ValueTypeScalar,
 | |
| 				Result: promql.Scalar{
 | |
| 					V: 0.333,
 | |
| 					T: timestamp.FromTime(start.Add(123 * time.Second)),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.query,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"0.333"},
 | |
| 				"time":  []string{"1970-01-01T01:02:03+01:00"},
 | |
| 			},
 | |
| 			response: &queryData{
 | |
| 				ResultType: promql.ValueTypeScalar,
 | |
| 				Result: promql.Scalar{
 | |
| 					V: 0.333,
 | |
| 					T: timestamp.FromTime(start.Add(123 * time.Second)),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.query,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"0.333"},
 | |
| 			},
 | |
| 			response: &queryData{
 | |
| 				ResultType: promql.ValueTypeScalar,
 | |
| 				Result: promql.Scalar{
 | |
| 					V: 0.333,
 | |
| 					T: timestamp.FromTime(now),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"start": []string{"0"},
 | |
| 				"end":   []string{"2"},
 | |
| 				"step":  []string{"1"},
 | |
| 			},
 | |
| 			response: &queryData{
 | |
| 				ResultType: promql.ValueTypeMatrix,
 | |
| 				Result: promql.Matrix{
 | |
| 					promql.Series{
 | |
| 						Points: []promql.Point{
 | |
| 							{V: 0, T: timestamp.FromTime(start)},
 | |
| 							{V: 1, T: timestamp.FromTime(start.Add(1 * time.Second))},
 | |
| 							{V: 2, T: timestamp.FromTime(start.Add(2 * time.Second))},
 | |
| 						},
 | |
| 						Metric: nil,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		// Missing query params in range queries.
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"end":   []string{"2"},
 | |
| 				"step":  []string{"1"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"start": []string{"0"},
 | |
| 				"step":  []string{"1"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"start": []string{"0"},
 | |
| 				"end":   []string{"2"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		// Bad query expression.
 | |
| 		{
 | |
| 			endpoint: api.query,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"invalid][query"},
 | |
| 				"time":  []string{"1970-01-01T01:02:03+01:00"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"invalid][query"},
 | |
| 				"start": []string{"0"},
 | |
| 				"end":   []string{"100"},
 | |
| 				"step":  []string{"1"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		// Invalid step.
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"start": []string{"1"},
 | |
| 				"end":   []string{"2"},
 | |
| 				"step":  []string{"0"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		// Start after end.
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"start": []string{"2"},
 | |
| 				"end":   []string{"1"},
 | |
| 				"step":  []string{"1"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		// Start overflows int64 internally.
 | |
| 		{
 | |
| 			endpoint: api.queryRange,
 | |
| 			query: url.Values{
 | |
| 				"query": []string{"time()"},
 | |
| 				"start": []string{"148966367200.372"},
 | |
| 				"end":   []string{"1489667272.372"},
 | |
| 				"step":  []string{"1"},
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.labelValues,
 | |
| 			params: map[string]string{
 | |
| 				"name": "__name__",
 | |
| 			},
 | |
| 			response: []string{
 | |
| 				"test_metric1",
 | |
| 				"test_metric2",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.labelValues,
 | |
| 			params: map[string]string{
 | |
| 				"name": "foo",
 | |
| 			},
 | |
| 			response: []string{
 | |
| 				"bar",
 | |
| 				"boo",
 | |
| 			},
 | |
| 		},
 | |
| 		// Bad name parameter.
 | |
| 		{
 | |
| 			endpoint: api.labelValues,
 | |
| 			params: map[string]string{
 | |
| 				"name": "not!!!allowed",
 | |
| 			},
 | |
| 			errType: errorBadData,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric2", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric1{foo=~".+o"}`},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric1", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric1{foo=~".+o$"}`, `test_metric1{foo=~".+o"}`},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric1", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric1{foo=~".+o"}`, `none`},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric1", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		// Start and end before series starts.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 				"start":   []string{"-2"},
 | |
| 				"end":     []string{"-1"},
 | |
| 			},
 | |
| 			response: []labels.Labels{},
 | |
| 		},
 | |
| 		// Start and end after series ends.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 				"start":   []string{"100000"},
 | |
| 				"end":     []string{"100001"},
 | |
| 			},
 | |
| 			response: []labels.Labels{},
 | |
| 		},
 | |
| 		// Start before series starts, end after series ends.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 				"start":   []string{"-1"},
 | |
| 				"end":     []string{"100000"},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric2", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		// Start and end within series.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 				"start":   []string{"1"},
 | |
| 				"end":     []string{"100"},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric2", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		// Start within series, end after.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 				"start":   []string{"1"},
 | |
| 				"end":     []string{"100000"},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric2", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		// Start before series, end within series.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			query: url.Values{
 | |
| 				"match[]": []string{`test_metric2`},
 | |
| 				"start":   []string{"-1"},
 | |
| 				"end":     []string{"1"},
 | |
| 			},
 | |
| 			response: []labels.Labels{
 | |
| 				labels.FromStrings("__name__", "test_metric2", "foo", "boo"),
 | |
| 			},
 | |
| 		},
 | |
| 		// Missing match[] query params in series requests.
 | |
| 		{
 | |
| 			endpoint: api.series,
 | |
| 			errType:  errorBadData,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.dropSeries,
 | |
| 			errType:  errorInternal,
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.targets,
 | |
| 			response: &TargetDiscovery{
 | |
| 				ActiveTargets: []*Target{
 | |
| 					{
 | |
| 						DiscoveredLabels: map[string]string{},
 | |
| 						Labels:           map[string]string{},
 | |
| 						ScrapeURL:        "http://example.com:8080/metrics",
 | |
| 						Health:           "unknown",
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.alertmanagers,
 | |
| 			response: &AlertmanagerDiscovery{
 | |
| 				ActiveAlertmanagers: []*AlertmanagerTarget{
 | |
| 					{
 | |
| 						URL: "http://alertmanager.example.com:8080/api/v1/alerts",
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			endpoint: api.serveConfig,
 | |
| 			response: &prometheusConfig{
 | |
| 				YAML: samplePrometheusCfg.String(),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	methods := func(f apiFunc) []string {
 | |
| 		fp := reflect.ValueOf(f).Pointer()
 | |
| 		if fp == reflect.ValueOf(api.query).Pointer() || fp == reflect.ValueOf(api.queryRange).Pointer() {
 | |
| 			return []string{http.MethodGet, http.MethodPost}
 | |
| 		}
 | |
| 		return []string{http.MethodGet}
 | |
| 	}
 | |
| 
 | |
| 	request := func(m string, q url.Values) (*http.Request, error) {
 | |
| 		if m == http.MethodPost {
 | |
| 			r, err := http.NewRequest(m, "http://example.com", strings.NewReader(q.Encode()))
 | |
| 			r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 | |
| 			return r, err
 | |
| 		}
 | |
| 		return http.NewRequest(m, fmt.Sprintf("http://example.com?%s", q.Encode()), nil)
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range tests {
 | |
| 		for _, method := range methods(test.endpoint) {
 | |
| 			// Build a context with the correct request params.
 | |
| 			ctx := context.Background()
 | |
| 			for p, v := range test.params {
 | |
| 				ctx = route.WithParam(ctx, p, v)
 | |
| 			}
 | |
| 			t.Logf("run %s\t%q", method, test.query.Encode())
 | |
| 
 | |
| 			req, err := request(method, test.query)
 | |
| 			if err != nil {
 | |
| 				t.Fatal(err)
 | |
| 			}
 | |
| 			resp, apiErr := test.endpoint(req.WithContext(ctx))
 | |
| 			if apiErr != nil {
 | |
| 				if test.errType == errorNone {
 | |
| 					t.Fatalf("Unexpected error: %s", apiErr)
 | |
| 				}
 | |
| 				if test.errType != apiErr.typ {
 | |
| 					t.Fatalf("Expected error of type %q but got type %q", test.errType, apiErr.typ)
 | |
| 				}
 | |
| 				continue
 | |
| 			}
 | |
| 			if apiErr == nil && test.errType != errorNone {
 | |
| 				t.Fatalf("Expected error of type %q but got none", test.errType)
 | |
| 			}
 | |
| 			if !reflect.DeepEqual(resp, test.response) {
 | |
| 				t.Fatalf("Response does not match, expected:\n%+v\ngot:\n%+v", test.response, resp)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestReadEndpoint(t *testing.T) {
 | |
| 	suite, err := promql.NewTest(t, `
 | |
| 		load 1m
 | |
| 			test_metric1{foo="bar",baz="qux"} 1
 | |
| 	`)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer suite.Close()
 | |
| 
 | |
| 	if err := suite.Run(); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	api := &API{
 | |
| 		Queryable:   suite.Storage(),
 | |
| 		QueryEngine: suite.QueryEngine(),
 | |
| 		config: func() config.Config {
 | |
| 			return config.Config{
 | |
| 				GlobalConfig: config.GlobalConfig{
 | |
| 					ExternalLabels: model.LabelSet{
 | |
| 						"baz": "a",
 | |
| 						"b":   "c",
 | |
| 						"d":   "e",
 | |
| 					},
 | |
| 				},
 | |
| 			}
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// Encode the request.
 | |
| 	matcher1, err := labels.NewMatcher(labels.MatchEqual, "__name__", "test_metric1")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	matcher2, err := labels.NewMatcher(labels.MatchEqual, "d", "e")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	query, err := remote.ToQuery(0, 1, []*labels.Matcher{matcher1, matcher2})
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	req := &prompb.ReadRequest{Queries: []*prompb.Query{query}}
 | |
| 	data, err := proto.Marshal(req)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	compressed := snappy.Encode(nil, data)
 | |
| 	request, err := http.NewRequest("POST", "", bytes.NewBuffer(compressed))
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	recorder := httptest.NewRecorder()
 | |
| 	api.remoteRead(recorder, request)
 | |
| 
 | |
| 	// Decode the response.
 | |
| 	compressed, err = ioutil.ReadAll(recorder.Result().Body)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	uncompressed, err := snappy.Decode(nil, compressed)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	var resp prompb.ReadResponse
 | |
| 	err = proto.Unmarshal(uncompressed, &resp)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	if len(resp.Results) != 1 {
 | |
| 		t.Fatalf("Expected 1 result, got %d", len(resp.Results))
 | |
| 	}
 | |
| 
 | |
| 	result := resp.Results[0]
 | |
| 	expected := &prompb.QueryResult{
 | |
| 		Timeseries: []*prompb.TimeSeries{
 | |
| 			{
 | |
| 				Labels: []*prompb.Label{
 | |
| 					{Name: "__name__", Value: "test_metric1"},
 | |
| 					{Name: "b", Value: "c"},
 | |
| 					{Name: "baz", Value: "qux"},
 | |
| 					{Name: "d", Value: "e"},
 | |
| 					{Name: "foo", Value: "bar"},
 | |
| 				},
 | |
| 				Samples: []*prompb.Sample{{Value: 1, Timestamp: 0}},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(result, expected) {
 | |
| 		t.Fatalf("Expected response \n%v\n but got \n%v\n", result, expected)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRespondSuccess(t *testing.T) {
 | |
| 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		respond(w, "test")
 | |
| 	}))
 | |
| 	defer s.Close()
 | |
| 
 | |
| 	resp, err := http.Get(s.URL)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error on test request: %s", err)
 | |
| 	}
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	defer resp.Body.Close()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error reading response body: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode != 200 {
 | |
| 		t.Fatalf("Return code %d expected in success response but got %d", 200, resp.StatusCode)
 | |
| 	}
 | |
| 	if h := resp.Header.Get("Content-Type"); h != "application/json" {
 | |
| 		t.Fatalf("Expected Content-Type %q but got %q", "application/json", h)
 | |
| 	}
 | |
| 
 | |
| 	var res response
 | |
| 	if err = json.Unmarshal([]byte(body), &res); err != nil {
 | |
| 		t.Fatalf("Error unmarshaling JSON body: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	exp := &response{
 | |
| 		Status: statusSuccess,
 | |
| 		Data:   "test",
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(&res, exp) {
 | |
| 		t.Fatalf("Expected response \n%v\n but got \n%v\n", res, exp)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRespondError(t *testing.T) {
 | |
| 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		respondError(w, &apiError{errorTimeout, errors.New("message")}, "test")
 | |
| 	}))
 | |
| 	defer s.Close()
 | |
| 
 | |
| 	resp, err := http.Get(s.URL)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error on test request: %s", err)
 | |
| 	}
 | |
| 	body, err := ioutil.ReadAll(resp.Body)
 | |
| 	defer resp.Body.Close()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error reading response body: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	if want, have := http.StatusServiceUnavailable, resp.StatusCode; want != have {
 | |
| 		t.Fatalf("Return code %d expected in error response but got %d", want, have)
 | |
| 	}
 | |
| 	if h := resp.Header.Get("Content-Type"); h != "application/json" {
 | |
| 		t.Fatalf("Expected Content-Type %q but got %q", "application/json", h)
 | |
| 	}
 | |
| 
 | |
| 	var res response
 | |
| 	if err = json.Unmarshal([]byte(body), &res); err != nil {
 | |
| 		t.Fatalf("Error unmarshaling JSON body: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	exp := &response{
 | |
| 		Status:    statusError,
 | |
| 		Data:      "test",
 | |
| 		ErrorType: errorTimeout,
 | |
| 		Error:     "message",
 | |
| 	}
 | |
| 	if !reflect.DeepEqual(&res, exp) {
 | |
| 		t.Fatalf("Expected response \n%v\n but got \n%v\n", res, exp)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParseTime(t *testing.T) {
 | |
| 	ts, err := time.Parse(time.RFC3339Nano, "2015-06-03T13:21:58.555Z")
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 
 | |
| 	var tests = []struct {
 | |
| 		input  string
 | |
| 		fail   bool
 | |
| 		result time.Time
 | |
| 	}{
 | |
| 		{
 | |
| 			input: "",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			input: "abc",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			input: "30s",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			input:  "123",
 | |
| 			result: time.Unix(123, 0),
 | |
| 		}, {
 | |
| 			input:  "123.123",
 | |
| 			result: time.Unix(123, 123000000),
 | |
| 		}, {
 | |
| 			input:  "2015-06-03T13:21:58.555Z",
 | |
| 			result: ts,
 | |
| 		}, {
 | |
| 			input:  "2015-06-03T14:21:58.555+01:00",
 | |
| 			result: ts,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range tests {
 | |
| 		ts, err := parseTime(test.input)
 | |
| 		if err != nil && !test.fail {
 | |
| 			t.Errorf("Unexpected error for %q: %s", test.input, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		if err == nil && test.fail {
 | |
| 			t.Errorf("Expected error for %q but got none", test.input)
 | |
| 			continue
 | |
| 		}
 | |
| 		if !test.fail && !ts.Equal(test.result) {
 | |
| 			t.Errorf("Expected time %v for input %q but got %v", test.result, test.input, ts)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParseDuration(t *testing.T) {
 | |
| 	var tests = []struct {
 | |
| 		input  string
 | |
| 		fail   bool
 | |
| 		result time.Duration
 | |
| 	}{
 | |
| 		{
 | |
| 			input: "",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			input: "abc",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			input: "2015-06-03T13:21:58.555Z",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			// Internal int64 overflow.
 | |
| 			input: "-148966367200.372",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			// Internal int64 overflow.
 | |
| 			input: "148966367200.372",
 | |
| 			fail:  true,
 | |
| 		}, {
 | |
| 			input:  "123",
 | |
| 			result: 123 * time.Second,
 | |
| 		}, {
 | |
| 			input:  "123.333",
 | |
| 			result: 123*time.Second + 333*time.Millisecond,
 | |
| 		}, {
 | |
| 			input:  "15s",
 | |
| 			result: 15 * time.Second,
 | |
| 		}, {
 | |
| 			input:  "5m",
 | |
| 			result: 5 * time.Minute,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, test := range tests {
 | |
| 		d, err := parseDuration(test.input)
 | |
| 		if err != nil && !test.fail {
 | |
| 			t.Errorf("Unexpected error for %q: %s", test.input, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		if err == nil && test.fail {
 | |
| 			t.Errorf("Expected error for %q but got none", test.input)
 | |
| 			continue
 | |
| 		}
 | |
| 		if !test.fail && d != test.result {
 | |
| 			t.Errorf("Expected duration %v for input %q but got %v", test.result, test.input, d)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestOptionsMethod(t *testing.T) {
 | |
| 	r := route.New()
 | |
| 	api := &API{ready: func(f http.HandlerFunc) http.HandlerFunc { return f }}
 | |
| 	api.Register(r)
 | |
| 
 | |
| 	s := httptest.NewServer(r)
 | |
| 	defer s.Close()
 | |
| 
 | |
| 	req, err := http.NewRequest("OPTIONS", s.URL+"/any_path", nil)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error creating OPTIONS request: %s", err)
 | |
| 	}
 | |
| 	client := &http.Client{}
 | |
| 	resp, err := client.Do(req)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error executing OPTIONS request: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode != http.StatusNoContent {
 | |
| 		t.Fatalf("Expected status %d, got %d", http.StatusNoContent, resp.StatusCode)
 | |
| 	}
 | |
| 
 | |
| 	for h, v := range corsHeaders {
 | |
| 		if resp.Header.Get(h) != v {
 | |
| 			t.Fatalf("Expected %q for header %q, got %q", v, h, resp.Header.Get(h))
 | |
| 		}
 | |
| 	}
 | |
| }
 |