package web
import (
"net/http"
"net/url"
"slices"
"strings"
"testing"
)
// Two accounts in one browser, the way GitHub does it: signing in while
// signed in keeps the first session in a stash; the menu offers a
// switch; switching swaps which session is active; signing out wakes
// the stashed one; a third account pushes the oldest out.
func TestAccountSwitcherHoldsTwoAndSwaps(t *testing.T) {
srv, accts, sess := betaForge(t)
_ = accts.AddAccount("eve", "longenough1", false)
_ = accts.AddAccount("bob", "longenough1", false)
cookies := func(res *http.Response) (session, others string, othersCleared bool) {
for _, c := range res.Cookies() {
switch c.Name {
case sessionCookie:
session = c.Value
case othersCookie:
others, othersCleared = c.Value, c.MaxAge < 0
}
}
return
}
post := func(path string, form url.Values, jar ...*http.Cookie) *http.Response {
req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
for _, c := range jar {
req.AddCookie(c)
}
res, err := (&http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}).Do(req)
if err != nil {
t.Fatal(err)
}
res.Body.Close()
return res
}
// First sign-in: one cookie, no stash.
res := post("/login", url.Values{"user": {"root"}, "password": {"root-pw"}})
s1, o1, _ := cookies(res)
if s1 != "tok-root" || o1 != "" {
t.Fatalf("first sign-in: session %q others %q", s1, o1)
}
// Second account while signed in: the first is stashed, not lost.
res = post("/login", url.Values{"user": {"eve"}, "password": {"longenough1"}}, &http.Cookie{Name: sessionCookie, Value: s1})
s2, o2, _ := cookies(res)
if s2 != "tok-eve" || o2 != "tok-root" {
t.Fatalf("second sign-in: session %q others %q", s2, o2)
}
// The menu offers the switch, by name, and no room for a third.
req, _ := http.NewRequest("GET", srv.URL+"/", nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: s2})
req.AddCookie(&http.Cookie{Name: othersCookie, Value: o2})
pres, _ := http.DefaultClient.Do(req)
page := body(pres)
if !strings.Contains(page, `action="/switch"`) || !strings.Contains(page, `name="to" value="root"`) {
t.Errorf("the menu offers no switch to root")
}
if strings.Contains(page, `href="/login?add=1"`) {
t.Errorf("a full stash still offers Add account")
}
// Switching swaps active and stashed; a token never crosses the wire.
res = post("/switch", url.Values{"to": {"root"}, "back": {"/r/alpha"}}, &http.Cookie{Name: sessionCookie, Value: s2}, &http.Cookie{Name: othersCookie, Value: o2})
s3, o3, _ := cookies(res)
if s3 != "tok-root" || o3 != "tok-eve" || res.Header.Get("Location") != "/r/alpha" {
t.Fatalf("switch: session %q others %q -> %s", s3, o3, res.Header.Get("Location"))
}
// A third account: the oldest stashed session is ended, not kept.
res = post("/login", url.Values{"user": {"bob"}, "password": {"longenough1"}}, &http.Cookie{Name: sessionCookie, Value: s3}, &http.Cookie{Name: othersCookie, Value: o3})
s4, o4, _ := cookies(res)
if s4 != "tok-bob" || o4 != "tok-root" {
t.Fatalf("third sign-in: session %q others %q", s4, o4)
}
if !slices.Contains(sess.out, "tok-eve") {
t.Errorf("the pushed-out session was not ended server-side: %v", sess.out)
}
// Signing out ends the active session and wakes the stashed one.
res = post("/logout", nil, &http.Cookie{Name: sessionCookie, Value: s4}, &http.Cookie{Name: othersCookie, Value: o4})
s5, _, cleared := cookies(res)
if s5 != "tok-root" || !cleared {
t.Fatalf("sign out: session %q, stash cleared %v", s5, cleared)
}
if !slices.Contains(sess.out, "tok-bob") {
t.Errorf("sign out did not end the active session: %v", sess.out)
}
// Signing out with nothing stashed clears the browser as before.
res = post("/logout", nil, &http.Cookie{Name: sessionCookie, Value: s5})
if s6, _, _ := cookies(res); s6 != "" {
t.Fatalf("final sign out left a session %q", s6)
}
// The add-account door tells the reader the first account stays.
req, _ = http.NewRequest("GET", srv.URL+"/login?add=1", nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "tok-root"})
pres, _ = http.DefaultClient.Do(req)
if !strings.Contains(body(pres), `data-hook="login-add"`) {
t.Errorf("the add-account login page does not say the first account stays")
}
}