e2e/websessions_test.go

v1.28.0
gitbay/e2e/websessions_test.go history · blame · raw

110 lines · 4105 bytes

  1package e2e
  2
  3import (
  4	"encoding/json"
  5	"net/http"
  6	"strings"
  7	"testing"
  8)
  9
 10// A browser session can be listed and ended from SSH, one at a time or
 11// all at once, and only its owner sees it.
 12func TestWebSessionsListRevoke(t *testing.T) {
 13	inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
 14	aliceKey := inst.newKey(t, "alice")
 15	bobKey := inst.newKey(t, "bob")
 16	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
 17	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
 18
 19	if out, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "list", "--json"); code != 0 || !strings.Contains(out, `"data":[]`) {
 20		t.Fatalf("no sessions yet: exit %d %s", code, out)
 21	}
 22	first := inst.login(t, aliceKey)
 23	second := inst.login(t, aliceKey)
 24	list := func() []struct {
 25		ID string `json:"id"`
 26	} {
 27		t.Helper()
 28		out, errOut, code := inst.ssh(t, aliceKey, "", "web", "sessions", "list", "--json")
 29		if code != 0 {
 30			t.Fatalf("list: %s", errOut)
 31		}
 32		var env struct {
 33			Data []struct {
 34				ID string `json:"id"`
 35			} `json:"data"`
 36		}
 37		if err := json.Unmarshal([]byte(out), &env); err != nil {
 38			t.Fatalf("list json: %v\n%s", err, out)
 39		}
 40		return env.Data
 41	}
 42	sessions := list()
 43	if len(sessions) != 2 || len(sessions[0].ID) != 12 {
 44		t.Fatalf("two sessions expected: %+v", sessions)
 45	}
 46	// Bob sees none of them, and cannot revoke one by id.
 47	if out, _, _ := inst.ssh(t, bobKey, "", "web", "sessions", "list", "--json"); !strings.Contains(out, `"data":[]`) {
 48		t.Fatalf("bob sees alice's sessions:\n%s", out)
 49	}
 50	if _, _, code := inst.ssh(t, bobKey, "", "web", "sessions", "revoke", sessions[0].ID); code != 3 {
 51		t.Fatalf("bob revoked alice's session: exit %d", code)
 52	}
 53	// Both browsers work; revoking the newest logs that one out.
 54	// The client follows the logged-out redirect to /login, so the page
 55	// body tells the two apart, not the status.
 56	loggedIn := func(c *http.Client) bool {
 57		_, body := browserGet(t, c, inst.base()+"/settings")
 58		return strings.Contains(body, "SSH keys")
 59	}
 60	if !loggedIn(first) || !loggedIn(second) {
 61		t.Fatal("both browsers should be logged in")
 62	}
 63	if out, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "revoke", sessions[0].ID); code != 0 || !strings.Contains(out, "revoked browser session") {
 64		t.Fatalf("revoke: exit %d %s", code, out)
 65	}
 66	if got := list(); len(got) != 1 {
 67		t.Fatalf("one session left expected: %+v", got)
 68	}
 69	okCount := 0
 70	for _, c := range []*http.Client{first, second} {
 71		if loggedIn(c) {
 72			okCount++
 73		}
 74	}
 75	if okCount != 1 {
 76		t.Fatalf("exactly one browser should still be logged in, got %d", okCount)
 77	}
 78	if out, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "revoke", "--all"); code != 0 || !strings.Contains(out, "revoked 1 browser sessions") {
 79		t.Fatalf("revoke --all: exit %d %s", code, out)
 80	}
 81	if loggedIn(first) || loggedIn(second) {
 82		t.Fatal("a browser is still logged in after revoke --all")
 83	}
 84	if _, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "revoke", "abcdefabcdef"); code != 3 {
 85		t.Fatal("unknown id accepted")
 86	}
 87	// An anonymous visit to a page that needs a session lands on the
 88	// login page, which says where the visitor was going; the login
 89	// link then returns them there.
 90	anon := newBrowser(t)
 91	status, body := browserGet(t, anon, inst.base()+"/settings")
 92	if status != 200 || !strings.Contains(body, "continue to <code>/settings</code>") {
 93		t.Fatalf("login page without the destination: %d\n%s", status, body)
 94	}
 95	out, _, _ := inst.ssh(t, aliceKey, "", "web", "login", "--json")
 96	var env struct {
 97		Data struct {
 98			URL string `json:"url"`
 99		} `json:"data"`
100	}
101	json.Unmarshal([]byte(out), &env)
102	link := inst.base() + env.Data.URL[strings.Index(env.Data.URL, "/login"):]
103	if status, body := browserGet(t, anon, link); status != 200 || !strings.Contains(body, "Account settings") {
104		t.Fatalf("login did not return to /settings: %d\n%s", status, body)
105	}
106	// The destination is used once.
107	if _, body := browserGet(t, anon, inst.base()+"/login"); strings.Contains(body, "continue to") {
108		t.Fatal("next survived its use")
109	}
110}