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") } }