package calls import ( "bytes" "context" "encoding/base64" "io" "log/slog" "net/http/httptest" "net/url" "path/filepath" "sync" "testing" "time" "amadan.net/rastrillo/rastrillo/db" ) // blockingMailer holds every Send until release is closed — an SMTP // relay that takes as long as the test likes. type blockingMailer struct { release chan struct{} mu sync.Mutex to []string } func (m *blockingMailer) Send(_ context.Context, to, _, _ string) error { <-m.release m.mu.Lock() m.to = append(m.to, to) m.mu.Unlock() return nil } func (m *blockingMailer) sent() []string { m.mu.Lock() defer m.mu.Unlock() return append([]string(nil), m.to...) } // TestSigninNeverWaitsForTheMailer: when the send ran inside the // request, a member waited for SMTP before its ?sent=1 and a stranger // did not (Astra measured 252 ms against 0.35 ms on Slides, which had // the same shape). With the mailer blocked, every POST — member, // stranger, and both with force=1 — must answer, and the member's // links must then go out. func TestSigninNeverWaitsForTheMailer(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) d, err := db.Open(filepath.Join(t.TempDir(), "calls.db"), logger) if err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) bm := &blockingMailer{release: make(chan struct{})} var once sync.Once free := func() { once.Do(func() { close(bm.release) }) } t.Cleanup(free) mux, _, err := buildForTest(t, d, Config{Origin: keymailOrigin, InstanceKey: "test-only-instance", DEK: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{3}, 32)), AllowedAddresses: []string{plainMember}, Mailer: bm, Logger: logger}) if err != nil { t.Fatal(err) } f := &keymailFixture{handler: mux} for _, form := range []url.Values{ {"address": {plainMember}}, {"address": {"someone@else.test"}}, {"address": {plainMember}, "force": {"1"}}, {"address": {"someone@else.test"}, "force": {"1"}}, } { done := make(chan *httptest.ResponseRecorder, 1) go func() { done <- f.post(form) }() select { case rec := <-done: if rec.Code != 303 || rec.Header().Get("Location") != "/signin?sent=1" { t.Errorf("POST %v: %d %q, want 303 /signin?sent=1", form, rec.Code, rec.Header().Get("Location")) } case <-time.After(2 * time.Second): t.Errorf("POST %v waited on the mailer: the response time says the address is a member", form) free() <-done } } free() deadline := time.Now().Add(5 * time.Second) for len(bm.sent()) < 2 && time.Now().Before(deadline) { time.Sleep(10 * time.Millisecond) } time.Sleep(50 * time.Millisecond) if got := bm.sent(); len(got) != 2 || got[0] != plainMember || got[1] != plainMember { t.Fatalf("mail sent = %v, want two links, both to %s", got, plainMember) } }