Files
nmon/internal/backup/helpers_test.go
Vibe Coder c4d14a67d1 Add the optional IEEE vendor registry for MAC addresses and the l2/oui settings
The owner asked to see the vendor of each MAC where the IEEE registry can be
fetched, and never to depend on it. internal/oui downloads MA-L, MA-M and MA-S
weekly through the outbound allow-list (timeouts, a size cap and a row-count
sanity check, so a truncated or wrong download keeps the previous file), stores
one checksummed, atomically written file in <data-dir>/oui that nmon backup
includes, and looks MACs up by the longest prefix. A missing registry only
leaves the vendor empty. `nmon oui import DIR|FILE` loads the CSVs offline, and
the Ansible role can ship them next to the MIB sidecar.

internal/hostaddr parses the MAC, prefix, IP and CIDR forms the lookup accepts
and classifies a MAC as global, local or multicast. config gains the l2 and oui
sections (validated here; the L2 job that reads l2 follows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
2026-10-24 01:30:11 +00:00

138 lines
4.0 KiB
Go

package backup
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync"
"testing"
"time"
"github.com/v10networks/nmon/internal/clock"
"github.com/v10networks/nmon/internal/secrets"
"github.com/v10networks/nmon/internal/store"
)
type fixture struct {
t *testing.T
dir string
keyFile string
clk *clock.Fake
st store.Store
}
// newFixture builds a data dir with a migrated, populated SQLite inventory, a key file, config and a profile.
func newFixture(t *testing.T) *fixture {
t.Helper()
dir := t.TempDir()
keyFile := filepath.Join(t.TempDir(), "secret.key")
if err := secrets.GenerateKeyFile(keyFile); err != nil {
t.Fatal(err)
}
kr, err := secrets.LoadKeyring(keyFile)
if err != nil {
t.Fatal(err)
}
clk := clock.NewFake(time.Date(2026, 10, 22, 12, 0, 0, 0, time.UTC))
ctx := context.Background()
st, err := store.Open(ctx, store.DriverSQLite, filepath.Join(dir, "inventory.db"), kr, clk)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate(ctx); err != nil {
t.Fatal(err)
}
for _, n := range []string{"r1", "r2", "r3"} {
if err := st.CreateDevice(ctx, &store.Device{Name: n, Kind: store.KindNetwork, Enabled: true}); err != nil {
t.Fatal(err)
}
}
if err := st.PutSecret(ctx, "proxmox", "tok"); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(dir, "config.yaml"), "timezone: UTC\n")
writeFile(t, filepath.Join(dir, "profiles", "custom.yaml"), "name: custom\n")
writeFile(t, filepath.Join(dir, "oui", "registry.gz"), "vendor registry bytes")
return &fixture{t: t, dir: dir, keyFile: keyFile, clk: clk, st: st}
}
func (f *fixture) options(out string) Options {
return Options{DataDir: f.dir, KeyFile: f.keyFile, OutPath: out, Store: f.st, Clock: f.clk}
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
// fakeVM answers the parts of the VictoriaMetrics API that backup uses and records snapshot lifecycle.
type fakeVM struct {
*httptest.Server
mu sync.Mutex
dataDir string
created int
deleted []string
failSnap bool
}
func newFakeVM(t *testing.T, dataDir string) *fakeVM {
t.Helper()
v := &fakeVM{dataDir: dataDir}
mux := http.NewServeMux()
mux.HandleFunc("/snapshot/create", func(w http.ResponseWriter, _ *http.Request) {
v.mu.Lock()
defer v.mu.Unlock()
if v.failSnap {
_ = json.NewEncoder(w).Encode(map[string]string{"status": "error", "msg": "boom"})
return
}
v.created++
name := "snap-1"
writeFile(t, filepath.Join(dataDir, "snapshots", name, "data", "part-1"), "samples")
writeFile(t, filepath.Join(dataDir, "snapshots", name, "indexdb", "idx-1"), "index")
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok", "snapshot": name})
})
mux.HandleFunc("/snapshot/delete", func(w http.ResponseWriter, r *http.Request) {
v.mu.Lock()
v.deleted = append(v.deleted, r.URL.Query().Get("snapshot"))
v.mu.Unlock()
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("/api/v1/series", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"status":"success","data":[{"__name__":"m","device":"d1"}]}`))
})
mux.HandleFunc("/api/v1/export", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"metric":{"__name__":"m"},"values":[1,2],"timestamps":[1000,2000]}` + "\n"))
})
v.Server = httptest.NewServer(mux)
t.Cleanup(v.Close)
return v
}
// stubVerifier records what Restore asked it to check.
type stubVerifier struct {
called bool
stores []StoreManifest
err error
seen map[string]bool
}
func (s *stubVerifier) VerifySeries(_ context.Context, dataDir string, _ time.Time, stores []StoreManifest) error {
s.called, s.stores = true, stores
s.seen = map[string]bool{}
for _, st := range stores {
_, err := os.Stat(filepath.Join(dataDir, "vm-"+st.Name, "data", "part-1"))
s.seen[st.Name] = err == nil
}
return s.err
}