feat: search — GET /search.json with grouped hits
Client.Search(term, page) url-encodes the term (operators like @user / #category / order:latest pass through) and decodes the four hit groups Discourse returns: posts (with blurb and highlighted topic title), topics, users and categories. Empty term is rejected locally with ErrInvalidRequest; server failures keep the typed error classes. Fake-server tests cover encoding, parsing and the 403 path. Part of Redmine 507 (Discourse Go client): search is how the 495 briefing pipeline finds landing spots and prior threads.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package discourse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSearchRequiresTerm(t *testing.T) {
|
||||
f := newFake(t)
|
||||
c := testClient(t, f)
|
||||
_, err := c.Search(context.Background(), SearchRequest{})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("want ErrInvalidRequest for empty term, got %v", err)
|
||||
}
|
||||
if f.saw("/search.json") {
|
||||
t.Fatal("no request should have been sent for an empty term")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchQueriesAndParses(t *testing.T) {
|
||||
f := newFake(t)
|
||||
c := testClient(t, f)
|
||||
res, err := c.Search(context.Background(), SearchRequest{Term: "fleet briefing"})
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if !f.sawQuery("/search.json", "term=fleet+briefing") {
|
||||
t.Fatal("search term not sent as the term= query parameter")
|
||||
}
|
||||
if len(res.Posts) != 1 || res.Posts[0].TopicID != 12 || res.Posts[0].Username != "reachableceo" {
|
||||
t.Fatalf("posts not parsed: %+v", res.Posts)
|
||||
}
|
||||
if res.Posts[0].Blurb == "" {
|
||||
t.Fatal("blurb not parsed")
|
||||
}
|
||||
if len(res.Topics) != 1 || res.Topics[0].Title != "Fleet briefing 2026-09-01" {
|
||||
t.Fatalf("topics not parsed: %+v", res.Topics)
|
||||
}
|
||||
if len(res.Users) != 1 || res.Users[0].Username != "reachableceo" {
|
||||
t.Fatalf("users not parsed: %+v", res.Users)
|
||||
}
|
||||
if len(res.Categories) != 1 || res.Categories[0].Slug != "mopac-briefings" {
|
||||
t.Fatalf("categories not parsed: %+v", res.Categories)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchEncodesTermAndPage(t *testing.T) {
|
||||
f := newFake(t)
|
||||
c := testClient(t, f)
|
||||
if _, err := c.Search(context.Background(), SearchRequest{Term: "a b&c=d", Page: 2}); err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if !f.sawQuery("/search.json", "term=a+b%26c%3Dd") {
|
||||
t.Fatal("term not url-encoded (a b&c=d must not smuggle extra params)")
|
||||
}
|
||||
if !f.sawQuery("/search.json", "page=2") {
|
||||
t.Fatal("page not sent as the page= query parameter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchTypedErrors(t *testing.T) {
|
||||
f := newFake(t)
|
||||
c := testClient(t, f)
|
||||
f.searchFail = 403
|
||||
_, err := c.Search(context.Background(), SearchRequest{Term: "x"})
|
||||
if !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("want ErrForbidden, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user