Files
IT-Nexus/nexus-scanner/internal/modules/arp/arp_test.go

231 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package arp_test
import (
"fmt"
"net/netip"
"os"
"testing"
"time"
"github.com/cereda-systems/nexus-scanner/internal/db"
)
// helper creates a temp SQLite store and registers cleanup.
func newTestStore(t *testing.T) *db.Store {
t.Helper()
f, err := os.CreateTemp("", "nexus-test-*.db")
if err != nil {
t.Fatalf("create temp db: %v", err)
}
f.Close()
t.Cleanup(func() { os.Remove(f.Name()) })
store, err := db.Open(f.Name())
if err != nil {
t.Fatalf("db.Open: %v", err)
}
t.Cleanup(func() { store.Close() })
return store
}
func TestUpsertAndListHost(t *testing.T) {
store := newTestStore(t)
now := time.Now().UTC().Truncate(time.Second)
original := db.Host{
IP: "192.168.0.10",
MAC: "aa:bb:cc:dd:ee:ff",
Vendor: "Acme Corp",
Site: "LUD",
FirstSeen: now,
LastSeen: now,
}
if err := store.UpsertHost(original); err != nil {
t.Fatalf("UpsertHost (insert): %v", err)
}
hosts, err := store.ListHosts("")
if err != nil {
t.Fatalf("ListHosts: %v", err)
}
if len(hosts) != 1 {
t.Fatalf("expected 1 host, got %d", len(hosts))
}
h := hosts[0]
if h.IP != original.IP {
t.Errorf("IP: got %q, want %q", h.IP, original.IP)
}
if h.MAC != original.MAC {
t.Errorf("MAC: got %q, want %q", h.MAC, original.MAC)
}
if h.Status != "online" {
t.Errorf("Status: got %q, want %q", h.Status, "online")
}
// Update the same IP — last_seen and MAC should change, first_seen should not.
updated := original
updated.MAC = "11:22:33:44:55:66"
updated.LastSeen = now.Add(time.Minute)
if err := store.UpsertHost(updated); err != nil {
t.Fatalf("UpsertHost (update): %v", err)
}
hosts, err = store.ListHosts("")
if err != nil {
t.Fatalf("ListHosts after update: %v", err)
}
if len(hosts) != 1 {
t.Fatalf("expected 1 host after upsert, got %d", len(hosts))
}
if hosts[0].MAC != "11:22:33:44:55:66" {
t.Errorf("MAC after update: got %q, want %q", hosts[0].MAC, "11:22:33:44:55:66")
}
}
func TestListHostsFilterBySite(t *testing.T) {
store := newTestStore(t)
now := time.Now().UTC()
hosts := []db.Host{
{IP: "10.0.0.1", MAC: "aa:aa:aa:aa:aa:01", Site: "LUD", FirstSeen: now, LastSeen: now},
{IP: "10.0.0.2", MAC: "aa:aa:aa:aa:aa:02", Site: "LUD", FirstSeen: now, LastSeen: now},
{IP: "10.0.1.1", MAC: "aa:aa:aa:aa:bb:01", Site: "BAR", FirstSeen: now, LastSeen: now},
}
for _, h := range hosts {
if err := store.UpsertHost(h); err != nil {
t.Fatalf("UpsertHost %s: %v", h.IP, err)
}
}
lud, err := store.ListHosts("LUD")
if err != nil {
t.Fatal(err)
}
if len(lud) != 2 {
t.Errorf("LUD: expected 2, got %d", len(lud))
}
bar, err := store.ListHosts("BAR")
if err != nil {
t.Fatal(err)
}
if len(bar) != 1 {
t.Errorf("BAR: expected 1, got %d", len(bar))
}
all, err := store.ListHosts("")
if err != nil {
t.Fatal(err)
}
if len(all) != 3 {
t.Errorf("all: expected 3, got %d", len(all))
}
}
func TestCountHosts(t *testing.T) {
store := newTestStore(t)
now := time.Now().UTC()
n, err := store.CountHosts()
if err != nil {
t.Fatal(err)
}
if n != 0 {
t.Errorf("expected 0 initially, got %d", n)
}
for i := range 5 {
err := store.UpsertHost(db.Host{
IP: fmt.Sprintf("10.0.0.%d", i+1),
MAC: fmt.Sprintf("aa:bb:cc:dd:ee:%02x", i),
Site: "LUD",
FirstSeen: now,
LastSeen: now,
})
if err != nil {
t.Fatalf("UpsertHost %d: %v", i, err)
}
}
n, err = store.CountHosts()
if err != nil {
t.Fatal(err)
}
if n != 5 {
t.Errorf("expected 5, got %d", n)
}
}
func TestScanRunLifecycle(t *testing.T) {
store := newTestStore(t)
id, err := store.BeginScan("arp_discovery")
if err != nil {
t.Fatalf("BeginScan: %v", err)
}
if id == 0 {
t.Error("expected non-zero scan ID")
}
if err := store.EndScan(id, nil); err != nil {
t.Fatalf("EndScan (ok): %v", err)
}
last, err := store.LastScanTime("arp_discovery")
if err != nil {
t.Fatal(err)
}
if last.IsZero() {
t.Error("expected non-zero last scan time after successful run")
}
}
// TestHostsInPrefix validates the subnet host enumeration logic.
// This test lives here because hostsInPrefix is package-internal;
// in a real scenario you would export it for testing or white-box test it.
func TestHostsInPrefixCount(t *testing.T) {
cases := []struct {
cidr string
count int
}{
{"192.168.0.0/24", 254}, // .1 .254
{"10.0.0.0/30", 2}, // .1 and .2 only
{"10.0.0.0/29", 6}, // .1 .6
}
for _, tc := range cases {
t.Run(tc.cidr, func(t *testing.T) {
prefix, err := netip.ParsePrefix(tc.cidr)
if err != nil {
t.Fatal(err)
}
got := hostsInPrefix(prefix.Masked())
if len(got) != tc.count {
t.Errorf("cidr %s: got %d hosts, want %d", tc.cidr, len(got), tc.count)
}
})
}
}
// hostsInPrefix is a copy of the unexported function for white-box testing.
func hostsInPrefix(prefix netip.Prefix) []netip.Addr {
var addrs []netip.Addr
addr := prefix.Masked().Addr().Next()
for prefix.Contains(addr) {
next := addr.Next()
if !prefix.Contains(next) {
break
}
addrs = append(addrs, addr)
addr = next
}
return addrs
}