262 lines
7.1 KiB
Go
262 lines
7.1 KiB
Go
package netbox
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// newTestServer returns an httptest.Server that serves fixed responses per path.
|
|
func newTestServer(t *testing.T, handlers map[string]any) *httptest.Server {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
for path, body := range handlers {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatalf("marshalling handler for %s: %v", path, err)
|
|
}
|
|
captured := b
|
|
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(captured)
|
|
})
|
|
}
|
|
return httptest.NewServer(mux)
|
|
}
|
|
|
|
func deviceListResponse(devices ...netboxDevice) netboxListResponse[netboxDevice] {
|
|
return netboxListResponse[netboxDevice]{Count: len(devices), Results: devices}
|
|
}
|
|
|
|
func vmListResponse(vms ...netboxVM) netboxListResponse[netboxVM] {
|
|
return netboxListResponse[netboxVM]{Count: len(vms), Results: vms}
|
|
}
|
|
|
|
func ipListResponse(addrs ...string) netboxIPListResponse {
|
|
resp := netboxIPListResponse{Count: len(addrs)}
|
|
for _, a := range addrs {
|
|
resp.Results = append(resp.Results, struct {
|
|
Address string `json:"address"`
|
|
Interface *struct {
|
|
Name string `json:"name"`
|
|
} `json:"assigned_object"`
|
|
}{Address: a})
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func TestSearch_ReturnsBothDevicesAndVMs(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/dcim/devices/": deviceListResponse(
|
|
netboxDevice{ID: 1, Name: "router-01", PrimaryIP4: &netboxIP{Address: "10.0.0.1/24"}},
|
|
),
|
|
"/api/virtualization/virtual-machines/": vmListResponse(
|
|
netboxVM{ID: 2, Name: "vm-01", PrimaryIP4: &netboxIP{Address: "10.0.0.2/24"}},
|
|
),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
results, err := c.Search(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("got %d results, want 2", len(results))
|
|
}
|
|
|
|
names := map[string]bool{}
|
|
for _, r := range results {
|
|
names[r.Name] = true
|
|
}
|
|
if !names["router-01"] || !names["vm-01"] {
|
|
t.Errorf("missing expected hosts in results: %v", names)
|
|
}
|
|
}
|
|
|
|
func TestSearch_MapsKindCorrectly(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/dcim/devices/": deviceListResponse(
|
|
netboxDevice{ID: 1, Name: "sw-01"},
|
|
),
|
|
"/api/virtualization/virtual-machines/": vmListResponse(
|
|
netboxVM{ID: 2, Name: "vm-01"},
|
|
),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
results, _ := c.Search(context.Background(), "")
|
|
|
|
for _, r := range results {
|
|
switch r.Name {
|
|
case "sw-01":
|
|
if r.Kind != "device" {
|
|
t.Errorf("sw-01 kind: got %q, want %q", r.Kind, "device")
|
|
}
|
|
case "vm-01":
|
|
if r.Kind != "vm" {
|
|
t.Errorf("vm-01 kind: got %q, want %q", r.Kind, "vm")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSearch_StripsPrefixFromPrimaryIP(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/dcim/devices/": deviceListResponse(
|
|
netboxDevice{ID: 1, Name: "host", PrimaryIP4: &netboxIP{Address: "192.168.1.10/24"}},
|
|
),
|
|
"/api/virtualization/virtual-machines/": vmListResponse(),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
results, _ := c.Search(context.Background(), "host")
|
|
if len(results) == 0 {
|
|
t.Fatal("expected at least one result")
|
|
}
|
|
if results[0].PrimaryIP4 != "192.168.1.10" {
|
|
t.Errorf("PrimaryIP4: got %q, want %q", results[0].PrimaryIP4, "192.168.1.10")
|
|
}
|
|
}
|
|
|
|
func TestSearch_TagsAreMapped(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/dcim/devices/": deviceListResponse(
|
|
netboxDevice{
|
|
ID: 1,
|
|
Name: "host",
|
|
Tags: []struct {
|
|
Name string `json:"name"`
|
|
}{{Name: "prod"}, {Name: "mgmt"}},
|
|
},
|
|
),
|
|
"/api/virtualization/virtual-machines/": vmListResponse(),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
results, _ := c.Search(context.Background(), "")
|
|
if len(results[0].Tags) != 2 {
|
|
t.Errorf("tags: got %v, want [prod mgmt]", results[0].Tags)
|
|
}
|
|
}
|
|
|
|
func TestSearch_PartialFailure_ReturnsAvailableResults(t *testing.T) {
|
|
// Only devices endpoint works; VMs returns 500.
|
|
mux := http.NewServeMux()
|
|
body, _ := json.Marshal(deviceListResponse(netboxDevice{ID: 1, Name: "sw-01"}))
|
|
mux.HandleFunc("/api/dcim/devices/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(body)
|
|
})
|
|
mux.HandleFunc("/api/virtualization/virtual-machines/", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
results, err := c.Search(context.Background(), "")
|
|
if err != nil {
|
|
t.Fatalf("partial failure should not return error, got: %v", err)
|
|
}
|
|
if len(results) != 1 || results[0].Name != "sw-01" {
|
|
t.Errorf("expected device results, got %v", results)
|
|
}
|
|
}
|
|
|
|
func TestSearch_BothFail_ReturnsError(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "error", http.StatusInternalServerError)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
_, err := c.Search(context.Background(), "")
|
|
if err == nil {
|
|
t.Error("both endpoints failing should return an error")
|
|
}
|
|
}
|
|
|
|
func TestGetIPs_Device(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/ipam/ip-addresses/": ipListResponse("10.0.0.1/24", "10.0.0.2/24"),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
ips, err := c.GetIPs(context.Background(), HostEntry{ID: 1, Kind: "device"})
|
|
if err != nil {
|
|
t.Fatalf("GetIPs: %v", err)
|
|
}
|
|
if len(ips) != 2 {
|
|
t.Errorf("got %d IPs, want 2", len(ips))
|
|
}
|
|
if ips[0] != "10.0.0.1" || ips[1] != "10.0.0.2" {
|
|
t.Errorf("IPs: got %v, want [10.0.0.1 10.0.0.2]", ips)
|
|
}
|
|
}
|
|
|
|
func TestGetIPs_VM(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/ipam/ip-addresses/": ipListResponse("172.16.0.5/16"),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
ips, err := c.GetIPs(context.Background(), HostEntry{ID: 2, Kind: "vm"})
|
|
if err != nil {
|
|
t.Fatalf("GetIPs: %v", err)
|
|
}
|
|
if len(ips) != 1 || ips[0] != "172.16.0.5" {
|
|
t.Errorf("IPs: got %v, want [172.16.0.5]", ips)
|
|
}
|
|
}
|
|
|
|
func TestGetIPs_UnknownKind(t *testing.T) {
|
|
c := NewClient("http://localhost", "token")
|
|
_, err := c.GetIPs(context.Background(), HostEntry{ID: 1, Kind: "unknown"})
|
|
if err == nil {
|
|
t.Error("unknown kind should return an error")
|
|
}
|
|
}
|
|
|
|
func TestGetIPsWithFilter(t *testing.T) {
|
|
srv := newTestServer(t, map[string]any{
|
|
"/api/ipam/ip-addresses/": ipListResponse("10.10.10.1/24"),
|
|
})
|
|
defer srv.Close()
|
|
|
|
c := NewClient(srv.URL, "token")
|
|
ips, err := c.GetIPsWithFilter(context.Background(), "device_id=1&interface_name=mgmt0")
|
|
if err != nil {
|
|
t.Fatalf("GetIPsWithFilter: %v", err)
|
|
}
|
|
if len(ips) != 1 || ips[0] != "10.10.10.1" {
|
|
t.Errorf("IPs: got %v, want [10.10.10.1]", ips)
|
|
}
|
|
}
|
|
|
|
func TestStripPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
}{
|
|
{"10.0.0.1/24", "10.0.0.1"},
|
|
{"::1/128", "::1"},
|
|
{"192.168.1.1", "192.168.1.1"}, // no prefix — unchanged
|
|
{"", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := stripPrefix(tt.in); got != tt.want {
|
|
t.Errorf("stripPrefix(%q) = %q, want %q", tt.in, got, tt.want)
|
|
}
|
|
}
|
|
}
|