package web // Corrections and undo on the mail page (novox/hq ADR 0459 §8, §13; issue 1523). import ( "context" "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // 1523/AC1: posting a correction with the session's own token calls mail-reading.correct with the mail's id // and the change, then shows the page again. func TestMailCorrectHandlerCallsCorrectAndShowsThePageAgain(t *testing.T) { sessions := NewSessions(nil) sess, cookie := sessionAndCookie(t, sessions, "alice") auth := &Auth{Sessions: sessions} accountsRaw, err := accountsAnswerJSON() if err != nil { t.Fatal(err) } var gotArgs map[string]any 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{}}), MailGroups: seatAskJSON(t, map[string]any{"groups": []map[string]any{}}), MailLessons: seatAskJSON(t, map[string]any{"lessons": []map[string]any{}}), MailCorrect: func(_ context.Context, args map[string]any) (json.RawMessage, error) { gotArgs = args return json.Marshal(map[string]any{"id": args["id"]}) }, } handler := auth.RequireSession(RequireFormToken(b.mailCorrectHandler)) form := url.Values{} form.Set("id", "42") form.Set("mark", "matters") form.Set(FormTokenField, sess.Token) r := httptest.NewRequest(http.MethodPost, "/mail-correct", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(cookie) w := httptest.NewRecorder() handler(w, r) if w.Code != http.StatusOK { t.Fatalf("status %d, want 200: %s", w.Code, w.Body.String()) } if gotArgs["id"] != float64(42) || gotArgs["mark"] != "matters" { t.Fatalf("mail-reading.correct asked with %v", gotArgs) } if !strings.Contains(w.Body.String(), "

Accounts

") { t.Fatalf("the page was not shown again: %s", w.Body.String()) } } // 1523/AC1: without the token, the POST is answered 403 and mail-reading.correct is never called. func TestMailCorrectHandlerRefusedWithoutToken(t *testing.T) { sessions := NewSessions(nil) _, cookie := sessionAndCookie(t, sessions, "alice") auth := &Auth{Sessions: sessions} called := false b := &Bundle{MailCorrect: func(context.Context, map[string]any) (json.RawMessage, error) { called = true return nil, nil }} handler := auth.RequireSession(RequireFormToken(b.mailCorrectHandler)) form := url.Values{} form.Set("id", "42") r := httptest.NewRequest(http.MethodPost, "/mail-correct", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(cookie) w := httptest.NewRecorder() handler(w, r) if w.Code != http.StatusForbidden { t.Fatalf("status %d, want 403", w.Code) } if called { t.Fatal("mail-reading.correct was called with no token") } } // 1523/AC2: an undo form carrying the session's own token calls mail-reading.undo. func TestMailUndoHandlerCallsUndo(t *testing.T) { sessions := NewSessions(nil) sess, cookie := sessionAndCookie(t, sessions, "alice") auth := &Auth{Sessions: sessions} accountsRaw, err := accountsAnswerJSON() if err != nil { t.Fatal(err) } var gotArgs map[string]any 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{}}), MailGroups: seatAskJSON(t, map[string]any{"groups": []map[string]any{}}), MailLessons: seatAskJSON(t, map[string]any{"lessons": []map[string]any{}}), MailUndo: func(_ context.Context, args map[string]any) (json.RawMessage, error) { gotArgs = args return json.Marshal(map[string]any{"undone": args["id"]}) }, } handler := auth.RequireSession(RequireFormToken(b.mailUndoHandler)) form := url.Values{} form.Set("id", "7") form.Set(FormTokenField, sess.Token) r := httptest.NewRequest(http.MethodPost, "/mail-undo", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(cookie) w := httptest.NewRecorder() handler(w, r) if w.Code != http.StatusOK { t.Fatalf("status %d, want 200: %s", w.Code, w.Body.String()) } if gotArgs["id"] != float64(7) { t.Fatalf("mail-reading.undo asked with %v", gotArgs) } } // 1523/AC2, AC3: the page lists the reading lessons newest first, each with an undo form carrying the // session's token, and shows reading lessons per 100 read mails for each of the last weeks, from status. func TestAccountsPageListsLessonsWithUndoFormsAndWeeklyMeasure(t *testing.T) { sessions := NewSessions(nil) sess, cookie := sessionAndCookie(t, sessions, "alice") auth := &Auth{Sessions: sessions} 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, "weeks": []map[string]any{ {"week-start": "2026-10-13", "lessons-per-100": 25.0}, {"week-start": "2026-10-20", "lessons-per-100": nil, "why": "no mail was read this week"}, }, }), MailEntries: seatAskJSON(t, map[string]any{"entries": []map[string]any{}}), MailGroups: seatAskJSON(t, map[string]any{"groups": []map[string]any{}}), MailLessons: seatAskJSON(t, map[string]any{"lessons": []map[string]any{ {"id": 5, "sender-name": "Alice", "subject": "pay the bill", "change": "mark changed", "made": "2026-10-20T10:00:00Z"}, }}), } handler := auth.RequireSession(b.accountsPage) r := httptest.NewRequest(http.MethodGet, "/", nil) r.AddCookie(cookie) w := httptest.NewRecorder() handler(w, r) body := w.Body.String() for _, want := range []string{"Alice", "pay the bill", "mark changed", `action="/mail-undo"`, sess.Token, "25 per 100", "no mail was read this week"} { if !strings.Contains(body, want) { t.Fatalf("body does not contain %q:\n%s", want, body) } } }