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, "