package web // The write page and a send's state (novox/hq ADR 0440 §6, issue 1267), and (beside 1267's own) issue 1284's // form token wired into this real write form and its real POST. import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // 1267/AC1: mail-store answers accounts a and b; the write page offers exactly a and b to send from. func TestWritePageOffersExactlyTheGrantedAccounts(t *testing.T) { raw, _ := accountsAnswerJSON( map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "current"}, map[string]any{"account": "b", "kind": "imap", "address": "b@example.org", "state": "current"}, ) b := &Bundle{Accounts: func(context.Context) (json.RawMessage, error) { return raw, nil }} w := httptest.NewRecorder() b.writePage(w, httptest.NewRequest(http.MethodGet, "/write", nil)) body := w.Body.String() if !strings.Contains(body, `value="a"`) || !strings.Contains(body, `value="b"`) { t.Fatalf("the form does not offer a and b:\n%s", body) } if strings.Contains(body, `value="c"`) { t.Fatalf("the form offers an account not granted:\n%s", body) } } // writeBundle is a Bundle whose Accounts answers exactly the given names, each granted, for the write/send // tests below. func writeBundle(t *testing.T, names []string, send SeatAsk, sendStatus SeatAsk) *Bundle { t.Helper() accounts := make([]map[string]any, 0, len(names)) for _, n := range names { accounts = append(accounts, map[string]any{"account": n, "kind": "imap", "address": n + "@example.org", "state": "current"}) } raw, err := json.Marshal(map[string]any{"accounts": accounts}) if err != nil { t.Fatal(err) } return &Bundle{ Accounts: func(context.Context) (json.RawMessage, error) { return raw, nil }, Send: send, SendStatus: sendStatus, } } // 1267/AC2: a POST with the session's token, from a, to, subject and body calls mail-store.send with exactly // account a, to, subject and body, and the page says the Approve is asked on Telegram and shows the send's // state. func TestSendHandlerCallsSendAndShowsTelegramNoteAndState(t *testing.T) { var gotArgs map[string]any b := writeBundle(t, []string{"a"}, func(ctx context.Context, args map[string]any) (json.RawMessage, error) { gotArgs = args return json.Marshal(map[string]any{"ask": "send-1"}) }, func(ctx context.Context, args map[string]any) (json.RawMessage, error) { return json.Marshal(map[string]any{"status": "waiting", "words": "the operator has not answered yet"}) }, ) form := url.Values{"account": {"a"}, "to": {"ops@example.org"}, "subject": {"hi"}, "body": {"the body"}} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/send", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") b.sendHandler(w, r) wantArgs := map[string]any{"account": "a", "to": []string{"ops@example.org"}, "subject": "hi", "body": "the body"} gotTo, _ := gotArgs["to"].([]string) if gotArgs["account"] != wantArgs["account"] || gotArgs["subject"] != wantArgs["subject"] || gotArgs["body"] != wantArgs["body"] || len(gotTo) != 1 || gotTo[0] != "ops@example.org" { t.Fatalf("mail-store.send called with %#v", gotArgs) } body := w.Body.String() if !strings.Contains(body, "Telegram") { t.Fatalf("the page does not say the Approve is asked on Telegram:\n%s", body) } if !strings.Contains(body, "waiting") { t.Fatalf("the page does not show the send's state:\n%s", body) } } // 1267/AC3: send-status answers a non-final state (waiting): the page says it and refreshes itself without // script; each final state (sent, failed, refused, not proven, declined, expired, unknown) says it and stops // refreshing. func TestSendStatusPageRefreshesOnlyWhileWaiting(t *testing.T) { cases := []struct { status string wantsRefresh bool }{ {"waiting", true}, {"sent", false}, {"failed", false}, {"refused", false}, {"not proven", false}, {"declined", false}, {"expired", false}, {"unknown", false}, } for _, c := range cases { t.Run(c.status, func(t *testing.T) { b := &Bundle{SendStatus: func(ctx context.Context, args map[string]any) (json.RawMessage, error) { return json.Marshal(map[string]any{"status": c.status}) }} w := httptest.NewRecorder() b.sendStatusPage(w, httptest.NewRequest(http.MethodGet, "/send-status?id=send-1", nil)) body := w.Body.String() if !strings.Contains(body, c.status) { t.Fatalf("%s: the page does not say the state:\n%s", c.status, body) } hasRefresh := strings.Contains(body, "http-equiv=\"refresh\"") if hasRefresh != c.wantsRefresh { t.Fatalf("%s: refreshes=%v, want %v:\n%s", c.status, hasRefresh, c.wantsRefresh, body) } }) } } // 1267/AC4: a POST naming an account mail-store did not answer sends nothing, and the page says the account // is not granted. func TestSendHandlerRefusesAnAccountNotGranted(t *testing.T) { sendCalled := false b := writeBundle(t, []string{"a"}, func(context.Context, map[string]any) (json.RawMessage, error) { sendCalled = true; return nil, nil }, nil, ) form := url.Values{"account": {"c"}, "to": {"ops@example.org"}, "subject": {"hi"}, "body": {"body"}} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/send", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") b.sendHandler(w, r) if sendCalled { t.Fatal("mail-store.send was called for an account not granted") } if !strings.Contains(w.Body.String(), "not granted") { t.Fatalf("the page does not say the account is not granted:\n%s", w.Body.String()) } } // ---- issue 1284/AC1-AC4, against the real write page and its real POST /send (beside 1267's own tests // above, and beside csrf_test.go's stand-in handler tests, as the brief asked) -------------------------------- // writeServerBundle wires the real write page and send handler behind a real Auth (RequireSession, // RequireFormToken), exactly as server.go's NewServer does, so these tests exercise the production chain, not // a stand-in. func writeServerBundle(t *testing.T, names []string, send SeatAsk) (*Bundle, *Sessions, *Auth) { t.Helper() b := writeBundle(t, names, send, nil) sessions := NewSessions(nil) auth := &Auth{Sessions: sessions} return b, sessions, auth } // 1284/AC1: a signed-in session's write page carries that session's own token. func TestRealWritePageFormCarriesTheSessionsToken(t *testing.T) { b, sessions, auth := writeServerBundle(t, []string{"a"}, nil) sess, cookie := sessionAndCookie(t, sessions, "alice") handler := auth.RequireSession(b.writePage) r := httptest.NewRequest(http.MethodGet, "/write", nil) r.AddCookie(cookie) w := httptest.NewRecorder() handler(w, r) if !strings.Contains(w.Body.String(), sess.Token) { t.Fatalf("the real write page's form does not carry the session's token:\n%s", w.Body.String()) } } // 1284/AC2: a real POST /send without the token is answered 403, and mail-store.send is not called. func TestRealSendPOSTWithoutTheTokenIsRefused(t *testing.T) { var sendCalled bool b, sessions, auth := writeServerBundle(t, []string{"a"}, func(context.Context, map[string]any) (json.RawMessage, error) { sendCalled = true return nil, errors.New("should not be called") }) _, cookie := sessionAndCookie(t, sessions, "alice") handler := auth.RequireSession(RequireFormToken(b.sendHandler)) form := url.Values{"account": {"a"}, "to": {"x@example.org"}, "subject": {"s"}, "body": {"b"}} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/send", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(cookie) handler(w, r) if w.Code != http.StatusForbidden { t.Fatalf("status %d, want 403", w.Code) } if sendCalled { t.Fatal("mail-store.send was called with no token") } } // 1284/AC3: a real POST /send carrying another session's token is answered 403, and mail-store.send is not // called. func TestRealSendPOSTWithAnotherSessionsTokenIsRefused(t *testing.T) { var sendCalled bool b, sessions, auth := writeServerBundle(t, []string{"a"}, func(context.Context, map[string]any) (json.RawMessage, error) { sendCalled = true return nil, errors.New("should not be called") }) _, cookie := sessionAndCookie(t, sessions, "alice") other, _ := sessionAndCookie(t, sessions, "bob") handler := auth.RequireSession(RequireFormToken(b.sendHandler)) form := url.Values{"account": {"a"}, "to": {"x@example.org"}, "subject": {"s"}, "body": {"b"}, FormTokenField: {other.Token}} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/send", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(cookie) handler(w, r) if w.Code != http.StatusForbidden { t.Fatalf("status %d, want 403", w.Code) } if sendCalled { t.Fatal("mail-store.send was called with another session's token") } } // 1284/AC4: a real POST /send carrying its own session's token proceeds — mail-store.send is called. func TestRealSendPOSTWithItsOwnSessionsTokenProceeds(t *testing.T) { var sendCalled bool b, sessions, auth := writeServerBundle(t, []string{"a"}, func(context.Context, map[string]any) (json.RawMessage, error) { sendCalled = true return json.Marshal(map[string]any{"ask": "send-1"}) }) sess, cookie := sessionAndCookie(t, sessions, "alice") handler := auth.RequireSession(RequireFormToken(b.sendHandler)) form := url.Values{"account": {"a"}, "to": {"x@example.org"}, "subject": {"s"}, "body": {"b"}, FormTokenField: {sess.Token}} w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPost, "/send", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(cookie) handler(w, r) if !sendCalled { t.Fatal("mail-store.send was not called despite the matching token") } }