test: add HTTP API integration tests
Test GET /healthz, /, /search, /autocompleter endpoints. Verify response codes, content types, JSON decoding, empty-query redirect, and source URL presence in footer. Also fix dead code in Search handler: the redirect for empty q was unreachable because ParseSearchRequest errors on empty q first. Move the q/format check before ParseSearchRequest to fix the redirect.
This commit is contained in:
parent
f6128689f1
commit
f1cf23745e
2 changed files with 236 additions and 4 deletions
|
|
@ -72,17 +72,19 @@ func (h *Handler) OpenSearch(baseURL string) http.HandlerFunc {
|
|||
}
|
||||
|
||||
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.FormValue("q")
|
||||
format := r.FormValue("format")
|
||||
|
||||
// For HTML format with no query, redirect to homepage.
|
||||
if r.FormValue("q") == "" && (r.FormValue("format") == "" || r.FormValue("format") == "html") {
|
||||
if q == "" && (format == "" || format == "html") {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := search.ParseSearchRequest(r)
|
||||
if err != nil {
|
||||
// For HTML, render error on the results page.
|
||||
if req.Format == contracts.FormatHTML || r.FormValue("format") == "html" {
|
||||
pd := views.PageData{SourceURL: h.sourceURL, Query: r.FormValue("q")}
|
||||
if format == "html" || format == "" {
|
||||
pd := views.PageData{SourceURL: h.sourceURL, Query: q}
|
||||
if views.IsHTMXRequest(r) {
|
||||
views.RenderSearchFragment(w, pd)
|
||||
} else {
|
||||
|
|
|
|||
230
internal/httpapi/httpapi_test.go
Normal file
230
internal/httpapi/httpapi_test.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// kafka — a privacy-respecting metasearch engine
|
||||
// Copyright (C) 2026-present metamorphosis-dev
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package httpapi_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/metamorphosis-dev/kafka/internal/contracts"
|
||||
"github.com/metamorphosis-dev/kafka/internal/httpapi"
|
||||
"github.com/metamorphosis-dev/kafka/internal/search"
|
||||
)
|
||||
|
||||
// mockUpstreamHandler returns controlled JSON responses.
|
||||
func mockUpstreamJSON(query string) contracts.SearchResponse {
|
||||
return contracts.SearchResponse{
|
||||
Query: query,
|
||||
NumberOfResults: 2,
|
||||
Results: []contracts.MainResult{
|
||||
{Title: "Upstream Result 1", URL: ptr("https://upstream.example/1"), Content: "From upstream", Engine: "upstream"},
|
||||
{Title: "Upstream Result 2", URL: ptr("https://upstream.example/2"), Content: "From upstream", Engine: "upstream"},
|
||||
},
|
||||
Answers: []map[string]any{},
|
||||
Corrections: []string{},
|
||||
Infoboxes: []map[string]any{},
|
||||
Suggestions: []string{"upstream suggestion"},
|
||||
UnresponsiveEngines: [][2]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func ptr(s string) *string { return &s }
|
||||
|
||||
func newTestServer(t *testing.T) (*httptest.Server, *httpapi.Handler) {
|
||||
t.Helper()
|
||||
|
||||
// Mock upstream server that returns controlled JSON.
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.FormValue("q")
|
||||
resp := mockUpstreamJSON(query)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
t.Cleanup(upstream.Close)
|
||||
|
||||
svc := search.NewService(search.ServiceConfig{
|
||||
UpstreamURL: upstream.URL,
|
||||
HTTPTimeout: 0,
|
||||
Cache: nil,
|
||||
EnginesConfig: nil,
|
||||
})
|
||||
|
||||
h := httpapi.NewHandler(svc, nil, "https://src.example.com")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", h.Healthz)
|
||||
mux.HandleFunc("/", h.Index)
|
||||
mux.HandleFunc("/search", h.Search)
|
||||
mux.HandleFunc("/autocompleter", h.Autocompleter)
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
t.Cleanup(server.Close)
|
||||
return server, h
|
||||
}
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/healthz")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "text/plain") {
|
||||
t.Errorf("expected text/plain, got %s", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("expected text/html, got %s", ct)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !strings.Contains(string(body), "<!DOCTYPE html") {
|
||||
t.Error("expected HTML DOCTYPE")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_RedirectOnEmptyQuery(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}}
|
||||
req, _ := http.NewRequest("GET", server.URL+"/search?format=html", nil)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Errorf("expected redirect 302, got %d", resp.StatusCode)
|
||||
}
|
||||
loc, _ := resp.Location()
|
||||
if loc == nil || loc.Path != "/" {
|
||||
t.Errorf("expected redirect to /, got %v", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_JSONResponse(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/search?q=test&format=json")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") {
|
||||
t.Errorf("expected application/json, got %s", ct)
|
||||
}
|
||||
|
||||
var result contracts.SearchResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("failed to decode JSON: %v", err)
|
||||
}
|
||||
if result.Query != "test" {
|
||||
t.Errorf("expected query 'test', got %q", result.Query)
|
||||
}
|
||||
if len(result.Results) == 0 {
|
||||
t.Error("expected at least one result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_HTMLResponse(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/search?q=test&format=html")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("expected text/html, got %s", ct)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !strings.Contains(string(body), "<!DOCTYPE html") {
|
||||
t.Error("expected HTML DOCTYPE in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutocompleter_EmptyQuery(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/autocompleter?q=")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutocompleter_NoQuery(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/autocompleter")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected status 400, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch_SourceURLInFooter(t *testing.T) {
|
||||
server, _ := newTestServer(t)
|
||||
resp, err := http.Get(server.URL + "/?q=test")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if !strings.Contains(string(body), "https://src.example.com") {
|
||||
t.Error("expected source URL in footer")
|
||||
}
|
||||
if !strings.Contains(string(body), "AGPLv3") {
|
||||
t.Error("expected AGPLv3 link in footer")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue