119 lines
4.4 KiB
Go
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, "<script>") {
|
|
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)
|
|
}
|
|
}
|