815 lines
22 KiB
Go
815 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gosnmp/gosnmp"
|
|
"codeberg.org/towerops-agent/towerops-agent/pb"
|
|
)
|
|
|
|
func TestSnmpValueToString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pdu gosnmp.SnmpPDU
|
|
want string
|
|
}{
|
|
{
|
|
name: "integer",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Integer, Value: 42},
|
|
want: "42",
|
|
},
|
|
{
|
|
name: "string",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte("Linux router")},
|
|
want: "Linux router",
|
|
},
|
|
{
|
|
name: "hex bytes",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{0x00, 0x1a, 0x2b}},
|
|
want: "00:1a:2b",
|
|
},
|
|
{
|
|
name: "oid",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.ObjectIdentifier, Value: "1.3.6.1.2.1.1.1.0"},
|
|
want: "1.3.6.1.2.1.1.1.0",
|
|
},
|
|
{
|
|
name: "counter32",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Counter32, Value: uint(12345)},
|
|
want: "12345",
|
|
},
|
|
{
|
|
name: "counter64",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Counter64, Value: uint64(9876543210)},
|
|
want: "9876543210",
|
|
},
|
|
{
|
|
name: "gauge32",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Gauge32, Value: uint(999)},
|
|
want: "999",
|
|
},
|
|
{
|
|
name: "timeticks",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.TimeTicks, Value: uint32(12345678)},
|
|
want: "12345678",
|
|
},
|
|
{
|
|
name: "ip address",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.IPAddress, Value: "192.168.1.1"},
|
|
want: "192.168.1.1",
|
|
},
|
|
{
|
|
name: "null",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Null, Value: nil},
|
|
want: "null",
|
|
},
|
|
{
|
|
name: "no such object",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.NoSuchObject, Value: nil},
|
|
want: "null",
|
|
},
|
|
{
|
|
name: "invalid utf8",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x80, 0xFE}},
|
|
want: "48:65:6c:6c:6f:80:fe",
|
|
},
|
|
{
|
|
name: "opaque",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Opaque, Value: []byte{0xDE, 0xAD}},
|
|
want: "de:ad",
|
|
},
|
|
{
|
|
name: "end of mib view",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.EndOfMibView, Value: nil},
|
|
want: "null",
|
|
},
|
|
{
|
|
name: "no such instance",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.NoSuchInstance, Value: nil},
|
|
want: "null",
|
|
},
|
|
{
|
|
name: "unknown type",
|
|
pdu: gosnmp.SnmpPDU{Type: gosnmp.Asn1BER(0xFF), Value: "something"},
|
|
want: "something",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := snmpValueToString(tt.pdu)
|
|
if got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMapAuthProtocol(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want gosnmp.SnmpV3AuthProtocol
|
|
}{
|
|
{"MD5", gosnmp.MD5},
|
|
{"SHA", gosnmp.SHA},
|
|
{"SHA-1", gosnmp.SHA},
|
|
{"SHA-224", gosnmp.SHA224},
|
|
{"SHA-256", gosnmp.SHA256},
|
|
{"SHA-384", gosnmp.SHA384},
|
|
{"SHA-512", gosnmp.SHA512},
|
|
{"unknown", gosnmp.SHA},
|
|
}
|
|
for _, tt := range tests {
|
|
got := mapAuthProtocol(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("mapAuthProtocol(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMapPrivProtocol(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want gosnmp.SnmpV3PrivProtocol
|
|
}{
|
|
{"DES", gosnmp.DES},
|
|
{"AES", gosnmp.AES},
|
|
{"AES-128", gosnmp.AES},
|
|
{"AES-192", gosnmp.AES192},
|
|
{"AES-256", gosnmp.AES256},
|
|
{"AES-192-C", gosnmp.AES192C},
|
|
{"AES-256-C", gosnmp.AES256C},
|
|
{"unknown", gosnmp.AES},
|
|
}
|
|
for _, tt := range tests {
|
|
got := mapPrivProtocol(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("mapPrivProtocol(%q) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatHex(t *testing.T) {
|
|
tests := []struct {
|
|
input []byte
|
|
want string
|
|
}{
|
|
{nil, ""},
|
|
{[]byte{}, ""},
|
|
{[]byte{0xAB}, "ab"},
|
|
{[]byte{0x00, 0xFF, 0x1A}, "00:ff:1a"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := formatHex(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("formatHex(%v) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewSnmpConn(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dev *pb.SnmpDevice
|
|
}{
|
|
{
|
|
name: "v1",
|
|
dev: &pb.SnmpDevice{Ip: "127.0.0.1", Port: 0, Version: "v1", Community: "public"},
|
|
},
|
|
{
|
|
name: "v2c default",
|
|
dev: &pb.SnmpDevice{Ip: "127.0.0.1", Port: 0, Version: "2c", Community: "public"},
|
|
},
|
|
{
|
|
name: "v2c empty version",
|
|
dev: &pb.SnmpDevice{Ip: "127.0.0.1", Port: 0, Community: "public"},
|
|
},
|
|
{
|
|
name: "v3 noAuthNoPriv",
|
|
dev: &pb.SnmpDevice{Ip: "127.0.0.1", Port: 0, Version: "v3", V3Username: "user", V3SecurityLevel: "noAuthNoPriv"},
|
|
},
|
|
{
|
|
name: "v3 authNoPriv",
|
|
dev: &pb.SnmpDevice{
|
|
Ip: "127.0.0.1", Port: 0, Version: "v3",
|
|
V3Username: "user", V3SecurityLevel: "authNoPriv",
|
|
V3AuthProtocol: "SHA-256", V3AuthPassword: "pass1234",
|
|
},
|
|
},
|
|
{
|
|
name: "v3 authPriv",
|
|
dev: &pb.SnmpDevice{
|
|
Ip: "127.0.0.1", Port: 0, Version: "v3",
|
|
V3Username: "user", V3SecurityLevel: "authPriv",
|
|
V3AuthProtocol: "SHA-256", V3AuthPassword: "pass1234",
|
|
V3PrivProtocol: "AES-256", V3PrivPassword: "priv1234",
|
|
},
|
|
},
|
|
{
|
|
name: "tcp transport",
|
|
dev: &pb.SnmpDevice{Ip: "127.0.0.1", Port: 0, Version: "2c", Community: "public", Transport: "tcp"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
conn, err := newSnmpConn(tt.dev)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = conn.Conn.Close() }()
|
|
|
|
// Verify version was set correctly
|
|
switch tt.dev.Version {
|
|
case "1", "v1":
|
|
if conn.Version != gosnmp.Version1 {
|
|
t.Errorf("expected Version1, got %v", conn.Version)
|
|
}
|
|
case "3", "v3":
|
|
if conn.Version != gosnmp.Version3 {
|
|
t.Errorf("expected Version3, got %v", conn.Version)
|
|
}
|
|
default:
|
|
if conn.Version != gosnmp.Version2c {
|
|
t.Errorf("expected Version2c, got %v", conn.Version)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSnmpConnTCPError(t *testing.T) {
|
|
// TCP transport on port 1 should fail to connect
|
|
_, err := newSnmpConn(&pb.SnmpDevice{Ip: "127.0.0.1", Port: 1, Version: "2c", Community: "public", Transport: "tcp"})
|
|
if err == nil {
|
|
t.Error("expected connection error on TCP port 1")
|
|
}
|
|
}
|
|
|
|
func TestSnmpDialDefault(t *testing.T) {
|
|
// Test the default snmpDial function variable (wraps newSnmpConn)
|
|
origDial := snmpDial
|
|
defer func() { snmpDial = origDial }()
|
|
|
|
// Reset to default behavior
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
conn, err := newSnmpConn(dev)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return conn, func() { _ = conn.Conn.Close() }, nil
|
|
}
|
|
|
|
q, closeFn, err := snmpDial(&pb.SnmpDevice{Ip: "127.0.0.1", Port: 16100, Version: "2c", Community: "public"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
defer closeFn()
|
|
if q == nil {
|
|
t.Error("expected non-nil querier")
|
|
}
|
|
}
|
|
|
|
// mockSnmpQuerier implements snmpQuerier for testing.
|
|
type mockSnmpQuerier struct {
|
|
getFunc func(oids []string) (*gosnmp.SnmpPacket, error)
|
|
walkFunc func(rootOid string) ([]gosnmp.SnmpPDU, error)
|
|
bulkWalkFunc func(rootOid string) ([]gosnmp.SnmpPDU, error)
|
|
closeCalled bool
|
|
walkAllCalled bool
|
|
bulkWalkCalled bool
|
|
}
|
|
|
|
func (m *mockSnmpQuerier) Get(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return m.getFunc(oids)
|
|
}
|
|
|
|
func (m *mockSnmpQuerier) WalkAll(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
m.walkAllCalled = true
|
|
return m.walkFunc(rootOid)
|
|
}
|
|
|
|
func (m *mockSnmpQuerier) BulkWalkAll(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
m.bulkWalkCalled = true
|
|
if m.bulkWalkFunc != nil {
|
|
return m.bulkWalkFunc(rootOid)
|
|
}
|
|
return m.walkFunc(rootOid)
|
|
}
|
|
|
|
func TestExecuteSnmpJob(t *testing.T) {
|
|
t.Run("nil device", func(t *testing.T) {
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{JobId: "1", JobType: pb.JobType_POLL}, ch)
|
|
result := <-ch
|
|
if len(result.OidValues) != 0 {
|
|
t.Errorf("expected empty oid values for nil device, got %d", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("dial error", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return nil, nil, fmt.Errorf("connection refused")
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Port: 161},
|
|
}, ch)
|
|
result := <-ch
|
|
if len(result.OidValues) != 0 {
|
|
t.Errorf("expected empty oid values on dial error, got %d", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("GET success", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return &gosnmp.SnmpPacket{
|
|
Variables: []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("Linux")},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() { mock.closeCalled = true }, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
DeviceId: "dev-1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Port: 161},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_GET, Oids: []string{".1.3.6.1.2.1.1.1.0"}},
|
|
},
|
|
}, ch)
|
|
|
|
if len(ch) != 1 {
|
|
t.Fatal("expected one result")
|
|
}
|
|
result := <-ch
|
|
if result.OidValues[".1.3.6.1.2.1.1.1.0"] != "Linux" {
|
|
t.Errorf("got %q, want Linux", result.OidValues[".1.3.6.1.2.1.1.1.0"])
|
|
}
|
|
if !mock.closeCalled {
|
|
t.Error("expected close to be called")
|
|
}
|
|
})
|
|
|
|
t.Run("WALK success", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
walkFunc: func(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
return []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.1", Type: gosnmp.Integer, Value: 1},
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.2", Type: gosnmp.Integer, Value: 2},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_WALK, Oids: []string{".1.3.6.1.2.1.2.2.1.1"}},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 2 {
|
|
t.Errorf("got %d oid values, want 2", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("GET error continues", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return nil, fmt.Errorf("timeout")
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_GET, Oids: []string{".1.3.6.1.2.1.1.1.0"}},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 0 {
|
|
t.Errorf("got %d oid values, want 0 on error", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("WALK error continues", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
walkFunc: func(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
return nil, fmt.Errorf("timeout")
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_WALK, Oids: []string{".1.3.6.1.2.1.2"}},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 0 {
|
|
t.Errorf("got %d oid values, want 0 on error", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("NoSuchObject skipped", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return &gosnmp.SnmpPacket{
|
|
Variables: []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.1.1.0", Type: gosnmp.NoSuchObject, Value: nil},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_GET, Oids: []string{".1.3.6.1.2.1.1.1.0"}},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 0 {
|
|
t.Errorf("NoSuchObject should be skipped, got %d oid values", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("WALK NoSuchObject skipped", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
walkFunc: func(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
return []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.1", Type: gosnmp.Integer, Value: 1},
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.2", Type: gosnmp.NoSuchInstance, Value: nil},
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.3", Type: gosnmp.EndOfMibView, Value: nil},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Port: 161},
|
|
}, ch)
|
|
result := <-ch
|
|
if len(result.OidValues) != 0 {
|
|
t.Errorf("expected empty oid values on dial error, got %d", len(result.OidValues))
|
|
}
|
|
})
|
|
|
|
t.Run("WALK v1 uses WalkAll", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
walkFunc: func(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
return []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.1", Type: gosnmp.Integer, Value: 1},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Version: "1"},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_WALK, Oids: []string{".1.3.6.1.2.1.2.2.1.1"}},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 1 {
|
|
t.Errorf("got %d oid values, want 1", len(result.OidValues))
|
|
}
|
|
if !mock.walkAllCalled {
|
|
t.Error("expected WalkAll to be called for v1")
|
|
}
|
|
if mock.bulkWalkCalled {
|
|
t.Error("BulkWalkAll should not be called for v1")
|
|
}
|
|
})
|
|
|
|
t.Run("WALK v2c uses BulkWalkAll", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
walkFunc: func(rootOid string) ([]gosnmp.SnmpPDU, error) {
|
|
return []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.2.2.1.1.1", Type: gosnmp.Integer, Value: 1},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Version: "2c"},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_WALK, Oids: []string{".1.3.6.1.2.1.2.2.1.1"}},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 1 {
|
|
t.Errorf("got %d oid values, want 1", len(result.OidValues))
|
|
}
|
|
if !mock.bulkWalkCalled {
|
|
t.Error("expected BulkWalkAll to be called for v2c")
|
|
}
|
|
if mock.walkAllCalled {
|
|
t.Error("WalkAll should not be called for v2c")
|
|
}
|
|
})
|
|
|
|
t.Run("channel full drops", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return &gosnmp.SnmpPacket{}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult) // unbuffered, no reader — will be full
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
}, ch)
|
|
// Should not block — the result is dropped
|
|
})
|
|
}
|
|
|
|
func TestExecuteSnmpJobBatchesGets(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
var getCalls [][]string
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
getCalls = append(getCalls, oids)
|
|
var vars []gosnmp.SnmpPDU
|
|
for _, oid := range oids {
|
|
vars = append(vars, gosnmp.SnmpPDU{Name: oid, Type: gosnmp.Integer, Value: 42})
|
|
}
|
|
return &gosnmp.SnmpPacket{Variables: vars}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
// Create 150 OIDs — should be split into 3 batches of 60, 60, 30
|
|
oids := make([]string, 150)
|
|
for i := range oids {
|
|
oids[i] = fmt.Sprintf(".1.3.6.1.2.1.1.%d.0", i)
|
|
}
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(context.Background(), &pb.AgentJob{
|
|
JobId: "batch-test",
|
|
DeviceId: "dev-1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Port: 161},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_GET, Oids: oids},
|
|
},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if len(result.OidValues) != 150 {
|
|
t.Errorf("got %d oid values, want 150", len(result.OidValues))
|
|
}
|
|
if len(getCalls) != 3 {
|
|
t.Errorf("got %d GET calls, want 3 (batches of 60)", len(getCalls))
|
|
}
|
|
for i, call := range getCalls {
|
|
if i < 2 && len(call) != 60 {
|
|
t.Errorf("batch %d: got %d oids, want 60", i, len(call))
|
|
}
|
|
if i == 2 && len(call) != 30 {
|
|
t.Errorf("batch %d: got %d oids, want 30", i, len(call))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExecuteSnmpJobCtxCancelled(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return &gosnmp.SnmpPacket{}, nil
|
|
},
|
|
}, func() {}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // cancel before queries run
|
|
|
|
ch := make(chan *pb.SnmpResult, 1)
|
|
executeSnmpJob(ctx, &pb.AgentJob{
|
|
JobId: "ctx-test",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Port: 161},
|
|
Queries: []*pb.SnmpQuery{
|
|
{QueryType: pb.QueryType_GET, Oids: []string{".1.3.6.1.2.1.1.1.0"}},
|
|
{QueryType: pb.QueryType_GET, Oids: []string{".1.3.6.1.2.1.1.2.0"}},
|
|
},
|
|
}, ch)
|
|
|
|
// With cancelled context, the function should return before processing queries
|
|
// (no result sent because it returns early in the ctx.Err() check)
|
|
select {
|
|
case <-ch:
|
|
// Might get a result if the first query ran before ctx check
|
|
default:
|
|
// Expected — returned early
|
|
}
|
|
}
|
|
|
|
func TestExecuteCredentialTest(t *testing.T) {
|
|
t.Run("nil device", func(t *testing.T) {
|
|
ch := make(chan *pb.CredentialTestResult, 1)
|
|
executeCredentialTest(context.Background(), &pb.AgentJob{JobId: "1"}, ch)
|
|
result := <-ch
|
|
if result.Success {
|
|
t.Error("expected failure for nil device")
|
|
}
|
|
if !strings.Contains(result.ErrorMessage, "missing device") {
|
|
t.Errorf("expected 'missing device' in error, got: %s", result.ErrorMessage)
|
|
}
|
|
})
|
|
|
|
t.Run("dial error", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return nil, nil, fmt.Errorf("connection refused")
|
|
}
|
|
|
|
ch := make(chan *pb.CredentialTestResult, 1)
|
|
executeCredentialTest(context.Background(), &pb.AgentJob{
|
|
JobId: "test-1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1", Port: 161},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if result.Success {
|
|
t.Error("expected failure")
|
|
}
|
|
if result.ErrorMessage == "" {
|
|
t.Error("expected error message")
|
|
}
|
|
})
|
|
|
|
t.Run("get error", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return nil, fmt.Errorf("timeout")
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.CredentialTestResult, 1)
|
|
executeCredentialTest(context.Background(), &pb.AgentJob{
|
|
JobId: "test-1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if result.Success {
|
|
t.Error("expected failure on get error")
|
|
}
|
|
})
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return &gosnmp.SnmpPacket{
|
|
Variables: []gosnmp.SnmpPDU{
|
|
{Name: ".1.3.6.1.2.1.1.1.0", Type: gosnmp.OctetString, Value: []byte("RouterOS 7.1")},
|
|
},
|
|
}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.CredentialTestResult, 1)
|
|
executeCredentialTest(context.Background(), &pb.AgentJob{
|
|
JobId: "test-1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if !result.Success {
|
|
t.Error("expected success")
|
|
}
|
|
if result.SystemDescription != "RouterOS 7.1" {
|
|
t.Errorf("sysDescr: got %q, want %q", result.SystemDescription, "RouterOS 7.1")
|
|
}
|
|
})
|
|
|
|
t.Run("success no variables", func(t *testing.T) {
|
|
orig := snmpDial
|
|
defer func() { snmpDial = orig }()
|
|
|
|
mock := &mockSnmpQuerier{
|
|
getFunc: func(oids []string) (*gosnmp.SnmpPacket, error) {
|
|
return &gosnmp.SnmpPacket{Variables: nil}, nil
|
|
},
|
|
}
|
|
snmpDial = func(dev *pb.SnmpDevice) (snmpQuerier, func(), error) {
|
|
return mock, func() {}, nil
|
|
}
|
|
|
|
ch := make(chan *pb.CredentialTestResult, 1)
|
|
executeCredentialTest(context.Background(), &pb.AgentJob{
|
|
JobId: "test-1",
|
|
SnmpDevice: &pb.SnmpDevice{Ip: "10.0.0.1"},
|
|
}, ch)
|
|
|
|
result := <-ch
|
|
if !result.Success {
|
|
t.Error("expected success even with no variables")
|
|
}
|
|
if result.SystemDescription != "" {
|
|
t.Errorf("expected empty sysDescr, got %q", result.SystemDescription)
|
|
}
|
|
})
|
|
}
|