Files
mesh-catalog/modules/mesh-mail-web/internal/web/mailreading_test.go
T
jochen f24a4d858a issue 1859: move the mail page into internal/web, so the local run can serve it
A program cannot import a main package; the page's code moves to modules/mesh-mail-web/internal/web, and
cmd/mesh-mail-web is a two-line program calling it, so cmd/mail-page-dev (1859.5) serves the same handlers.
Nothing the module does changes.
2026-10-22 19:58:43 +02:00

118 lines
4.6 KiB
Go

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 <the> 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, "<the>") {
t.Fatalf("the summary was not escaped: %s", body)
}
if !strings.Contains(body, "&lt;the&gt;") {
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"}