Skip to content

Commit 014e2df

Browse files
committed
Add unit tests
Signed-off-by: rustyclock <rustyclock@protonmail.com>
1 parent a6b9654 commit 014e2df

File tree

8 files changed

+311
-13
lines changed

8 files changed

+311
-13
lines changed

cmd/main.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"encoding/json"
1919
"net/http"
2020
"os"
21-
"time"
2221

2322
"github.com/go-kit/kit/log"
2423
"github.com/go-kit/kit/log/level"
@@ -78,7 +77,7 @@ func Run() {
7877

7978
func probeHandler(w http.ResponseWriter, r *http.Request, logger log.Logger, config config.Config) {
8079

81-
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Global.TimeoutSeconds*float64(time.Second)))
80+
ctx, cancel := context.WithCancel(r.Context())
8281
defer cancel()
8382
r = r.WithContext(ctx)
8483

cmd/main_test.go

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright 2020 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+
package cmd
15+
16+
import (
17+
"encoding/base64"
18+
"io/ioutil"
19+
"net/http"
20+
"net/http/httptest"
21+
"testing"
22+
23+
"github.com/go-kit/kit/log"
24+
"github.com/prometheus-community/json_exporter/config"
25+
pconfig "github.com/prometheus/common/config"
26+
)
27+
28+
func TestFailIfSelfSignedCA(t *testing.T) {
29+
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30+
}))
31+
defer target.Close()
32+
33+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil)
34+
recorder := httptest.NewRecorder()
35+
probeHandler(recorder, req, log.NewNopLogger(), config.Config{})
36+
37+
resp := recorder.Result()
38+
body, _ := ioutil.ReadAll(resp.Body)
39+
40+
if resp.StatusCode != http.StatusServiceUnavailable {
41+
t.Fatalf("Fail if (not strict) selfsigned CA test fails unexpectedly, got %s", body)
42+
}
43+
}
44+
45+
func TestSucceedIfSelfSignedCA(t *testing.T) {
46+
c := config.Config{}
47+
c.HTTPClientConfig.TLSConfig.InsecureSkipVerify = true
48+
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
}))
50+
defer target.Close()
51+
52+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil)
53+
recorder := httptest.NewRecorder()
54+
probeHandler(recorder, req, log.NewNopLogger(), c)
55+
56+
resp := recorder.Result()
57+
body, _ := ioutil.ReadAll(resp.Body)
58+
59+
if resp.StatusCode != http.StatusOK {
60+
t.Fatalf("Succeed if (not strict) selfsigned CA test fails unexpectedly, got %s", body)
61+
}
62+
}
63+
64+
func TestFailIfTargetMissing(t *testing.T) {
65+
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
66+
recorder := httptest.NewRecorder()
67+
probeHandler(recorder, req, log.NewNopLogger(), config.Config{})
68+
69+
resp := recorder.Result()
70+
body, _ := ioutil.ReadAll(resp.Body)
71+
72+
if resp.StatusCode != http.StatusBadRequest {
73+
t.Fatalf("Fail if 'target' query parameter missing test fails unexpectedly, got %s", body)
74+
}
75+
}
76+
77+
func TestDefaultAcceptHeader(t *testing.T) {
78+
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
79+
expected := "application/json"
80+
if got := r.Header.Get("Accept"); got != expected {
81+
t.Errorf("Default 'Accept' header mismatch, got %s, expected: %s", got, expected)
82+
w.WriteHeader(http.StatusNotAcceptable)
83+
}
84+
}))
85+
defer target.Close()
86+
87+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil)
88+
recorder := httptest.NewRecorder()
89+
probeHandler(recorder, req, log.NewNopLogger(), config.Config{})
90+
91+
resp := recorder.Result()
92+
body, _ := ioutil.ReadAll(resp.Body)
93+
94+
if resp.StatusCode != http.StatusOK {
95+
t.Fatalf("Default 'Accept: application/json' header test fails unexpectedly, got %s", body)
96+
}
97+
}
98+
99+
func TestCorrectResponse(t *testing.T) {
100+
tests := []struct {
101+
ConfigFile string
102+
ServeFile string
103+
ResponseFile string
104+
ShouldSucceed bool
105+
}{
106+
{"../test/config/good.yml", "/serve/good.json", "../test/response/good.txt", true},
107+
{"../test/config/good.yml", "/serve/repeat-metric.json", "../test/response/good.txt", false},
108+
}
109+
110+
target := httptest.NewServer(http.FileServer(http.Dir("../test")))
111+
defer target.Close()
112+
113+
for i, test := range tests {
114+
c, err := config.LoadConfig(test.ConfigFile)
115+
if err != nil {
116+
t.Fatalf("Failed to load config file %s", test.ConfigFile)
117+
}
118+
119+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL+test.ServeFile, nil)
120+
recorder := httptest.NewRecorder()
121+
probeHandler(recorder, req, log.NewNopLogger(), c)
122+
123+
resp := recorder.Result()
124+
body, _ := ioutil.ReadAll(resp.Body)
125+
126+
expected, _ := ioutil.ReadFile(test.ResponseFile)
127+
128+
if test.ShouldSucceed && string(body) != string(expected) {
129+
t.Fatalf("Correct response validation test %d fails unexpectedly.\nGOT:\n%s\nEXPECTED:\n%s", i, body, expected)
130+
}
131+
}
132+
}
133+
134+
func TestBasicAuth(t *testing.T) {
135+
username := "myUser"
136+
password := "mySecretPassword"
137+
expected := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
138+
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
139+
if got := r.Header.Get("Authorization"); got != expected {
140+
t.Errorf("BasicAuth mismatch, got: %s, expected: %s", got, expected)
141+
w.WriteHeader(http.StatusUnauthorized)
142+
}
143+
}))
144+
defer target.Close()
145+
146+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil)
147+
recorder := httptest.NewRecorder()
148+
c := config.Config{}
149+
auth := &pconfig.BasicAuth{
150+
Username: username,
151+
Password: pconfig.Secret(password),
152+
}
153+
154+
c.HTTPClientConfig.BasicAuth = auth
155+
probeHandler(recorder, req, log.NewNopLogger(), c)
156+
157+
resp := recorder.Result()
158+
body, _ := ioutil.ReadAll(resp.Body)
159+
160+
if resp.StatusCode != http.StatusOK {
161+
t.Fatalf("BasicAuth test fails unexpectedly. Got: %s", body)
162+
}
163+
}
164+
165+
func TestBearerToken(t *testing.T) {
166+
token := "mySecretToken"
167+
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
168+
expected := "Bearer " + token
169+
if got := r.Header.Get("Authorization"); got != expected {
170+
t.Errorf("BearerToken mismatch, got: %s, expected: %s", got, expected)
171+
w.WriteHeader(http.StatusUnauthorized)
172+
}
173+
}))
174+
defer target.Close()
175+
176+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil)
177+
recorder := httptest.NewRecorder()
178+
c := config.Config{}
179+
180+
c.HTTPClientConfig.BearerToken = pconfig.Secret(token)
181+
probeHandler(recorder, req, log.NewNopLogger(), c)
182+
183+
resp := recorder.Result()
184+
body, _ := ioutil.ReadAll(resp.Body)
185+
186+
if resp.StatusCode != http.StatusOK {
187+
t.Fatalf("BearerToken test fails unexpectedly. Got: %s", body)
188+
}
189+
}
190+
191+
func TestHTTPHeaders(t *testing.T) {
192+
headers := map[string]string{
193+
"X-Dummy": "test",
194+
"User-Agent": "unsuspicious user",
195+
"Accept-Language": "en-US",
196+
}
197+
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
198+
for key, value := range headers {
199+
if got := r.Header.Get(key); got != value {
200+
t.Errorf("Unexpected value of header %q: expected %q, got %q", key, value, got)
201+
}
202+
}
203+
w.WriteHeader(http.StatusOK)
204+
}))
205+
defer target.Close()
206+
207+
req := httptest.NewRequest("GET", "http://example.com/foo"+"?target="+target.URL, nil)
208+
recorder := httptest.NewRecorder()
209+
c := config.Config{}
210+
c.Headers = headers
211+
212+
probeHandler(recorder, req, log.NewNopLogger(), c)
213+
214+
resp := recorder.Result()
215+
body, _ := ioutil.ReadAll(resp.Body)
216+
217+
if resp.StatusCode != http.StatusOK {
218+
t.Fatalf("Setting custom headers failed unexpectedly. Got: %s", body)
219+
}
220+
}

