package server import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" ) // postWith is postForm carrying cookies, which /logout needs: it is a guarded // route, so without a session it never reaches the handler. func postWith(router http.Handler, path string, cookies []*http.Cookie) *httptest.ResponseRecorder { req := httptest.NewRequest("POST", path, nil) for _, c := range cookies { req.AddCookie(c) } w := httptest.NewRecorder() router.ServeHTTP(w, req) return w } // sessionCookie returns the session cookie set on a response. A cleared cookie // carries an empty value and does not count as one. func sessionCookie(cookies []*http.Cookie) *http.Cookie { for _, c := range cookies { if c.Name == "session" && c.Value != "" { return c } } return nil } func TestAuthGuardsRoutes(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() // Past the test wrapper that signs every request in: these requests must // arrive without a session. router := srv.Server.Router() for _, path := range []string{"/", "/library", "/queue", "/settings", "/subscriptions", "/download"} { got := getWith(router, path, nil) if got.Code != http.StatusSeeOther || got.Header().Get("Location") != "/login" { t.Errorf("%s without a session = %d %q, want 303 /login", path, got.Code, got.Header().Get("Location")) } } // Mutating routes must be guarded too, not just the pages that link to them. if got := postForm(router, "/queue/clear", nil); got.Header().Get("Location") != "/login" { t.Errorf("clearing the queue without a session redirected to %q, want /login", got.Header().Get("Location")) } login := getWith(router, "/login", nil) if login.Code != http.StatusOK { t.Errorf("login page = %d, want 200", login.Code) } // Nothing the navigation points at is reachable without a session, so the // login page shows none of it. for _, link := range []string{`href="/library"`, `href="/queue"`, `href="/settings"`, "/logout"} { if strings.Contains(login.Body.String(), link) { t.Errorf("login page shows %s", link) } } if got := getWith(router, "/healthz", nil); got.Code == http.StatusSeeOther { t.Error("health check redirected to login") } } func TestLoginRejectsWrongCredentials(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Server.Router() for _, tc := range []struct { name, user, pass string }{ {"wrong password", testUsername, "not-the-password"}, {"wrong user", "intruder", testPassword}, {"empty password", testUsername, ""}, // The stored value is a hash, so posting it must not be accepted as if it // were the password itself. {"the hash itself", testUsername, testPasswordHash}, } { w := postForm(router, "/login", url.Values{"username": {tc.user}, "password": {tc.pass}}) if c := sessionCookie(w.Result().Cookies()); c != nil { t.Errorf("%s issued a session", tc.name) } if k, _, ok := flash(w); !ok || k != "error" { t.Errorf("%s did not flash an error", tc.name) } } } func TestLoginGrantsAccess(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Server.Router() w := postForm(router, "/login", url.Values{ "username": {testUsername}, "password": {testPassword}, }) session := sessionCookie(w.Result().Cookies()) if session == nil { t.Fatal("correct credentials issued no session") } if !session.HttpOnly { t.Error("session cookie is not HttpOnly") } if got := getWith(router, "/library", []*http.Cookie{session}); got.Code != http.StatusOK { t.Errorf("library with a session = %d, want 200", got.Code) } if got := getWith(router, "/login", []*http.Cookie{session}); got.Code != http.StatusSeeOther { t.Errorf("login page with a session = %d, want 303", got.Code) } // The cookie is signed, so an edited expiry must not extend the session. forged := &http.Cookie{Name: "session", Value: strings.Replace(session.Value, "|", "9|", 1)} if got := getWith(router, "/library", []*http.Cookie{forged}); got.Code != http.StatusSeeOther { t.Errorf("library with a forged session = %d, want 303", got.Code) } out := postWith(router, "/logout", []*http.Cookie{session}) if sessionCookie(out.Result().Cookies()) != nil { t.Error("logout left a session cookie behind") } } func TestLogoutRevokesTheCookie(t *testing.T) { srv, _, cleanup := setupTestServer(t) defer cleanup() router := srv.Server.Router() in := postForm(router, "/login", url.Values{ "username": {testUsername}, "password": {testPassword}, }) session := sessionCookie(in.Result().Cookies()) if session == nil { t.Fatal("login issued no session") } postWith(router, "/logout", []*http.Cookie{session}) // Clearing the browser's cookie is not enough: a copy taken before the // sign-out must stop working too. if got := getWith(router, "/library", []*http.Cookie{session}); got.Code != http.StatusSeeOther { t.Errorf("a captured cookie still works after logout: %d, want 303", got.Code) } }