package web
// The search page (novox/hq ADR 0440 ยง4, issue 1265).
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
)
// 1265/AC1: the search form filled with words, sender, recipient, a date range and has-attachment calls
// mail-store.search with exactly query, from, to, on-or-after, before and has-attachment carrying those
// values (limit is the page's own mechanical paging, not a form field).
func TestSearchPageCallsSearchWithExactlyTheFilledFormFields(t *testing.T) {
var gotArgs map[string]any
b := &Bundle{Search: func(ctx context.Context, args map[string]any) (json.RawMessage, error) {
gotArgs = args
return json.Marshal(map[string]any{"found": []any{}})
}}
q := url.Values{
"query": {"invoice"}, "from": {"ops@example.org lead@example.org"}, "to": {"me@example.org"},
"on-or-after": {"2026-01-01"}, "before": {"2026-02-01"}, "has-attachment": {"on"},
}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/search?"+q.Encode(), nil)
b.searchPage(w, r)
want := map[string]any{
"query": "invoice", "from": []string{"ops@example.org", "lead@example.org"}, "to": "me@example.org",
"on-or-after": "2026-01-01", "before": "2026-02-01", "has-attachment": true, "limit": float64(50),
}
if !reflect.DeepEqual(gotArgs, want) {
t.Fatalf("args %#v, want %#v", gotArgs, want)
}
}
// Unfilled fields are left out of the call entirely, never sent as "".
func TestSearchPageLeavesEmptyFieldsOutOfTheCall(t *testing.T) {
var gotArgs map[string]any
b := &Bundle{Search: func(ctx context.Context, args map[string]any) (json.RawMessage, error) {
gotArgs = args
return json.Marshal(map[string]any{"found": []any{}})
}}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/search?query=hi", nil)
b.searchPage(w, r)
want := map[string]any{"query": "hi", "limit": float64(50)}
if !reflect.DeepEqual(gotArgs, want) {
t.Fatalf("args %#v, want %#v", gotArgs, want)
}
}
// 1265/AC2: more than 50 results shows 50 newest first, and the next link asks again with before the date of
// the 50th shown.
func TestSearchPageShows50AndLinksToTheNextPageByDate(t *testing.T) {
var found []map[string]any
for i := 0; i < 50; i++ {
found = append(found, map[string]any{
"account": "mesh", "folder": "INBOX", "uid": 200 - i, "subject": "s",
"date": "2026-03-01T08:00:00Z", "from": []map[string]any{{"address": "x@example.org"}},
})
}
raw, _ := json.Marshal(map[string]any{"found": found})
b := &Bundle{Search: func(context.Context, map[string]any) (json.RawMessage, error) { return raw, nil }}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/search?query=x", nil)
b.searchPage(w, r)
body := strings.ToLower(w.Body.String())
if !strings.Contains(body, "before=2026-03-01t08") {
t.Fatalf("no next link with the 50th mail's date:\n%s", w.Body.String())
}
}
// 1265/AC3: words holding ",
"date": "2026-01-01T00:00:00Z", "from": []map[string]any{{"address": "x@example.org"}}},
}})
b := &Bundle{Search: func(context.Context, map[string]any) (json.RawMessage, error) { return raw, nil }}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/search?query=x", nil)
b.searchPage(w, r)
body := w.Body.String()
if strings.Contains(body, "