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.
118 lines
4.6 KiB
Go
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, "<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"}
|