test / test (push) Canceled after 0s
Migration 0010 (DDL only; 0009 went to stage 2b). EXPLAIN QUERY PLAN of
every send_log statement showed, beyond M2 as written (stage 2 had already
moved the by-domain counts onto (domain_id, created_at)):
- the tailer's sweep of open rows (every 5 min) and its count scanned the
table: partial idx_send_log_open (created_at) WHERE status IN ('queued',
'deferred'), the statuses literal in the statements, ORDER BY +id (else
SQLite walks the table in id order to skip a sort);
- AttachRefusalLine's NOT EXISTS (... refusal_line = ?1) scanned the table
for every refusal line: partial idx_send_log_refusal_line, the query
carrying the index's WHERE; its row found by idx_send_log_rejected
(status literal, +queue_id) instead of every refusal without a queue id;
- an application's level-2 count went through its domain's index:
+domain_id in appByLogin;
- a page of one domain's or one application's log sorted all of it:
idx_send_log_domain_id, idx_send_log_app_login (id order; several domains
still sort, top-N). idx_send_log_domain stays for the global filter.
The statements are constants and builders (countMessagesQuery,
peakPerHourQuery, listOpenQuery, countOpenQuery, sendLogPageQuery,
attachRefusalLineQuery, limitRefusalsQuery) and
TestSendLogQueriesUseTheirIndexes explains exactly those: the index each
must use, no SCAN of send_log, no sort where none is wanted; the literal
statuses checked against the constants. It replaces the hand-written plan
check of TestLimitRefusalsSince.
Tested on the test server: go vet + go test of 24 packages, make e2e (all
pass) on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
7.5 KiB
Go
207 lines
7.5 KiB
Go
package store
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The instance switch is on until someone turns it off: a database that never
|
|
// saw the setting sends notifications.
|
|
func TestNotificationsEnabledByDefault(t *testing.T) {
|
|
s := openTestStore(t)
|
|
if on, err := s.NotificationsEnabled(); err != nil || !on {
|
|
t.Fatalf("a fresh database: enabled = %v, %v; want on", on, err)
|
|
}
|
|
if err := s.SetNotificationsEnabled(false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if on, err := s.NotificationsEnabled(); err != nil || on {
|
|
t.Errorf("after turning off: enabled = %v, %v", on, err)
|
|
}
|
|
if err := s.SetNotificationsEnabled(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if on, err := s.NotificationsEnabled(); err != nil || !on {
|
|
t.Errorf("after turning on again: enabled = %v, %v", on, err)
|
|
}
|
|
}
|
|
|
|
// Who is written to: every global user about everything, a domain user about
|
|
// the domains on their list or all of them - and nobody without an e-mail or
|
|
// with their own switch off.
|
|
func TestNotifyRecipients(t *testing.T) {
|
|
s := openTestStore(t)
|
|
mine, err := s.AddDomain("mine.example.com", "sp")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := s.AddDomain("other.example.com", "sp")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
user := func(name string, role Role, reach Reach, email string, notify bool) {
|
|
t.Helper()
|
|
id, err := s.CreateUser(name, "hash", role, reach)
|
|
if err != nil {
|
|
t.Fatalf("create %s: %v", name, err)
|
|
}
|
|
if err := s.UpdateUser(id, name, "hash", email); err != nil {
|
|
t.Fatalf("update %s: %v", name, err)
|
|
}
|
|
if u, err := s.GetUser(id); err != nil || !u.Notify {
|
|
t.Fatalf("%s: a new user's switch is %v, %v; want on", name, u.Notify, err)
|
|
}
|
|
if !notify {
|
|
if err := s.SetUserNotify(id, false); err != nil {
|
|
t.Fatalf("switch off %s: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
user("root", RoleGlobal, Reach{}, "root@example.org", true)
|
|
user("root2", RoleGlobal, Reach{}, "root@example.org", true) // the same mailbox
|
|
user("quiet", RoleGlobal, Reach{}, "quiet@example.org", false)
|
|
user("nomail", RoleGlobal, Reach{}, "", true)
|
|
user("mine", RoleDomain, Reach{DomainIDs: []int64{mine.ID}}, "mine@example.org", true)
|
|
user("theirs", RoleDomain, Reach{DomainIDs: []int64{other.ID}}, "theirs@example.org", true)
|
|
user("every", RoleDomain, Reach{AllDomains: true}, "every@example.org", true)
|
|
user("mineoff", RoleDomain, Reach{DomainIDs: []int64{mine.ID}}, "mineoff@example.org", false)
|
|
|
|
for _, c := range []struct{ domain, want string }{
|
|
{"", "root@example.org"},
|
|
{"mine.example.com", "every@example.org mine@example.org root@example.org"},
|
|
{"other.example.com", "every@example.org root@example.org theirs@example.org"},
|
|
{"gone.example.com", "every@example.org root@example.org"},
|
|
} {
|
|
got, err := s.NotifyRecipients(c.domain)
|
|
if err != nil {
|
|
t.Fatalf("recipients for %q: %v", c.domain, err)
|
|
}
|
|
if strings.Join(got, " ") != c.want {
|
|
t.Errorf("recipients for %q = %q, want %q", c.domain, strings.Join(got, " "), c.want)
|
|
}
|
|
}
|
|
|
|
if err := s.SetUserNotify(99999, true); err != ErrUserNotFound {
|
|
t.Errorf("switch of a user that does not exist: %v", err)
|
|
}
|
|
}
|
|
|
|
// What has been said is remembered by key, replaced whole, and forgotten.
|
|
func TestNotifyStateRoundTrip(t *testing.T) {
|
|
s := openTestStore(t)
|
|
if _, ok, err := s.GetNotifyState("dns:example.com"); err != nil || ok {
|
|
t.Fatalf("no state yet: ok = %v, %v", ok, err)
|
|
}
|
|
since := time.Date(2026, 10, 12, 9, 0, 0, 0, time.UTC)
|
|
st := NotifyState{Key: "dns:example.com", Phase: "pending", Count: 1, Since: since}
|
|
if err := s.SetNotifyState(st); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, ok, err := s.GetNotifyState("dns:example.com")
|
|
if err != nil || !ok || got.Phase != "pending" || got.Count != 1 || !got.Since.Equal(since) || !got.SentAt.IsZero() {
|
|
t.Fatalf("state = %+v, %v, %v", got, ok, err)
|
|
}
|
|
|
|
st.Phase, st.Count, st.Mark, st.SentAt = "open", 0, "x", since.Add(time.Hour)
|
|
if err := s.SetNotifyState(st); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _, _ = s.GetNotifyState("dns:example.com")
|
|
if got.Phase != "open" || got.Mark != "x" || !got.SentAt.Equal(since.Add(time.Hour)) {
|
|
t.Errorf("replaced state = %+v", got)
|
|
}
|
|
|
|
for _, key := range []string{"dns:other.example.com", "queue", "limit:example.com:app"} {
|
|
if err := s.SetNotifyState(NotifyState{Key: key, Phase: "open", Since: since}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
keys, err := s.ListNotifyStateKeys("dns:")
|
|
if err != nil || strings.Join(keys, " ") != "dns:example.com dns:other.example.com" {
|
|
t.Errorf("dns keys = %v, %v", keys, err)
|
|
}
|
|
if err := s.DeleteNotifyState("dns:example.com"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok, _ := s.GetNotifyState("dns:example.com"); ok {
|
|
t.Error("state still there after delete")
|
|
}
|
|
}
|
|
|
|
// Refused mail is counted per sending domain from a point in time - and only
|
|
// what was refused under a limit. The row's own domain is what the client
|
|
// typed in MAIL FROM; the login's registered domain is what counts. A login
|
|
// that is nobody's is not counted, nor is one refused with no limit set (its
|
|
// client address was not on its list).
|
|
func TestLimitRefusalsSince(t *testing.T) {
|
|
s := openTestStore(t)
|
|
d, err := s.AddDomain("example.com", "sp")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := s.AddDomain("other.example", "sp")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app := func(domainID int64, login string) int64 {
|
|
t.Helper()
|
|
a, err := s.AddApplication(domainID, login, AddressModeWildcard, nil)
|
|
if err != nil {
|
|
t.Fatalf("add %s: %v", login, err)
|
|
}
|
|
return a.ID
|
|
}
|
|
limit := func(scope string, id int64) {
|
|
t.Helper()
|
|
if err := s.SetRateLimit(RateLimit{Scope: scope, RefID: id, MaxMessages: 1, WindowSeconds: 3600}); err != nil {
|
|
t.Fatalf("set limit: %v", err)
|
|
}
|
|
}
|
|
limit(RateLimitScopeApp, app(d.ID, "app1"))
|
|
limit(RateLimitScopeApp, app(d.ID, "app2"))
|
|
app(d.ID, "nolimit") // refused for its client address
|
|
app(other.ID, "byitsdomain") // limited through its domain
|
|
limit(RateLimitScopeDomain, other.ID)
|
|
|
|
for _, e := range []SendLogEntry{
|
|
{Domain: "example.com", AppLogin: "app1", From: "a@example.com"},
|
|
{Domain: "victim.example", AppLogin: "app1", From: "a@victim.example"}, // a domain the client made up
|
|
{Domain: "other.example", AppLogin: "app2", From: "b@other.example"}, // another tenant's name
|
|
{Domain: "example.com", AppLogin: "nolimit", From: "c@example.com"},
|
|
{Domain: "example.com", AppLogin: "ghost", From: "d@example.com"},
|
|
{Domain: "other.example", AppLogin: "byitsdomain", From: "e@other.example"},
|
|
} {
|
|
if err := s.InsertRejected(e); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := s.InsertQueued(SendLogEntry{QueueID: "ABC123", Domain: "example.com", AppLogin: "app1", To: "x@example.net"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.InsertRejected(SendLogEntry{Domain: "example.com", AppLogin: "app2", From: "old@example.com"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.db.Exec(`UPDATE send_log SET created_at = '2026-01-01T00:00:00Z' WHERE from_addr = 'old@example.com'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := s.LimitRefusalsSince(time.Now().UTC().Add(-time.Hour))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("refusals = %+v, want two domains", got)
|
|
}
|
|
if g := got[0]; g.Domain != "example.com" || strings.Join(g.Apps, " ") != "app1 app2" || g.Count != 3 || g.First.IsZero() {
|
|
t.Errorf("example.com = %+v; want app1 (2, one under a made-up domain) and app2 (1, under another tenant's name)", g)
|
|
}
|
|
if g := got[1]; g.Domain != "other.example" || strings.Join(g.Apps, " ") != "byitsdomain" || g.Count != 1 {
|
|
t.Errorf("other.example = %+v", g)
|
|
}
|
|
|
|
// The question is answered from the index of the refused rows alone:
|
|
// TestSendLogQueriesUseTheirIndexes.
|
|
}
|