package callstest // The shared chrome (estilo spec/shared-chrome, round 2), black-box: // every page wears the 52px estilo bar with one account menu and one // Sign out, is titled " · Calls", answers errors as pages for a // browser and as text for a fetch, and sends a signed-out person back to // the page they asked for once they have signed in. import ( "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "regexp" "strings" "testing" "time" "amadan.net/oficina/estilo.git/base" "amadan.net/oficina/estilo.git/chrome" estilomenu "amadan.net/oficina/estilo.git/menu" ) var titleRe = regexp.MustCompile(`([^<]*)`) func pageTitle(t *testing.T, body string) string { t.Helper() m := titleRe.FindStringSubmatch(body) if m == nil { t.Fatalf("no :\n%s", body) } return m[1] } // Spec §10: every page's <title> is "<Page> · … · Calls", the page's h1 // first, and the app root is "Calls" alone. func TestEveryPageIsTitledPageDotCalls(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") for path, want := range map[string]string{ "/": "Calls", "/settings": "Settings · Calls", "/m/" + sqid + "/join": "Board meeting · Calls", "/m/" + sqid: "Board meeting · Calls", "/meetings/" + sqid + "/settings": "Meeting settings · Board meeting · Calls", "/meetings/" + sqid + "/end": "End meeting for everyone? · Board meeting · Calls", "/help/open-source": "Open source · Calls", "/no/such/page": "Page not found · Calls", "/m/zzzzzzzzzz/join": "Meeting not available · Calls", } { if got := pageTitle(t, c.getNav(path).Body.String()); got != want { t.Errorf("GET %s: title %q, want %q", path, got, want) } } out := x.browser(t) for path, want := range map[string]string{ "/signin": "Sign in · Calls", "/signin?sent=1": "Check your email · Calls", "/signed-out": "Signed out · Calls", "/no/such/page": "Page not found · Calls", } { if got := pageTitle(t, out.getNav(path).Body.String()); got != want { t.Errorf("signed out GET %s: title %q, want %q", path, got, want) } } } // Spec §5 and §8: the estilo bar, not rastrillo's topbar; signed in, one // account menu holding the only Sign out; signed out, a "Sign in" that // comes back to this page; on the sign-in pages themselves, nothing. func TestPagesWearTheEstiloBarWithOneAccountMenu(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") for _, path := range []string{"/", "/settings", "/m/" + sqid + "/join", "/meetings/" + sqid + "/settings", "/help/open-source", "/no/such/page"} { body := c.getNav(path).Body.String() if !strings.Contains(body, "<header estilo-bar>") || strings.Contains(body, "rst-shell-bar") { t.Errorf("GET %s does not wear the estilo bar", path) } if n := strings.Count(body, ">Sign out<"); n != 1 { t.Errorf("GET %s has %d Sign out, want exactly 1", path, n) } if !strings.Contains(body, `<summary estilo-bar-trigger aria-label="Account: paul@example.com">`) { t.Errorf("GET %s has no account menu", path) } for _, want := range []string{`<span estilo-menu-identity-label>Signed in as</span>`, `<p estilo-menu-heading id="estilo-appearance">Appearance</p>`, `<a estilo-menu-item href="/help/open-source"><span estilo-menu-label>Open source</span></a>`} { if !strings.Contains(body, want) { t.Errorf("GET %s: the account menu lacks %q", path, want) } } for _, gone := range []string{"Help: open source", "rst-shell-account", ">Help</summary>"} { if strings.Contains(body, gone) { t.Errorf("GET %s still carries %q", path, gone) } } } if body := c.getNav("/settings").Body.String(); !strings.Contains(body, `<a href="/settings" aria-current="page">Settings</a>`) { t.Error("the bar's nav does not mark Settings as the current page") } out := x.browser(t) body := out.getNav("/no/such/page").Body.String() if !strings.Contains(body, `<a rst-btn href="/signin?return_to=%2Fno%2Fsuch%2Fpage" estilo-bar-signin>Sign in</a>`) || strings.Contains(body, "estilo-account") { t.Errorf("a signed-out page's corner is not Sign in, back to it:\n%s", body) } for _, path := range []string{"/signin", "/signin?sent=1", "/signed-out"} { body := out.getNav(path).Body.String() if !strings.Contains(body, "<header estilo-bar>") || strings.Contains(body, "estilo-bar-signin") || strings.Contains(body, "estilo-account") { t.Errorf("GET %s: the sign-in pages' corner must be empty", path) } } } // Spec §8.4: a guest gets the same "Sign in" corner, never an account. func TestAGuestGetsTheSignInCorner(t *testing.T) { x, sqid, token := memberWithMeeting(t) g := x.browser(t) invite := g.getNav("/j/" + token).Body.String() if !strings.Contains(invite, `<a rst-btn href="/signin" estilo-bar-signin>Sign in</a>`) { t.Errorf("the invitation page's corner must be a bare Sign in (its path is the capability):\n%s", invite) } if rec := g.post("/j/"+token+"/guest", url.Values{"name": {"Visitor"}}); rec.Code != http.StatusSeeOther { t.Fatalf("guest join: %d", rec.Code) } body := g.getNav("/m/" + sqid + "/join").Body.String() if !strings.Contains(body, "estilo-bar-signin") || strings.Contains(body, "estilo-account") || strings.Contains(body, ">Sign out<") { t.Errorf("a guest's holding page corner is not Sign in:\n%s", body) } if !strings.Contains(body, "You’ll join as Visitor, a guest.") { t.Errorf("the holding page does not say who the guest joins as:\n%s", body) } } // Spec §3: an error is a page for a browser and plain text for anything // else, so the room's own fetches keep the answers room.js reads. func TestErrorPagesAreSplitByAccept(t *testing.T) { x, _, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") page := c.getNav("/no/such/page") if page.Code != http.StatusNotFound || !strings.HasPrefix(page.Header().Get("Content-Type"), "text/html") || !strings.Contains(page.Body.String(), "We can’t find that page") || !strings.Contains(page.Body.String(), `<a rst-btn="primary" href="/">Go to Calls</a>`) { t.Errorf("unknown route for a browser: %d %q\n%s", page.Code, page.Header().Get("Content-Type"), page.Body.String()) } plain := c.get("/no/such/page") if plain.Code != http.StatusNotFound || !strings.HasPrefix(plain.Header().Get("Content-Type"), "text/plain") { t.Errorf("unknown route for a fetch: %d %q", plain.Code, plain.Header().Get("Content-Type")) } meeting := c.getNav("/m/zzzzzzzzzz/join") if meeting.Code != http.StatusNotFound || !strings.Contains(meeting.Body.String(), "This meeting isn’t available") { t.Errorf("missing meeting for a browser: %d\n%s", meeting.Code, meeting.Body.String()) } if fetch := c.get("/m/zzzzzzzzzz/info"); fetch.Code != http.StatusNotFound || !strings.HasPrefix(fetch.Header().Get("Content-Type"), "text/plain") { t.Errorf("missing meeting for a fetch: %d %q", fetch.Code, fetch.Header().Get("Content-Type")) } // A POST-only path opened from history is no page, not a 405. if rec := c.getNav("/signout"); rec.Code != http.StatusNotFound || !strings.Contains(rec.Body.String(), "We can’t find that page") { t.Errorf("GET /signout: %d", rec.Code) } del := c.do(httptest.NewRequest(http.MethodDelete, "/signin", nil)) if del.Code != http.StatusMethodNotAllowed || !strings.Contains(del.Header().Get("Allow"), "GET") { t.Errorf("DELETE /signin: %d Allow %q", del.Code, del.Header().Get("Allow")) } } // Spec §9.4: a signed-out page request carries the page to sign-in, and // the emailed link lands back on it. The room's fetches keep today's // bare redirect, which room.js reads as "signed out". func TestSignInComesBackToTheDeepLink(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) rec := c.getNav("/meetings/" + sqid + "/settings") want := "/signin?return_to=" + url.QueryEscape("/meetings/"+sqid+"/settings") if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != want { t.Fatalf("signed-out deep link: %d %q, want %q", rec.Code, rec.Header().Get("Location"), want) } if fetch := c.getStream("/m/" + sqid + "/stream"); fetch.Code != http.StatusSeeOther || fetch.Header().Get("Location") != "/signin" { t.Errorf("the stream's signed-out answer changed: %d %q", fetch.Code, fetch.Header().Get("Location")) } door := c.getNav(want).Body.String() if !strings.Contains(door, `name="return_to" value="/meetings/`+sqid+`/settings"`) { t.Errorf("the door does not carry return_to:\n%s", door) } if rec := c.post("/signin", url.Values{"address": {"paul@example.com"}}); rec.Code != http.StatusSeeOther { t.Fatalf("POST /signin: %d", rec.Code) } // "Use a different address" keeps the destination (Docs #65, 2). sent := c.getNav("/signin?sent=1").Body.String() if !strings.Contains(sent, `href="/signin?return_to=`+url.QueryEscape("/meetings/"+sqid+"/settings")+`">Use a different address</a>`) { t.Errorf("Use a different address loses the destination:\n%s", sent) } _, link := x.mail.last() if dest := c.follow(link); dest != "/meetings/"+sqid+"/settings" { t.Fatalf("after the link: %q, want the deep link", dest) } // Signed in, /signin skips the form and goes where it was asked to. if rec := c.getNav("/signin?return_to=%2Fsettings"); rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings" { t.Errorf("signed in, GET /signin: %d %q", rec.Code, rec.Header().Get("Location")) } // And a destination off this origin is never followed. if rec := c.getNav("/signin?return_to=%2F%2Fevil.example"); rec.Header().Get("Location") != "/" { t.Errorf("signed in, GET /signin?return_to=//evil.example: %q", rec.Header().Get("Location")) } } // The remembered page is sealed: a hand-written cookie is ignored. func TestAForgedReturnCookieIsIgnored(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.cookies["calls_return"] = &http.Cookie{Name: "calls_return", Value: "%2F%2Fevil.example"} if dest := c.signIn("paul@example.com"); dest != "/" { t.Fatalf("a forged return cookie was followed: %q", dest) } } // r3 audit C6: a malformed address is the field's own error, not // "Check your email" — and nothing is sent. func TestAMalformedAddressIsTheFieldsError(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) rec := c.post("/signin", url.Values{"address": {"not-an-email@"}}) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?err=address" { t.Fatalf("malformed address: %d %q", rec.Code, rec.Header().Get("Location")) } if x.mail.count() != 0 { t.Fatal("a malformed address was mailed") } } // Paul, 2026-10-01: the door's button says "Sign in"; the sent page // says what happened in the suite's words (spec §9.2). func TestTheDoorSpeaksTheSuitesWords(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) door := c.getNav("/signin").Body.String() for _, want := range []string{`type="submit">Sign in</button>`, "We’ll email you a link that signs you in. It works once and expires in 15 minutes.", `<a href="/help/open-source">Open source</a>`} { if !strings.Contains(door, want) { t.Errorf("the door lacks %q", want) } } if strings.Contains(door, "Email me a link") { t.Error("the door still says Email me a link") } sent := c.getNav("/signin?sent=1").Body.String() if !strings.Contains(sent, "If that address can sign in here, we’ve sent it a link. It works once and expires in 15 minutes.") || strings.Contains(sent, "should be on its way") { t.Errorf("the sent page's copy:\n%s", sent) } } // Sign out lands on a page that says so (spec §9.2), not on a bare form. func TestSignOutLandsOnTheSignedOutPage(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") rec := c.post("/signout", nil) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signed-out" { t.Fatalf("POST /signout: %d %q", rec.Code, rec.Header().Get("Location")) } page := c.getNav("/signed-out") if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "You’re signed out") || !strings.Contains(page.Body.String(), `<a rst-btn="primary" href="/signin">Sign in</a>`) { t.Errorf("GET /signed-out: %d\n%s", page.Code, page.Body.String()) } } // Spec §8.3: Appearance is a cookie the layout writes into <html>. func TestAppearanceIsRememberedPerApp(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") rec := c.post("/appearance", url.Values{"mode": {"dark"}, "return_to": {"/settings"}}) if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/settings" { t.Fatalf("POST /appearance: %d %q", rec.Code, rec.Header().Get("Location")) } body := c.getNav("/settings").Body.String() if !strings.Contains(body, `<html lang="en" dir="ltr" data-theme="dark">`) || !strings.Contains(body, `value="dark" aria-pressed="true"`) { t.Errorf("dark was not remembered:\n%s", body[:min(len(body), 600)]) } } // Real list rows (spec §2 of the audit): each meeting is an rst-row whose // actions are one ⋯ menu in estilo's floating menu, with one label for // ending a meeting. func TestMeetingsAreListRowsWithOneMenu(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") body := c.getNav("/").Body.String() for _, want := range []string{ `<span rst-row-main><a href="/m/` + sqid + `/join">Board meeting</a>`, `<details rst-row-menu name="estilo-menus">`, `<summary aria-label="Actions for Board meeting">`, `<a estilo-menu-item="danger" href="/meetings/` + sqid + `/end"><span estilo-menu-label>End meeting for everyone</span></a>`, } { if !strings.Contains(body, want) { t.Errorf("the meeting list lacks %q", want) } } if strings.Contains(body, "<article>") { t.Error("the meeting list still renders bare articles") } } // r3 audit C4 / spec S10: Start a call names the call in the starter's // own time. Without script it is the instance's time, and since // 2026-10-02 the title no longer names that zone ("… UTC"): the zone is // kept with the meeting instead (Meeting.TitleZone). func TestStartACallTitlesInTheStartersZone(t *testing.T) { x := newHarness(t, "paul@example.com") c := x.browser(t) c.signIn("paul@example.com") tokyo, _ := time.LoadLocation("Asia/Tokyo") rec := c.post("/meetings/start", url.Values{"zone": {"Asia/Tokyo"}}) if want := "Call, " + x.now.In(tokyo).Format("2 Jan 15:04"); !strings.Contains(rec.Body.String(), want) { t.Errorf("with the browser's zone the title is not %q:\n%s", want, rec.Body.String()) } rec = c.post("/meetings/start", nil) if want := "Call, " + x.now.UTC().Format("2 Jan 15:04") + "</"; !strings.Contains(rec.Body.String(), want) { t.Errorf("without one the title is not %q", want) } if strings.Contains(rec.Body.String(), x.now.UTC().Format("2 Jan 15:04")+" UTC") { t.Error("the title still names the instance zone") } } // Spec §4's grep gate: one notice vocabulary, rastrillo's callout in a // tone. rst-notice is always success green, and carried the relay // warning and "open in another tab". func TestTemplatesUseCalloutTonesOnly(t *testing.T) { bad := regexp.MustCompile(`rst-notice|rst-callout="|rst-tone="warn"`) n := 0 err := filepath.WalkDir(filepath.Join("..", "calls", "templates"), func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(path, ".html") { return err } n++ b, err := os.ReadFile(path) if err != nil { return err } if m := bad.Find(b); m != nil { t.Errorf("%s uses %q", path, m) } return nil }) if err != nil || n < 10 { t.Fatalf("walked %d templates: %v", n, err) } } // Spec §3.3's second gate: every remaining plain-text http.Error is // listed here with why it is not a page. A new one fails until it is // either a page (errorPage) or classified. func TestEveryPlainTextErrorIsClassified(t *testing.T) { classified := map[string]int{ // The native app's JSON endpoints (/native/*): a Swift client // reads the status, and a page is the wrong answer to it. The // thirteenth is /native/request's 410 for an offer that has gone // (2026-10-02), which callscore reads as ErrOfferExpired. "native_auth.go": 13, // POST /native/devices: JSON from the native app. "knockpush.go": 7, // methodNotAllowed's 405 (no person navigates with DELETE) and // plainError, the answer to a request that did not ask for a page. "errors.go": 2, // render's last resort when a template itself fails. "render.go": 1, } files, err := filepath.Glob(filepath.Join("..", "calls", "*.go")) if err != nil || len(files) < 20 { t.Fatalf("glob: %d files, %v", len(files), err) } got := map[string]int{} for _, f := range files { if strings.HasSuffix(f, "_test.go") { continue } b, err := os.ReadFile(f) if err != nil { t.Fatal(err) } if n := strings.Count(string(b), "http.Error("); n > 0 { got[filepath.Base(f)] = n } } for f, n := range got { if classified[f] != n { t.Errorf("%s has %d http.Error calls, %d classified: make it a page, or classify it here with the reason", f, n, classified[f]) } } for f, n := range classified { if got[f] != n { t.Errorf("%s: %d classified, %d found", f, n, got[f]) } } } // The two-pin check (estilo AGENTS.md; spec §12.1 item 1): Calls pins // rastrillo v0.26, estilo's own token tests run against v0.23, so every // var(--rst-*) estilo's base, menu and chrome sheets use must be defined // in the tokens.css and theme.css Calls actually serves. func TestEstilosChromeUsesTokensCallsDefines(t *testing.T) { var defined strings.Builder for _, f := range []string{"tokens.css", "theme.css"} { b, err := os.ReadFile(filepath.Join("..", "calls", "static", f)) if err != nil { t.Fatal(err) } defined.Write(b) } defs := map[string]bool{} for _, m := range regexp.MustCompile(`(--rst-[a-z0-9-]+)\s*:`).FindAllStringSubmatch(defined.String(), -1) { defs[m[1]] = true } use := regexp.MustCompile(`var\((--rst-[a-z0-9-]+)`) for name, css := range map[string][]byte{"base.css": base.CSS(), "menu.css": estilomenu.CSS(), "chrome.css": chrome.CSS()} { uses := use.FindAllSubmatch(css, -1) if len(uses) == 0 && name != "base.css" { t.Errorf("%s uses no tokens: this check proves nothing", name) } for _, m := range uses { if !defs[string(m[1])] { t.Errorf("estilo %s uses %s, which Calls' tokens.css and theme.css do not define", name, m[1]) } } } } // Spec §9.5, and Astra's P3 on meet #22: a page that shows Sign out sends // Referrer-Policy strict-origin, never no-referrer — in Home mode the // chained sign-out is admitted by id.* on the Referer's origin, and // no-referrer strands it on "Finish signing out". Open source and Share // meeting show the account menu, so they are strict-origin; the native // hand-off pages keep no-referrer and so show no Sign out at all. func TestPagesWithSignOutSendStrictOrigin(t *testing.T) { x, sqid, _ := memberWithMeeting(t) c := x.browser(t) c.signIn("paul@example.com") for name, rec := range map[string]*httptest.ResponseRecorder{ "Open source": c.getNav("/help/open-source"), "Share meeting": c.post("/m/"+sqid+"/share", nil), } { if !strings.Contains(rec.Body.String(), ">Sign out<") { t.Errorf("%s: no Sign out, so this check proves nothing", name) } if got := rec.Header().Get("Referrer-Policy"); got != "strict-origin" { t.Errorf("%s shows Sign out with Referrer-Policy %q, want strict-origin", name, got) } } // Any page with Sign out and no-referrer is the bug, wherever it is. for _, path := range []string{"/", "/settings", "/m/" + sqid + "/join", "/meetings/" + sqid + "/settings", "/no/such/page"} { rec := c.getNav(path) if strings.Contains(rec.Body.String(), ">Sign out<") && rec.Header().Get("Referrer-Policy") == "no-referrer" { t.Errorf("GET %s shows Sign out under no-referrer", path) } } }