Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
185 lines
5.6 KiB
Go
185 lines
5.6 KiB
Go
package importcsv_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/v10networks/nmon/internal/clock"
|
|
"github.com/v10networks/nmon/internal/deps"
|
|
"github.com/v10networks/nmon/internal/importcsv"
|
|
"github.com/v10networks/nmon/internal/store"
|
|
"github.com/v10networks/nmon/internal/testutil"
|
|
"github.com/v10networks/nmon/internal/ulid"
|
|
)
|
|
|
|
func newStore(t *testing.T) store.Store {
|
|
t.Helper()
|
|
return testutil.OpenStore(t, clock.NewFake(time.Date(2026, 10, 22, 0, 0, 0, 0, time.UTC)))
|
|
}
|
|
|
|
func build(t *testing.T, st store.Store, devs, ports string) (*importcsv.Plan, error) {
|
|
t.Helper()
|
|
in := importcsv.Inputs{}
|
|
if devs != "" {
|
|
in.Devices = strings.NewReader(devs)
|
|
}
|
|
if ports != "" {
|
|
in.Ports = strings.NewReader(ports)
|
|
}
|
|
return importcsv.Build(context.Background(), st, in)
|
|
}
|
|
|
|
func deviceCount(t *testing.T, st store.Store) int64 {
|
|
t.Helper()
|
|
_, n, err := st.ListDevices(context.Background(), store.DeviceFilter{IncludeRetired: true}, store.Page{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestImportHappyPathCreatesDevicesParentsCredentialsAndAliases(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newStore(t)
|
|
existing := &store.Device{Name: "core", Kind: store.KindNetwork, Enabled: true}
|
|
if err := st.CreateDevice(ctx, existing); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.CreateCredential(ctx, &store.SNMPCredential{Name: "lab-v2", Version: "v2c", Community: "x"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ifID := ulid.New(time.Unix(1700000000, 0))
|
|
if err := st.UpsertInterfaces(ctx, existing.ID, []store.Interface{{ID: ifID, IfName: "ge-0/0/1", IfIndex: 1}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
devs := "Name,Addresses,Site,Role,Kind,SNMP_Version,Community,Credential,Port,Parents\n" +
|
|
"sw1,10.0.0.2;2001:db8::2,pop1,access,,,,lab-v2,1161,core\n" +
|
|
"ap1,10.0.0.3,pop1,ap,,v1,public,,,sw1;core\n" +
|
|
"\n"
|
|
ports := "device,ifname,alias\n" + existing.Slug + ",GE-0/0/1,Customer ACME\n"
|
|
plan, err := build(t, st, devs, ports)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res, err := importcsv.Apply(ctx, st, plan)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.DevicesCreated != 2 || res.CredentialsCreated != 1 || res.PortsUpdated != 1 {
|
|
t.Fatalf("result %+v", res)
|
|
}
|
|
sw, _, err := st.ListDevices(ctx, store.DeviceFilter{Q: "sw1"}, store.Page{})
|
|
if err != nil || len(sw) == 0 {
|
|
t.Fatalf("sw1 missing: %v", err)
|
|
}
|
|
d := sw[0]
|
|
if d.SNMPPort != 1161 || d.SNMPCredentialID == nil || d.Site != "pop1" {
|
|
t.Fatalf("sw1 %+v", d)
|
|
}
|
|
addrs, _ := st.ListAddresses(ctx, d.ID)
|
|
if len(addrs) != 2 {
|
|
t.Fatalf("addresses %v", addrs)
|
|
}
|
|
ps, _ := st.ListParents(ctx, d.ID, store.RelNetwork)
|
|
if len(ps) != 1 || ps[0].ParentID != existing.ID {
|
|
t.Fatalf("sw1 parents %+v", ps)
|
|
}
|
|
ap, _, _ := st.ListDevices(ctx, store.DeviceFilter{Q: "ap1"}, store.Page{})
|
|
aps, _ := st.ListParents(ctx, ap[0].ID, store.RelNetwork)
|
|
if len(aps) != 2 {
|
|
t.Fatalf("ap1 parents %+v", aps)
|
|
}
|
|
ifs, _ := st.ListInterfaces(ctx, existing.ID, false)
|
|
if ifs[0].Description != "Customer ACME" {
|
|
t.Fatalf("description %q", ifs[0].Description)
|
|
}
|
|
}
|
|
|
|
func TestRowErrorsCarryRowNumbersAndNothingIsWritten(t *testing.T) {
|
|
st := newStore(t)
|
|
devs := "name,addresses,kind,port,parents,credential\n" +
|
|
"ok1,10.0.0.1,,,,\n" +
|
|
"bad,not-an-ip,router,70000,ghost,nocred\n" +
|
|
"ok1,10.0.0.9,,,,\n"
|
|
_, err := build(t, st, devs, "")
|
|
var pe *importcsv.PlanError
|
|
if !errors.As(err, &pe) {
|
|
t.Fatalf("want PlanError, got %v", err)
|
|
}
|
|
rows := map[int]int{}
|
|
for _, e := range pe.Errors {
|
|
rows[e.Row]++
|
|
}
|
|
if rows[3] != 5 || rows[4] != 1 || rows[2] != 0 {
|
|
t.Fatalf("errors by row %v: %v", rows, err)
|
|
}
|
|
if !strings.Contains(err.Error(), "devices csv row 3") {
|
|
t.Fatal(err)
|
|
}
|
|
if n := deviceCount(t, st); n != 0 {
|
|
t.Fatalf("%d devices written", n)
|
|
}
|
|
}
|
|
|
|
func TestPortErrors(t *testing.T) {
|
|
st := newStore(t)
|
|
d := &store.Device{Name: "core", Kind: store.KindNetwork, Enabled: true}
|
|
if err := st.CreateDevice(context.Background(), d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := build(t, st, "", "device,ifname,alias\ncore,nope,x\nghost,eth0,y\n")
|
|
var pe *importcsv.PlanError
|
|
if !errors.As(err, &pe) || len(pe.Errors) != 2 || pe.Errors[0].File != importcsv.FilePorts || pe.Errors[1].Row != 3 {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMissingRequiredColumn(t *testing.T) {
|
|
if _, err := build(t, newStore(t), "addresses\n10.0.0.1\n", ""); err == nil || !strings.Contains(err.Error(), `missing column "name"`) {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParentCycleRejectedWithPathAndNothingWritten(t *testing.T) {
|
|
st := newStore(t)
|
|
devs := "name,parents\na,c\nb,a\nc,b\n"
|
|
_, err := build(t, st, devs, "")
|
|
var ce *deps.CycleError
|
|
if !errors.As(err, &ce) {
|
|
t.Fatalf("want cycle, got %v", err)
|
|
}
|
|
if len(ce.Path) != 4 || ce.Path[0] != ce.Path[3] || !strings.Contains(err.Error(), " -> ") {
|
|
t.Fatalf("path %v", ce.Path)
|
|
}
|
|
if n := deviceCount(t, st); n != 0 {
|
|
t.Fatalf("%d devices written", n)
|
|
}
|
|
}
|
|
|
|
func TestSelfParentIsRejected(t *testing.T) {
|
|
st := newStore(t)
|
|
if _, err := build(t, st, "name,parents\nloop,loop\n", ""); err == nil {
|
|
t.Fatal("self parent accepted")
|
|
}
|
|
}
|
|
|
|
func TestApplyFailureRollsEverythingBack(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newStore(t)
|
|
plan, err := build(t, st, "name,addresses\nr1,10.0.0.1\nr2,10.0.0.2\n", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Corrupt the second row after planning so Apply fails after the first device was inserted.
|
|
plan.Devices[1].Kind = "bogus"
|
|
if _, err := importcsv.Apply(ctx, st, plan); err == nil || !strings.Contains(err.Error(), "row 3") {
|
|
t.Fatalf("got %v", err)
|
|
}
|
|
if n := deviceCount(t, st); n != 0 {
|
|
t.Fatalf("%d devices survived a failed import", n)
|
|
}
|
|
}
|