/forgot mails a link to set a new password when the cluster's mail settings say how - SMTP relay, sender, and the public base_url the link points at, never the request's Host - and says the same whether or not the address has an account. /reset sets it once, closing every session. Links are kept by their hash in password_resets, good for an hour, a newer one replacing an older. Without mail, /forgot says to ask an operator, and llmgw users reset-link makes the link to hand over. Single sign-on accounts get none. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// signupAccounts is an account service whose sign-up answers with err.
|
|
type signupAccounts struct {
|
|
fakeAccounts
|
|
err error
|
|
}
|
|
|
|
func (s signupAccounts) SignUp(context.Context, string, string, string) error { return s.err }
|
|
func (signupAccounts) SignupOpen() bool { return true }
|
|
|
|
func signupBody(t *testing.T, err error) string {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
NewAccountPages(signupAccounts{err: err}, nil, nil, "v0", slog.New(slog.DiscardHandler)).Register(mux)
|
|
form := url.Values{"email": {"a@b.c"}, "name": {"A"}, "password": {"long enough password"}}
|
|
req := httptest.NewRequest(http.MethodPost, "/signup", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
return rec.Body.String()
|
|
}
|
|
|
|
func TestASignupThatFailsSaysSoWithAReferenceAndNoDatabaseWords(t *testing.T) {
|
|
body := signupBody(t, errors.New(`duplicate key value violates unique constraint "users_email_key"`))
|
|
if strings.Contains(body, "users_email_key") || strings.Contains(body, "constraint") {
|
|
t.Errorf("the database's own words reached the page: %s", body)
|
|
}
|
|
if !strings.Contains(body, "was not created") || !strings.Contains(body, "reference: err-") {
|
|
t.Errorf("no plain failure with a reference to give an operator: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestASignupSaysWhatToDoIfTheAddressHadAnAccount(t *testing.T) {
|
|
// The service answers a taken address as it does a new one, so nobody
|
|
// can find out from this page who has an account; what the page can do
|
|
// is say what to do in either case.
|
|
body := signupBody(t, nil)
|
|
if !strings.Contains(body, "already had an account") || !strings.Contains(body, `href="/login"`) ||
|
|
!strings.Contains(body, `href="/forgot"`) {
|
|
t.Errorf("the done page leaves someone who already had an account stuck: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestTheSignInFormWantsItsOwnTokenAndRationsAnAddress(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
NewAccountPages(signupAccounts{}, nil, nil, "v0", slog.New(slog.DiscardHandler)).Register(mux)
|
|
post := func(form url.Values, cookie *http.Cookie) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.RemoteAddr = "203.0.113.9:1234"
|
|
if cookie != nil {
|
|
req.AddCookie(cookie)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
// No token: another site's form.
|
|
if rec := post(url.Values{"email": {"a@b.c"}, "password": {"x"}}, nil); rec.Code != http.StatusBadRequest {
|
|
t.Errorf("a sign-in without the form's token: %d", rec.Code)
|
|
}
|
|
// The form's token, matched by its cookie, reaches the sign-in.
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/login", nil))
|
|
var cookie *http.Cookie
|
|
for _, c := range rec.Result().Cookies() {
|
|
if c.Name == loginCSRFCookie {
|
|
cookie = c
|
|
}
|
|
}
|
|
if cookie == nil || !strings.Contains(rec.Body.String(), cookie.Value) {
|
|
t.Fatal("the form carries no token")
|
|
}
|
|
if rec := post(url.Values{"email": {"a@b.c"}, "password": {"x"}, "csrf": {cookie.Value}}, cookie); rec.Code == http.StatusBadRequest {
|
|
t.Errorf("the form's own token was refused")
|
|
}
|
|
// A script posting it is rationed.
|
|
limited := false
|
|
for range 40 {
|
|
if post(url.Values{"csrf": {cookie.Value}}, cookie).Code == http.StatusTooManyRequests {
|
|
limited = true
|
|
}
|
|
}
|
|
if !limited {
|
|
t.Error("forty sign-ins a moment from one address were all taken")
|
|
}
|
|
}
|