154 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|