Files
mesh-catalog/modules/mesh-mail-web/internal/web/readpane_test.go
T
jochen af89a69670 issue 1859: the page shown after an act records nothing, so each act leaves one line
Review round 1 F1 (ADR 0517 §3, §5): a form act went back to the list or the pane, which recorded page
opened or opened again, and the script's refresh did too, so last looked became the last act and opened
counts grew. The redirect after an act carries acted=1 and the refresh an X-Mail-Refresh header; that page
records nothing. The 1860 test now follows each redirect and counts every line, and reminds the task (F2,
1858/AC9), which the store's own integration test also reads back as a date.
2026-10-22 20:26:54 +02:00

142 lines
6.6 KiB
Go

package web
// Issue 1859/AC6, AC7 (novox/hq ADR 0515 §5, §6; issue 1850): the reading pane shows one mail as escaped
// text, with its corrections, each calling correct; a sender with no display name is named by its address.
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
// paneEntry is mail-reading.entries' answer for one entry (mesh-mail-reader tools.go entryOut).
func paneEntry() map[string]any {
return map[string]any{
"id": 3, "account": "work", "message-id": "<h@harbor.example>", "date": "2026-10-21T08:00:00Z",
"sender-name": "", "sender-address": "lena@harbor.example", "subject": "Hostco renewal", "group": "suppliers",
"summary": "Hostco renews on 1 November", "mark": "matters", "why": "a contract to approve", "due-date": "",
"folder": "INBOX", "uid": 103, "read-later": false,
"tasks": []any{map[string]any{"n": 1, "line": "Approve the Hostco renewal", "due-date": "2026-10-24"}, map[string]any{"n": 2, "line": "Forward the invoice", "due-date": ""}},
"promises": []any{map[string]any{"n": 1, "kind": "on its way", "line": "The new contract", "who": "Hostco", "date": ""}},
}
}
func paneBundle(t *testing.T) (*readingStandIn, *Bundle) {
t.Helper()
reading := &readingStandIn{list: fullList(), entry: paneEntry()}
b := reading.bundle(time.Now())
b.Read = func(_ context.Context, args map[string]any) (json.RawMessage, error) {
reading.mu.Lock()
reading.calls = append(reading.calls, readingCall{"read", args})
reading.mu.Unlock()
return json.Marshal(map[string]any{"account": "work", "folder": "INBOX", "uid": 103, "date": "2026-10-21T08:00:00Z",
"from": []any{map[string]any{"address": "lena@harbor.example"}}, "to": []any{map[string]any{"address": "op@work.example"}},
"subject": "Hostco renewal", "text": "Hi,\n<script>alert(1)</script> the renewal is attached.",
"attachments": []any{map[string]any{"key": "att1", "name": "renewal.pdf", "size": 2048, "type": "application/pdf"}}})
}
return reading, b
}
// 1859/AC7: the pane shows one mail as escaped text with its corrections, and records it opened.
func TestTheReadingPaneShowsOneMailWithItsCorrections(t *testing.T) {
reading, b := paneBundle(t)
s, cookie, _ := signedIn(t, b)
r := newGet("/read?id=3&mode=work", cookie)
w := serve(s, r)
body := w.Body.String()
if w.Code != http.StatusOK {
t.Fatalf("status %d:\n%s", w.Code, body)
}
for _, want := range []string{`id="pane"`, "Hostco renewal", "&lt;script&gt;alert(1)&lt;/script&gt;", "renewal.pdf",
"Approve the Hostco renewal", `name="not-a-task" value="2"`, `name="remove-promise" value="1"`,
`name="read-later" value="on"`, `name="mark"`, `name="group"`, `id="today"`} {
if !strings.Contains(body, want) {
t.Fatalf("the pane does not show %q:\n%s", want, body)
}
}
if strings.Contains(body, "<script>alert") {
t.Fatal("the mail's text was not escaped")
}
if e := reading.callsOf("entries"); len(e) != 1 || e[0].Args["id"] != float64(3) {
t.Fatalf("entries asks %v, want the one entry by its id", e)
}
if rd := reading.callsOf("read"); len(rd) != 1 || rd[0].Args["uid"] != float64(103) || rd[0].Args["folder"] != "INBOX" {
t.Fatalf("mail-store.read asks %v, want the entry's own mail", rd)
}
if rec := reading.callsOf("record"); len(rec) != 1 || rec[0].Args["what"] != "opened" || rec[0].Args["id"] != float64(3) {
t.Fatalf("record asks %v, want one opened of mail 3", rec)
}
}
// 1859/AC7: each correction calls correct with what it says and the mode, then shows the pane again.
func TestEachCorrectionCallsCorrect(t *testing.T) {
for _, tc := range []struct {
form url.Values
want map[string]any
}{
{url.Values{"mark": {"neither"}, "group": {"suppliers"}}, map[string]any{"mark": "neither", "group": "suppliers"}},
{url.Values{"not-a-task": {"2"}}, map[string]any{"not-a-task": float64(2)}},
{url.Values{"remove-promise": {"1"}}, map[string]any{"remove-promise": float64(1)}},
{url.Values{"read-later": {"on"}}, map[string]any{"read-later": true}},
{url.Values{"read-later": {"off"}}, map[string]any{"read-later": false}},
} {
reading, b := paneBundle(t)
s, cookie, token := signedIn(t, b)
tc.form.Set("id", "3")
tc.form.Set("mode", "work")
tc.form.Set(FormTokenField, token)
code, _, h := post(t, s, "/correct", tc.form, cookie, false)
calls := reading.callsOf("correct")
if code != http.StatusSeeOther || h.Get("Location") != "/read?id=3&mode=work&acted=1" || len(calls) != 1 {
t.Fatalf("correct %v: status %d to %q, calls %v", tc.form, code, h.Get("Location"), calls)
}
want := map[string]any{"id": float64(3), "mode": "work"}
for k, v := range tc.want {
want[k] = v
}
if len(calls[0].Args) != len(want) {
t.Fatalf("correct %v asked %v, want %v", tc.form, calls[0].Args, want)
}
for k, v := range want {
if calls[0].Args[k] != v {
t.Fatalf("correct %v asked %v, want %v", tc.form, calls[0].Args, want)
}
}
tc.form.Del(FormTokenField)
if code, _, _ := post(t, s, "/correct", tc.form, cookie, false); code != http.StatusForbidden {
t.Fatalf("a correction with no token: status %d, want 403", code)
}
}
}
// 1859/AC6, issue 1850: a sender with no display name is named by its address — on the list, in the pane, and
// on All mail's mail that matters.
func TestASenderWithNoNameIsNamedByItsAddress(t *testing.T) {
list := fullList()
list["today"] = []any{line(1, "task 1", "task", "Read the digest", "sender-name", "", "sender-address", "announce@lists.example")}
b := (&readingStandIn{list: list}).bundle(time.Now())
if _, body, _ := get(t, NewServer(testHost, nil, b), "/"); !strings.Contains(body, `<span class="sender">announce@lists.example</span>`) {
t.Fatalf("the list does not name the sender by its address:\n%s", body)
}
_, b2 := paneBundle(t)
if _, body, _ := get(t, NewServer(testHost, nil, b2), "/read?id=3"); !strings.Contains(body, "lena@harbor.example") {
t.Fatalf("the pane does not name the sender by its address:\n%s", body)
}
accountsRaw, _ := accountsAnswerJSON()
b3 := &Bundle{
Accounts: func(context.Context) (json.RawMessage, error) { return accountsRaw, nil },
MailStatus: seatAskJSON(t, map[string]any{"healthy": true}),
MailEntries: seatAskJSON(t, map[string]any{"entries": []any{map[string]any{"id": 1, "sender-name": "",
"sender-address": "announce@lists.example", "summary": "the Arch digest", "mark": "matters"}}}),
MailGroups: seatAskJSON(t, map[string]any{"groups": []any{}}),
MailLessons: seatAskJSON(t, map[string]any{"lessons": []any{}}),
}
if _, body, _ := get(t, NewServer(testHost, nil, b3), "/all"); !strings.Contains(body, "announce@lists.example — the Arch digest") {
t.Fatalf("All mail's mail that matters does not name the sender by its address:\n%s", body)
}
}