package user import ( "encoding/json" "net/http" "net/http/httptest" "strconv" "strings" "testing" "git.golem15.com/golem15/sm-user-plugin/classes" "git.golem15.com/golem15/sm-user-plugin/controllers" "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/golem15/summercms/modules/postcard" ) func TestForgotPassword(t *testing.T) { app, gdb := sessionApp(t, "0s") loc := "en" user := insertUser(t, gdb, "forgot@example.test", "correct-horse") if err := gdb.Model(user).Update("preferred_locale", loc).Error; err != nil { t.Fatal(err) } mail := &captureMail{} if err := app.Publish[postcard.Mailer](mail); err != nil { t.Fatal(err) } known := postJSON(t, controllers.ForgotPassword(app), "/_user/api/v1/forgot-password", `{"email":"forgot@example.test"}`) unknown := postJSON(t, controllers.ForgotPassword(app), "/_user/api/v1/forgot-password", `{"email":"missing@example.test"}`) const body = `{"message":"If that email exists, a reset link has been sent."}` if known.Code != http.StatusOK || known.Body.String() != body || unknown.Body.String() != body { t.Fatalf("known %d %s unknown %d %s", known.Code, known.Body.String(), unknown.Code, unknown.Body.String()) } if len(mail.got) != 1 || mail.got[0] != "golem15.user::mail.restore-en" { t.Fatalf("template = %#v", mail.got) } bad := postJSON(t, controllers.ForgotPassword(app), "/_user/api/v1/forgot-password", `{"email":"nope"}`) if bad.Code != http.StatusUnprocessableEntity || !strings.Contains(bad.Body.String(), `"errors":`) { t.Fatalf("malformed %d %s", bad.Code, bad.Body.String()) } var verr map[string]any if err := json.Unmarshal(bad.Body.Bytes(), &verr); err != nil { t.Fatal(err) } errorsBody, _ := verr["errors"].(map[string]any) if _, ok := errorsBody["email"]; !ok { t.Fatalf("errors = %#v", verr) } } func TestResetPassword(t *testing.T) { app, gdb := sessionApp(t, "0s") user := insertUser(t, gdb, "reset-me@example.test", "correct-horse") code, err := classes.IssueResetCode(t.Context(), gdb, user) if err != nil { t.Fatal(err) } packed := strconv.FormatUint(uint64(user.ID), 10) + "!" + code ok := postJSON(t, controllers.ResetPassword(app), "/_user/api/v1/reset-password", `{"code":"`+packed+`","password":"new-password","password_confirmation":"new-password"}`) if ok.Code != http.StatusOK || ok.Body.String() != `{"message":"Password has been reset"}` { t.Fatalf("reset %d %s", ok.Code, ok.Body.String()) } loginNew := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"reset-me@example.test","password":"new-password"}`) if loginNew.Code != http.StatusOK { t.Fatalf("new password %d %s", loginNew.Code, loginNew.Body.String()) } loginOld := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"reset-me@example.test","password":"correct-horse"}`) if loginOld.Code != http.StatusUnauthorized { t.Fatalf("old password still works: %d %s", loginOld.Code, loginOld.Body.String()) } malformed := postJSON(t, controllers.ResetPassword(app), "/_user/api/v1/reset-password", `{"code":"not-a-code","password":"new-password","password_confirmation":"new-password"}`) if malformed.Code != http.StatusUnprocessableEntity || malformed.Body.String() != `{"error":"Invalid reset code"}` { t.Fatalf("malformed %d %s", malformed.Code, malformed.Body.String()) } wrong := postJSON(t, controllers.ResetPassword(app), "/_user/api/v1/reset-password", `{"code":"`+strconv.FormatUint(uint64(user.ID), 10)+`!nope","password":"new-password","password_confirmation":"new-password"}`) if wrong.Body.String() != `{"error":"Invalid or expired reset code"}` { t.Fatalf("wrong %d %s", wrong.Code, wrong.Body.String()) } } func TestActivate(t *testing.T) { app, gdb := sessionApp(t, "0s") token := loginToken(t, app, gdb, "activate-auth@example.test") rec := bearerJSON(t, controllers.Activate(app), "/_user/api/v1/activate", token, `{"code":"wrong"}`) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"is_activated":false`) { t.Fatalf("activate %d %s", rec.Code, rec.Body.String()) } already := loginToken(t, app, gdb, "activate-already@example.test") if err := gdb.Model(&models.User{}).Where("email = ?", "activate-already@example.test").Update("is_activated", true).Error; err != nil { t.Fatal(err) } locked := bearerJSON(t, controllers.Activate(app), "/_user/api/v1/activate", already, `{"code":"wrong"}`) assertWinterErrorPage(t, locked) } func TestActivateByCode(t *testing.T) { app, gdb := sessionApp(t, "0s") user := insertUser(t, gdb, "activate-code@example.test", "correct-horse") code, err := classes.IssueActivationCode(t.Context(), gdb, user) if err != nil { t.Fatal(err) } packed := strconv.FormatUint(uint64(user.ID), 10) + "!" + code rec := postJSON(t, controllers.ActivateByCode(app), "/_user/api/v1/activate-by-code", `{"code":"`+packed+`"}`) if rec.Code != http.StatusOK { t.Fatalf("activate-by-code %d %s", rec.Code, rec.Body.String()) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } token, _ := body["token"].(string) userBody, _ := body["user"].(map[string]any) if token == "" || userBody["is_activated"] != true || body["message"] != "Account activated" { t.Fatalf("body %#v", body) } fetch := bearer(t, controllers.Fetch(app), http.MethodGet, "/_user/api/v1/fetch", token) if fetch.Code != http.StatusOK { t.Fatalf("fetch %d %s", fetch.Code, fetch.Body.String()) } missing := postJSON(t, controllers.ActivateByCode(app), "/_user/api/v1/activate-by-code", `{}`) if missing.Code != http.StatusUnprocessableEntity { t.Fatalf("missing %d %s", missing.Code, missing.Body.String()) } var verr map[string]any if err := json.Unmarshal(missing.Body.Bytes(), &verr); err != nil { t.Fatal(err) } errorsBody, _ := verr["errors"].(map[string]any) if _, ok := errorsBody["code"]; !ok { t.Fatalf("errors = %#v", verr) } bad := postJSON(t, controllers.ActivateByCode(app), "/_user/api/v1/activate-by-code", `{"code":"`+strconv.FormatUint(uint64(user.ID), 10)+`!nope"}`) assertWinterErrorPage(t, bad) } func assertWinterErrorPage(t *testing.T, rec *httptest.ResponseRecorder) { t.Helper() if rec.Code != http.StatusInternalServerError { t.Fatalf("status %d body %s", rec.Code, rec.Body.String()) } if rec.Header().Get("Content-Type") != "text/html; charset=UTF-8" { t.Fatalf("content-type %q", rec.Header().Get("Content-Type")) } if !strings.Contains(rec.Body.String(), "Błąd strony") { t.Fatalf("missing winter error marker: %s", rec.Body.String()) } } func bearerJSON(t *testing.T, h http.HandlerFunc, path, token, body string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body)) req.Host = "example.com" req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rec := httptest.NewRecorder() h(rec, req) return rec }