package slidestest
import (
"fmt"
"html"
"io"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strings"
"testing"
"time"
"amadan.net/oficina/slides/internal/slides/keymailtest"
)
// TestMain keeps every sign-in in this package inside the process: no
// DNS query or HTTPS probe made by Keymail classification leaves it
// (keymailtest's package comment says how). An address at a domain no
// test registered classifies as plain email.
func TestMain(m *testing.M) {
restore, err := keymailtest.Install()
if err != nil {
fmt.Fprintln(os.Stderr, "keymailtest.Install:", err)
os.Exit(1)
}
code := m.Run()
// No test may leave a sign-in mail worker running: every app and
// every queue a test builds is closed in its cleanup.
if code == 0 {
deadline := time.Now().Add(5 * time.Second)
for mailWorkers() > 0 && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
}
if n := mailWorkers(); n > 0 {
fmt.Fprintf(os.Stderr, "FAIL: %d sign-in mail queue goroutines still running after the tests\n", n)
code = 1
}
}
restore()
os.Exit(code)
}
// The Keymail deployment these tests sign in through: keymail.dev's
// shape, a domain that is its own Keymail (no `_keymail` record).
const (
keymailHost = "keymail.slides.test"
keymailMember = "paul@" + keymailHost
keymailStranger = "stranger@" + keymailHost
)
// newKeymailApp is newApp with keymailMember on the allowlist beside
// allowedAddress, and a Keymail deployment holding both keymail
// addresses — one permitted here, one not.
func newKeymailApp(t *testing.T) (*testApp, *keymailtest.Server) {
t.Helper()
km := keymailtest.Serve(t, keymailHost, keymailMember, keymailStranger)
cfg, mailer := newConfig(t)
cfg.AllowedAddresses = append(cfg.AllowedAddresses, keymailMember)
return newAppWith(t, cfg, mailer), km
}
var authorizeHref = regexp.MustCompile(`href="(https://[^"]+/oauth/authorize\?[^"]+)"`)
// continuation asserts rec is the page between the form and Keymail —
// a 200 document, never a redirect — and returns the authorize URL it
// carries.
func continuation(t *testing.T, label string, rec interface {
Result() *http.Response
}, body string) *url.URL {
t.Helper()
res := rec.Result()
if res.StatusCode != http.StatusOK {
t.Fatalf("%s: status %d, want 200 — the hop to Keymail must be a page, not a redirect from the form POST (Safari applies form-action to it)", label, res.StatusCode)
}
if loc := res.Header.Get("Location"); loc != "" {
t.Fatalf("%s: Location %q, want none", label, loc)
}
m := authorizeHref.FindStringSubmatch(body)
if m == nil {
t.Fatalf("%s: no Keymail authorize link on the page:\n%s", label, body)
}
u, err := url.Parse(html.UnescapeString(m[1]))
if err != nil {
t.Fatalf("%s: parse authorize link: %v", label, err)
}
if u.Host != keymailHost {
t.Errorf("%s: authorize link goes to %q, want %q", label, u.Host, keymailHost)
}
if !strings.Contains(body, `http-equiv="refresh"`) {
t.Errorf("%s: the continuation does not navigate on by itself (no meta refresh)", label)
}
return u
}
// TestKeymailAddressGoesToKeymailWhetherPermittedOrNot is the oracle
// test. Whether an address goes to Keymail depends on its domain alone:
// a permitted and a non-permitted Keymail address get the same first
// answer, a continuation page to Keymail, so the answer says nothing
// about the allowlist. Neither is mailed anything.
func TestKeymailAddressGoesToKeymailWhetherPermittedOrNot(t *testing.T) {
app, km := newKeymailApp(t)
member := post(t, app, "/signin", url.Values{"address": {keymailMember}})
stranger := post(t, app, "/signin", url.Values{"address": {keymailStranger}})
mu := continuation(t, "permitted Keymail address", member, member.Body.String())
su := continuation(t, "non-permitted Keymail address", stranger, stranger.Body.String())
// The same page but for the per-attempt state and PKCE challenge.
mask := func(body string, u *url.URL) string {
return strings.ReplaceAll(body, html.EscapeString(u.String()), "AUTHORIZE")
}
if a, b := mask(member.Body.String(), mu), mask(stranger.Body.String(), su); a != b {
t.Errorf("the two Keymail answers differ beyond the authorize URL:\npermitted:\n%s\nnot permitted:\n%s", a, b)
}
cookieNames := func(res *http.Response) string {
var n []string
for _, c := range res.Cookies() {
n = append(n, c.Name)
}
return strings.Join(n, ",")
}
if a, b := cookieNames(member.Result()), cookieNames(stranger.Result()); a != b {
t.Errorf("cookies differ: permitted %q, not permitted %q", a, b)
}
if n := len(app.mailer.messages()); n != 0 {
t.Errorf("mailer captured %d messages, want 0: a Keymail address is never mailed", n)
}
if n := km.Lookups(); n != 2 {
t.Errorf("Keymail saw %d federation lookups, want 2 (both addresses classified)", n)
}
}
// TestNonPermittedPlainAddressIsAnsweredSentAndNotMailed: an address at
// a domain with no Keymail still gets the ordinary ?sent=1, permitted
// or not, and only the permitted one is mailed.
func TestNonPermittedPlainAddressIsAnsweredSentAndNotMailed(t *testing.T) {
app, _ := newKeymailApp(t)
for _, address := range []string{deniedAddress, allowedAddress} {
rec := post(t, app, "/signin", url.Values{"address": {address}})
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
t.Fatalf("POST /signin %q: %d %q, want 303 /signin?sent=1", address, rec.Code, rec.Header().Get("Location"))
}
}
msgs := app.mailer.messages()
if len(msgs) != 1 || msgs[0].To != allowedAddress {
t.Fatalf("mail sent = %+v, want exactly one, to %q", msgs, allowedAddress)
}
}
// TestKeymailFailureFallsBackToAnEmailLink: rastrillo answers a failed
// Keymail exchange with ?force=1&err=keymail. That page must carry
// force back so the next POST mails a link instead of returning to the
// Keymail that just failed — Paul is never stuck. The ordinary form
// carries no force at all.
func TestKeymailFailureFallsBackToAnEmailLink(t *testing.T) {
app, km := newKeymailApp(t)
forced := `name="force" value="1"`
if body := get(t, app, "/signin").Body.String(); strings.Contains(body, forced) {
t.Fatalf("the ordinary sign-in form forces an email link; Keymail addresses would never reach Keymail")
}
page := get(t, app, "/signin?force=1&err=keymail")
if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), forced) {
t.Fatalf("GET /signin?force=1&err=keymail: %d, carries force=%v", page.Code, strings.Contains(page.Body.String(), forced))
}
rec := post(t, app, "/signin", url.Values{"address": {keymailMember}, "force": {"1"}})
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/signin?sent=1" {
t.Fatalf("forced POST: %d %q, want 303 /signin?sent=1", rec.Code, rec.Header().Get("Location"))
}
msgs := app.mailer.messages()
if len(msgs) != 1 || msgs[0].To != keymailMember || magicLinkPattern.FindString(msgs[0].Body) == "" {
t.Fatalf("mail sent = %+v, want one magic link to %q", msgs, keymailMember)
}
if n := km.Lookups(); n != 0 {
t.Errorf("a forced sign-in classified the address (%d lookups)", n)
}
}
// keymailRoundTrip runs POST /signin → continuation → Keymail approves
// → GET /auth/callback for address and returns the callback's answer
// and the cookies collected along the way.
func keymailRoundTrip(t *testing.T, app *testApp, km *keymailtest.Server, address string) (*http.Response, []*http.Cookie) {
t.Helper()
begin := post(t, app, "/signin", url.Values{"address": {address}})
u := continuation(t, address, begin, begin.Body.String())
cookies := begin.Result().Cookies()
cb := get(t, app, "/auth/callback?"+url.Values{"state": {u.Query().Get("state")}, "code": {km.Approve(address)}}.Encode(), cookies...)
return cb.Result(), append(cookies, cb.Result().Cookies()...)
}
// TestKeymailMemberIsSignedIn: the whole Keymail round trip for a
// permitted address ends signed in.
func TestKeymailMemberIsSignedIn(t *testing.T) {
app, km := newKeymailApp(t)
res, cookies := keymailRoundTrip(t, app, km, keymailMember)
if res.StatusCode != http.StatusSeeOther {
t.Fatalf("callback: %d, want 303 into the app", res.StatusCode)
}
if rec := get(t, app, "/", cookies...); rec.Code != http.StatusOK {
t.Fatalf("GET / after Keymail sign-in: %d, want 200", rec.Code)
}
}
// TestKeymailRefusalIsTheDoor: a non-permitted address that Keymail
// has verified is refused AFTER verification, and the refusal is the
// sign-in door, rendered right there with the error and a form for a
// different address by rauth's Refused hook (refused) — not rastrillo's
// bare-text 403. No session is minted. And the sentence cannot be summoned by anybody else: visiting
// /signin?err=refused shows no such thing (Astra, round 3), because the
// refusal is never a query parameter.
func TestKeymailRefusalIsTheDoor(t *testing.T) {
app, km := newKeymailApp(t)
res, cookies := keymailRoundTrip(t, app, km, keymailStranger)
body, _ := io.ReadAll(res.Body)
if res.StatusCode != http.StatusForbidden || res.Header.Get("Location") != "" {
t.Fatalf("callback for a non-permitted address: %d %q, want 403 and the door itself", res.StatusCode, res.Header.Get("Location"))
}
for _, want := range []string{"estilo-door", `name="address"`, "can’t sign in to Slides"} {
if !strings.Contains(string(body), want) {
t.Errorf("refusal page lacks %q:\n%s", want, body)
}
}
if strings.Contains(string(body), "not admitted here") {
t.Error("the refusal is rastrillo's bare text")
}
if got := res.Header.Get("Cache-Control"); got != "no-store" {
t.Errorf("refusal door Cache-Control = %q, want no-store", got)
}
if rec := get(t, app, "/", cookies...); rec.Code == http.StatusOK {
t.Fatal("a refused Keymail sign-in left a working session")
}
if spoof := get(t, app, "/signin?err=refused").Body.String(); strings.Contains(spoof, "can’t sign in to Slides") {
t.Fatal("anybody can show the refusal by visiting /signin?err=refused")
}
}
// mailWorkers counts goroutines running a sign-in mail queue's worker
// or log pump (internal/slides/mailqueue.go) — the targeted leak check
// TestMain runs after the package's tests.
func mailWorkers() int {
buf := make([]byte, 1<<22)
stacks := string(buf[:runtime.Stack(buf, true)])
return strings.Count(stacks, "(*mailQueue).run(") + strings.Count(stacks, "(*mailQueue).pump(")
}