Skip to content
quarry Artifact [5a59baa2f1]
Login

Artifact [5a59baa2f1]

Artifact 5a59baa2f1719c22cf365ba53c8d2ad9334aed4d15eaab815f3a6ca0dc585ed8:


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)
	}
	// The flash rides the redirect; follow it with the cookie jar's help.
	req, _ := http.NewRequest("GET", srv.URL+"/fleet/people", 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)
		}
	}
}