package web import ( "bytes" "context" "encoding/json" "html/template" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/example/glpi-ai-agent/internal/config" "github.com/example/glpi-ai-agent/internal/knowledge" "github.com/example/glpi-ai-agent/internal/metrics" "github.com/example/glpi-ai-agent/internal/model" "github.com/example/glpi-ai-agent/internal/queue" ) func TestExtractTicketID(t *testing.T) { tests := []struct { name string body string want int64 }{ {name: "explicit ticket id", body: `{"ticket_id":42}`, want: 42}, {name: "camel case ticket id", body: `{"ticketId":"43"}`, want: 43}, {name: "typed ticket", body: `{"itemtype":"Ticket","id":44}`, want: 44}, {name: "nested ticket", body: `{"ticket":{"id":45}}`, want: 45}, {name: "ticket url", body: `{"url":"https://glpi.example/api.php/v2.3/Assistance/Ticket/46"}`, want: 46}, {name: "unrelated generic id", body: `{"itemtype":"User","id":99}`, want: 0}, {name: "wrapped unrelated generic id", body: `{"data":{"id":100}}`, want: 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := extractTicketID([]byte(tt.body)); got != tt.want { t.Fatalf("extractTicketID() = %d, want %d", got, tt.want) } }) } } func TestDashboardTemplateParses(t *testing.T) { if _, err := template.ParseFS(files, "templates/dashboard.html"); err != nil { t.Fatal(err) } } type fakeFeedback struct{ cats []model.Category } func (f fakeFeedback) Categories(context.Context) ([]model.Category, error) { return f.cats, nil } func (f fakeFeedback) RecordCategoryFeedback(context.Context, string, int64) (model.LearningExample, error) { return model.LearningExample{}, nil } func (f fakeFeedback) LearningExamples() []model.LearningExample { return nil } func (f fakeFeedback) DeleteLearning(string) error { return os.ErrNotExist } func (f fakeFeedback) LearningCount() int { return 0 } func newKnowledgeTestServer(t *testing.T) (http.Handler, *knowledge.Store) { t.Helper() root := t.TempDir() staticDir := filepath.Join(root, "knowledge") dataDir := filepath.Join(root, "data") if err := os.MkdirAll(staticDir, 0o750); err != nil { t.Fatal(err) } store, err := knowledge.Load(context.Background(), staticDir, dataDir, nil, false, []string{"internal-kb"}) if err != nil { t.Fatal(err) } cfg := config.Config{WebAllowAnonymous: true, KnowledgeWebEditEnabled: true, CommunicationLanguage: "de-DE", CommunicationStyle: "formal", KnowledgeAllowedSources: []string{"internal-kb"}} srv, err := New(cfg, metrics.New(), nil, queue.New(8), store, fakeFeedback{cats: []model.Category{{ID: 2, Name: "Active Directory", CompleteName: "IT > Active Directory"}}}) if err != nil { t.Fatal(err) } return srv.Handler(), store } func mutationRequest(t *testing.T, h http.Handler, method, path string, body any) *httptest.ResponseRecorder { t.Helper() var b bytes.Buffer if body != nil { if err := json.NewEncoder(&b).Encode(body); err != nil { t.Fatal(err) } } req := httptest.NewRequest(method, path, &b) req.Header.Set("X-Requested-With", "GLPI-AI-Agent") if body != nil { req.Header.Set("Content-Type", "application/json") } rr := httptest.NewRecorder() h.ServeHTTP(rr, req) return rr } func TestKnowledgeCreateUpdateDeleteFlow(t *testing.T) { h, store := newKnowledgeTestServer(t) doc := model.KnowledgeDoc{ID: "KB-TEST-1", Title: "Benutzerkonto gesperrt", Text: "Konto ist gesperrt.", Answer: "Bitte versuchen Sie die Anmeldung erneut.", Source: "internal-kb", Language: "de-DE", CommunicationStyle: "formal", MinScore: .7, Categories: []int64{2}} if rr := mutationRequest(t, h, http.MethodPost, "/api/knowledge", doc); rr.Code != http.StatusCreated { t.Fatalf("create = %d: %s", rr.Code, rr.Body.String()) } if _, ok := store.ByID(doc.ID); !ok { t.Fatal("created document missing") } if rr := mutationRequest(t, h, http.MethodPost, "/api/knowledge", doc); rr.Code != http.StatusConflict { t.Fatalf("duplicate create = %d, want 409", rr.Code) } doc.Title = "Benutzerkonto dauerhaft gesperrt" if rr := mutationRequest(t, h, http.MethodPut, "/api/knowledge/KB-TEST-1", doc); rr.Code != http.StatusOK { t.Fatalf("update = %d: %s", rr.Code, rr.Body.String()) } got, ok := store.ByID(doc.ID) if !ok || got.Title != doc.Title { t.Fatalf("updated doc = %#v, ok=%v", got, ok) } renamed := doc renamed.ID = "KB-RENAMED" if rr := mutationRequest(t, h, http.MethodPut, "/api/knowledge/KB-TEST-1", renamed); rr.Code != http.StatusConflict { t.Fatalf("rename update = %d, want 409", rr.Code) } if _, ok := store.ByID("KB-RENAMED"); ok { t.Fatal("rename unexpectedly created a second document") } if rr := mutationRequest(t, h, http.MethodDelete, "/api/knowledge/KB-TEST-1", nil); rr.Code != http.StatusNoContent { t.Fatalf("delete = %d: %s", rr.Code, rr.Body.String()) } if _, ok := store.ByID(doc.ID); ok { t.Fatal("deleted document still present") } } func TestKnowledgeGetReturnsManagedState(t *testing.T) { h, _ := newKnowledgeTestServer(t) doc := model.KnowledgeDoc{ID: "KB-TEST-2", Title: "VPN", Text: "VPN Hilfe", Source: "internal-kb", Language: "de-DE", CommunicationStyle: "formal", MinScore: .7} if rr := mutationRequest(t, h, http.MethodPost, "/api/knowledge", doc); rr.Code != http.StatusCreated { t.Fatalf("create = %d: %s", rr.Code, rr.Body.String()) } rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/api/knowledge/KB-TEST-2", nil)) if rr.Code != http.StatusOK { t.Fatalf("get = %d: %s", rr.Code, rr.Body.String()) } var got struct { Managed bool `json:"managed"` Document model.KnowledgeDoc `json:"document"` } if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil { t.Fatal(err) } if !got.Managed || got.Document.ID != doc.ID { t.Fatalf("unexpected get response: %#v", got) } } func TestDashboardContainsControlCenterSections(t *testing.T) { h, _ := newKnowledgeTestServer(t) rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil)) if rr.Code != http.StatusOK { t.Fatalf("dashboard = %d: %s", rr.Code, rr.Body.String()) } body := rr.Body.String() for _, want := range []string{"Control Center", "Verarbeitungen", "Knowledge Base", "Effektive Konfiguration", "Knowledge-Ranking"} { if !bytes.Contains([]byte(body), []byte(want)) { t.Fatalf("dashboard missing %q", want) } } }