package classes_test import ( "context" "errors" "net/http" "slices" "strings" "testing" "git.golem15.com/jakub/sm-bm-plugin/classes" "git.golem15.com/jakub/sm-bm-plugin/internal/searchtest" ) func TestMaterialSearchGate(t *testing.T) { gate := classes.MaterialSearchGate() if !gate.Enabled(classes.MarkMaterialSearch(context.Background()), nil) { t.Error("a marked context is off") } if gate.Enabled(context.Background(), nil) { t.Error("an unmarked context (a chat message sync) is on") } } func TestMaterialSearchRequest(t *testing.T) { t.Run("unavailable", materialSearchUnavailable) stub := &searchtest.Stub{Hits: []searchtest.Hit{ {ID: "12", Field: "body", Snippet: "o " + searchtest.HighlightStart + "globalizacji" + searchtest.HighlightEnd + " dziś"}, {ID: "abc"}, {ID: "0"}, {ID: "5"}, }} ix := classes.NewMaterialIndexer(searchtest.App(t, searchtest.Start(t, stub)...)) got, err := ix.Search(context.Background(), classes.MaterialSearch{Q: "globalizacja", CourseID: 7, MeetingID: 3, Type: "pdf", Tag: "globalizacja"}) if err != nil { t.Fatalf("Search: %v", err) } searches := stub.Searches() if len(searches) != 1 { t.Fatalf("searches = %d, want 1", len(searches)) } q := searches[0].Query want := map[string]string{ "q": "globalizacja", "query_by": "title,tags,source,course,url,body,file_texts", "query_by_weights": "6,4,3,3,1,2,1", "filter_by": "course_id:=7 && meeting_id:=3 && type:=`pdf` && tag_slugs:=`globalizacja`", "sort_by": "_text_match:desc,published_at:desc", "page": "1", "per_page": "250", "exclude_fields": "body,file_texts", "highlight_fields": "body,file_texts", } for k, v := range want { if q.Get(k) != v { t.Errorf("%s = %q, want %q", k, q.Get(k), v) } } if !slices.Equal(got.IDs, []uint{12, 5}) { t.Errorf("ids = %v, want [12 5]", got.IDs) } if !got.Highlights { t.Error("Highlights is false against the typesense engine") } if h := got.Hits[12]; len(h.Highlights) != 1 || h.Highlights[0].Field != "body" { t.Errorf("hit 12 = %+v", h) } else if segs := h.Highlights[0].Segments; len(segs) != 3 || segs[1].Text != "globalizacji" || !segs[1].Match { t.Errorf("hit 12 segments = %+v", segs) } if _, ok := got.Hits[5]; !ok { t.Error("hit 5 is missing") } if len(got.Hits) != 2 { t.Errorf("hits = %v, want only 12 and 5", got.Hits) } // A tag that is not a plain slug and an unknown type stay out of the // engine filter; SQL applies them. if _, err := ix.Search(context.Background(), classes.MaterialSearch{Q: "xx", Tag: "x`y", Type: "exe"}); err != nil { t.Fatal(err) } searches = stub.Searches() if f := searches[len(searches)-1].Query.Get("filter_by"); f != "" { t.Errorf("filter_by = %q, want empty", f) } if _, err := ix.Search(context.Background(), classes.MaterialSearch{Q: "xx", Tag: "zła-Wielka"}); err != nil { t.Fatal(err) } searches = stub.Searches() if f := searches[len(searches)-1].Query.Get("filter_by"); strings.Contains(f, "tag_slugs") { t.Errorf("filter_by = %q carries a tag that is not a slug", f) } } // materialSearchUnavailable runs under TestMaterialSearchRequest. func materialSearchUnavailable(t *testing.T) { // No key: the engine is not configured and nothing is sent. stub := &searchtest.Stub{} env := searchtest.Start(t, stub) noKey := slices.DeleteFunc(slices.Clone(env), func(s string) bool { return strings.Contains(s, "API_KEY") }) ix := classes.NewMaterialIndexer(searchtest.App(t, noKey...)) if _, err := ix.Search(context.Background(), classes.MaterialSearch{Q: "xx"}); !errors.Is(err, classes.ErrSearchUnavailable) { t.Errorf("unconfigured engine: err = %v, want ErrSearchUnavailable", err) } if n := len(stub.Requests()); n != 0 { t.Errorf("unconfigured engine sent %d requests", n) } failing := &searchtest.Stub{Status: http.StatusInternalServerError} ix = classes.NewMaterialIndexer(searchtest.App(t, searchtest.Start(t, failing)...)) if _, err := ix.Search(context.Background(), classes.MaterialSearch{Q: "xx"}); !errors.Is(err, classes.ErrSearchUnavailable) { t.Errorf("failing engine: err = %v, want ErrSearchUnavailable", err) } var none *classes.MaterialIndexer if _, err := none.Search(context.Background(), classes.MaterialSearch{Q: "xx"}); !errors.Is(err, classes.ErrSearchUnavailable) { t.Errorf("nil indexer: err = %v, want ErrSearchUnavailable", err) } none.Sync(context.Background(), nil, 1) none.RemoveAfterCommit(context.Background(), nil, 1) }