auth_test.go
⎇
Raw
1package server
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "strings"
8 "testing"
9)
10
11// postWith is postForm carrying cookies, which /logout needs: it is a guarded
12// route, so without a session it never reaches the handler.
13func postWith(router http.Handler, path string, cookies []*http.Cookie) *httptest.ResponseRecorder {
14 req := httptest.NewRequest("POST", path, nil)
15 for _, c := range cookies {
16 req.AddCookie(c)
17 }
18 w := httptest.NewRecorder()
19 router.ServeHTTP(w, req)
20 return w
21}
22
23// sessionCookie returns the session cookie set on a response. A cleared cookie
24// carries an empty value and does not count as one.
25func sessionCookie(cookies []*http.Cookie) *http.Cookie {
26 for _, c := range cookies {
27 if c.Name == "session" && c.Value != "" {
28 return c
29 }
30 }
31 return nil
32}
33
34func TestAuthGuardsRoutes(t *testing.T) {
35 srv, _, cleanup := setupTestServer(t)
36 defer cleanup()
37 // Past the test wrapper that signs every request in: these requests must
38 // arrive without a session.
39 router := srv.Server.Router()
40
41 for _, path := range []string{"/", "/library", "/queue", "/settings", "/subscriptions", "/download"} {
42 got := getWith(router, path, nil)
43 if got.Code != http.StatusSeeOther || got.Header().Get("Location") != "/login" {
44 t.Errorf("%s without a session = %d %q, want 303 /login", path, got.Code, got.Header().Get("Location"))
45 }
46 }
47
48 // Mutating routes must be guarded too, not just the pages that link to them.
49 if got := postForm(router, "/queue/clear", nil); got.Header().Get("Location") != "/login" {
50 t.Errorf("clearing the queue without a session redirected to %q, want /login", got.Header().Get("Location"))
51 }
52
53 login := getWith(router, "/login", nil)
54 if login.Code != http.StatusOK {
55 t.Errorf("login page = %d, want 200", login.Code)
56 }
57 // Nothing the navigation points at is reachable without a session, so the
58 // login page shows none of it.
59 for _, link := range []string{`href="/library"`, `href="/queue"`, `href="/settings"`, "/logout"} {
60 if strings.Contains(login.Body.String(), link) {
61 t.Errorf("login page shows %s", link)
62 }
63 }
64 if got := getWith(router, "/healthz", nil); got.Code == http.StatusSeeOther {
65 t.Error("health check redirected to login")
66 }
67}
68
69func TestLoginRejectsWrongCredentials(t *testing.T) {
70 srv, _, cleanup := setupTestServer(t)
71 defer cleanup()
72 router := srv.Server.Router()
73
74 for _, tc := range []struct {
75 name, user, pass string
76 }{
77 {"wrong password", testUsername, "not-the-password"},
78 {"wrong user", "intruder", testPassword},
79 {"empty password", testUsername, ""},
80 // The stored value is a hash, so posting it must not be accepted as if it
81 // were the password itself.
82 {"the hash itself", testUsername, testPasswordHash},
83 } {
84 w := postForm(router, "/login", url.Values{"username": {tc.user}, "password": {tc.pass}})
85 if c := sessionCookie(w.Result().Cookies()); c != nil {
86 t.Errorf("%s issued a session", tc.name)
87 }
88 if k, _, ok := flash(w); !ok || k != "error" {
89 t.Errorf("%s did not flash an error", tc.name)
90 }
91 }
92}
93
94func TestLoginGrantsAccess(t *testing.T) {
95 srv, _, cleanup := setupTestServer(t)
96 defer cleanup()
97 router := srv.Server.Router()
98
99 w := postForm(router, "/login", url.Values{
100 "username": {testUsername},
101 "password": {testPassword},
102 })
103 session := sessionCookie(w.Result().Cookies())
104 if session == nil {
105 t.Fatal("correct credentials issued no session")
106 }
107 if !session.HttpOnly {
108 t.Error("session cookie is not HttpOnly")
109 }
110 if got := getWith(router, "/library", []*http.Cookie{session}); got.Code != http.StatusOK {
111 t.Errorf("library with a session = %d, want 200", got.Code)
112 }
113 if got := getWith(router, "/login", []*http.Cookie{session}); got.Code != http.StatusSeeOther {
114 t.Errorf("login page with a session = %d, want 303", got.Code)
115 }
116
117 // The cookie is signed, so an edited expiry must not extend the session.
118 forged := &http.Cookie{Name: "session", Value: strings.Replace(session.Value, "|", "9|", 1)}
119 if got := getWith(router, "/library", []*http.Cookie{forged}); got.Code != http.StatusSeeOther {
120 t.Errorf("library with a forged session = %d, want 303", got.Code)
121 }
122
123 out := postWith(router, "/logout", []*http.Cookie{session})
124 if sessionCookie(out.Result().Cookies()) != nil {
125 t.Error("logout left a session cookie behind")
126 }
127}
128
129func TestLogoutRevokesTheCookie(t *testing.T) {
130 srv, _, cleanup := setupTestServer(t)
131 defer cleanup()
132 router := srv.Server.Router()
133
134 in := postForm(router, "/login", url.Values{
135 "username": {testUsername},
136 "password": {testPassword},
137 })
138 session := sessionCookie(in.Result().Cookies())
139 if session == nil {
140 t.Fatal("login issued no session")
141 }
142
143 postWith(router, "/logout", []*http.Cookie{session})
144
145 // Clearing the browser's cookie is not enough: a copy taken before the
146 // sign-out must stop working too.
147 if got := getWith(router, "/library", []*http.Cookie{session}); got.Code != http.StatusSeeOther {
148 t.Errorf("a captured cookie still works after logout: %d, want 303", got.Code)
149 }
150}
151