package view import ( "fmt" "strings" "testing" "time" ) // ---- Outbound queue // The page is the mockup's table, a row per recipient with its last reply and // the link to the message, and it polls from its box; the retry policy sits // under it, outside the poll. func TestOutQueueShowsTheTableAndPollsItsBox(t *testing.T) { out := renderSignedIn(t, "out-queue", outQueueFixture()) pageHas(t, "Outbound queue", out, `

Outbound queue

`, `

Waiting

`, `Messages accepted from applications and still waiting for the receiving server. Refreshes on its own.`, `id="out-queue" data-poll aria-live="polite" hx-get="/outbound/queue/fragment" hx-trigger="load" hx-swap="outerHTML"`, `1 message · 3.1 kB`, `href="/help#queue"`, `Queue idArrivedFromToLast reply`, `4XcB7k2Jm9z109-21 13:58:07`, `alerts@example.orgnoc@example.org`, `451 4.7.1 Greylisted, try again in 5 minutesDetails`, `aria-current="page">Queue`, ) pageLacks(t, "Outbound queue", out, `class="sp-log"`, "Nothing is waiting", "postqueue -p") frag := renderFragment(t, "queue_body", outQueueFixture()) if !strings.HasPrefix(frag, `
`) != 2 { t.Error("the two recipients of a message without a send-log row should have an empty Details cell") } } // A backlog is cut at maxQueueRows, oldest kept, and the foot says how many // there are. func TestOutQueueCutsALongBacklog(t *testing.T) { t0 := time.Date(2026, 10, 18, 9, 0, 0, 0, time.Local) var entries []QueueEntry for i := 0; i < maxQueueRows+5; i++ { entries = append(entries, QueueEntry{ID: fmt.Sprintf("Q%04d", i), Queue: "deferred", Arrival: t0.Add(time.Duration(i) * time.Minute), Recipient: "r@example.net"}) } out := renderSignedIn(t, "out-queue", NewOutQueue(admin(), entries, "")) pageHas(t, "long queue", out, "Q0000", fmt.Sprintf("Q%04d", maxQueueRows-1), fmt.Sprintf("The oldest %d of %d recipients waiting.", maxQueueRows, maxQueueRows+5), `class="sp-box-foot"`) pageLacks(t, "long queue", out, fmt.Sprintf("Q%04d", maxQueueRows)) } // The policy is the page's, wherever it was read from: its facts, and a head // that says so when they are the compiled-in defaults. func TestOutQueueStatesTheRetryPolicy(t *testing.T) { out := renderSignedIn(t, "out-queue", NewOutQueue(admin(), nil, "").WithRetryPolicy("10 minutes", "about 1 hour 7 minutes", "2 days", false)) pageHas(t, "Outbound queue", out, `

How retries work

`, `This Postfix's policy, read at panel start`, `
10 minutes
`, `
doubling
`, `capped at about 1 hour 7 minutes`, `
2 days
`, `
bounced
`, `no attempt counter, only time`, ) pageLacks(t, "Outbound queue", out, "Compiled-in defaults") out = renderSignedIn(t, "out-queue", NewOutQueue(admin(), nil, "").WithRetryPolicy("5 minutes", "about 1 hour 7 minutes", "5 days", true)) pageHas(t, "Outbound queue", out, "Compiled-in defaults: the Postfix configuration could not be read") pageLacks(t, "Outbound queue", out, "read at panel start") } // With nothing to list the box says why instead: the queue holds nothing, or // it could not be read — and never both. func TestOutQueueSaysWhyItHasNoRows(t *testing.T) { out := renderSignedIn(t, "out-queue", NewOutQueue(admin(), nil, "")) pageHas(t, "empty queue", out, `class="sp-empty"`, "Nothing is waiting. Every message has reached its receiving server.", "Refreshes on its own") pageLacks(t, "empty queue", out, "Inbound queue`, `The same Postfix queue as outbound, filtered by recipient domain.`, `id="in-queue" data-poll aria-live="polite" hx-get="/inbound/queue/fragment" hx-trigger="load" hx-swap="outerHTML"`, `Nothing is waiting. Every accepted message has reached its upstream.`, `href="/help#inbound-queue"`, `back to the original sender`, ) rows := []QueueEntry{{ID: "IN0001", Queue: "deferred", Arrival: time.Date(2026, 10, 18, 9, 0, 0, 0, time.Local), Size: 5000, Sender: "someone@example.net", Recipient: "staff@lists.example.com", Reason: "451 4.7.1 Greylisted"}} out = renderSignedIn(t, "in-queue", NewInQueue(admin(), rows, "")) pageHas(t, "Inbound queue", out, `1 message · 4.9 kB`, `IN0001`, `staff@lists.example.com`, `451 4.7.1 Greylisted`, `Last reply`) pageLacks(t, "Inbound queue", out, "Details", `sp-actions`) }