test / test (push) Canceled after 0s
Technical review (Opus) and security review (Fable) of a59e3d4..cb5a4a2;
reports in the session's scratchpad. Nothing critical, high or medium; no
outright bug. Fixed:
- F2, F3 (0008 backfill, before it runs on the live database): rows are
filled by the rule new rows follow - the application's domain, then the
name's - and only by a domain and application that existed when the row
was written (created_at); the inbound log likewise. Test: the migration's
UPDATEs re-run on prepared rows (before the domain, after, null sender,
application gone, a stranger's name). Dry run on a copy of the live
database: 68/68 and 10/10 rows filled, none disagreeing with its login's
domain, integrity ok.
- V2, F4: an application's statistics and level-2 count are its login's
rows of its domain (appByLogin), and StatsWindow starts no earlier than
the entity's creation; the message page's "may send as" only for a login
still of the row's domain. Test: a login taken again in another domain
starts at zero.
- F7: MAIL FROM:<> is held to the domain limit of its login
(ApplicationByLogin fills DomainName).
- V3: an imported DKIM selector is held to DNS labels (validate.Selector).
- F5, F6: an imported auto limit's numbers are dropped and recalculated
right after the import; its mode is stored trimmed.
- V6: the store refuses a role that is neither global nor domain.
- F9: SaveUser keeps the stored hash when no new password is given (the
form no longer writes back the hash it read). F10: a reach naming a
domain deleted meanwhile is ErrNoSuchDomain, answered on the form.
- V4, F12: from-lines-other.txt - lines naming another sender ({other}) in
a quoted string, a quoted pair, capitals, a comment, an encoded word, a
quoted local part; the unit test wants each refused, the e2e
(lines_naming_another_sender_never_signed_for_it) wants each refused and
none signed for it. The passed list gained a.{DOMAIN}, a comment after
the angle address, a quoted local part, and "b@ {other}" - passed by
design (security.md), and the e2e shows it signed for the own domain.
- F13: e2e login_in_other_capitals_does_not_authenticate - AUTH with the
login upper-cased gets 535.
Not changed, written down: V1/F1 - L8 is half closed (refused or passed
still tells an application which domains are hosted here); security.md's
accepted risks and the CHANGELOG say so, and the owner is asked whether to
refuse every foreign address in a display name (changes the 2.4.0 rule).
F11 - deleting a domain rewrites its rows' domain_id (seconds at 1e5 rows),
accepted at this size. F8 (test cannot tell write-first from read-first;
the reviewer verified it with two stores, 30/30), V5, V7, P2: noted.
Evidence on the test server for this tree: go vet clean, go test ./... 23
packages ok, make e2e exit 0 with the three new cases.
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 TestCountQueuedOlderThan(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.CountQueuedOlderThan(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.CountQueuedOlderThan(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)
|
|
}
|
|
}
|