| 1 | package calendartest |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/url" |
| 6 | "regexp" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // The sign-in pages are the Oficina door (estilo chrome, Paul |
| 12 | // 2026-09-29): one card on the suite's sky, Calendar's mark and name at |
| 13 | // its head. Only the drawing changed — the form still posts "address" to |
| 14 | // /signin and gets the same answer it always did. |
| 15 | func TestSignInPageIsTheDoor(t *testing.T) { |
| 16 | app := newApp(t) |
| 17 | h := app.Handler() |
| 18 | |
| 19 | rec := get(t, h, "/signin") |
| 20 | if rec.Code != http.StatusOK { |
| 21 | t.Fatalf("GET /signin: status %d, want 200", rec.Code) |
| 22 | } |
| 23 | body := rec.Body.String() |
| 24 | for _, want := range []string{ |
| 25 | "estilo-door", |
| 26 | "<span>Calendar</span>", |
| 27 | `<form rst-form estilo-signin-form method="post" action="/signin"`, |
| 28 | `name="address"`, |
| 29 | "<title>Sign in · Calendar</title>", |
| 30 | // Paul, 2026-10-01: the door's button says "Sign in". |
| 31 | `<button rst-btn="primary" type="submit">Sign in</button>`, |
| 32 | "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", |
| 33 | } { |
| 34 | if !strings.Contains(body, want) { |
| 35 | t.Errorf("sign-in page is missing %q:\n%s", want, body) |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | // The door owns its words (Paul, 2026-10-01): the submit is "Sign in", |
| 40 | // and Calendar's own lede and button copy are gone. |
| 41 | for _, gone := range []string{"Email me a link", "There is no password", "A calendar for one team"} { |
| 42 | if strings.Contains(body, gone) { |
| 43 | t.Errorf("sign-in page still says %q", gone) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | // The sky: an inline placeholder, and served layers reachable signed |
| 48 | // out through estilo's handler, not the app's /static/ one. |
| 49 | if !regexp.MustCompile(`<img estilo-sky-layer="placeholder"[^>]*src="data:image/png;base64,`).MatchString(body) { |
| 50 | t.Errorf("sign-in page has no inline estilo-sky-layer placeholder:\n%s", body) |
| 51 | } |
| 52 | layer := regexp.MustCompile(`<img estilo-sky-layer="[a-z]+"[^>]*src="(/static/estilo/sky/v1/[^"]+)"`).FindStringSubmatch(body) |
| 53 | if layer == nil { |
| 54 | t.Fatalf("sign-in page links no served estilo-sky-layer img:\n%s", body) |
| 55 | } |
| 56 | if got := get(t, h, layer[1]); got.Code != http.StatusOK || got.Header().Get("Content-Type") != "image/svg+xml" { |
| 57 | t.Errorf("GET %s signed out: %d %q, want 200 image/svg+xml", layer[1], got.Code, got.Header().Get("Content-Type")) |
| 58 | } |
| 59 | |
| 60 | // The door's own styles are linked and served. |
| 61 | for _, name := range []string{"chrome", "sky"} { |
| 62 | href := regexp.MustCompile(`/static/` + name + `\.[0-9a-f]{16}\.css`).FindString(body) |
| 63 | if href == "" { |
| 64 | t.Errorf("sign-in page does not link static/%s.css", name) |
| 65 | continue |
| 66 | } |
| 67 | if got := get(t, h, href); got.Code != http.StatusOK { |
| 68 | t.Errorf("GET %s: status %d, want 200", href, got.Code) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // POST address= still begins sign-in exactly as before. |
| 73 | posted := post(t, h, "/signin", url.Values{"address": {"someone@example.com"}}) |
| 74 | if posted.Code != http.StatusSeeOther || posted.Header().Get("Location") != "/signin?sent=1" { |
| 75 | t.Fatalf("POST /signin: %d to %q, want 303 to /signin?sent=1", posted.Code, posted.Header().Get("Location")) |
| 76 | } |
| 77 | if mail, err := app.LastMailForTest(); err != nil || !strings.Contains(mail, "/auth/verify?token=") { |
| 78 | t.Fatalf("POST /signin address= mailed no link (err %v)", err) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | // Every outcome the auth plugin redirects back with keeps its sentence; |
| 83 | // a bad address is the field's own error, the rest are the page's. |
| 84 | func TestSignInDoorOutcomes(t *testing.T) { |
| 85 | h := newApp(t).Handler() |
| 86 | for query, want := range map[string][]string{ |
| 87 | "sent=1": {"estilo-door", "<title>Check your email · Calendar</title>", "Check your email", "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.", `href="/signin"`, "Use a different address"}, |
| 88 | "err=rate": {"estilo-signin-form", `role="alert"`, "Too many attempts. Wait a few minutes, then try again."}, |
| 89 | "err=expired": {"estilo-signin-form", "That link has expired. Ask for a new one."}, |
| 90 | "err=nope": {"estilo-signin-form", "That link did not work. Ask for a new one."}, |
| 91 | "err=expired&sent=1": {"estilo-signin-form", "<title>Sign in · Calendar</title>", "That link has expired. Ask for a new one.", "If that address can sign in here, we’ve sent it a link."}, |
| 92 | "err=address": {`rst-field-error id="estilo-signin-address-error" role="alert">That does not look like an email address.`}, |
| 93 | } { |
| 94 | body := get(t, h, "/signin?"+query).Body.String() |
| 95 | for _, w := range want { |
| 96 | if !strings.Contains(body, w) { |
| 97 | t.Errorf("/signin?%s is missing %q:\n%s", query, w, body) |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | if body := get(t, h, "/signin?sent=1").Body.String(); strings.Contains(body, "estilo-signin-form") { |
| 102 | t.Error("/signin?sent=1 still draws the form; the check-your-email card replaces it") |
| 103 | } |
| 104 | for _, query := range []string{"sent=1", "err=expired&sent=1"} { |
| 105 | body := get(t, h, "/signin?"+query).Body.String() |
| 106 | for _, gone := range []string{"expires shortly", "Send another link"} { |
| 107 | if strings.Contains(body, gone) { |
| 108 | t.Errorf("/signin?%s still says %q", query, gone) |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | |