package search import ( "encoding/json" "encoding/xml" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" ) // newTestHandler builds a router with the search handler mounted at /. func newTestHandler(t *testing.T) http.Handler { t.Helper() svc, err := NewService("gh", "https://example.com") if err != nil { t.Fatalf("NewService: %v", err) } r := chi.NewRouter() NewHandler(svc).RegisterRoutes(r) return r } func TestSearchRedirects(t *testing.T) { h := newTestHandler(t) cases := []struct { name string query string wantLoc string }{ {"unknown prefix uses default gh", "foo bar", "https://github.com/search?q=foo+bar"}, {"ud prefix routes to urban dictionary", "ud meme", "https://www.urbandictionary.com/define.php?term=meme"}, {"steam prefix routes to steam", "steam half life", "https://store.steampowered.com/search/?term=half+life"}, {"colon prefix", "ud:lit", "https://www.urbandictionary.com/define.php?term=lit"}, {"empty query goes to default home", "", "https://github.com/"}, {"bare prefix goes to provider home", "ud", "https://www.urbandictionary.com/"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { req := httptest.NewRequestWithContext(t.Context(), "GET", "/search?q="+encodeQ(tc.query), http.NoBody) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) if rr.Code != http.StatusFound { t.Fatalf("status = %d, want 302", rr.Code) } if got := rr.Header().Get("Location"); got != tc.wantLoc { t.Errorf("Location = %q\n want %q", got, tc.wantLoc) } }) } } // encodeQ percent-encodes spaces only — enough for these tests. func encodeQ(s string) string { return strings.ReplaceAll(s, " ", "+") } func TestListProvidersJSON(t *testing.T) { h := newTestHandler(t) rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequestWithContext(t.Context(), "GET", "/providers", http.NoBody)) if rr.Code != http.StatusOK { t.Fatalf("status = %d", rr.Code) } if got := rr.Header().Get("Content-Type"); got != "application/json" { t.Errorf("content-type = %q", got) } var body struct { Default string `json:"default"` Providers []struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` HomeURL string `json:"home_url"` OpenSearchURL string `json:"opensearch_url"` } `json:"providers"` } if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil { t.Fatalf("decode: %v", err) } if body.Default != "gh" { t.Errorf("default = %q", body.Default) } if len(body.Providers) != 3 { t.Fatalf("expected 3 providers, got %d", len(body.Providers)) } // Order should match AllProviders(). wantIDs := []string{"ud", "gh", "steam"} for i, want := range wantIDs { if body.Providers[i].ID != want { t.Errorf("providers[%d].ID = %q, want %q", i, body.Providers[i].ID, want) } } // Per-provider OSD URL is built from the public URL. if body.Providers[0].OpenSearchURL != "https://example.com/opensearch/ud.xml" { t.Errorf("opensearch_url = %q", body.Providers[0].OpenSearchURL) } } func TestOpenSearchRouterXML(t *testing.T) { h := newTestHandler(t) rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequestWithContext(t.Context(), "GET", "/opensearch.xml", http.NoBody)) if rr.Code != http.StatusOK { t.Fatalf("status = %d", rr.Code) } if got := rr.Header().Get("Content-Type"); got != "application/opensearchdescription+xml" { t.Errorf("content-type = %q", got) } body := rr.Body.String() if !strings.Contains(body, "