e2e/websessions_test.go
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}