package calendartest import ( "net/http" "net/url" "strings" "testing" calendar "amadan.net/oficina/calendar/internal/calendar" ) // TestEmailedLinkOpensTheConfirmDoor walks the magic-link sign-in end to // end: begin, the mail, GET of the emailed link (the confirm door, which // spends nothing), the door's POST, and a session that opens a view. // // GET /auth/verify used to sign in on sight, so a mail-security gateway // that fetches every link in an inbound message (Safe Links, Proofpoint) // spent it before the person clicked, and they were told it had expired. // rastrillo now confirms on GET and redeems on POST; this pins that the // confirm step is Calendar's own door with one Sign in button, not // rastrillo's plain English fallback, and that the POST is mounted. func TestEmailedLinkOpensTheConfirmDoor(t *testing.T) { app := newApp(t) h := app.Handler() if w := post(t, h, "/signin", url.Values{"address": {"someone@example.com"}, "force": {"1"}}); w.Code != http.StatusSeeOther || w.Header().Get("Location") != "/signin?sent=1" { t.Fatalf("POST /signin = %d %q, want 303 /signin?sent=1", w.Code, w.Header().Get("Location")) } body, err := app.LastMailForTest() if err != nil { t.Fatal(err) } link := magicLink(t, body) u, err := url.Parse(link) if err != nil { t.Fatal(err) } // Opening it, twice, as a scanner and then the person would. for _, who := range []string{"scanner", "person"} { rec := get(t, h, link) if rec.Code != http.StatusOK || len(rec.Result().Cookies()) != 0 { t.Fatalf("%s's GET = %d with %d cookies, want 200 and nothing minted", who, rec.Code, len(rec.Result().Cookies())) } page := rec.Body.String() for _, want := range []string{"estilo-door", "estilo-signin-form", "Calendar", "Sign in ยท Calendar", `name="token" value="` + u.Query().Get("token") + `"`} { if !strings.Contains(page, want) { t.Errorf("%s: confirm door lacks %q:\n%s", who, want, page) } } if n := strings.Count(page, "Sign in`) { t.Errorf("%s: confirm door has %d buttons, want exactly one Sign in:\n%s", who, n, page) } if strings.Contains(page, `name="address"`) { t.Errorf("%s: confirm door asks for an address again", who) } if strings.Contains(page, "Sign in to ") { t.Errorf("%s: rastrillo's fallback confirm page, not the door", who) } // auth sets these before RenderConfirm; render must not undo them. for header, want := range map[string]string{ "Cache-Control": "no-store", "Referrer-Policy": "strict-origin", "X-Frame-Options": "DENY", } { if got := rec.Header().Get(header); got != want { t.Errorf("%s: %s = %q, want %q", who, header, got, want) } } } // A POST with no browser's same-origin evidence is refused and // spends nothing. action, token := openLink(t, h, link) cross := calendar.RequestForTest(nil, http.MethodPost, action, url.Values{"token": {token}}) cross.Header.Del("Origin") cross.Header.Set("Sec-Fetch-Site", "cross-site") if rec := serve(h, cross); rec.Code != http.StatusForbidden || len(rec.Result().Cookies()) != 0 { t.Fatalf("cross-site POST = %d with %d cookies, want 403 and nothing minted", rec.Code, len(rec.Result().Cookies())) } // Sign in: the POST spends the link and mints the session. signed := postLink(t, h, action, token) if signed.Code != http.StatusSeeOther || signed.Header().Get("Location") != "/" { t.Fatalf("POST %s = %d %q, want 303 /", action, signed.Code, signed.Header().Get("Location")) } var session *http.Cookie for _, c := range signed.Result().Cookies() { if c.Value != "" && c.MaxAge >= 0 { session = c } } if session == nil { t.Fatal("the confirm POST minted no session") } if view := follow(t, h, session, "/"); view.Code != http.StatusOK || !strings.Contains(view.Body.String(), `aria-label="Account: someone@example.com"`) { t.Fatalf("GET / with the new session = %d, or it does not name the person", view.Code) } // Spent: the door still opens (it looks nothing up), but its button // now says the link expired. action, token = openLink(t, h, link) again := postLink(t, h, action, token) if len(again.Result().Cookies()) != 0 || again.Header().Get("Location") != "/signin?err=expired" { t.Fatalf("second POST = %d %q, want /signin?err=expired and no session", again.Code, again.Header().Get("Location")) } }