Files
mesh-catalog/modules/mesh-mail-web/cmd/mesh-mail-web/search_test.go
T

119 lines
4.4 KiB
Go

package main
// 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 <script> show as text, and the page carries no script element.
func TestSearchPageEscapesScriptInResults(t *testing.T) {
raw, _ := json.Marshal(map[string]any{"found": []map[string]any{
{"account": "mesh", "folder": "INBOX", "uid": 1, "subject": "<script>alert(1)</script>",
"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, "<script>") {
t.Fatalf("the page carries a script element:\n%s", body)
}
if !strings.Contains(body, "&lt;script&gt;") {
t.Fatalf("the subject is not shown escaped as text:\n%s", body)
}
}
// 1265/AC4: mail-store refuses or fails; the page says the search could not be answered, and nothing else
// (no partial results, no error detail).
func TestSearchPageSaysItCouldNotBeAnsweredOnFailure(t *testing.T) {
b := &Bundle{Search: func(context.Context, map[string]any) (json.RawMessage, error) {
return nil, context.DeadlineExceeded
}}
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/search?query=x", nil)
b.searchPage(w, r)
body := w.Body.String()
if !strings.Contains(body, "could not be answered") {
t.Fatalf("the page does not say the search failed:\n%s", body)
}
if strings.Contains(body, "<li>") {
t.Fatalf("the page shows results despite the failure:\n%s", body)
}
}