package callstest import ( "bytes" "context" "errors" "fmt" "log/slog" "net/http" "net/url" "path/filepath" "regexp" "strings" "testing" "time" "amadan.net/rastrillo/rastrillo/db" "amadan.net/oficina/calls/internal/calls" ) func TestMemberSignsInAndLandsOnTheList(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) if dest := c.signIn("paul@example.com"); dest != "/" { t.Fatalf("member landed on %s, want /", dest) } if rec := c.get("/"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Meetings") { t.Fatalf("GET / after sign-in: %d", rec.Code) } } func TestOutsiderWithoutALinkGetsNoMailAndTheSameAnswer(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) rec := c.post("/signin", url.Values{"address": {"stranger@example.com"}}) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" { t.Fatalf("refusal differs from success: %d %s", rec.Code, rec.Header().Get("Location")) } if x.mail.count() != 0 { t.Fatal("mail was sent to a non-member with no link") } } func TestSignedOutIsRedirectedToSignin(t *testing.T) { x := newHarness(t, "paul@example.com") rec := x.browser(t).get("/") if rec.Code != http.StatusSeeOther || !strings.HasPrefix(rec.Header().Get("Location"), "/signin") { t.Fatalf("GET / signed out: %d %s", rec.Code, rec.Header().Get("Location")) } } func TestNonMemberSessionSeesWelcomeNotTheList(t *testing.T) { // Development with no allowlist admits every address at sign-in but // makes nobody a member; that is the shape the guest tests rely on. x := newHarness(t) c := x.browser(t) if dest := c.signIn("anyone@example.com"); dest != "/welcome" { t.Fatalf("non-member landed on %s, want /welcome", dest) } rec := c.get("/") if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/welcome" { t.Fatalf("non-member GET /: %d %s, want 303 /welcome", rec.Code, rec.Header().Get("Location")) } if rec := c.get("/welcome"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "anyone@example.com") { t.Fatal("welcome page does not name the address") } if rec := c.post("/meetings/start", nil); rec.Code != http.StatusNotFound { t.Fatalf("guest can access member action: %d", rec.Code) } } // Codex round 2, C2-4: mailredact_test.go proves the wrapper is safe by // calling it directly, which would stay green even if cmd/calls/main.go // stopped wiring it up. This is the production-path check instead: an // app built with calls.App — the same entry point main.go calls, over a // mailer that fails and a real *slog.Logger — answers a real // POST /signin, and nothing that logger captured names the address. // (Internal 3 / review C1-2's resolve-wraps-everything design is what // this is actually testing: no call site can opt out of the wrap.) func TestSigninOverAFailingMailerLeavesNoAddressInTheLog(t *testing.T) { const address = "paul@example.com" var buf bytes.Buffer logger := slog.New(slog.NewTextHandler(&buf, nil)) d, err := db.Open(filepath.Join(t.TempDir(), "app.db"), logger) if err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) failing := &failingMailer{err: fmt.Errorf("rastrillo/mail: send to %s: %w", address, errFakeSMTPFailure)} mux, rt, err := calls.App(testCtx(t), d, calls.Config{ Env: calls.EnvDevelopment, Origin: testOrigin, AllowedAddresses: []string{address}, Mailer: failing, Logger: logger, }) if err != nil { t.Fatal(err) } t.Cleanup(rt.Close) // App's queue outlives its ctx until Close rec := post(t, mux, "/signin", url.Values{"address": {address}}) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" { t.Fatalf("POST /signin over a failing mailer: %d %s, want 303 /signin?sent=1 (the same answer as success)", rec.Code, rec.Header().Get("Location")) } failing.flush() // the send, and its failure, happen after the response log := buf.String() if strings.Contains(strings.ToLower(log), strings.ToLower(address)) { t.Fatalf("the log carries the address after a failing send:\n%s", log) } if !strings.Contains(log, "calls: mail delivery failed") { t.Fatalf("no redacted delivery failure was logged at all — the wrapper may not be wired:\n%s", log) } } var errFakeSMTPFailure = errors.New("550 mailbox unavailable") type failingMailer struct { err error waits []func() // the sign-in mail queue in front of it } func (f *failingMailer) Send(ctx context.Context, to, subject, body string) error { return f.err } func (f *failingMailer) AttachSigninQueue(wait func()) { f.waits = append(f.waits, wait) } func (f *failingMailer) flush() { for _, w := range f.waits { w() } } // Re-review of 9e067e4: a plain member sign-in — no meeting-link cookie, // so no pending_joins row at all — still leaves rastrillo's own // auth_links row for the magic link it mailed. nextExpiry (and so // NextDue) has to see that row, or an app that hibernates right after // would never be woken to sweep it. func TestNextDueWakesForAPlainMemberSignin(t *testing.T) { x := newHarness(t, "paul@example.com") if rec := x.browser(t).post("/signin", url.Values{"address": {"paul@example.com"}}); rec.Header().Get("Location") != "/signin?sent=1" { t.Fatalf("sign-in request: %s", rec.Header().Get("Location")) } if n := x.authLinks(t); n != 1 { t.Fatalf("auth_links rows after a plain sign-in: %d, want 1", n) } next := x.ctl.NextDue() if next.IsZero() { t.Fatal("NextDue is zero after a plain sign-in left an unclicked magic link outstanding") } // signin.Flow.LinkTTL defaults to 15 minutes // (keymaildev/signin@v0.1.1/flow.go:57,186); the wide window is // about not coupling this test to that constant, not about // precision. if until := next.Sub(x.now); until <= 0 || until > 20*time.Minute { t.Fatalf("NextDue is %v away, want a positive duration within the magic link's TTL window", until) } } // The sign-in page is the Oficina door (estilo chrome): one card on the // suite's sky with Calls' mark and name at its head. The form still posts // the field "address" to /signin, and a member's POST still answers // 303 /signin?sent=1 with a link in the mail, the sent page drawn as the // door too. func TestSigninPageIsTheOficinaDoor(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) rec := c.get("/signin") if rec.Code != http.StatusOK { t.Fatalf("GET /signin: %d", rec.Code) } body := rec.Body.String() for _, want := range []string{ "
Calls", ` · Calls". func TestSigninDoorSpeaksTheSuitesCopy(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) form := c.get("/signin").Body.String() for _, want := range []string{ "Sign in · Calls", "

Sign in

", ">Email address<", "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", ``, } { if !strings.Contains(form, want) { t.Errorf("sign-in form is missing %q:\n%s", want, form) } } if strings.Contains(form, "Email me a link") { t.Errorf("sign-in form still says \"Email me a link\":\n%s", form) } sent := c.get("/signin?sent=1").Body.String() for _, want := range []string{ "Check your email · Calls", "

Check your email

", "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.", `Use a different address`, } { if !strings.Contains(sent, want) { t.Errorf("sent page is missing %q:\n%s", want, sent) } } } // The sign-in email's subject names the app: "Sign in to Calls". func TestSigninEmailSubjectNamesCalls(t *testing.T) { x := newHarness(t, "paul@example.com") x.browser(t).post("/signin", url.Values{"address": {"paul@example.com"}}) if x.mail.count() != 1 { t.Fatalf("sent %d mails, want 1", x.mail.count()) } if got := x.mail.lastSubject(t); got != "Sign in to Calls" { t.Errorf("sign-in email subject %q, want %q", got, "Sign in to Calls") } }