package web import ( "context" "log/slog" "net/http" "net/http/httptest" "net/url" "path/filepath" "regexp" "strings" "testing" "time" "gitea.mynodes.xyz/ilker/llmgw/internal/account" "gitea.mynodes.xyz/ilker/llmgw/internal/store" ) type sentMail struct{ to, subject, body string } type fakeMailer struct{ sent []sentMail } func (*fakeMailer) Enabled() bool { return true } func (m *fakeMailer) Send(_ context.Context, to, subject, body string) error { m.sent = append(m.sent, sentMail{to, subject, body}) return nil } func TestAForgottenPasswordIsSetThroughAMailedLink(t *testing.T) { ctx := context.Background() st, err := store.OpenSQLite(ctx, filepath.Join(t.TempDir(), "w.db")) if err != nil { t.Fatal(err) } defer st.Close() hash, _ := account.HashPassword("an old long password") if err := st.CreateUser(ctx, store.User{ID: "usr-a", Email: "ayse@example.com", Name: "Ayşe", PasswordHash: hash, State: store.UserActive, CreatedAt: time.Now()}); err != nil { t.Fatal(err) } svc := account.New(st, account.DefaultConfig(), slog.New(slog.DiscardHandler)) p := NewAccountPages(svc, st, st, "v0", slog.New(slog.DiscardHandler)) mailer := &fakeMailer{} p.SetPasswordReset(st, mailer, "https://llm.example.com/") mux := http.NewServeMux() p.Register(mux) formToken := func(path string) *http.Cookie { rec := httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil)) for _, c := range rec.Result().Cookies() { if c.Name == loginCSRFCookie { return c } } t.Fatalf("%s carries no form token", path) return nil } post := func(path string, form url.Values, c *http.Cookie, host string) string { form.Set("csrf", c.Value) req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Host = host req.AddCookie(c) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) return rec.Body.String() } // Asked from a Host somebody chose: the link still points at the // gateway's own address. c := formToken("/forgot") sentPage := post("/forgot", url.Values{"email": {"ayse@example.com"}}, c, "evil.example") if len(mailer.sent) != 1 || !strings.Contains(mailer.sent[0].body, "https://llm.example.com/reset?token=") || strings.Contains(mailer.sent[0].body, "evil.example") || mailer.sent[0].to != "ayse@example.com" { t.Fatalf("mail: %+v", mailer.sent) } // An address with no account gets the same page, and no mail. nobody := post("/forgot", url.Values{"email": {"nobody@example.com"}}, formToken("/forgot"), "llm.example.com") if len(mailer.sent) != 1 || !strings.Contains(nobody, "on its way") || !strings.Contains(sentPage, "on its way") { t.Errorf("an unknown address was told apart, or mailed: %d sent", len(mailer.sent)) } token := regexp.MustCompile(`token=([^\s]+)`).FindStringSubmatch(mailer.sent[0].body)[1] token, _ = url.QueryUnescape(token) rec := httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/reset?token="+url.QueryEscape(token), nil)) if rec.Header().Get("Referrer-Policy") != "no-referrer" || !strings.Contains(rec.Body.String(), `name="confirm"`) { t.Fatalf("the reset form: %s", rec.Body.String()) } mismatch := post("/reset", url.Values{"token": {token}, "password": {"a brand new password"}, "confirm": {"not the same"}}, formToken("/reset?token="+url.QueryEscape(token)), "llm.example.com") if !strings.Contains(mismatch, "not the same") { t.Errorf("two different passwords: %s", mismatch) } done := post("/reset", url.Values{"token": {token}, "password": {"a brand new password"}, "confirm": {"a brand new password"}}, formToken("/reset?token="+url.QueryEscape(token)), "llm.example.com") if !strings.Contains(done, "Your password is set") { t.Fatalf("setting it: %s", done) } if _, _, err := svc.SignIn(ctx, "ayse@example.com", "a brand new password"); err != nil { t.Errorf("the new password does not sign in: %v", err) } again := post("/reset", url.Values{"token": {token}, "password": {"yet another password"}, "confirm": {"yet another password"}}, formToken("/reset?token="+url.QueryEscape(token)), "llm.example.com") if !strings.Contains(again, "does not work") { t.Errorf("a link used twice: %s", again) } } func TestWithoutMailForgottenSaysAskAnOperator(t *testing.T) { st, err := store.OpenSQLite(context.Background(), filepath.Join(t.TempDir(), "w.db")) if err != nil { t.Fatal(err) } defer st.Close() svc := account.New(st, account.DefaultConfig(), slog.New(slog.DiscardHandler)) p := NewAccountPages(svc, st, st, "v0", slog.New(slog.DiscardHandler)) p.SetPasswordReset(st, nil, "") mux := http.NewServeMux() p.Register(mux) rec := httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/forgot", nil)) if !strings.Contains(rec.Body.String(), "llmgw users reset-link") { t.Errorf("%s", rec.Body.String()) } rec = httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/login", nil)) if !strings.Contains(rec.Body.String(), `href="/forgot"`) { t.Error("sign-in does not link to /forgot") } }