package beachcomber import ( "context" "errors" "reflect" "testing" ) // idsOnlyEngine implements Engine without PageSearcher. type idsOnlyEngine struct { ids []string err error got Query } func (*idsOnlyEngine) Name() string { return "ids-only" } func (*idsOnlyEngine) Configured() bool { return true } func (*idsOnlyEngine) Upsert(context.Context, string, map[string]any, []map[string]any) error { return nil } func (*idsOnlyEngine) Delete(context.Context, string, []string) error { return nil } func (*idsOnlyEngine) Flush(context.Context, string) error { return nil } func (e *idsOnlyEngine) SearchIDs(_ context.Context, _ string, q Query) ([]string, error) { e.got = q return e.ids, e.err } // pagedEngine implements PageSearcher. type pagedEngine struct { idsOnlyEngine found int } func (e *pagedEngine) SearchPage(_ context.Context, _ string, q Query) (SearchResult, error) { e.got = q return SearchResult{IDs: e.ids, Found: e.found}, nil } func TestSearchPageFallsBackToSearchIDs(t *testing.T) { e := &idsOnlyEngine{ids: []string{"3", "1"}} q := Query{Q: "blue", QueryBy: []string{"name", "notes"}, QueryByWeights: []int{10, 1}, PerPage: 20} res, err := SearchPage(context.Background(), e, "posts", q) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(res, SearchResult{IDs: []string{"3", "1"}, Found: 2, Hits: []Hit{{ID: "3"}, {ID: "1"}}}) { t.Fatalf("res = %+v", res) } if !reflect.DeepEqual(e.got, q) { t.Fatalf("query = %+v", e.got) } e.err = errors.New("down") if _, err := SearchPage(context.Background(), e, "posts", q); err == nil { t.Fatal("engine error must surface") } } func TestSearchPageUsesPageSearcher(t *testing.T) { e := &pagedEngine{idsOnlyEngine: idsOnlyEngine{ids: []string{"7"}}, found: 1234} res, err := SearchPage(context.Background(), e, "posts", Query{Q: "x"}) if err != nil { t.Fatal(err) } if res.Found != 1234 || !reflect.DeepEqual(res.IDs, []string{"7"}) { t.Fatalf("res = %+v", res) } } func TestSearchPageNullEngine(t *testing.T) { var e Engine = nullEngine{} if _, ok := e.(PageSearcher); !ok { t.Fatal("the null engine implements PageSearcher") } res, err := SearchPage(context.Background(), e, "posts", Query{}) if err != nil || res.Found != 0 || res.IDs == nil || len(res.IDs) != 0 || res.Hits == nil || len(res.Hits) != 0 { t.Fatalf("res = %+v err = %v", res, err) } } // highlightEngine implements HighlightSearcher with a configurable answer // and can return its own hits. type highlightEngine struct { pagedEngine supports bool hits []Hit } func (e *highlightEngine) SupportsHighlights() bool { return e.supports } func (e *highlightEngine) SearchPage(_ context.Context, _ string, q Query) (SearchResult, error) { e.got = q return SearchResult{IDs: e.ids, Found: e.found, Hits: e.hits}, nil } func TestSupportsHighlights(t *testing.T) { for name, e := range map[string]Engine{ "null": nullEngine{}, "ids-only": &idsOnlyEngine{}, "paged": &pagedEngine{}, "nil": nil, } { if SupportsHighlights(e) { t.Fatalf("%s: SupportsHighlights = true", name) } } if !SupportsHighlights(&highlightEngine{supports: true}) { t.Fatal("an engine answering true supports highlights") } if SupportsHighlights(&highlightEngine{supports: false}) { t.Fatal("an engine answering false does not support highlights") } } func TestSearchPageFillsHits(t *testing.T) { ctx := context.Background() res, err := SearchPage(ctx, &pagedEngine{idsOnlyEngine: idsOnlyEngine{ids: []string{"7"}}, found: 1}, "posts", Query{}) if err != nil || !reflect.DeepEqual(res.Hits, []Hit{{ID: "7"}}) { t.Fatalf("hits = %+v err = %v", res.Hits, err) } own := []Hit{{ID: "7", Highlights: []Snippet{{Field: "title", ArrayIndex: -1, Segments: []Segment{{Text: "x", Match: true}}}}}} he := &highlightEngine{pagedEngine: pagedEngine{idsOnlyEngine: idsOnlyEngine{ids: []string{"7"}}}, supports: true, hits: own} res, err = SearchPage(ctx, he, "posts", Query{Highlight: &Highlight{}}) if err != nil || !reflect.DeepEqual(res.Hits, own) { t.Fatalf("own hits = %+v err = %v", res.Hits, err) } res, err = SearchPage(ctx, &pagedEngine{}, "posts", Query{}) if err != nil || res.Hits == nil || len(res.Hits) != 0 { t.Fatalf("zero ids: hits = %#v err = %v", res.Hits, err) } } // flushEngine records Flush calls and can fail them. type flushEngine struct { idsOnlyEngine flushed []string err error } func (e *flushEngine) Flush(_ context.Context, index string) error { e.flushed = append(e.flushed, index) return e.err } // dropperEngine implements IndexDropper and IndexEnsurer. type dropperEngine struct { flushEngine existing map[string]bool ensured []string } func (e *dropperEngine) DropIndex(_ context.Context, index string) (bool, error) { existed := e.existing[index] delete(e.existing, index) return existed, nil } func (e *dropperEngine) EnsureIndex(_ context.Context, index string, _ map[string]any) error { e.ensured = append(e.ensured, index) e.existing[index] = true return nil } func TestDropIndex(t *testing.T) { ctx := context.Background() fallback := &flushEngine{} existed, err := DropIndex(ctx, fallback, "albums") if err != nil || !existed || !reflect.DeepEqual(fallback.flushed, []string{"albums"}) { t.Fatalf("fallback: existed=%v err=%v flushed=%v", existed, err, fallback.flushed) } boom := errors.New("boom") failing := &flushEngine{err: boom} if existed, err := DropIndex(ctx, failing, "albums"); !errors.Is(err, boom) || existed { t.Fatalf("fallback error: existed=%v err=%v", existed, err) } d := &dropperEngine{existing: map[string]bool{"albums": true}} if existed, err := DropIndex(ctx, d, "albums"); err != nil || !existed { t.Fatalf("dropper first: existed=%v err=%v", existed, err) } if existed, err := DropIndex(ctx, d, "albums"); err != nil || existed { t.Fatalf("dropper second: existed=%v err=%v, want already absent", existed, err) } if len(d.flushed) != 0 { t.Fatalf("dropper path must not call Flush: %v", d.flushed) } if err := EnsureIndex(ctx, d, "albums", map[string]any{"fields": []any{}}); err != nil || !reflect.DeepEqual(d.ensured, []string{"albums"}) { t.Fatalf("EnsureIndex: err=%v ensured=%v", err, d.ensured) } if err := EnsureIndex(ctx, fallback, "albums", nil); err != nil { t.Fatalf("EnsureIndex without IndexEnsurer must be a no-op: %v", err) } if err := EnsureIndex(ctx, nullEngine{}, "albums", nil); err != nil { t.Fatalf("null engine: %v", err) } }