Files
selfpost/internal/store/deliverylog_test.go
T
mixandClaude Opus 5.5 0ed03b2094
test / test (push) Canceled after 0s
log tailer: expiry, Postfix's time values and refusal lines as Postfix writes them
Expiry (assumptions A-8). Postfix 3.7.11 logs the end of a message's queue
lifetime once per message, "<id>: from=<..>, status=expired, returned to
sender" (src/qmgr/qmgr_active.c, qmgr_active_done_25_generic()), with no
line per recipient; the tailer waited for "to=..., status=expired", which
no Postfix writes, and the sweep closed only queued rows, so such rows
stayed deferred. The tailer now reads qmgr's line (ExpireMessage: each
recipient's newest open row of the id becomes bounced); the sweep closes a
deferred row too when Postfix no longer holds the message, and counts as
missed only a final line (sent, bounced, expiry). The queue listing's '#'
(forced expiry, showq_compat.c showq_message()) is read as a flag (B-PQ1).

Time values (B-PQ10, B-PQ11): conv_time() ported - one number, one
lower-case unit, the parameter's default unit when none (get_def_time_unit:
d for the lifetimes, h for delay_warning_time), the rest ignored; postconf
-h -x expands $name; bounce_queue_lifetime capped at maximal_queue_lifetime
as qmgr_post_init() caps it, so LongestLifetime is the queue lifetime.

Refusals (A-21, A-23, A-24, A-15): smtpd_whatsup() ends the NOQUEUE line
with sasl_username=<login> on an authenticated session; the line is now
matched to its row by client, login and sender, and a line without a login
is no application's. The client= line's login ends at the comma. The
"IPv6:" trim goes (namaddr is built from state->addr). A row recovered from
cleanup's line says that line names the last recipient only.

Stand (2026-10-14, make e2e, exit 0): new expired_message_is_closed -
defer_transports=smtp makes the retry service defer the message, postsuper
-e and postqueue -i force its expiry; mail.log has "<id>:
from=<alerts@sender.e2e.test>, status=force-expired, returned to sender"
and the row goes deferred -> bounced; refusals_show_postfixs_line passes
with the login match. go vet and go test ./... pass on the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:21:51 +03:00

94 lines
2.9 KiB
Go

package store
import (
"testing"
"time"
)
// The counters add up per UTC day, the stuck level is overwritten rather than
// added, nothing is written for nothing, and a year is kept.
func TestDeliveryLogDays(t *testing.T) {
st := openTestStore(t)
day1 := time.Date(2026, 10, 14, 23, 59, 0, 0, time.UTC)
day2 := day1.Add(2 * time.Minute)
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(st.AddReconcileClosed(day1, 2))
must(st.AddReconcileClosed(day1, 3))
must(st.AddReconcileClosed(day1, 0))
must(st.AddReconcileMissed(day1, 2))
must(st.AddTailerBlind(day1, 1500*time.Millisecond))
must(st.AddTailerBlind(day1, 900*time.Millisecond)) // under a second: nothing
must(st.SetQueuedOverLifetime(day1, 4))
must(st.SetQueuedOverLifetime(day1, 1))
must(st.AddTailerBlind(day2, time.Minute))
days, err := st.DeliveryLogDays()
must(err)
want := []DeliveryLogDay{
{Day: "2026-10-14", Closed: 5, Missed: 2, BlindSeconds: 1, Stuck: 1},
{Day: "2026-10-15", BlindSeconds: 60},
}
if len(days) != len(want) {
t.Fatalf("days = %+v, want %+v", days, want)
}
for i := range want {
if days[i] != want[i] {
t.Errorf("day %d = %+v, want %+v", i, days[i], want[i])
}
}
must(st.PruneDeliveryLogDays(day2.AddDate(1, 0, 1)))
days, err = st.DeliveryLogDays()
must(err)
if len(days) != 1 || days[0].Day != "2026-10-15" {
t.Fatalf("after pruning a year and a day later: %+v, want 2026-10-15 alone", days)
}
}
// Only the counters may be added to by name.
func TestAddDeliveryLogDayRefusesAnotherColumn(t *testing.T) {
st := openTestStore(t)
if err := st.addDeliveryLogDay(time.Now(), "day", 1); err == nil {
t.Fatal("addDeliveryLogDay took the column day")
}
}
func TestCountOpenOlderThan(t *testing.T) {
st := openTestStore(t)
for _, e := range []SendLogEntry{{QueueID: "Q1", To: "a@example.net"}, {QueueID: "Q2", To: "b@example.net"}} {
if err := st.InsertQueued(e); err != nil {
t.Fatal(err)
}
}
if _, err := st.UpdateStatus("Q2", "b@example.net", StatusSent); err != nil {
t.Fatal(err)
}
if n, err := st.CountOpenOlderThan(time.Now().Add(time.Hour)); err != nil || n != 1 {
t.Fatalf("queued rows before an hour from now = %d, %v; want 1", n, err)
}
if n, err := st.CountOpenOlderThan(time.Now().Add(-time.Hour)); err != nil || n != 0 {
t.Fatalf("queued rows before an hour ago = %d, %v; want 0", n, err)
}
}
// Statistics start no earlier than the entity: what came before is a
// predecessor's of the same name (review of audit stage 2).
func TestStatsWindowStartsAtTheEntity(t *testing.T) {
created := time.Now().UTC().Add(-48 * time.Hour)
since, _, _ := StatsWindow(90, created)
if !since.Equal(created) {
t.Fatalf("since = %s, want the entity's creation %s", since, created)
}
old := time.Now().UTC().AddDate(-1, 0, 0)
since, _, _ = StatsWindow(90, old)
if since.Before(time.Now().UTC().AddDate(0, 0, -31)) {
t.Fatalf("since = %s, want within the 30-day window", since)
}
}