Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/hear_test.go
T

154 lines
5.5 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func addedEventBody(t *testing.T, account, folder, messageID string, uid uint32) json.RawMessage {
t.Helper()
raw, err := json.Marshal(map[string]any{
"account": account, "folder": folder, "uid": uid, "uidvalidity": uint32(1),
"message-id": messageID, "date": "2026-10-20T10:00:00Z",
"from": []Person{{Name: "Alice", Address: "alice@example.org"}},
"subject": "hello", "size": int64(1234), "flags": []string{},
})
if err != nil {
t.Fatal(err)
}
return raw
}
// 1518/AC2: an added event is recorded as a waiting mail before the handler returns success.
func TestHearRecordsTheEventAsAWaitingMailBeforeSucceeding(t *testing.T) {
store := newMemStore()
now := func() time.Time { return time.Date(2026, 10, 20, 10, 0, 1, 0, time.UTC) }
raw := addedEventBody(t, "bob", "INBOX", "<msg-1@example.org>", 7)
if err := Hear(context.Background(), store, raw, now); err != nil {
t.Fatal(err)
}
waiting, err := store.Waiting(context.Background())
if err != nil {
t.Fatal(err)
}
if len(waiting) != 1 {
t.Fatalf("waiting = %v, want one waiting mail", waiting)
}
w := waiting[0]
if w.Account != "bob" || w.Folder != "INBOX" || w.MessageID != "<msg-1@example.org>" || w.UID != 7 ||
w.Subject != "hello" || w.Size != 1234 || len(w.From) != 1 || w.From[0].Address != "alice@example.org" {
t.Fatalf("waiting mail: %+v", w)
}
}
// 1518/AC2: a store failure is returned unchanged, so the event is redelivered rather than lost.
func TestHearReturnsAStoreFailureSoTheEventIsRedelivered(t *testing.T) {
store := newMemStore()
store.failNext = errStoreFake
raw := addedEventBody(t, "bob", "INBOX", "<msg-2@example.org>", 8)
err := Hear(context.Background(), store, raw, time.Now)
if err != errStoreFake {
t.Fatalf("Hear with a failing store: %v, want %v", err, errStoreFake)
}
waiting, _ := store.Waiting(context.Background())
if len(waiting) != 0 {
t.Fatalf("waiting = %v, want none (the failed add kept nothing)", waiting)
}
}
// 1518/AC3: the same event heard twice gives one waiting mail.
func TestHearTwiceGivesOneWaitingMail(t *testing.T) {
store := newMemStore()
raw := addedEventBody(t, "bob", "INBOX", "<msg-3@example.org>", 9)
if err := Hear(context.Background(), store, raw, time.Now); err != nil {
t.Fatal(err)
}
if err := Hear(context.Background(), store, raw, time.Now); err != nil {
t.Fatal(err)
}
waiting, _ := store.Waiting(context.Background())
if len(waiting) != 1 {
t.Fatalf("waiting = %v, want exactly one", waiting)
}
}
// 1518/AC4: waiting mails are handed out in arrival order (the order they were heard), oldest first — not by
// their own date, and regardless of whether reading was healthy when they arrived (Hear never consults
// health; a mail heard while unhealthy is recorded exactly the same way).
func TestHearKeepsArrivalOrder(t *testing.T) {
store := newMemStore()
// Heard out of date order: the third mail (by date) arrives first.
first := addedEventBody(t, "bob", "INBOX", "<a@example.org>", 1)
second := addedEventBody(t, "bob", "INBOX", "<b@example.org>", 2)
third := addedEventBody(t, "bob", "INBOX", "<c@example.org>", 3)
for _, raw := range []json.RawMessage{first, second, third} {
if err := Hear(context.Background(), store, raw, time.Now); err != nil {
t.Fatal(err)
}
}
waiting, _ := store.Waiting(context.Background())
if len(waiting) != 3 {
t.Fatalf("waiting = %v, want three", waiting)
}
wantOrder := []string{"<a@example.org>", "<b@example.org>", "<c@example.org>"}
for i, w := range waiting {
if w.MessageID != wantOrder[i] {
t.Fatalf("waiting[%d].MessageID = %q, want %q (arrival order)", i, w.MessageID, wantOrder[i])
}
}
}
// A malformed event is taken and dropped, never redelivered forever (mirrors mesh-mail-notifier's watchWith).
func TestHearDropsAMalformedEvent(t *testing.T) {
store := newMemStore()
if err := Hear(context.Background(), store, json.RawMessage(`not json`), time.Now); err != nil {
t.Fatalf("Hear on a malformed event: %v, want it taken and dropped", err)
}
waiting, _ := store.Waiting(context.Background())
if len(waiting) != 0 {
t.Fatalf("waiting = %v, want none", waiting)
}
}
// TestHearLoopRetriesStartingAt250msDoublingToAMinute (issue 1518, fixed per the lead's try feedback on
// bb594652): a subscribe the runtime refuses is retried starting at hearFirstRetryWait (250ms), doubling
// each time, capped at a minute — never mesh-mail-notifier's own 2s start, so a bundle hears within about a
// second of stdio.ServeOn beginning to serve. storeFor is never called: every refusal is the subscribe's own.
func TestHearLoopRetriesStartingAt250msDoublingToAMinute(t *testing.T) {
var waits []time.Duration
recordSleep := func(d time.Duration) { waits = append(waits, d) }
calls := 0
subscribe := func(string, func(stdio.Envelope) error) error {
calls++
if calls <= 4 {
return errors.New("the runtime refused this subscribe")
}
return nil
}
storeFor := func(context.Context) (Store, error) {
t.Fatal("hearLoop must not call storeFor merely to retry a subscribe refusal")
return nil, nil
}
hearLoop(storeFor, subscribe, recordSleep)
want := []time.Duration{250 * time.Millisecond, 500 * time.Millisecond, time.Second, 2 * time.Second}
if len(waits) != len(want) {
t.Fatalf("waited %v, want %v", waits, want)
}
for i, w := range want {
if waits[i] != w {
t.Fatalf("wait[%d] = %s, want %s", i, waits[i], w)
}
}
if calls != 5 {
t.Fatalf("subscribe was called %d time(s), want 5 (four refusals, then the one that succeeded)", calls)
}
}