package web
import (
"net/http"
"net/url"
"strings"
"testing"
)
// A forgotten password used to have exactly one answer on this forge:
// an operator choosing a password for somebody and repeating it down a
// channel neither of them controls. This is the other one — the person
// chooses their own, through the same one-time link an invite already
// used, and nobody else ever holds it.
func TestAForgottenPasswordIsResetThroughALink(t *testing.T) {
srv, accts, _ := betaForge(t)
// Somebody with an account and a password they have forgotten.
if err := accts.AddAccount("jo", "the-old-password", false); err != nil {
t.Fatal(err)
}
res := as(t, srv, "root", "POST", "/fleet/people/reset", url.Values{"name": {"jo"}})
body(res)
if res.StatusCode != 303 {
t.Fatalf("FAIL: reset answered %d", res.StatusCode)
}
// FOLLOW the redirect, query and all. The link no longer rides the
// flash — the flash carries the outcome and the URL carries a
// single-use nonce the vault swaps for the link (secretvault.go) —
// so a test that rebuilds the path by hand drops the nonce and
// reports the reveal broken when it is not.
where := res.Header.Get("Location")
if !strings.Contains(where, "?s=") {
t.Fatalf("FAIL: the redirect carries no vault nonce: %q", where)
}
req, _ := http.NewRequest("GET", srv.URL+where, nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "tok-root"})
for _, c := range res.Cookies() {
req.AddCookie(c)
}
res2, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
page := body(res2)
token := strings.Repeat("cd", 32)
if !strings.Contains(page, "/invite/"+token) {
t.Fatalf("FAIL: the People tab does not show the reset link")
}
t.Log("ok: the operator is handed a link")
// It says which of the two things happened. An operator who reads
// "Invited" here has been told the wrong thing about somebody who
// already had an account.
if !strings.Contains(page, "Reset.") {
t.Error("FAIL: the banner does not say this was a reset")
} else {
t.Log("ok: the banner names it a reset")
}
// The old password still works: a reset that locked the account
// would make a lost link worse than the problem it was sent about.
if res := as(t, srv, "", "POST", "/login",
url.Values{"user": {"jo"}, "password": {"the-old-password"}}); res.StatusCode != 303 {
t.Errorf("FAIL: minting a reset locked jo out (login answered %d)", res.StatusCode)
} else {
t.Log("ok: the old password holds until the link is used")
}
// The link greets the right person with the right words.
open := body(as(t, srv, "", "GET", "/invite/"+token, nil))
if !strings.Contains(open, "Set a new password") || !strings.Contains(open, "jo") {
t.Error("FAIL: the link does not offer jo a new password")
} else {
t.Log("ok: the link offers a new password, not a welcome")
}
// And it works.
if res := as(t, srv, "", "POST", "/invite/"+token,
url.Values{"password": {"a-brand-new-password"}, "again": {"a-brand-new-password"}}); res.StatusCode != 303 {
t.Fatalf("FAIL: setting the new password answered %d", res.StatusCode)
}
if res := as(t, srv, "", "POST", "/login",
url.Values{"user": {"jo"}, "password": {"a-brand-new-password"}}); res.StatusCode != 303 {
t.Error("FAIL: the new password does not sign jo in")
} else {
t.Log("ok: jo signs in with the password jo chose")
}
}
// Only a site admin may mint one. A reset link is a credential for
// somebody else's account, so the gate here is the gate on the page.
func TestOnlyAnAdminMayResetAPassword(t *testing.T) {
srv, accts, _ := betaForge(t)
if err := accts.AddAccount("jo", "the-old-password", false); err != nil {
t.Fatal(err)
}
if err := accts.AddAccount("mallory", "mallorys-password", false); err != nil {
t.Fatal(err)
}
for _, who := range []string{"", "mallory"} {
res := as(t, srv, who, "POST", "/fleet/people/reset", url.Values{"name": {"jo"}})
body(res)
// A 303 is not success here: signed out, the gate answers with
// a redirect to the login page. Read WHERE it points, or a
// refusal reads as a reset.
where := res.Header.Get("Location")
refused := res.StatusCode != 303 || strings.HasPrefix(where, "/login")
if !refused {
t.Errorf("FAIL: %q minted a reset for somebody else (%d -> %s)", who, res.StatusCode, where)
} else {
t.Logf("ok: %q was refused (%d -> %s)", who, res.StatusCode, where)
}
}
}