mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-10 07:34:04 -08:00
ddd46de6f4
Fix race by properly locking access to scrape pools. Use separate mutex for information needed by UI so that UI isn't blocked when targets are being updated.
856 lines
20 KiB
Go
856 lines
20 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/scrape"
|
|
"github.com/prometheus/prometheus/storage/remote"
|
|
)
|
|
|
|
type testTargetRetriever struct{}
|
|
|
|
func (t testTargetRetriever) TargetsActive() []*scrape.Target {
|
|
return []*scrape.Target{
|
|
scrape.NewTarget(
|
|
labels.FromMap(map[string]string{
|
|
model.SchemeLabel: "http",
|
|
model.AddressLabel: "example.com:8080",
|
|
model.MetricsPathLabel: "/metrics",
|
|
}),
|
|
nil,
|
|
url.Values{},
|
|
),
|
|
}
|
|
}
|
|
func (t testTargetRetriever) TargetsDropped() []*scrape.Target {
|
|
return []*scrape.Target{
|
|
scrape.NewTarget(
|
|
nil,
|
|
labels.FromMap(map[string]string{
|
|
model.AddressLabel: "http://dropped.example.com:9115",
|
|
model.MetricsPathLabel: "/probe",
|
|
model.SchemeLabel: "http",
|
|
model.JobLabel: "blackbox",
|
|
}),
|
|
url.Values{},
|
|
),
|
|
}
|
|
}
|
|
|
|
type testAlertmanagerRetriever struct{}
|
|
|
|
func (t testAlertmanagerRetriever) Alertmanagers() []*url.URL {
|
|
return []*url.URL{
|
|
{
|
|
Scheme: "http",
|
|
Host: "alertmanager.example.com:8080",
|
|
Path: "/api/v1/alerts",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (t testAlertmanagerRetriever) DroppedAlertmanagers() []*url.URL {
|
|
return []*url.URL{
|
|
{
|
|
Scheme: "http",
|
|
Host: "dropped.alertmanager.example.com:8080",
|
|
Path: "/api/v1/alerts",
|
|
},
|
|
}
|
|
}
|
|
|
|
var samplePrometheusCfg = config.Config{
|
|
GlobalConfig: config.GlobalConfig{},
|
|
AlertingConfig: config.AlertingConfig{},
|
|
RuleFiles: []string{},
|
|
ScrapeConfigs: []*config.ScrapeConfig{},
|
|
RemoteWriteConfigs: []*config.RemoteWriteConfig{},
|
|
RemoteReadConfigs: []*config.RemoteReadConfig{},
|
|
}
|
|
|
|
var sampleFlagMap = map[string]string{
|
|
"flag1": "value1",
|
|
"flag2": "value2",
|
|
}
|
|
|
|
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()
|
|
|
|
var tr testTargetRetriever
|
|
|
|
var ar testAlertmanagerRetriever
|
|
|
|
api := &API{
|
|
Queryable: suite.Storage(),
|
|
QueryEngine: suite.QueryEngine(),
|
|
targetRetriever: tr,
|
|
alertmanagerRetriever: ar,
|
|
now: func() time.Time { return now },
|
|
config: func() config.Config { return samplePrometheusCfg },
|
|
flagsMap: sampleFlagMap,
|
|
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",
|
|
},
|
|
},
|
|
DroppedTargets: []*DroppedTarget{
|
|
{
|
|
DiscoveredLabels: map[string]string{
|
|
"__address__": "http://dropped.example.com:9115",
|
|
"__metrics_path__": "/probe",
|
|
"__scheme__": "http",
|
|
"job": "blackbox",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
endpoint: api.alertmanagers,
|
|
response: &AlertmanagerDiscovery{
|
|
ActiveAlertmanagers: []*AlertmanagerTarget{
|
|
{
|
|
URL: "http://alertmanager.example.com:8080/api/v1/alerts",
|
|
},
|
|
},
|
|
DroppedAlertmanagers: []*AlertmanagerTarget{
|
|
{
|
|
URL: "http://dropped.alertmanager.example.com:8080/api/v1/alerts",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
endpoint: api.serveConfig,
|
|
response: &prometheusConfig{
|
|
YAML: samplePrometheusCfg.String(),
|
|
},
|
|
},
|
|
{
|
|
endpoint: api.serveFlags,
|
|
response: sampleFlagMap,
|
|
},
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|
|
}
|