config/config.go

-8
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,9 @@ const (
4141
type Config struct {
4242
Headers map[string]string `yaml:"headers,omitempty"`
4343
Metrics []Metric `yaml:"metrics"`
44-
Global GlobalConfig `yaml:"global_config,omitempty"`
4544
HTTPClientConfig pconfig.HTTPClientConfig `yaml:"http_client_config,omitempty"`
4645
}
4746

48-
type GlobalConfig struct {
49-
TimeoutSeconds float64 `yaml:"timeout_seconds,omitempty"`
50-
}
51-
5247
func LoadConfig(configPath string) (Config, error) {
5348
var config Config
5449
data, err := ioutil.ReadFile(configPath)
@@ -70,9 +65,6 @@ func LoadConfig(configPath string) (Config, error) {
7065
config.Metrics[i].Help = config.Metrics[i].Name
7166
}
7267
}
73-
if config.Global.TimeoutSeconds == 0 {
74-
config.Global.TimeoutSeconds = 10
75-
}
7668

7769
return config, nil
7870
}

examples/config.yml

-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,3 @@ headers:
3131
# username: myuser
3232
# #password: veryverysecret
3333
# password_file: /tmp/mysecret.txt
34-
#
35-
# global_config:
36-
# timeout_seconds: 30 // defaults to 10

