internal/httpd/account_test.go

dd06e80a071d451ebb7b083363e0580550481114
gitbay/internal/httpd/account_test.go history · blame · raw

177 lines · 5387 bytes

  1package httpd
  2
  3import (
  4	"net/http"
  5	"net/http/httptest"
  6	"net/url"
  7	"strconv"
  8	"strings"
  9	"testing"
 10
 11	"gitbay.org/gitbay/internal/config"
 12	"gitbay.org/gitbay/internal/store"
 13)
 14
 15// The settings page carries a push toggle beside the mail and watch ones,
 16// and lists registered devices by label and truncated token. The full
 17// token is device-identifying and must never reach the page.
 18func TestAccountPagePushToggleAndDevices(t *testing.T) {
 19	st, err := store.Open(":memory:")
 20	if err != nil {
 21		t.Fatal(err)
 22	}
 23	defer st.Close()
 24	if err := st.MigrateUp(); err != nil {
 25		t.Fatal(err)
 26	}
 27
 28	uid, err := st.CreateUser("alice", false)
 29	if err != nil {
 30		t.Fatal(err)
 31	}
 32	token := strings.Repeat("a", 64)
 33	if _, err := st.AddPushDevice(uid, token, "iphone"); err != nil {
 34		t.Fatal(err)
 35	}
 36
 37	s := New(config.Default(), st)
 38	rr := httptest.NewRecorder()
 39	req := httptest.NewRequest("GET", "/settings", nil)
 40	s.accountPage(rr, req, store.User{ID: uid, Username: "alice"})
 41
 42	body := rr.Body.String()
 43	if !strings.Contains(body, `value="notify-push"`) {
 44		t.Fatal("no push toggle")
 45	}
 46	if !strings.Contains(body, "iphone") {
 47		t.Fatal("the device is not listed")
 48	}
 49	// A token is device-identifying and is never printed in full.
 50	if strings.Contains(body, token) {
 51		t.Fatal("the page printed a device token in full")
 52	}
 53}
 54
 55// submit posts an account settings form as u and returns the recorder.
 56func submitAccountForm(t *testing.T, s *Server, u store.User, form url.Values) *httptest.ResponseRecorder {
 57	t.Helper()
 58	req := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode()))
 59	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 60	rr := httptest.NewRecorder()
 61	s.accountSubmit(rr, req, u)
 62	return rr
 63}
 64
 65// Posting notify-push dispatches to notifications settings push, the same
 66// path the mail and watch toggles already use.
 67func TestAccountSubmitNotifyPush(t *testing.T) {
 68	st, err := store.Open(":memory:")
 69	if err != nil {
 70		t.Fatal(err)
 71	}
 72	defer st.Close()
 73	if err := st.MigrateUp(); err != nil {
 74		t.Fatal(err)
 75	}
 76	uid, err := st.CreateUser("alice", false)
 77	if err != nil {
 78		t.Fatal(err)
 79	}
 80	u := store.User{ID: uid, Username: "alice"}
 81	s := New(config.Default(), st)
 82
 83	rr := submitAccountForm(t, s, u, url.Values{"field": {"notify-push"}, "push": {"on"}})
 84	if rr.Code != http.StatusSeeOther {
 85		t.Fatalf("status %d, body %s", rr.Code, rr.Body.String())
 86	}
 87	if on, err := st.PushEnabled(uid); err != nil || !on {
 88		t.Fatalf("PushEnabled after notify-push=on: %v %v", on, err)
 89	}
 90
 91	submitAccountForm(t, s, u, url.Values{"field": {"notify-push"}})
 92	if on, err := st.PushEnabled(uid); err != nil || on {
 93		t.Fatalf("PushEnabled after notify-push off: %v %v", on, err)
 94	}
 95}
 96
 97// Removing a device requires the device id typed back, and then
 98// dispatches to notifications device remove, scoped to the caller's own
 99// account. The id is what the form dispatches on, so the guard is
100// derived server-side the way key-remove derives its own.
101func TestAccountSubmitDeviceRemove(t *testing.T) {
102	st, err := store.Open(":memory:")
103	if err != nil {
104		t.Fatal(err)
105	}
106	defer st.Close()
107	if err := st.MigrateUp(); err != nil {
108		t.Fatal(err)
109	}
110	uid, err := st.CreateUser("alice", false)
111	if err != nil {
112		t.Fatal(err)
113	}
114	u := store.User{ID: uid, Username: "alice"}
115	token := strings.Repeat("b", 64)
116	id, err := st.AddPushDevice(uid, token, "iphone")
117	if err != nil {
118		t.Fatal(err)
119	}
120	s := New(config.Default(), st)
121
122	idStr := strconv.FormatInt(id, 10)
123
124	// Without the typed confirmation, the device survives.
125	submitAccountForm(t, s, u, url.Values{"field": {"device-remove"}, "id": {idStr}})
126	if devices, _ := st.PushDevices(uid); len(devices) != 1 {
127		t.Fatalf("device removed without confirmation: %v", devices)
128	}
129
130	rr := submitAccountForm(t, s, u, url.Values{"field": {"device-remove"}, "id": {idStr}, "confirm": {idStr}})
131	if rr.Code != http.StatusSeeOther {
132		t.Fatalf("status %d, body %s", rr.Code, rr.Body.String())
133	}
134	if devices, _ := st.PushDevices(uid); len(devices) != 0 {
135		t.Fatalf("device not removed: %v", devices)
136	}
137}
138
139// A short token reaches no part of the page — not the visible column,
140// and not a hidden input, aria-label or placeholder either. Device add
141// enforces no minimum length, so a token this short is a value the store
142// can hold, and it is device-identifying whatever its length.
143func TestAccountPageMasksAShortDeviceToken(t *testing.T) {
144	st, err := store.Open(":memory:")
145	if err != nil {
146		t.Fatal(err)
147	}
148	defer st.Close()
149	if err := st.MigrateUp(); err != nil {
150		t.Fatal(err)
151	}
152	uid, err := st.CreateUser("alice", false)
153	if err != nil {
154		t.Fatal(err)
155	}
156	id, err := st.AddPushDevice(uid, "abc123", "iphone")
157	if err != nil {
158		t.Fatal(err)
159	}
160
161	s := New(config.Default(), st)
162	rr := httptest.NewRecorder()
163	s.accountPage(rr, httptest.NewRequest("GET", "/settings", nil), store.User{ID: uid, Username: "alice"})
164
165	body := rr.Body.String()
166	if strings.Contains(body, "abc123") {
167		t.Fatalf("the short token reached the page:\n%s", body)
168	}
169	// What the removal asks for has to be on screen to be typed back.
170	idStr := strconv.FormatInt(id, 10)
171	if !strings.Contains(body, `aria-label="Type `+idStr+` to confirm"`) {
172		t.Fatalf("removal does not confirm on the device id:\n%s", body)
173	}
174	if !strings.Contains(body, `<th scope="col">id</th>`) {
175		t.Fatalf("the device table has no id column:\n%s", body)
176	}
177}