Files
selfpost/internal/store/sendercheck_test.go
T
mixandClaude Opus 5.5 c452606175
test / test (push) Canceled after 0s
logtail: the deferred counter remembers a place in the log, not a second (stage 2b)
The counter passed over, while catching up after a start, every warning no
later than the last one counted - by time; mail.log stamps whole seconds,
so a second warning of that second, later in the file, was lost.

- The tailer gives each line its place (Place: the file's fingerprint and
  the offset just past the line): follow tracks the bytes the reader hands
  over, readRotated likewise in mail.log.1. Run hands tr.line to the
  counter.
- store.AddSenderCheckDeferred(at, n, place) counts, moves the
  last-deferral mark (never back - an upsert WHERE) and saves the place
  (settings sender_check_counted_place) in one transaction.
- Catching up, a warning in the file of the last one counted is passed over
  by place (end <= counted end); in another file - a rotated log read again
  - by time, as before. The fingerprint carries the inode, so the place
  stays true across a rotation.

Tests: two warnings of one second, an unclean stop between them, all lines
read again - each counted once; the store saves the place with the count
and keeps the latest deferral time. Existing counter tests pass a zero
place (time rule).

CHANGELOG [Unreleased], audit plan line ticked, roadmap 24/62.
Evidence: go vet ./... clean; go test ./internal/store, ./internal/logtail ok
(but for the Windows-only test); the server run follows before the push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:28:13 +03:00

134 lines
4.5 KiB
Go

package store
import (
"path/filepath"
"testing"
"time"
)
func TestSenderCheckStats(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "selfpost.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
now := time.Date(2026, 10, 20, 15, 0, 0, 0, time.UTC)
day := func(n int) time.Time { return now.AddDate(0, 0, -n) }
// Nothing counted yet: zeros, and no first day.
got, err := st.SenderCheckStats(now)
if err != nil {
t.Fatal(err)
}
if got != (SenderCheckStats{}) {
t.Fatalf("empty statistics = %+v", got)
}
// The first day is recorded once.
if err := st.StartSenderCheckStats(day(40)); err != nil {
t.Fatal(err)
}
if err := st.StartSenderCheckStats(now); err != nil {
t.Fatal(err)
}
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(st.AddSenderCheckDown(now, 3*time.Second))
must(st.AddSenderCheckDown(now, 1500*time.Millisecond)) // whole seconds: one
must(st.AddSenderCheckDown(now, 400*time.Millisecond)) // nothing
must(st.AddSenderCheckDeferred(now.Add(-time.Hour), 2, ""))
must(st.AddSenderCheckDown(day(10), 30*time.Second))
must(st.AddSenderCheckDeferred(day(10), 1, ""))
must(st.AddSenderCheckDown(day(29), time.Minute))
must(st.AddSenderCheckDown(day(30), time.Hour)) // the 31st day back: the year's only
must(st.AddSenderCheckDeferred(day(200), 5, ""))
must(st.AddSenderCheckDown(day(365), 24*time.Hour)) // a year and a day: nobody's
got, err = st.SenderCheckStats(now)
if err != nil {
t.Fatal(err)
}
want := SenderCheckStats{
Today: SenderCheckTotals{Down: 4 * time.Second, Deferred: 2},
Month: SenderCheckTotals{Down: 94 * time.Second, Deferred: 3},
Year: SenderCheckTotals{Down: 94*time.Second + time.Hour, Deferred: 8},
Since: time.Date(2026, 9, 10, 0, 0, 0, 0, time.UTC),
DeferredAt: now.Add(-time.Hour),
}
if got != want {
t.Fatalf("statistics\n got %+v\nwant %+v", got, want)
}
// The last deferral is the latest one seen, not the last one written: a
// log read again after an unclean stop does not move it back.
if got.DeferredAt != now.Add(-time.Hour) {
t.Fatalf("an older deferral moved the mark to %s", got.DeferredAt)
}
// A year on, the old days are gone.
must(st.PruneSenderCheckDays(now.AddDate(1, 0, 0)))
var n int
if err := st.db.QueryRow(`SELECT COUNT(*) FROM sender_check_days`).Scan(&n); err != nil || n != 1 {
t.Fatalf("%d days left after a year's prune (%v), want the newest alone", n, err)
}
}
// A refused row keeps why it was refused and, for its From: line, what the
// line said; an accepted row has neither.
func TestRefusedRowKeepsItsReason(t *testing.T) {
st, err := Open(filepath.Join(t.TempDir(), "selfpost.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
if err := st.InsertRejected(SendLogEntry{Domain: "shop.example.org", AppLogin: "shop", From: "shop@shop.example.org",
To: "k.berg@web.de", Subject: "s", Reason: ReasonFrom, HeaderFrom: "director@example.org"}); err != nil {
t.Fatal(err)
}
if err := st.InsertQueued(SendLogEntry{QueueID: "4XcB7k2Jm9z1", Domain: "shop.example.org", AppLogin: "shop",
From: "shop@shop.example.org", To: "k.berg@web.de"}); err != nil {
t.Fatal(err)
}
rows, err := st.QuerySendLog(SendLogFilter{AllDomains: true}, 10, 0)
if err != nil || len(rows) != 2 {
t.Fatalf("rows = %d, %v", len(rows), err)
}
queued, refused := rows[0], rows[1]
if refused.Status != StatusRejected || refused.Reason != ReasonFrom || refused.HeaderFrom != "director@example.org" || refused.To != "k.berg@web.de" {
t.Errorf("refused row = %+v", refused)
}
if queued.Reason != "" || queued.HeaderFrom != "" {
t.Errorf("queued row carries a refusal: %+v", queued)
}
one, err := st.GetSendLog(refused.ID)
if err != nil || one.Reason != ReasonFrom || one.HeaderFrom != "director@example.org" {
t.Errorf("GetSendLog = %+v, %v", one, err)
}
}
// The place in the log is saved with the count, and the moment of the last
// deferral does not move back when a log is read again.
func TestSenderCheckDeferredSavesItsPlace(t *testing.T) {
st := openTestStore(t)
later := time.Date(2026, 10, 14, 12, 0, 1, 0, time.UTC)
if err := st.AddSenderCheckDeferred(later, 1, "abc:1.2@100"); err != nil {
t.Fatal(err)
}
if err := st.AddSenderCheckDeferred(later.Add(-time.Hour), 1, "abc:1.2@200"); err != nil {
t.Fatal(err)
}
if p, err := st.SenderCheckCountedPlace(); err != nil || p != "abc:1.2@200" {
t.Fatalf("place = %q, %v", p, err)
}
if at, err := st.SenderCheckDeferredAt(); err != nil || !at.Equal(later) {
t.Fatalf("last deferral = %s, %v; want %s", at, err, later)
}
}