test / test (push) Canceled after 0s
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>
94 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|