| 1 | package home |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // The door's words are estilo's chrome.English, the one copy set every |
| 11 | // app's door renders (Paul, 2026-10-01: the submit reads "Sign in", never |
| 12 | // "Continue" or "Email me a link"). They are pinned here as literals, not |
| 13 | // read back from chrome.English, so a change upstream shows up as a red |
| 14 | // test on the hub rather than as silently different copy. |
| 15 | const ( |
| 16 | doorHelp = "We’ll email you a link that signs you in. It works once and expires in 15 minutes." |
| 17 | doorSentBody = "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes." |
| 18 | ) |
| 19 | |
| 20 | func getCredential(t *testing.T, app *testApp, path string) string { |
| 21 | t.Helper() |
| 22 | req := httptest.NewRequest(http.MethodGet, path, nil) |
| 23 | req.Host = credentialHost |
| 24 | w := httptest.NewRecorder() |
| 25 | app.Router().ServeHTTP(w, req) |
| 26 | if w.Code != http.StatusOK { |
| 27 | t.Fatalf("GET %s: %d", path, w.Code) |
| 28 | } |
| 29 | return w.Body.String() |
| 30 | } |
| 31 | |
| 32 | func mustContain(t *testing.T, page, body string, wants ...string) { |
| 33 | t.Helper() |
| 34 | for _, want := range wants { |
| 35 | if !strings.Contains(body, want) { |
| 36 | t.Errorf("%s: missing %q", page, want) |
| 37 | } |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func mustNotContain(t *testing.T, page, body string, nots ...string) { |
| 42 | t.Helper() |
| 43 | for _, not := range nots { |
| 44 | if strings.Contains(body, not) { |
| 45 | t.Errorf("%s: still says %q", page, not) |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | func TestDoorSpeaksTheSuiteCopy(t *testing.T) { |
| 51 | app := newTestApp(t) |
| 52 | |
| 53 | form := getCredential(t, app, "/signin") |
| 54 | mustContain(t, "/signin", form, |
| 55 | "<title>Sign in · Oficina</title>", |
| 56 | "<h1>Sign in</h1>", |
| 57 | ">Email address</label>", |
| 58 | doorHelp, |
| 59 | `<button type="submit" rst-btn="primary">Sign in</button>`) |
| 60 | mustNotContain(t, "/signin", form, ">Continue<", "Email me a link", "Home") |
| 61 | |
| 62 | sent := getCredential(t, app, "/signin/sent") |
| 63 | mustContain(t, "/signin/sent", sent, |
| 64 | "<title>Check your email · Oficina</title>", |
| 65 | "<h1>Check your email</h1>", |
| 66 | doorSentBody, |
| 67 | `<a href="/signin">Use a different address</a>`) |
| 68 | mustNotContain(t, "/signin/sent", sent, "fifteen", "Check your mail", "Home") |
| 69 | |
| 70 | failed := redeem(t, app, "not-a-token").Body.String() |
| 71 | mustContain(t, "signin failed", failed, "Sign-in links work once and expire after 15 minutes.") |
| 72 | mustNotContain(t, "signin failed", failed, "fifteen") |
| 73 | |
| 74 | f := newMountedChooser(t) |
| 75 | out := f.request("GET", "/signed-out", "", "", nil) |
| 76 | if out.Code != http.StatusOK { |
| 77 | t.Fatalf("GET /signed-out: %d", out.Code) |
| 78 | } |
| 79 | mustContain(t, "/signed-out", out.Body.String(), |
| 80 | "<title>Signed out · Oficina</title>", |
| 81 | "<h1>You’re signed out</h1>", |
| 82 | "You’ve signed out of Oficina on this device.", |
| 83 | `rst-btn="primary">Sign in</a>`) |
| 84 | mustNotContain(t, "/signed-out", out.Body.String(), "You are signed out", "Home") |
| 85 | } |
| 86 | |
| 87 | // The hub builds its own signin.Flow, so it sets the email itself: the |
| 88 | // suite's subject, and a body that says how long the link lasts rather |
| 89 | // than keymaildev's default "expires shortly". |
| 90 | func TestSigninEmailSpeaksTheSuiteCopy(t *testing.T) { |
| 91 | app := newTestApp(t) |
| 92 | signIn(t, app, "someone@example.com") |
| 93 | mail, ok := app.mailer.last() |
| 94 | if !ok { |
| 95 | t.Fatal("no mail was sent") |
| 96 | } |
| 97 | if mail.Subject != "Sign in to Oficina" { |
| 98 | t.Errorf("subject = %q", mail.Subject) |
| 99 | } |
| 100 | for _, want := range []string{"Here’s your link to sign in to Oficina:", Origin + linkPath + "?token=", "It works once and expires in 15 minutes."} { |
| 101 | if !strings.Contains(mail.Body, want) { |
| 102 | t.Errorf("body missing %q:\n%s", want, mail.Body) |
| 103 | } |
| 104 | } |
| 105 | if strings.Contains(mail.Body, "shortly") || strings.Contains(mail.Body, "Home") { |
| 106 | t.Errorf("body is not the suite's copy:\n%s", mail.Body) |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | // Never name the hub to people (Paul, 2026-09-23): the suite is Oficina. |
| 111 | func TestHubNeverCallsItselfHome(t *testing.T) { |
| 112 | f := newMountedChooser(t) |
| 113 | page := f.request("GET", "/", "", "", f.session(t, chooserPerson)) |
| 114 | mustNotContain(t, "directory", page.Body.String(), "to Home") |
| 115 | for _, app := range directoryCatalog(Runtime{}) { |
| 116 | if strings.Contains(app.Status, "Home") { |
| 117 | t.Errorf("%s: status %q names Home", app.Name, app.Status) |
| 118 | } |
| 119 | } |
| 120 | files, err := templatesFS.ReadDir("templates") |
| 121 | if err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | for _, e := range files { |
| 125 | b, err := templatesFS.ReadFile("templates/" + e.Name()) |
| 126 | if err != nil { |
| 127 | t.Fatal(err) |
| 128 | } |
| 129 | for _, bad := range []string{"Back to Home", "Return to Home", "to Home"} { |
| 130 | if strings.Contains(string(b), bad) { |
| 131 | t.Errorf("%s says %q", e.Name(), bad) |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | |