master
go 665 lines 17.8 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package snmp
4
5 import (
6 "context"
7 "testing"
8
9 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12 )
13
14 // newTestFuncInterfaces creates a funcInterfaces for testing with the given cache.
15 func newTestFuncInterfaces(cache *ifaceCache) *funcInterfaces {
16 r := &funcRouter{ifaceCache: cache}
17 return newFuncInterfaces(r)
18 }
19
20 func TestSnmpMethods(t *testing.T) {
21 methods := snmpMethods()
22
23 var ifacesMethod *funcapi.MethodConfig
24 var licensesMethod *funcapi.MethodConfig
25 var bgpMethod *funcapi.MethodConfig
26 for i := range methods {
27 switch methods[i].ID {
28 case "interfaces":
29 ifacesMethod = &methods[i]
30 case "licenses":
31 licensesMethod = &methods[i]
32 case "bgp-peers":
33 bgpMethod = &methods[i]
34 }
35 }
36
37 require.NotNil(t, ifacesMethod)
38 assert.Equal(t, "Network Interfaces", ifacesMethod.Name)
39 require.NotEmpty(t, ifacesMethod.RequiredParams)
40
41 require.NotNil(t, licensesMethod)
42 assert.Equal(t, "Licenses", licensesMethod.Name)
43
44 require.NotNil(t, bgpMethod)
45 assert.Equal(t, "BGP Peers", bgpMethod.Name)
46 require.NotEmpty(t, bgpMethod.RequiredParams)
47
48 // Verify type group param exists
49 var typeGroupParam *funcapi.ParamConfig
50 for i := range ifacesMethod.RequiredParams {
51 if ifacesMethod.RequiredParams[i].ID == "if_type_group" {
52 typeGroupParam = &ifacesMethod.RequiredParams[i]
53 break
54 }
55 }
56 require.NotNil(t, typeGroupParam, "expected if_type_group required param")
57 require.NotEmpty(t, typeGroupParam.Options)
58
59 // Verify default type group option exists
60 hasDefault := false
61 for _, opt := range typeGroupParam.Options {
62 if opt.Default {
63 hasDefault = true
64 assert.Equal(t, "ethernet", opt.ID)
65 break
66 }
67 }
68 assert.True(t, hasDefault, "should have a default type group option")
69
70 var bgpViewParam *funcapi.ParamConfig
71 for i := range bgpMethod.RequiredParams {
72 if bgpMethod.RequiredParams[i].ID == "view" {
73 bgpViewParam = &bgpMethod.RequiredParams[i]
74 break
75 }
76 }
77 require.NotNil(t, bgpViewParam, "expected BGP view required param")
78 require.Len(t, bgpViewParam.Options, 3)
79 assert.Equal(t, "peers", bgpViewParam.Options[0].ID)
80 assert.True(t, bgpViewParam.Options[0].Default)
81 }
82
83 func TestFuncIfacesColumns(t *testing.T) {
84 tests := map[string]struct {
85 validate func(t *testing.T)
86 }{
87 "has required columns": {
88 validate: func(t *testing.T) {
89 requiredKeys := []string{
90 "Interface", "Type", "Type Group",
91 "Admin Status", "Oper Status",
92 "Traffic In", "Traffic Out",
93 "Unicast In", "Unicast Out",
94 "Broadcast In", "Broadcast Out",
95 "Packets In", "Packets Out",
96 "Errors In", "Errors Out",
97 "Discards In", "Discards Out",
98 "Multicast In", "Multicast Out",
99 }
100
101 cs := snmpColumnSet(snmpAllColumns)
102 for _, key := range requiredKeys {
103 assert.True(t, cs.ContainsColumn(key), "column %s should be defined", key)
104 }
105 },
106 },
107 "has valid metadata": {
108 validate: func(t *testing.T) {
109 for _, col := range snmpAllColumns {
110 assert.NotEmpty(t, col.Name, "column must have ID")
111 assert.NotEqual(t, funcapi.FieldTypeNone, col.Type, "column %s must have Type", col.Name)
112 assert.NotNil(t, col.Value, "column %s must have Value extractor", col.Name)
113 }
114 },
115 },
116 "value extractors work correctly": {
117 validate: func(t *testing.T) {
118 rate := 100.0
119 entry := &ifaceEntry{
120 name: "eth0",
121 ifType: "ethernetCsmacd",
122 ifTypeGroup: "ethernet",
123 adminStatus: "up",
124 operStatus: "up",
125 rates: ifaceRates{
126 trafficIn: &rate,
127 ucastPktsIn: &rate,
128 errorsIn: &rate,
129 discardsIn: &rate,
130 },
131 }
132
133 // Test each column's value extractor
134 for _, col := range snmpAllColumns {
135 // Should not panic
136 _ = col.Value(entry)
137 }
138
139 // Verify specific values
140 for _, col := range snmpAllColumns {
141 switch col.Name {
142 case "Interface":
143 assert.Equal(t, "eth0", col.Value(entry))
144 case "Type":
145 assert.Equal(t, "ethernetCsmacd", col.Value(entry))
146 case "Type Group":
147 assert.Equal(t, "ethernet", col.Value(entry))
148 case "Traffic In":
149 assert.Equal(t, rate, col.Value(entry))
150 case "Packets In":
151 assert.Equal(t, rate, col.Value(entry))
152 case "Errors In":
153 assert.Equal(t, rate, col.Value(entry))
154 case "Discards In":
155 assert.Equal(t, rate, col.Value(entry))
156 case "Admin Status":
157 assert.Equal(t, "up", col.Value(entry))
158 }
159 }
160 },
161 },
162 "column count matches row length": {
163 validate: func(t *testing.T) {
164 entry := &ifaceEntry{
165 name: "eth0",
166 ifType: "ethernetCsmacd",
167 ifTypeGroup: "ethernet",
168 adminStatus: "up",
169 operStatus: "up",
170 }
171 f := &funcInterfaces{}
172 row := f.buildRow(entry)
173 assert.Len(t, row, len(snmpAllColumns)+1, "row length must match column count")
174 },
175 },
176 }
177
178 for name, tc := range tests {
179 t.Run(name, func(t *testing.T) {
180 tc.validate(t)
181 })
182 }
183 }
184
185 func TestFuncInterfaces_buildColumns(t *testing.T) {
186 f := &funcInterfaces{}
187 cs := snmpColumnSet(snmpAllColumns)
188 columns := f.buildColumns(cs)
189
190 require.NotEmpty(t, columns)
191 assert.Len(t, columns, len(snmpAllColumns)+1)
192
193 // Verify all columns are present
194 for _, col := range snmpAllColumns {
195 colDef, ok := columns[col.Name]
196 assert.True(t, ok, "column %s should be in result", col.Name)
197 assert.NotNil(t, colDef)
198
199 // Verify column is a map with expected fields
200 colMap, ok := colDef.(map[string]any)
201 require.True(t, ok, "column %s should be a map", col.Name)
202 assert.Equal(t, col.Tooltip, colMap["name"])
203 }
204
205 rowOptions, ok := columns["rowOptions"]
206 require.True(t, ok, "rowOptions column should be in result")
207 rowOptionsMap, ok := rowOptions.(map[string]any)
208 require.True(t, ok, "rowOptions column should be a map")
209 assert.Equal(t, "rowOptions", rowOptionsMap["name"])
210 assert.Equal(t, "none", rowOptionsMap["type"])
211 assert.Equal(t, "rowOptions", rowOptionsMap["visualization"])
212
213 }
214
215 func TestFuncInterfaces_buildRow(t *testing.T) {
216 rate1 := 1000.5
217 rate2 := 2000.5
218
219 tests := map[string]struct {
220 entry *ifaceEntry
221 validate func(t *testing.T, row []any)
222 }{
223 "all fields populated": {
224 entry: &ifaceEntry{
225 name: "eth0",
226 ifType: "ethernetCsmacd",
227 ifTypeGroup: "ethernet",
228 adminStatus: "up",
229 operStatus: "up",
230 rates: ifaceRates{
231 trafficIn: &rate1,
232 trafficOut: &rate2,
233 ucastPktsIn: &rate1,
234 ucastPktsOut: &rate2,
235 bcastPktsIn: &rate1,
236 bcastPktsOut: &rate2,
237 errorsIn: &rate1,
238 errorsOut: &rate2,
239 discardsIn: &rate1,
240 discardsOut: &rate2,
241 mcastPktsIn: &rate1,
242 mcastPktsOut: &rate2,
243 },
244 },
245 validate: func(t *testing.T, row []any) {
246 // Find column indices by key
247 nameIdx := findColIdx("Interface")
248 typeIdx := findColIdx("Type")
249 typeGroupIdx := findColIdx("Type Group")
250 trafficInIdx := findColIdx("Traffic In")
251 trafficOutIdx := findColIdx("Traffic Out")
252 packetsInIdx := findColIdx("Packets In")
253 packetsOutIdx := findColIdx("Packets Out")
254 adminIdx := findColIdx("Admin Status")
255 operIdx := findColIdx("Oper Status")
256 rowOptionsIdx := len(snmpAllColumns)
257
258 assert.Equal(t, "eth0", row[nameIdx])
259 assert.Equal(t, "ethernetCsmacd", row[typeIdx])
260 assert.Equal(t, "ethernet", row[typeGroupIdx])
261 assert.Equal(t, rate1, row[trafficInIdx])
262 assert.Equal(t, rate2, row[trafficOutIdx])
263 assert.Equal(t, rate1*3, row[packetsInIdx])
264 assert.Equal(t, rate2*3, row[packetsOutIdx])
265 assert.Equal(t, "up", row[adminIdx])
266 assert.Equal(t, "up", row[operIdx])
267 assert.Nil(t, row[rowOptionsIdx])
268 },
269 },
270 "nil rates produce nil values": {
271 entry: &ifaceEntry{
272 name: "eth1",
273 ifType: "other",
274 ifTypeGroup: "other",
275 adminStatus: "down",
276 operStatus: "down",
277 rates: ifaceRates{}, // all nil
278 },
279 validate: func(t *testing.T, row []any) {
280 nameIdx := findColIdx("Interface")
281 typeIdx := findColIdx("Type")
282 trafficInIdx := findColIdx("Traffic In")
283 trafficOutIdx := findColIdx("Traffic Out")
284 adminIdx := findColIdx("Admin Status")
285 operIdx := findColIdx("Oper Status")
286 rowOptionsIdx := len(snmpAllColumns)
287
288 assert.Equal(t, "eth1", row[nameIdx])
289 assert.Equal(t, "other", row[typeIdx])
290 assert.Nil(t, row[trafficInIdx])
291 assert.Nil(t, row[trafficOutIdx])
292 assert.Equal(t, "down", row[adminIdx])
293 assert.Equal(t, "down", row[operIdx])
294 assert.Nil(t, row[rowOptionsIdx])
295 },
296 },
297 "down interface hides metrics": {
298 entry: &ifaceEntry{
299 name: "eth3",
300 ifType: "ethernetCsmacd",
301 ifTypeGroup: "ethernet",
302 adminStatus: "up",
303 operStatus: "down",
304 rates: ifaceRates{
305 trafficIn: &rate1,
306 trafficOut: &rate2,
307 errorsIn: &rate1,
308 },
309 },
310 validate: func(t *testing.T, row []any) {
311 trafficInIdx := findColIdx("Traffic In")
312 trafficOutIdx := findColIdx("Traffic Out")
313 errorsInIdx := findColIdx("Errors In")
314 rowOptionsIdx := len(snmpAllColumns)
315
316 assert.Nil(t, row[trafficInIdx])
317 assert.Nil(t, row[trafficOutIdx])
318 assert.Nil(t, row[errorsInIdx])
319 assert.Nil(t, row[rowOptionsIdx])
320 },
321 },
322 "partial rates": {
323 entry: &ifaceEntry{
324 name: "eth2",
325 ifType: "",
326 ifTypeGroup: "",
327 adminStatus: "up",
328 operStatus: "unknown",
329 rates: ifaceRates{
330 trafficIn: &rate1,
331 trafficOut: &rate2,
332 // rest nil
333 },
334 },
335 validate: func(t *testing.T, row []any) {
336 nameIdx := findColIdx("Interface")
337 trafficInIdx := findColIdx("Traffic In")
338 trafficOutIdx := findColIdx("Traffic Out")
339 ucastInIdx := findColIdx("Unicast In")
340 rowOptionsIdx := len(snmpAllColumns)
341
342 assert.Equal(t, "eth2", row[nameIdx])
343 assert.Nil(t, row[trafficInIdx])
344 assert.Nil(t, row[trafficOutIdx])
345 assert.Nil(t, row[ucastInIdx])
346 assert.Nil(t, row[rowOptionsIdx])
347 },
348 },
349 }
350
351 for name, tc := range tests {
352 t.Run(name, func(t *testing.T) {
353 f := &funcInterfaces{}
354 row := f.buildRow(tc.entry)
355 require.Len(t, row, len(snmpAllColumns)+1)
356 tc.validate(t, row)
357 })
358 }
359 }
360
361 func TestFuncInterfaces_sortData(t *testing.T) {
362 rate100 := 100.0
363 rate200 := 200.0
364 rate300 := 300.0
365
366 // Helper to build a test row with name and trafficIn
367 buildTestRow := func(name string, trafficIn *float64) []any {
368 row := make([]any, len(snmpAllColumns)+1)
369 for i, col := range snmpAllColumns {
370 switch col.Name {
371 case "Interface":
372 row[i] = name
373 case "Traffic In":
374 row[i] = ptrToAny(trafficIn)
375 case "Traffic Out":
376 row[i] = ptrToAny(trafficIn) // reuse for simplicity
377 default:
378 if col.Type == funcapi.FieldTypeString {
379 row[i] = "test"
380 } else {
381 row[i] = nil
382 }
383 }
384 }
385 row[len(snmpAllColumns)] = nil
386 return row
387 }
388
389 tests := map[string]struct {
390 data [][]any
391 sortColumn string
392 expected []string // expected order of names
393 }{
394 "sort by name ascending": {
395 data: [][]any{
396 buildTestRow("eth2", nil),
397 buildTestRow("eth0", nil),
398 buildTestRow("eth1", nil),
399 },
400 sortColumn: "Interface",
401 expected: []string{"eth0", "eth1", "eth2"},
402 },
403 "sort by trafficIn descending": {
404 data: [][]any{
405 buildTestRow("eth0", &rate100),
406 buildTestRow("eth1", &rate300),
407 buildTestRow("eth2", &rate200),
408 },
409 sortColumn: "Traffic In",
410 expected: []string{"eth1", "eth2", "eth0"},
411 },
412 "nil values go to end": {
413 data: [][]any{
414 buildTestRow("eth0", nil),
415 buildTestRow("eth1", &rate300),
416 buildTestRow("eth2", &rate100),
417 },
418 sortColumn: "Traffic In",
419 expected: []string{"eth1", "eth2", "eth0"},
420 },
421 "unknown column defaults to name": {
422 data: [][]any{
423 buildTestRow("eth2", nil),
424 buildTestRow("eth0", nil),
425 buildTestRow("eth1", nil),
426 },
427 sortColumn: "unknown_column",
428 expected: []string{"eth0", "eth1", "eth2"},
429 },
430 "empty data": {
431 data: [][]any{},
432 sortColumn: "Interface",
433 expected: nil,
434 },
435 }
436
437 for name, tc := range tests {
438 t.Run(name, func(t *testing.T) {
439 f := &funcInterfaces{}
440 f.sortData(tc.data, tc.sortColumn)
441
442 nameIdx := findColIdx("Interface")
443 var names []string
444 for _, row := range tc.data {
445 names = append(names, row[nameIdx].(string))
446 }
447 assert.Equal(t, tc.expected, names)
448 })
449 }
450 }
451
452 func TestFuncInterfaces_handle(t *testing.T) {
453 rate100 := 100.0
454 rate200 := 200.0
455
456 tests := map[string]struct {
457 setup func() *funcInterfaces
458 method string
459 params funcapi.ResolvedParams
460 validate func(t *testing.T, resp *funcapi.FunctionResponse)
461 }{
462 "unknown method returns 404": {
463 setup: func() *funcInterfaces {
464 return newTestFuncInterfaces(newIfaceCache())
465 },
466 method: "unknown",
467 params: funcapi.ResolvedParams{},
468 validate: func(t *testing.T, resp *funcapi.FunctionResponse) {
469 assert.Equal(t, 404, resp.Status)
470 assert.Contains(t, resp.Message, "unknown method")
471 },
472 },
473 "nil cache returns 503": {
474 setup: func() *funcInterfaces {
475 return newTestFuncInterfaces(nil)
476 },
477 method: "interfaces",
478 params: funcapi.ResolvedParams{},
479 validate: func(t *testing.T, resp *funcapi.FunctionResponse) {
480 assert.Equal(t, 503, resp.Status)
481 assert.Contains(t, resp.Message, "not available")
482 },
483 },
484 "empty cache returns 200 with empty data": {
485 setup: func() *funcInterfaces {
486 return newTestFuncInterfaces(newIfaceCache())
487 },
488 method: "interfaces",
489 params: resolveIfaceParams(nil),
490 validate: func(t *testing.T, resp *funcapi.FunctionResponse) {
491 assert.Equal(t, 200, resp.Status)
492 assert.NotNil(t, resp.Columns)
493 assert.Len(t, resp.Columns, len(snmpAllColumns)+1)
494
495 data, ok := resp.Data.([][]any)
496 require.True(t, ok)
497 assert.Len(t, data, 0)
498 },
499 },
500 "cache with data returns correct rows": {
501 setup: func() *funcInterfaces {
502 cache := newIfaceCache()
503 cache.interfaces["eth0"] = &ifaceEntry{
504 name: "eth0",
505 ifType: "ethernetCsmacd",
506 ifTypeGroup: "ethernet",
507 adminStatus: "up",
508 operStatus: "up",
509 rates: ifaceRates{
510 trafficIn: &rate100,
511 trafficOut: &rate200,
512 },
513 }
514 cache.interfaces["eth1"] = &ifaceEntry{
515 name: "eth1",
516 ifType: "other",
517 ifTypeGroup: "virtual",
518 adminStatus: "down",
519 operStatus: "down",
520 }
521 return newTestFuncInterfaces(cache)
522 },
523 method: "interfaces",
524 params: resolveIfaceParams(nil),
525 validate: func(t *testing.T, resp *funcapi.FunctionResponse) {
526 assert.Equal(t, 200, resp.Status)
527 assert.Equal(t, "Interface", resp.DefaultSortColumn)
528
529 data, ok := resp.Data.([][]any)
530 require.True(t, ok)
531 assert.Len(t, data, 1)
532
533 // Verify Charts
534 require.NotNil(t, resp.Charts)
535 assert.Contains(t, resp.Charts, "Traffic")
536 assert.Contains(t, resp.Charts, "UnicastPackets")
537 assert.Equal(t, []string{"Traffic In", "Traffic Out"}, resp.Charts["Traffic"].Columns)
538
539 // Verify DefaultCharts
540 require.NotEmpty(t, resp.DefaultCharts)
541 assert.ElementsMatch(t, funcapi.DefaultCharts{
542 {Chart: "Traffic", GroupBy: "Type"},
543 {Chart: "OperationalStatus", GroupBy: "Oper Status"},
544 }, resp.DefaultCharts)
545
546 // Verify GroupBy
547 require.NotNil(t, resp.GroupBy)
548 assert.Contains(t, resp.GroupBy, "Type")
549 },
550 },
551 "filter other includes non-target groups": {
552 setup: func() *funcInterfaces {
553 cache := newIfaceCache()
554 cache.interfaces["eth0"] = &ifaceEntry{
555 name: "eth0",
556 ifType: "ethernetCsmacd",
557 ifTypeGroup: "ethernet",
558 adminStatus: "up",
559 operStatus: "up",
560 }
561 cache.interfaces["lo0"] = &ifaceEntry{
562 name: "lo0",
563 ifType: "loopback",
564 ifTypeGroup: "virtual",
565 adminStatus: "up",
566 operStatus: "up",
567 }
568 cache.interfaces["vlan0"] = &ifaceEntry{
569 name: "vlan0",
570 ifType: "l2vlan",
571 ifTypeGroup: "virtual",
572 adminStatus: "up",
573 operStatus: "up",
574 }
575 cache.interfaces["tun0"] = &ifaceEntry{
576 name: "tun0",
577 ifType: "tunnel",
578 ifTypeGroup: "",
579 adminStatus: "up",
580 operStatus: "up",
581 }
582 cache.interfaces["wlan0"] = &ifaceEntry{
583 name: "wlan0",
584 ifType: "ieee80211",
585 ifTypeGroup: "wireless",
586 adminStatus: "up",
587 operStatus: "up",
588 }
589 return newTestFuncInterfaces(cache)
590 },
591 method: "interfaces",
592 params: resolveIfaceParams(map[string][]string{"if_type_group": {"other"}}),
593 validate: func(t *testing.T, resp *funcapi.FunctionResponse) {
594 assert.Equal(t, 200, resp.Status)
595
596 data, ok := resp.Data.([][]any)
597 require.True(t, ok)
598 assert.Len(t, data, 2)
599
600 nameIdx := findColIdx("Interface")
601 names := []string{data[0][nameIdx].(string), data[1][nameIdx].(string)}
602 assert.ElementsMatch(t, []string{"tun0", "wlan0"}, names)
603 },
604 },
605 }
606
607 for name, tc := range tests {
608 t.Run(name, func(t *testing.T) {
609 f := tc.setup()
610 resp := f.Handle(context.Background(), tc.method, tc.params)
611 tc.validate(t, resp)
612 })
613 }
614 }
615
616 func TestFuncInterfaces_defaultSortColumn(t *testing.T) {
617 f := &funcInterfaces{}
618 assert.Equal(t, "Interface", f.defaultSortColumn())
619 }
620
621 func TestPtrToAny(t *testing.T) {
622 tests := map[string]struct {
623 input *float64
624 expected any
625 }{
626 "nil pointer": {
627 input: nil,
628 expected: nil,
629 },
630 "non-nil pointer": {
631 input: func() *float64 { v := 123.45; return &v }(),
632 expected: 123.45,
633 },
634 "zero value pointer": {
635 input: func() *float64 { v := 0.0; return &v }(),
636 expected: 0.0,
637 },
638 }
639
640 for name, tc := range tests {
641 t.Run(name, func(t *testing.T) {
642 result := ptrToAny(tc.input)
643 assert.Equal(t, tc.expected, result)
644 })
645 }
646 }
647
648 // findColIdx finds the index of a column by key.
649 func findColIdx(key string) int {
650 for i, col := range snmpAllColumns {
651 if col.Name == key {
652 return i
653 }
654 }
655 return -1
656 }
657
658 func resolveIfaceParams(values map[string][]string) funcapi.ResolvedParams {
659 f := &funcInterfaces{}
660 params, err := f.MethodParams(context.Background(), "interfaces")
661 if err != nil {
662 return nil
663 }
664 return funcapi.ResolveParams(params, values)
665 }