e2e/websessions_test.go

0338e6ace3de199d5fc383852649919b68ef3e42
gitbay/e2e/websessions_test.go history · blame · raw

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