oficina / calendar Public

Home consumer on the shared chrome: tests, and All apps at the hub root

#23 rebased onto main dbee441 (#17), which already carries everything
#23 added to the app: connect 01166fc, the directory origin, the sign-out
chain to {issuer}/signout and the switcher. What remains is its tests:

- TestHomeSignoutChainsToIssuer and TestHomeSigninWhenSignedInReturnsToPage,
  as #23 wrote them, configured through connect.CredentialsFromEnv.
- TestAllAppsLinksToDirectory now reads estilo's switcher item, not the
  old layout's account link, and pins the href to the directory ROOT,
  {origin}/: #17 linked the bare origin, #23 the origin plus a slash. It
  failed on the bare origin before calendarChrome added the slash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev e2fbf81b957f0d3fa9bcffe347fd0071977f908b parent dbee441
2 files changed, +98 −1
  • internal/calendar/chrome_calendar.go +2 −1
  • internal/calendar/home_consumer_test.go +96 −0
diff --git a/internal/calendar/chrome_calendar.go b/internal/calendar/chrome_calendar.go
index ef7a1b8..0216ccd 100644
--- a/internal/calendar/chrome_calendar.go
+++ b/internal/calendar/chrome_calendar.go
@@ -3,6 +3,7 @@ package calendar
import (
"html/template"
"net/http"
+ "strings"
"amadan.net/oficina/estilo.git/chrome"
estilomenu "amadan.net/oficina/estilo.git/menu"
@@ -34,7 +35,7 @@ func calendarChrome(r *http.Request, signedIn bool, address string, mode chrome.
return bar
}
if directory != "" {
- bar.Switcher = &chrome.Switcher{Label: "Oficina apps", AllApps: estilomenu.Item{Href: directory, Label: "All apps"}}
+ bar.Switcher = &chrome.Switcher{Label: "Oficina apps", AllApps: estilomenu.Item{Href: strings.TrimSuffix(directory, "/") + "/", Label: "All apps"}}
}
// A live session whose person could not be resolved (a removed Home
// person on an error page) still gets the menu, to sign out from, but
diff --git a/internal/calendar/home_consumer_test.go b/internal/calendar/home_consumer_test.go
new file mode 100644
index 0000000..f6cd37f
--- /dev/null
+++ b/internal/calendar/home_consumer_test.go
@@ -0,0 +1,96 @@
+package calendar
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "io"
+ "log/slog"
+ "net/http"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ "amadan.net/oficina/home/connect"
+ assertioncrypto "amadan.net/rastrillo/rastrillo/crypto"
+)
+
+const homeTestDirectory = "https://directory.example.test"
+
+// newHomeConsumerFixture configures Home the way production does: from the
+// environment, through connect.CredentialsFromEnv, so HOME_DIRECTORY_ORIGIN
+// (unprefixed, the name Home itself reads) is read exactly as it is live.
+func newHomeConsumerFixture(t *testing.T, directory string) *homeRouterFixture {
+ t.Helper()
+ key, err := assertioncrypto.Generate()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, _ := json.Marshal(map[string]string{"key": base64.StdEncoding.EncodeToString(key.SignPub())})
+ t.Setenv("CALENDAR_HOME_ISSUER", homeTestIssuer)
+ t.Setenv("CALENDAR_HOME_CONSUMER_ID", "calendar-test")
+ t.Setenv("CALENDAR_HOME_KEYS_BASE64", base64.StdEncoding.EncodeToString(manifest))
+ t.Setenv("CALENDAR_HOME_DEK_BASE64", base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{7}, 32)))
+ t.Setenv("HOME_DIRECTORY_ORIGIN", directory)
+ a, err := New(Config{DBPath: filepath.Join(t.TempDir(), "calendar.db"), Env: "development", Origin: homeTestOrigin, AllowedAddresses: "alice@example.test", Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), Home: connect.CredentialsFromEnv("CALENDAR")})
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { a.Close() })
+ return &homeRouterFixture{a, key, map[string]*http.Cookie{}}
+}
+
+// Sign out in Calendar ends the id.* session too: the app session ends and
+// the same POST is handed on (307) to {issuer}/signout, which returns the
+// browser to /signed-out. Without it the next "Sign in" passed silently
+// through the live id.* session (spec shared-chrome §9.5).
+func TestHomeSignoutChainsToIssuer(t *testing.T) {
+ f := newHomeConsumerFixture(t, "")
+ f.local(t)
+ w := f.do("POST", "/signout", "")
+ if w.Code != http.StatusTemporaryRedirect || w.Header().Get("Location") != homeTestIssuer+"/signout" {
+ t.Fatalf("POST /signout: status %d Location %q, want 307 to %s/signout", w.Code, w.Header().Get("Location"), homeTestIssuer)
+ }
+ if got := w.Header().Get("Referrer-Policy"); got != "strict-origin-when-cross-origin" {
+ t.Fatalf("Referrer-Policy %q: id.* admits the null-Origin hop by Referer", got)
+ }
+ if f.cookies[f.app.home.sessions.CookieName()] != nil {
+ t.Fatal("sign-out kept the Calendar session cookie")
+ }
+}
+
+// A signed-in person who follows a "Sign in" link goes straight to where
+// they were headed, with no second exchange.
+func TestHomeSigninWhenSignedInReturnsToPage(t *testing.T) {
+ f := newHomeConsumerFixture(t, "")
+ f.local(t)
+ if got := homeRedirect(t, f.do("GET", "/signin?return_to=%2Fday%2F2026-09-10", "")); got != "/day/2026-09-10" {
+ t.Fatalf("signed-in /signin went to %q, want /day/2026-09-10", got)
+ }
+}
+
+// "All apps" is the estilo switcher's item (shared-chrome spec §6), linking
+// to the hub directory's root: HOME_DIRECTORY_ORIGIN (an origin, which
+// connect refuses with a path or a trailing slash) plus "/". No directory,
+// no switcher.
+func TestAllAppsLinksToDirectory(t *testing.T) {
+ item := regexp.MustCompile(`<details estilo-switcher[\s\S]*?<a estilo-menu-item href="([^"]*)"><span estilo-menu-label>All apps</span></a>`)
+ for _, configured := range []string{homeTestDirectory} {
+ f := newHomeConsumerFixture(t, configured)
+ f.local(t)
+ m := item.FindStringSubmatch(f.do("GET", "/day/2026-09-10", "").Body.String())
+ if m == nil {
+ t.Fatalf("HOME_DIRECTORY_ORIGIN=%s: no All apps item in the estilo switcher", configured)
+ }
+ if m[1] != homeTestDirectory+"/" {
+ t.Errorf("HOME_DIRECTORY_ORIGIN=%s: All apps links to %q, want %q", configured, m[1], homeTestDirectory+"/")
+ }
+ }
+
+ bare := newHomeConsumerFixture(t, "")
+ bare.local(t)
+ if body := bare.do("GET", "/day/2026-09-10", "").Body.String(); strings.Contains(body, "All apps") || strings.Contains(body, "estilo-switcher") {
+ t.Fatal("switcher shown with no directory configured")
+ }
+}