test/config/good.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
metrics:
3+
- name: example_global_value
4+
path: $.counter
5+
help: Example of a top-level global value scrape in the json
6+
labels:
7+
environment: beta # static label
8+
location: $.location # dynamic label
9+
10+
- name: example_value
11+
type: object
12+
help: Example of sub-level value scrapes from a json
13+
path: $.values[*]?(@.state == "ACTIVE")
14+
labels:
15+
environment: beta # static label
16+
id: $.id # dynamic label
17+
values:
18+
active: 1 # static value
19+
count: $.count # dynamic value
20+
boolean: $.some_boolean
21+

test/response/good.txt

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# HELP example_global_value Example of a top-level global value scrape in the json
2+
# TYPE example_global_value untyped
3+
example_global_value{environment="beta",location="mars"} 1234
4+
# HELP example_value_active Example of sub-level value scrapes from a json
5+
# TYPE example_value_active untyped
6+
example_value_active{environment="beta",id="id-A"} 1
7+
example_value_active{environment="beta",id="id-C"} 1
8+
# HELP example_value_boolean Example of sub-level value scrapes from a json
9+
# TYPE example_value_boolean untyped
10+
example_value_boolean{environment="beta",id="id-A"} 1
11+
example_value_boolean{environment="beta",id="id-C"} 0
12+
# HELP example_value_count Example of sub-level value scrapes from a json
13+
# TYPE example_value_count untyped
14+
example_value_count{environment="beta",id="id-A"} 1
15+
example_value_count{environment="beta",id="id-C"} 3

test/serve/good.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"counter": 1234,
3+
"values": [
4+
{
5+
"id": "id-A",
6+
"count": 1,
7+
"some_boolean": true,
8+
"state": "ACTIVE"
9+
},
10+
{
11+
"id": "id-B",
12+
"count": 2,
13+
"some_boolean": true,
14+
"state": "INACTIVE"
15+
},
16+
{
17+
"id": "id-C",
18+
"count": 3,
19+
"some_boolean": false,
20+
"state": "ACTIVE"
21+
}
22+
],
23+
"location": "mars"
24+
}

test/serve/repeat-metric.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"counter": 1234,
3+
"values": [
4+
{
5+
"id": "id-A",
6+
"count": 1,
7+
"some_boolean": true,
8+
"state": "ACTIVE"
9+
},
10+
{
11+
"id": "id-B",
12+
"count": 2,
13+
"some_boolean": true,
14+
"state": "INACTIVE"
15+
},
16+
{
17+
"id": "id-C",
18+
"count": 3,
19+
"some_boolean": true,
20+
"state": "ACTIVE"
21+
},
22+
{
23+
"id": "id-C",
24+
"count": 4,
25+
"some_boolean": false,
26+
"state": "ACTIVE"
27+
}
28+
],
29+
"location": "mars"
30+
}

0 commit comments

Comments
 (0)