//go:build browser package calendartest import ( "context" "io" "log/slog" "net" "net/http" "net/http/cookiejar" "net/url" "os/exec" "path/filepath" "runtime" "strings" "testing" "time" calendar "amadan.net/oficina/calendar/internal/calendar" ) // TestQuickAddInARealBrowser drives the quick-add box in Chromium, with // script and then without it. // // Go starts the app and signs in — the magic-link flow needs the mail // recorder, which only this side can read — and hands node a live // origin and a session cookie. Playwright rather than chromedp: in this // environment chromedp starts a browser and parks at about:blank until // the Go test times out, so the existing browser walk cannot run at // all. A gate that cannot run is not a gate, and the house rule that // one test must cross from a computed value to painted output was // therefore unmet. This meets it. func TestQuickAddInARealBrowser(t *testing.T) { // The listener comes first, because the origin is load-bearing: // csrf.Protect measures against it and the sign-in link is built // from it, so the app cannot be constructed until the port is known. ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } origin := "http://" + ln.Addr().String() app, err := calendar.New(calendar.Config{ DBPath: filepath.Join(t.TempDir(), "calendar.db"), Env: "development", Origin: origin, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatalf("calendar.New: %v", err) } t.Cleanup(func() { app.Close() }) srv := &http.Server{Handler: app.Handler()} go func() { _ = srv.Serve(ln) }() t.Cleanup(func() { _ = srv.Close() }) session := signInOverHTTP(t, app, origin, "someone@example.com") ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() cmd := exec.CommandContext(ctx, "node", "test/browser/quickadd.mjs", origin, session.Name, session.Value) cmd.Dir = repoRootForBrowser(t) out, err := cmd.CombinedOutput() t.Logf("node:\n%s", out) if err != nil { t.Fatalf("the browser checks failed: %v", err) } } // repoRootForBrowser finds the repository from this file's own path, never // from the working directory: go test runs in the package directory and // the node script is addressed from the root. func repoRootForBrowser(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } return filepath.Dir(filepath.Dir(filepath.Dir(file))) } // signInOverHTTP drives the real sign-in against the LIVE listener. // // App.SignInForTest cannot be used here: it presents calendar.TestOrigin // as its same-origin evidence, and this server is on a real loopback // port, so csrf.Protect refuses the POST and no link is ever mailed. // The failure reads as "no sign-in link was mailed", which names the // symptom and not the cause — hence this comment. func signInOverHTTP(t *testing.T, app *calendar.App, origin, address string) *http.Cookie { t.Helper() jar, err := cookiejar.New(nil) if err != nil { t.Fatalf("cookiejar: %v", err) } client := &http.Client{Jar: jar} form := url.Values{"address": {address}, "force": {"1"}} req, err := http.NewRequest(http.MethodPost, origin+"/signin", strings.NewReader(form.Encode())) if err != nil { t.Fatalf("build signin request: %v", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", origin) res, err := client.Do(req) if err != nil { t.Fatalf("post signin: %v", err) } res.Body.Close() body, err := app.LastMailForTest() if err != nil || body == "" { t.Fatalf("no sign-in link was mailed: %v", err) } link := magicLink(t, body) // Open the link — the confirm door, which spends nothing — and // press its Sign in button, which does. res, err = client.Get(origin + link) if err != nil { t.Fatalf("open the link: %v", err) } door, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { t.Fatalf("read the confirm door: %v", err) } action, token, ok := calendar.ConfirmFormForTest(string(door)) if !ok { t.Fatalf("opening the link drew no confirm door (status %d)", res.StatusCode) } req, err = http.NewRequest(http.MethodPost, origin+action, strings.NewReader(url.Values{"token": {token}}.Encode())) if err != nil { t.Fatalf("build confirm request: %v", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", origin) req.Header.Set("Sec-Fetch-Site", "same-origin") res, err = client.Do(req) if err != nil { t.Fatalf("confirm the link: %v", err) } res.Body.Close() u, _ := url.Parse(origin) cookies := jar.Cookies(u) if len(cookies) == 0 { t.Fatal("following the sign-in link minted no cookie") } return cookies[0] }