- CableRecord type added to types.go (ID, HWID, Label, TestData, CatalogStatus) - CreateCable(ctx, label, assetTag, testDataJSON) uses DcimCablesCreate - Sets test_data and catalog_status custom fields; hw_id if assetTag non-empty - Rejects empty label with sentinel error message - Unit tests use httptest.NewUnstartedServer (201 success, 422 error, empty label)
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
package netbox_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.georgsen.dk/hwlab/internal/netbox"
|
|
)
|
|
|
|
func TestNewClientValidation(t *testing.T) {
|
|
_, err := netbox.NewClient("", "token")
|
|
if err == nil {
|
|
t.Error("expected error for empty url")
|
|
}
|
|
|
|
_, err = netbox.NewClient("http://10.5.0.130:8000/api", "")
|
|
if err == nil {
|
|
t.Error("expected error for empty token")
|
|
}
|
|
|
|
c, err := netbox.NewClient("http://10.5.0.130:8000/api", "sometoken")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if c == nil {
|
|
t.Error("expected non-nil client")
|
|
}
|
|
}
|
|
|
|
// integrationToken returns the real NetBox token from env, or skips the test
|
|
// if only the placeholder is present (placeholder is never 40 hex chars).
|
|
func integrationToken(t *testing.T) string {
|
|
t.Helper()
|
|
token := os.Getenv("HWLAB_NETBOX_TOKEN")
|
|
if len(token) != 40 {
|
|
t.Skip("HWLAB_NETBOX_TOKEN is not a real 40-char token — skipping integration test")
|
|
}
|
|
return token
|
|
}
|
|
|
|
func TestPingLive(t *testing.T) {
|
|
token := integrationToken(t)
|
|
c, err := netbox.NewClient("http://10.5.0.130:8000/api", token)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
if err := c.Ping(context.Background()); err != nil {
|
|
t.Fatalf("Ping: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListDevicesLive(t *testing.T) {
|
|
token := integrationToken(t)
|
|
c, _ := netbox.NewClient("http://10.5.0.130:8000/api", token)
|
|
devices, err := c.ListDevices(context.Background(), 5)
|
|
if err != nil {
|
|
t.Fatalf("ListDevices: %v", err)
|
|
}
|
|
t.Logf("found %d devices in NetBox", len(devices))
|
|
// Not asserting count — NetBox may be empty; just assert no error
|
|
}
|
|
|
|
// TestCreateDeviceValidation verifies that CreateDevice rejects an empty name
|
|
// without making any NetBox API call.
|
|
func TestCreateDeviceValidation(t *testing.T) {
|
|
c, err := netbox.NewClient("http://10.5.0.130:8000/api", "sometoken")
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
_, err = c.CreateDevice(context.Background(), "", "HW-00001", 1, 1, 1)
|
|
if err == nil {
|
|
t.Error("expected error for empty device name")
|
|
}
|
|
}
|
|
|
|
// TestCreateDeviceLive creates a real device in NetBox and deletes it as cleanup.
|
|
// Requires HWLAB_NETBOX_TOKEN (40 chars) and HWLAB_TEST_SITE_ID to be set.
|
|
func TestCreateDeviceLive(t *testing.T) {
|
|
token := integrationToken(t)
|
|
siteIDStr := os.Getenv("HWLAB_TEST_SITE_ID")
|
|
if siteIDStr == "" {
|
|
t.Skip("HWLAB_TEST_SITE_ID not set — skipping live CreateDevice test")
|
|
}
|
|
|
|
// Parse site ID
|
|
var siteID int32
|
|
if _, err := fmt.Sscanf(siteIDStr, "%d", &siteID); err != nil {
|
|
t.Fatalf("HWLAB_TEST_SITE_ID is not a valid int: %v", err)
|
|
}
|
|
|
|
// These must exist in the NetBox instance used for integration testing.
|
|
// Device type 1 and role 1 are typically pre-provisioned.
|
|
const (
|
|
testDeviceTypeID int32 = 1
|
|
testRoleID int32 = 1
|
|
)
|
|
|
|
c, err := netbox.NewClient("http://10.5.0.130:8000/api", token)
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
|
|
id, err := c.CreateDevice(context.Background(), "hwlab-test-device", "HW-TEST-01", testDeviceTypeID, testRoleID, siteID)
|
|
if err != nil {
|
|
t.Fatalf("CreateDevice: %v", err)
|
|
}
|
|
if id <= 0 {
|
|
t.Errorf("expected positive device ID, got %d", id)
|
|
}
|
|
t.Logf("created device id=%d — will clean up", id)
|
|
|
|
// Cleanup: delete the test device
|
|
if err := c.DeleteDevice(context.Background(), id); err != nil {
|
|
t.Logf("cleanup warning: DeleteDevice(%d): %v", id, err)
|
|
}
|
|
}
|
|
|
|
// TestCreateCable_EmptyLabel verifies that CreateCable rejects an empty label
|
|
// without making any NetBox API call.
|
|
func TestCreateCable_EmptyLabel(t *testing.T) {
|
|
c, err := netbox.NewClient("http://localhost:8001", "token")
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
_, err = c.CreateCable(context.Background(), "", "HW-00001", `{"cable_type":0}`)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty label, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "cable label must not be empty") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCreateCable_Success verifies that CreateCable returns a positive ID on 201.
|
|
// Uses httptest.NewUnstartedServer so srv.URL is accessible inside the handler closure.
|
|
func TestCreateCable_Success(t *testing.T) {
|
|
var srvURL string
|
|
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPost && r.URL.Path == "/api/dcim/cables/" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
// Cable model requires id, url, and display fields.
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"id": 42,
|
|
"url": srvURL + "/api/dcim/cables/42/",
|
|
"display": "USB-C 2m Cable",
|
|
"label": "USB-C 2m Cable",
|
|
})
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
srv.Start()
|
|
srvURL = srv.URL
|
|
defer srv.Close()
|
|
|
|
c, err := netbox.NewClient(srv.URL, "token")
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
id, err := c.CreateCable(context.Background(), "USB-C 2m Cable", "HW-00042", `{"cable_type":0}`)
|
|
if err != nil {
|
|
t.Fatalf("CreateCable: %v", err)
|
|
}
|
|
if id <= 0 {
|
|
t.Fatalf("expected id > 0, got %d", id)
|
|
}
|
|
}
|
|
|
|
// TestCreateCable_UnprocessableEntity verifies that a 422 from NetBox returns an error.
|
|
func TestCreateCable_UnprocessableEntity(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPost && r.URL.Path == "/api/dcim/cables/" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"label": "This field may not be blank."})
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c, err := netbox.NewClient(srv.URL, "token")
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
_, err = c.CreateCable(context.Background(), "USB-C 2m Cable", "HW-00099", `{}`)
|
|
if err == nil {
|
|
t.Fatal("expected error on 422, got nil")
|
|
}
|
|
}
|