package web // The mail-reading section of the accounts page (novox/hq ADR 0459 §7, §12, §13; issue 1430). import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func seatAskJSON(t *testing.T, v any) SeatAsk { t.Helper() raw, err := json.Marshal(v) if err != nil { t.Fatal(err) } return func(context.Context, map[string]any) (json.RawMessage, error) { return raw, nil } } // 1430/AC1: with mail-reading healthy, the page shows the mail that matters first, the mail groups with // their counts, and the scam warnings in their own section, each escaped through html/template. func TestAccountsPageShowsMailThatMattersGroupsAndScamWarnings(t *testing.T) { accountsRaw, err := accountsAnswerJSON() if err != nil { t.Fatal(err) } b := &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": []map[string]any{ {"sender-name": "Alice", "summary": "pay bill", "mark": "matters", "why": "due soon", "due-date": "2026-11-01"}, {"sender-name": "Eve", "summary": "a scam", "mark": "scam warning", "why": "fake bank"}, }}), MailGroups: seatAskJSON(t, map[string]any{"groups": []map[string]any{{"label": "bills", "count": 3}}}), MailLessons: seatAskJSON(t, map[string]any{"lessons": []map[string]any{}}), } w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/", nil) b.accountsPage(w, r) body := w.Body.String() for _, want := range []string{"Alice", "due 2026-11-01", "due soon", "bills: 3", "Eve", "a scam", "fake bank"} { if !strings.Contains(body, want) { t.Fatalf("body does not contain %q:\n%s", want, body) } } // html/template escapes a mail's own words (AC1: "all through html/template, escaped"). if strings.Contains(body, "") { t.Fatalf("the summary was not escaped: %s", body) } if !strings.Contains(body, "<the>") { t.Fatalf("the escaped summary is missing: %s", body) } } // 1430/AC2: with status unhealthy, the page shows its cause and since when in place of the mail groups, and // the classical list still below. func TestAccountsPageShowsCauseInPlaceOfGroupsWhenUnhealthy(t *testing.T) { accountsRaw, err := accountsAnswerJSON(map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "current"}) if err != nil { t.Fatal(err) } b := &Bundle{ Accounts: func(context.Context) (json.RawMessage, error) { return accountsRaw, nil }, MailStatus: seatAskJSON(t, map[string]any{"healthy": false, "cause": "refused", "why": "holds no licence", "since": "2026-10-20T09:30:00Z"}), MailEntries: seatAskJSON(t, map[string]any{"entries": []map[string]any{}}), MailLessons: seatAskJSON(t, map[string]any{"lessons": []map[string]any{}}), } w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/", nil) b.accountsPage(w, r) body := w.Body.String() if !strings.Contains(body, "Unhealthy") || !strings.Contains(body, "refused") || !strings.Contains(body, "2026-10-20T09:30:00Z") { t.Fatalf("body does not show the cause and since when:\n%s", body) } if strings.Contains(body, "Mail groups") { t.Fatalf("the mail groups heading is shown even though unhealthy:\n%s", body) } // The classical list still renders below (AC2). if !strings.Contains(body, "a@example.org") || !strings.Contains(body, "current") { t.Fatalf("the classical list did not render:\n%s", body) } } // 1430/AC3: with mail-reading not answering or refusing, the page says mail reading is unavailable and why, // and the classical list still renders in full. func TestAccountsPageSaysMailReadingUnavailableAndStillRendersTheList(t *testing.T) { accountsRaw, err := accountsAnswerJSON(map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "current"}) if err != nil { t.Fatal(err) } b := &Bundle{ Accounts: func(context.Context) (json.RawMessage, error) { return accountsRaw, nil }, MailStatus: func(context.Context, map[string]any) (json.RawMessage, error) { return nil, errRefused }, } w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/", nil) b.accountsPage(w, r) body := w.Body.String() if !strings.Contains(body, "unavailable") { t.Fatalf("body does not say mail reading is unavailable:\n%s", body) } if !strings.Contains(body, "a@example.org") || !strings.Contains(body, "current") { t.Fatalf("the classical list did not render in full:\n%s", body) } } type signalError struct{ s string } func (e signalError) Error() string { return e.s } var errRefused = signalError{"refused"}