master
go 345 lines 9.51 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package pihole
4
5 import (
6 "context"
7 "encoding/json"
8 "io"
9 "net/http"
10 "net/http/httptest"
11 "os"
12 "testing"
13
14 "github.com/netdata/netdata/go/plugins/pkg/web"
15 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
16
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19 )
20
21 var (
22 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
23 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
24
25 dataStatsSummary, _ = os.ReadFile("testdata/v6.0.5/stats_summary.json")
26 )
27
28 func Test_testDataIsValid(t *testing.T) {
29 for name, data := range map[string][]byte{
30 "dataConfigJSON": dataConfigJSON,
31 "dataConfigYAML": dataConfigYAML,
32 "dataStatsSummary": dataStatsSummary,
33 } {
34 require.NotNil(t, data, name)
35 }
36 }
37
38 func TestCollector_ConfigurationSerialize(t *testing.T) {
39 collecttest.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
40 }
41
42 func TestCollector_Init(t *testing.T) {
43 tests := map[string]struct {
44 wantFail bool
45 config Config
46 }{
47 "fails with default": {
48 wantFail: true,
49 config: New().Config,
50 },
51 "fail when URL not set": {
52 wantFail: true,
53 config: Config{
54 HTTPConfig: web.HTTPConfig{
55 RequestConfig: web.RequestConfig{URL: ""},
56 },
57 },
58 },
59 "fail when password not set": {
60 wantFail: true,
61 config: Config{
62 HTTPConfig: web.HTTPConfig{
63 RequestConfig: web.RequestConfig{URL: "http://127.0.0.1", Password: ""},
64 },
65 },
66 },
67 }
68
69 for name, test := range tests {
70 t.Run(name, func(t *testing.T) {
71 collr := New()
72 collr.Config = test.config
73
74 if test.wantFail {
75 assert.Error(t, collr.Init(context.Background()))
76 } else {
77 assert.NoError(t, collr.Init(context.Background()))
78 }
79 })
80 }
81 }
82
83 func TestCollector_Check(t *testing.T) {
84 tests := map[string]struct {
85 wantFail bool
86 prepare func(t *testing.T) (collr *Collector, cleanup func())
87 }{
88 "case success": {
89 wantFail: false,
90 prepare: caseSuccess,
91 },
92 "case wrong password": {
93 wantFail: true,
94 prepare: caseWrongPassword,
95 },
96 "case error on stats summary": {
97 wantFail: true,
98 prepare: caseErrOnStatsSummary,
99 },
100 }
101
102 for name, test := range tests {
103 t.Run(name, func(t *testing.T) {
104 collr, cleanup := test.prepare(t)
105 defer cleanup()
106
107 if test.wantFail {
108 assert.Error(t, collr.Check(context.Background()))
109 } else {
110 assert.NoError(t, collr.Check(context.Background()))
111 }
112 })
113 }
114 }
115
116 func TestCollector_Charts(t *testing.T) {
117 assert.NotNil(t, New().Charts())
118 }
119
120 func TestCollector_Collect(t *testing.T) {
121 tests := map[string]struct {
122 prepare func(t *testing.T) (collr *Collector, cleanup func())
123 wantMetrics map[string]int64
124 wantNumCharts int
125 }{
126 "case success": {
127 prepare: caseSuccess,
128 wantNumCharts: len(summaryCharts),
129 wantMetrics: map[string]int64{
130 "clients_active": 2,
131 "clients_total": 2,
132 "gravity_domains_being_blocked": 131270,
133 "gravity_last_update": 1741494842,
134 "gravity_last_update_seconds_ago": 107202,
135 "queries_blocked": 1,
136 "queries_cached": 204,
137 "queries_forwarded": 45,
138 "queries_frequency": 0,
139 "queries_percent_blocked": 1100,
140 "queries_replies_BLOB": 1,
141 "queries_replies_CNAME": 1,
142 "queries_replies_DNSSEC": 1,
143 "queries_replies_DOMAIN": 72,
144 "queries_replies_IP": 124,
145 "queries_replies_NODATA": 49,
146 "queries_replies_NONE": 1,
147 "queries_replies_NOTIMP": 1,
148 "queries_replies_NXDOMAIN": 4,
149 "queries_replies_OTHER": 1,
150 "queries_replies_REFUSED": 1,
151 "queries_replies_RRNAME": 1,
152 "queries_replies_SERVFAIL": 1,
153 "queries_replies_UNKNOWN": 1,
154 "queries_status_CACHE": 121,
155 "queries_status_CACHE_STALE": 83,
156 "queries_status_DBBUSY": 1,
157 "queries_status_DENYLIST": 1,
158 "queries_status_DENYLIST_CNAME": 1,
159 "queries_status_EXTERNAL_BLOCKED_EDE15": 1,
160 "queries_status_EXTERNAL_BLOCKED_IP": 1,
161 "queries_status_EXTERNAL_BLOCKED_NULL": 1,
162 "queries_status_EXTERNAL_BLOCKED_NXRA": 1,
163 "queries_status_FORWARDED": 45,
164 "queries_status_GRAVITY": 1,
165 "queries_status_GRAVITY_CNAME": 1,
166 "queries_status_IN_PROGRESS": 1,
167 "queries_status_REGEX": 1,
168 "queries_status_REGEX_CNAME": 1,
169 "queries_status_RETRIED": 1,
170 "queries_status_RETRIED_DNSSEC": 1,
171 "queries_status_SPECIAL_DOMAIN": 1,
172 "queries_status_UNKNOWN": 1,
173 "queries_total": 249,
174 "queries_types_A": 84,
175 "queries_types_AAAA": 84,
176 "queries_types_ANY": 1,
177 "queries_types_DNSKEY": 1,
178 "queries_types_DS": 1,
179 "queries_types_HTTPS": 1,
180 "queries_types_MX": 1,
181 "queries_types_NAPTR": 1,
182 "queries_types_NS": 1,
183 "queries_types_OTHER": 1,
184 "queries_types_PTR": 73,
185 "queries_types_RRSIG": 1,
186 "queries_types_SOA": 1,
187 "queries_types_SRV": 8,
188 "queries_types_SVCB": 1,
189 "queries_types_TXT": 1,
190 "queries_unique_domains": 29,
191 },
192 },
193 "case wrong password": {
194 prepare: caseWrongPassword,
195 wantNumCharts: len(summaryCharts),
196 },
197 "case error on stats summary": {
198 prepare: caseErrOnStatsSummary,
199 wantNumCharts: len(summaryCharts),
200 },
201 }
202
203 for name, test := range tests {
204 t.Run(name, func(t *testing.T) {
205 collr, cleanup := test.prepare(t)
206 defer cleanup()
207
208 mx := collr.Collect(context.Background())
209
210 copyBlockListLastUpdate(mx, test.wantMetrics)
211
212 require.Equal(t, test.wantMetrics, mx)
213
214 assert.Len(t, *collr.Charts(), test.wantNumCharts)
215 if len(test.wantMetrics) > 0 {
216 collecttest.TestMetricsHasAllChartsDims(t, collr.Charts(), mx)
217 }
218 })
219 }
220 }
221
222 func caseSuccess(t *testing.T) (collr *Collector, cleanup func()) {
223 collr, mock := New(), mockPiholeServer{password: "secret"}
224 srv := mock.newPiholeHTTPServer()
225 collr.URL = srv.URL
226 collr.Password = mock.password
227
228 require.NoError(t, collr.Init(context.Background()))
229
230 return collr, srv.Close
231 }
232
233 func caseWrongPassword(t *testing.T) (collr *Collector, cleanup func()) {
234 collr, mock := New(), mockPiholeServer{password: "secret"}
235 srv := mock.newPiholeHTTPServer()
236 collr.URL = srv.URL
237 collr.Password = mock.password + "!"
238
239 require.NoError(t, collr.Init(context.Background()))
240
241 return collr, srv.Close
242 }
243
244 func caseErrOnStatsSummary(t *testing.T) (collr *Collector, cleanup func()) {
245 collr, mock := New(), mockPiholeServer{password: "secret", errOnStatsSummary: true}
246 srv := mock.newPiholeHTTPServer()
247 collr.URL = srv.URL
248 collr.Password = mock.password
249
250 require.NoError(t, collr.Init(context.Background()))
251
252 return collr, srv.Close
253 }
254
255 type mockPiholeServer struct {
256 password string
257 errOnStatsSummary bool
258 }
259
260 func (m mockPiholeServer) newPiholeHTTPServer() *httptest.Server {
261 const (
262 ftlSid = "ftl-sid"
263 ftlCsrf = "ftl-csrf"
264 )
265
266 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
267 switch r.URL.Path {
268 case urlPathAPIAuth:
269 switch r.Method {
270 case http.MethodGet:
271 if r.Header.Get("X-FTL-SID") != ftlSid || r.Header.Get("X-FTL-CSRF") != ftlCsrf {
272 var resp ftlErrorResponse
273 resp.Error.Key = "unauthorized"
274 resp.Error.Message = "Unauthorized"
275 w.WriteHeader(http.StatusUnauthorized)
276 bs, _ := json.Marshal(resp)
277 _, _ = w.Write(bs)
278 return
279 }
280
281 var resp ftlAPIAuthResponse
282 resp.Session.Valid = true
283 resp.Session.Sid = ftlSid
284 resp.Session.Csrf = ftlCsrf
285 bs, _ := json.Marshal(resp)
286 _, _ = w.Write(bs)
287 case http.MethodPost:
288 bs, err := io.ReadAll(r.Body)
289 if err != nil {
290 w.WriteHeader(http.StatusBadRequest)
291 return
292 }
293
294 var pass struct {
295 Password string `json:"password"`
296 }
297 if err := json.Unmarshal(bs, &pass); err != nil {
298 w.WriteHeader(http.StatusBadRequest)
299 return
300 }
301
302 if pass.Password != m.password {
303 var resp ftlAPIAuthResponse
304 w.WriteHeader(http.StatusUnauthorized)
305 bs, _ := json.Marshal(resp)
306 _, _ = w.Write(bs)
307 return
308 }
309
310 var resp ftlAPIAuthResponse
311 resp.Session.Valid = true
312 resp.Session.Sid = ftlSid
313 resp.Session.Csrf = ftlCsrf
314 bs, _ = json.Marshal(resp)
315 _, _ = w.Write(bs)
316 }
317 case urlPathAPIStatsSummary:
318 if m.errOnStatsSummary {
319 w.WriteHeader(http.StatusBadRequest)
320 return
321 }
322 if r.Header.Get("X-FTL-SID") != ftlSid || r.Header.Get("X-FTL-CSRF") != ftlCsrf {
323 var resp ftlErrorResponse
324 resp.Error.Key = "unauthorized"
325 resp.Error.Message = "Unauthorized"
326 w.WriteHeader(http.StatusUnauthorized)
327 bs, _ := json.Marshal(resp)
328 _, _ = w.Write(bs)
329 return
330 }
331 _, _ = w.Write(dataStatsSummary)
332 default:
333 w.WriteHeader(http.StatusBadRequest)
334 }
335 }))
336 }
337
338 func copyBlockListLastUpdate(dst, src map[string]int64) {
339 k := "gravity_last_update_seconds_ago"
340 if v, ok := src[k]; ok {
341 if _, ok := dst[k]; ok {
342 dst[k] = v
343 }
344 }
345 }