oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1package callstest
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "log/slog"
9 "net/http"
10 "net/url"
11 "path/filepath"
12 "regexp"
13 "strings"
14 "testing"
15 "time"
16
17 "amadan.net/rastrillo/rastrillo/db"
18
19 "amadan.net/oficina/calls/internal/calls"
20)
21
22func TestMemberSignsInAndLandsOnTheList(t *testing.T) {
23 x := newHarness(t, "paul@example.com")
24 c := x.browser(t)
25 if dest := c.signIn("paul@example.com"); dest != "/" {
26 t.Fatalf("member landed on %s, want /", dest)
27 }
28 if rec := c.get("/"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Meetings") {
29 t.Fatalf("GET / after sign-in: %d", rec.Code)
30 }
31}
32
33func TestOutsiderWithoutALinkGetsNoMailAndTheSameAnswer(t *testing.T) {
34 x := newHarness(t, "paul@example.com")
35 c := x.browser(t)
36 rec := c.post("/signin", url.Values{"address": {"stranger@example.com"}})
37 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
38 t.Fatalf("refusal differs from success: %d %s", rec.Code, rec.Header().Get("Location"))
39 }
40 if x.mail.count() != 0 {
41 t.Fatal("mail was sent to a non-member with no link")
42 }
43}
44
45func TestSignedOutIsRedirectedToSignin(t *testing.T) {
46 x := newHarness(t, "paul@example.com")
47 rec := x.browser(t).get("/")
48 if rec.Code != http.StatusSeeOther || !strings.HasPrefix(rec.Header().Get("Location"), "/signin") {
49 t.Fatalf("GET / signed out: %d %s", rec.Code, rec.Header().Get("Location"))
50 }
51}
52
53func TestNonMemberSessionSeesWelcomeNotTheList(t *testing.T) {
54 // Development with no allowlist admits every address at sign-in but
55 // makes nobody a member; that is the shape the guest tests rely on.
56 x := newHarness(t)
57 c := x.browser(t)
58 if dest := c.signIn("anyone@example.com"); dest != "/welcome" {
59 t.Fatalf("non-member landed on %s, want /welcome", dest)
60 }
61 rec := c.get("/")
62 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/welcome" {
63 t.Fatalf("non-member GET /: %d %s, want 303 /welcome", rec.Code, rec.Header().Get("Location"))
64 }
65 if rec := c.get("/welcome"); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "anyone@example.com") {
66 t.Fatal("welcome page does not name the address")
67 }
68 if rec := c.post("/meetings/start", nil); rec.Code != http.StatusNotFound {
69 t.Fatalf("guest can access member action: %d", rec.Code)
70 }
71}
72
73// Codex round 2, C2-4: mailredact_test.go proves the wrapper is safe by
74// calling it directly, which would stay green even if cmd/calls/main.go
75// stopped wiring it up. This is the production-path check instead: an
76// app built with calls.App — the same entry point main.go calls, over a
77// mailer that fails and a real *slog.Logger — answers a real
78// POST /signin, and nothing that logger captured names the address.
79// (Internal 3 / review C1-2's resolve-wraps-everything design is what
80// this is actually testing: no call site can opt out of the wrap.)
81func TestSigninOverAFailingMailerLeavesNoAddressInTheLog(t *testing.T) {
82 const address = "paul@example.com"
83 var buf bytes.Buffer
84 logger := slog.New(slog.NewTextHandler(&buf, nil))
85 d, err := db.Open(filepath.Join(t.TempDir(), "app.db"), logger)
86 if err != nil {
87 t.Fatal(err)
88 }
89 t.Cleanup(func() { d.Close() })
90
91 failing := &failingMailer{err: fmt.Errorf("rastrillo/mail: send to %s: %w", address, errFakeSMTPFailure)}
92 mux, rt, err := calls.App(testCtx(t), d, calls.Config{
93 Env: calls.EnvDevelopment, Origin: testOrigin, AllowedAddresses: []string{address},
94 Mailer: failing, Logger: logger,
95 })
96 if err != nil {
97 t.Fatal(err)
98 }
99 t.Cleanup(rt.Close) // App's queue outlives its ctx until Close
100
101 rec := post(t, mux, "/signin", url.Values{"address": {address}})
102 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
103 t.Fatalf("POST /signin over a failing mailer: %d %s, want 303 /signin?sent=1 (the same answer as success)",
104 rec.Code, rec.Header().Get("Location"))
105 }
106
107 failing.flush() // the send, and its failure, happen after the response
108 log := buf.String()
109 if strings.Contains(strings.ToLower(log), strings.ToLower(address)) {
110 t.Fatalf("the log carries the address after a failing send:\n%s", log)
111 }
112 if !strings.Contains(log, "calls: mail delivery failed") {
113 t.Fatalf("no redacted delivery failure was logged at all — the wrapper may not be wired:\n%s", log)
114 }
115}
116
117var errFakeSMTPFailure = errors.New("550 mailbox unavailable")
118
119type failingMailer struct {
120 err error
121 waits []func() // the sign-in mail queue in front of it
122}
123
124func (f *failingMailer) Send(ctx context.Context, to, subject, body string) error { return f.err }
125
126func (f *failingMailer) AttachSigninQueue(wait func()) { f.waits = append(f.waits, wait) }
127
128func (f *failingMailer) flush() {
129 for _, w := range f.waits {
130 w()
131 }
132}
133
134// Re-review of 9e067e4: a plain member sign-in — no meeting-link cookie,
135// so no pending_joins row at all — still leaves rastrillo's own
136// auth_links row for the magic link it mailed. nextExpiry (and so
137// NextDue) has to see that row, or an app that hibernates right after
138// would never be woken to sweep it.
139func TestNextDueWakesForAPlainMemberSignin(t *testing.T) {
140 x := newHarness(t, "paul@example.com")
141 if rec := x.browser(t).post("/signin", url.Values{"address": {"paul@example.com"}}); rec.Header().Get("Location") != "/signin?sent=1" {
142 t.Fatalf("sign-in request: %s", rec.Header().Get("Location"))
143 }
144 if n := x.authLinks(t); n != 1 {
145 t.Fatalf("auth_links rows after a plain sign-in: %d, want 1", n)
146 }
147
148 next := x.ctl.NextDue()
149 if next.IsZero() {
150 t.Fatal("NextDue is zero after a plain sign-in left an unclicked magic link outstanding")
151 }
152 // signin.Flow.LinkTTL defaults to 15 minutes
153 // (keymaildev/signin@v0.1.1/flow.go:57,186); the wide window is
154 // about not coupling this test to that constant, not about
155 // precision.
156 if until := next.Sub(x.now); until <= 0 || until > 20*time.Minute {
157 t.Fatalf("NextDue is %v away, want a positive duration within the magic link's TTL window", until)
158 }
159}
160
161// The sign-in page is the Oficina door (estilo chrome): one card on the
162// suite's sky with Calls' mark and name at its head. The form still posts
163// the field "address" to /signin, and a member's POST still answers
164// 303 /signin?sent=1 with a link in the mail, the sent page drawn as the
165// door too.
166func TestSigninPageIsTheOficinaDoor(t *testing.T) {
167 x := newHarness(t, "paul@example.com")
168 c := x.browser(t)
169 rec := c.get("/signin")
170 if rec.Code != http.StatusOK {
171 t.Fatalf("GET /signin: %d", rec.Code)
172 }
173 body := rec.Body.String()
174 for _, want := range []string{
175 "<div estilo-door", "<span>Calls</span>", `<img estilo-sky-layer="placeholder"`,
176 `<img estilo-sky-layer="grain"`, `action="/signin"`, `name="address"`,
177 } {
178 if !strings.Contains(body, want) {
179 t.Errorf("sign-in page is missing %q:\n%s", want, body)
180 }
181 }
182 // The sky's layers are served signed-out, from under estilo's prefix.
183 grain := regexp.MustCompile(`src="(/static/estilo/sky/v1/[0-9a-f]{16}/grain\.svg)"`).FindStringSubmatch(body)
184 if grain == nil {
185 t.Fatal("sign-in page links no sky grain layer")
186 }
187 if rec := c.get(grain[1]); rec.Code != http.StatusOK || rec.Header().Get("Content-Type") != "image/svg+xml" {
188 t.Fatalf("GET %s: %d %q", grain[1], rec.Code, rec.Header().Get("Content-Type"))
189 }
190
191 rec = c.post("/signin", url.Values{"address": {"paul@example.com"}})
192 if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
193 t.Fatalf("POST /signin: %d %s", rec.Code, rec.Header().Get("Location"))
194 }
195 if x.mail.count() != 1 {
196 t.Fatalf("member sign-in sent %d mails, want 1", x.mail.count())
197 }
198 sent := c.get("/signin?sent=1").Body.String()
199 if !strings.Contains(sent, "<div estilo-door") || !strings.Contains(sent, "Check your email") || strings.Contains(sent, `name="address"`) {
200 t.Errorf("sent page is not the door's check-your-email notice:\n%s", sent)
201 }
202
203 // A malformed address is the field's own error; other outcomes are
204 // the page's.
205 if page := c.get("/signin?err=address").Body.String(); !strings.Contains(page, `aria-invalid="true"`) || !strings.Contains(page, "That doesn’t look like an email address.") {
206 t.Errorf("err=address is not a field error:\n%s", page)
207 }
208 if page := c.get("/signin?err=rate").Body.String(); !strings.Contains(page, `rst-tone="negative" role="alert"`) || !strings.Contains(page, "Too many attempts. Wait a few minutes, then try again.") {
209 t.Errorf("err=rate is not a page error:\n%s", page)
210 }
211}
212
213// The door's words are estilo's, the same in every Oficina app: the
214// submit reads "Sign in" (never "Email me a link", Paul 2026-10-01), one
215// help line says what happens, the sent state says the one body and links
216// back with "Use a different address", and each tab title is
217// "<Page> · Calls".
218func TestSigninDoorSpeaksTheSuitesCopy(t *testing.T) {
219 x := newHarness(t, "paul@example.com")
220 c := x.browser(t)
221 form := c.get("/signin").Body.String()
222 for _, want := range []string{
223 "<title>Sign in · Calls</title>", "<h1>Sign in</h1>", ">Email address<",
224 "We’ll email you a link that signs you in. It works once and expires in 15 minutes.",
225 `<button rst-btn="primary" type="submit">Sign in</button>`,
226 } {
227 if !strings.Contains(form, want) {
228 t.Errorf("sign-in form is missing %q:\n%s", want, form)
229 }
230 }
231 if strings.Contains(form, "Email me a link") {
232 t.Errorf("sign-in form still says \"Email me a link\":\n%s", form)
233 }
234 sent := c.get("/signin?sent=1").Body.String()
235 for _, want := range []string{
236 "<title>Check your email · Calls</title>", "<h1>Check your email</h1>",
237 "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.",
238 `<a href="/signin">Use a different address</a>`,
239 } {
240 if !strings.Contains(sent, want) {
241 t.Errorf("sent page is missing %q:\n%s", want, sent)
242 }
243 }
244}
245
246// The sign-in email's subject names the app: "Sign in to Calls".
247func TestSigninEmailSubjectNamesCalls(t *testing.T) {
248 x := newHarness(t, "paul@example.com")
249 x.browser(t).post("/signin", url.Values{"address": {"paul@example.com"}})
250 if x.mail.count() != 1 {
251 t.Fatalf("sent %d mails, want 1", x.mail.count())
252 }
253 if got := x.mail.lastSubject(t); got != "Sign in to Calls" {
254 t.Errorf("sign-in email subject %q, want %q", got, "Sign in to Calls")
255 }
256}
257