package handlers_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "git.georgsen.dk/hwlab/internal/api/handlers" "git.georgsen.dk/hwlab/internal/netbox" ) // mockNetBoxClient satisfies the SearchNetBoxClient interface. type mockNetBoxClient struct { devices []netbox.Device err error } func (m *mockNetBoxClient) ListDevices(_ context.Context, _ int) ([]netbox.Device, error) { return m.devices, m.err } // mockAIClient returns canned JSON for TextComplete. type mockAIClient struct { response string err error } func (m *mockAIClient) TextComplete(_ context.Context, _ string) (string, error) { return m.response, m.err } func testDevices() []netbox.Device { return []netbox.Device{ {ID: 1, Name: "10GbE NIC Intel X550", CustomFields: netbox.CustomFields{CatalogStatus: "available", HWID: "HW-0001"}}, {ID: 2, Name: "Raspberry Pi 4B", CustomFields: netbox.CustomFields{CatalogStatus: "draft", HWID: "HW-0002"}}, {ID: 3, Name: "10GbE NIC Mellanox", CustomFields: netbox.CustomFields{CatalogStatus: "complete", HWID: "HW-0003"}}, {ID: 4, Name: "USB-C Hub", CustomFields: netbox.CustomFields{CatalogStatus: "available", HWID: "HW-0004"}}, } } // TestSearch_EmptyQ verifies that missing q parameter returns 400. func TestSearch_EmptyQ(t *testing.T) { h := handlers.NewSearchHandler(&mockNetBoxClient{}, &mockAIClient{}) req := httptest.NewRequest(http.MethodGet, "/api/search", nil) w := httptest.NewRecorder() h.SearchDevices(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } var body map[string]string if err := json.NewDecoder(w.Body).Decode(&body); err != nil { t.Fatalf("decode: %v", err) } if !strings.Contains(body["error"], "q parameter") { t.Errorf("expected error about q parameter, got %q", body["error"]) } } // TestSearch_LLMParseFallback verifies that when LLM returns unparseable JSON, // the handler falls back to name substring match against the raw query. func TestSearch_LLMParseFallback(t *testing.T) { nb := &mockNetBoxClient{devices: testDevices()} ai := &mockAIClient{response: "not valid json {{ broken"} h := handlers.NewSearchHandler(nb, ai) req := httptest.NewRequest(http.MethodGet, "/api/search?q=NIC", nil) w := httptest.NewRecorder() h.SearchDevices(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String()) } var items []map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&items); err != nil { t.Fatalf("decode: %v", err) } // "NIC" substring matches devices 1 and 3 (both contain "NIC") if len(items) != 2 { t.Errorf("expected 2 NIC results, got %d: %v", len(items), items) } } // TestSearch_CatalogStatusFilter verifies that LLM-extracted catalog_status filters results. func TestSearch_CatalogStatusFilter(t *testing.T) { nb := &mockNetBoxClient{devices: testDevices()} aiResp := `{"catalog_status": "available", "name_contains": "", "tag": ""}` ai := &mockAIClient{response: aiResp} h := handlers.NewSearchHandler(nb, ai) req := httptest.NewRequest(http.MethodGet, "/api/search?q=show+me+available+items", nil) w := httptest.NewRecorder() h.SearchDevices(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String()) } var items []map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&items); err != nil { t.Fatalf("decode: %v", err) } // Devices 1 and 4 have catalog_status "available" if len(items) != 2 { t.Errorf("expected 2 available items, got %d", len(items)) } for _, item := range items { if item["catalog_status"] != "available" { t.Errorf("unexpected catalog_status: %v", item["catalog_status"]) } } } // TestSearch_NameContainsAndStatus verifies combined filtering works. func TestSearch_NameContainsAndStatus(t *testing.T) { nb := &mockNetBoxClient{devices: testDevices()} aiResp := `{"catalog_status": "available", "name_contains": "NIC", "tag": ""}` ai := &mockAIClient{response: aiResp} h := handlers.NewSearchHandler(nb, ai) req := httptest.NewRequest(http.MethodGet, "/api/search?q=available+NICs", nil) w := httptest.NewRecorder() h.SearchDevices(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var items []map[string]interface{} if err := json.NewDecoder(w.Body).Decode(&items); err != nil { t.Fatalf("decode: %v", err) } // Only device 1: NIC + available (device 3 is "complete") if len(items) != 1 { t.Errorf("expected 1 result, got %d", len(items)) } }