| 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 | } |