Accounts bucket fixture (accountBucketValue/accountBucketFolder) copied from mesh-mail's own modules/mesh-mail/cmd/mesh-mail/mirror.go AccountValue/AccountFolderValue (issue 1255); mesh-mail-web is its own Go module, so this is a copy, not an import.
181 lines
6.8 KiB
Go
181 lines
6.8 KiB
Go
package main
|
|
|
|
// The accounts and folder pages (novox/hq ADR 0440 §4, issue 1264).
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// fakeAccountsBucket is a fake AccountsBucketGet (tools.go): a map, and a counter of every Get call so a test
|
|
// can prove which keys were read.
|
|
type fakeAccountsBucket struct {
|
|
values map[string]accountBucketValue
|
|
gets []string
|
|
}
|
|
|
|
func (f *fakeAccountsBucket) get(account string) (*stdio.StateEntry, error) {
|
|
f.gets = append(f.gets, account)
|
|
v, ok := f.values[account]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
raw, _ := json.Marshal(v)
|
|
return &stdio.StateEntry{Key: account, Value: raw}, nil
|
|
}
|
|
|
|
func accountsAnswerJSON(accounts ...map[string]any) ([]byte, error) {
|
|
return json.Marshal(map[string]any{"accounts": accounts})
|
|
}
|
|
|
|
// 1264/AC1: mail-store answers accounts a and b, both held in the bucket; the page lists both, each with its
|
|
// state and its folders' mail and unread counts.
|
|
func TestAccountsPageListsEachAccountWithItsStateAndFolderCounts(t *testing.T) {
|
|
raw, err := 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": "syncing"},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bucket := &fakeAccountsBucket{values: map[string]accountBucketValue{
|
|
"a": {State: "current", Folders: []accountBucketFolder{{Name: "INBOX", Mails: 10, Unread: 2}}},
|
|
"b": {State: "syncing", Folders: []accountBucketFolder{{Name: "INBOX", Mails: 3, Unread: 0}}},
|
|
}}
|
|
b := &Bundle{
|
|
Accounts: func(context.Context) (json.RawMessage, error) { return raw, nil },
|
|
AccountBucket: bucket.get,
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
b.accountsPage(w, r)
|
|
|
|
body := w.Body.String()
|
|
for _, want := range []string{"a", "current", "INBOX", "10 mail(s)", "2 unread", "b", "syncing", "3 mail(s)", "0 unread"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("the page is missing %q:\n%s", want, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1264/AC2: the bucket also holds account c, which mail-store did not answer; nothing of c appears, and its
|
|
// key is never read.
|
|
func TestAccountsPageNeverShowsOrReadsAnAccountMailStoreDidNotAnswer(t *testing.T) {
|
|
raw, err := accountsAnswerJSON(map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "current"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bucket := &fakeAccountsBucket{values: map[string]accountBucketValue{
|
|
"a": {State: "current", Folders: []accountBucketFolder{{Name: "INBOX", Mails: 1, Unread: 0}}},
|
|
"c": {State: "current", Folders: []accountBucketFolder{{Name: "Secret", Mails: 99, Unread: 99}}},
|
|
}}
|
|
b := &Bundle{
|
|
Accounts: func(context.Context) (json.RawMessage, error) { return raw, nil },
|
|
AccountBucket: bucket.get,
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
b.accountsPage(w, r)
|
|
|
|
if strings.Contains(w.Body.String(), "c") && strings.Contains(w.Body.String(), "Secret") {
|
|
t.Fatalf("the page names account c, which mail-store did not answer:\n%s", w.Body.String())
|
|
}
|
|
for _, g := range bucket.gets {
|
|
if g == "c" {
|
|
t.Fatalf("account c's key was read, though mail-store did not answer it: %v", bucket.gets)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1264/AC3: a folder with 120 mails shows the 50 newest, newest first, and its next link asks mail-store.search
|
|
// for that folder with before the date of the 50th shown.
|
|
func TestFolderPageShows50NewestAndLinksToTheNextPageByDate(t *testing.T) {
|
|
var found []map[string]any
|
|
for i := 0; i < 50; i++ {
|
|
found = append(found, map[string]any{
|
|
"account": "mesh", "folder": "INBOX", "uid": 120 - i, "subject": "s",
|
|
"date": "2026-01-01T12:00:00Z", "from": []map[string]any{{"address": "x@example.org"}},
|
|
})
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"found": found})
|
|
var gotArgs map[string]any
|
|
b := &Bundle{Search: func(ctx context.Context, args map[string]any) (json.RawMessage, error) {
|
|
gotArgs = args
|
|
return raw, nil
|
|
}}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest(http.MethodGet, "/folder?account=mesh&folder=INBOX", nil)
|
|
b.folderPage(w, r)
|
|
|
|
if gotArgs["account"] != "mesh" || gotArgs["folder"] != "INBOX" || gotArgs["limit"] != float64(50) {
|
|
t.Fatalf("mail-store.search called with %v", gotArgs)
|
|
}
|
|
body := strings.ToLower(w.Body.String())
|
|
if !strings.Contains(body, "before=2026-01-01t12") {
|
|
t.Fatalf("no next link with the 50th mail's date:\n%s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// 1264/AC4: an account mail-store answered with no key in the bucket yet is listed, its folders said not known
|
|
// yet, and the page does not fail.
|
|
func TestAccountsPageSaysFoldersNotKnownYetWithoutFailing(t *testing.T) {
|
|
raw, err := accountsAnswerJSON(map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "unknown"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bucket := &fakeAccountsBucket{values: map[string]accountBucketValue{}}
|
|
b := &Bundle{
|
|
Accounts: func(context.Context) (json.RawMessage, error) { return raw, nil },
|
|
AccountBucket: bucket.get,
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
b.accountsPage(w, r)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status %d, want 200", w.Code)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "not known yet") {
|
|
t.Fatalf("the page does not say folders are not known yet:\n%s", w.Body.String())
|
|
}
|
|
}
|
|
|
|
// 1264/AC5: across the accounts and folder pages, the only verbs called are mail-store.accounts and
|
|
// mail-store.search.
|
|
func TestAccountsAndFolderPagesCallOnlyAccountsAndSearch(t *testing.T) {
|
|
var accountsCalled, searchCalled int
|
|
raw, _ := accountsAnswerJSON(map[string]any{"account": "a", "kind": "imap", "address": "a@example.org", "state": "current"})
|
|
b := &Bundle{
|
|
Accounts: func(context.Context) (json.RawMessage, error) { accountsCalled++; return raw, nil },
|
|
Search: func(context.Context, map[string]any) (json.RawMessage, error) {
|
|
searchCalled++
|
|
return json.Marshal(map[string]any{"found": []any{}})
|
|
},
|
|
AccountBucket: (&fakeAccountsBucket{values: map[string]accountBucketValue{}}).get,
|
|
Read: func(context.Context, map[string]any) (json.RawMessage, error) {
|
|
t.Fatal("mail-store.read called")
|
|
return nil, nil
|
|
},
|
|
Send: func(context.Context, map[string]any) (json.RawMessage, error) {
|
|
t.Fatal("mail-store.send called")
|
|
return nil, nil
|
|
},
|
|
SendStatus: func(context.Context, map[string]any) (json.RawMessage, error) {
|
|
t.Fatal("mail-store.send-status called")
|
|
return nil, nil
|
|
},
|
|
}
|
|
b.accountsPage(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
|
|
b.folderPage(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/folder?account=a&folder=INBOX", nil))
|
|
|
|
if accountsCalled != 1 || searchCalled != 1 {
|
|
t.Fatalf("accounts called %d time(s), search %d time(s)", accountsCalled, searchCalled)
|
|
}
|
|
}
|