package web
import (
"net/http"
"net/http/cookiejar"
"net/url"
"regexp"
"strings"
"testing"
)
func newJar(t *testing.T) http.CookieJar {
t.Helper()
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatalf("cookiejar: %v", err)
}
return jar
}
var (
tokenPattern = regexp.MustCompile(`sg_[A-Za-z0-9_-]+`)
tokenIDPath = regexp.MustCompile(`/settings/tokens/(\d+)/revoke`)
)
func TestSettingsRequiresLogin(t *testing.T) {
httpServer, _, _ := newTestServer(t)
noFollow := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}}
resp, err := noFollow.Get(httpServer.URL + "/settings")
if err != nil {
t.Fatalf("anonymous GET /settings: %v", err)
}
readAll(t, resp)
if resp.StatusCode != http.StatusSeeOther || resp.Header.Get("Location") != "/login" {
t.Errorf("anonymous settings = %d %q, want 303 /login", resp.StatusCode, resp.Header.Get("Location"))
}
}
func TestChangePassword(t *testing.T) {
httpServer, _, _ := newTestServer(t)
josie := newLoggedInClient(t, httpServer)
other := newLoggedInClient(t, httpServer)
change := func(current, next, confirm string) string {
t.Helper()
resp, err := josie.PostForm(httpServer.URL+"/settings/password", url.Values{
"current_password": {current}, "new_password": {next}, "confirm_password": {confirm},
})
if err != nil {
t.Fatalf("POST /settings/password: %v", err)
}
return readAll(t, resp)
}
if body := change("wrong", "newpass", "newpass"); !strings.Contains(body, "current password is incorrect") {
t.Errorf("wrong current password: body = %q", body)
}
if body := change("hunter2", "newpass", "different"); !strings.Contains(body, "do not match") {
t.Errorf("mismatched confirm: body = %q", body)
}
if body := change("hunter2", "newpass", "newpass"); !strings.Contains(body, "password changed") {
t.Errorf("valid change: body = %q", body)
}
// The session that made the change stays signed in.
if resp, err := josie.Get(httpServer.URL + "/settings"); err != nil {
t.Fatalf("GET /settings after change: %v", err)
} else if readAll(t, resp); resp.StatusCode != http.StatusOK {
t.Errorf("current session after change = %d, want 200", resp.StatusCode)
}
// A different session is revoked.
noFollow := &http.Client{
Transport: other.Transport,
Jar: other.Jar,
CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := noFollow.Get(httpServer.URL + "/settings")
if err != nil {
t.Fatalf("other session GET: %v", err)
}
readAll(t, resp)
if resp.StatusCode != http.StatusSeeOther || resp.Header.Get("Location") != "/login" {
t.Errorf("other session = %d %q, want 303 /login", resp.StatusCode, resp.Header.Get("Location"))
}
// Old password no longer works; the new one does.
fresh := &http.Client{Transport: httpServer.Client().Transport, Jar: newJar(t)}
resp, err = fresh.PostForm(httpServer.URL+"/login", url.Values{"username": {"josie"}, "password": {"hunter2"}})
if err != nil {
t.Fatalf("login old password: %v", err)
}
readAll(t, resp)
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("old password login = %d, want 401", resp.StatusCode)
}
resp, err = fresh.PostForm(httpServer.URL+"/login", url.Values{"username": {"josie"}, "password": {"newpass"}})
if err != nil {
t.Fatalf("login new password: %v", err)
}
if body := readAll(t, resp); !strings.Contains(body, `href="/josie"`) {
t.Errorf("new password login rejected: %q", body)
}
}
func basicRefs(t *testing.T, httpServerURL, user, secret string) int {
t.Helper()
req, err := http.NewRequest("GET", httpServerURL+"/josie/sec.git/info/refs?service=git-upload-pack", nil)
if err != nil {
t.Fatalf("new request: %v", err)
}
req.SetBasicAuth(user, secret)
resp, err := (&http.Client{}).Do(req)
if err != nil {
t.Fatalf("basic refs: %v", err)
}
readAll(t, resp)
return resp.StatusCode
}
func TestTokenCreateUseRevoke(t *testing.T) {
httpServer, database, _ := newTestServer(t)
loggedIn := newLoggedInClient(t, httpServer)
createRepo(t, loggedIn, httpServer, "sec", "private")
resp, err := loggedIn.PostForm(httpServer.URL+"/settings/tokens", url.Values{"name": {"laptop"}})
if err != nil {
t.Fatalf("create token: %v", err)
}
body := readAll(t, resp)
if resp.StatusCode != http.StatusOK {
t.Fatalf("create token status = %d: %q", resp.StatusCode, body)
}
token := tokenPattern.FindString(body)
if token == "" {
t.Fatalf("response did not show a new token: %q", body)
}
list, err := loggedIn.Get(httpServer.URL + "/settings")
if err != nil {
t.Fatalf("GET /settings: %v", err)
}
listBody := readAll(t, list)
if !strings.Contains(listBody, "laptop") || !strings.Contains(listBody, token[:8]) {
t.Errorf("settings list lacks the token row: %q", listBody)
}
if strings.Contains(listBody, token) {
t.Error("full token secret leaked into the settings list")
}
if code := basicRefs(t, httpServer.URL, "josie", token); code != http.StatusOK {
t.Errorf("token as basic-auth password = %d, want 200", code)
}
if code := basicRefs(t, httpServer.URL, "josie", "sg_not-a-real-token"); code != http.StatusUnauthorized {
t.Errorf("bogus token = %d, want 401", code)
}
match := tokenIDPath.FindStringSubmatch(listBody)
if match == nil {
t.Fatalf("no revoke link in settings: %q", listBody)
}
// A different user must not be able to revoke someone else's token.
addUser(t, database, "mallory", "pw")
mallory := loginAs(t, httpServer, "mallory", "pw")
if resp, err := mallory.PostForm(httpServer.URL+"/settings/tokens/"+match[1]+"/revoke", nil); err == nil {
readAll(t, resp)
}
if code := basicRefs(t, httpServer.URL, "josie", token); code != http.StatusOK {
t.Errorf("token invalidated by a non-owner = %d, want still 200", code)
}
revoke, err := loggedIn.PostForm(httpServer.URL+"/settings/tokens/"+match[1]+"/revoke", nil)
if err != nil {
t.Fatalf("revoke token: %v", err)
}
readAll(t, revoke)
if code := basicRefs(t, httpServer.URL, "josie", token); code != http.StatusUnauthorized {
t.Errorf("revoked token = %d, want 401", code)
}
}