Files
hanko/backend/handler/user_test.go
2025-09-25 19:15:20 +02:00

524 lines
14 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gofrs/uuid"
_ "github.com/lib/pq"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/v2/dto"
"github.com/teamhanko/hanko/backend/v2/persistence/models"
"github.com/teamhanko/hanko/backend/v2/test"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
)
func TestUserSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(userSuite))
}
type userSuite struct {
test.Suite
}
func (s *userSuite) TestUserHandler_Create_TokenInCookie() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
cfg := test.DefaultConfig
e := NewPublicRouter(&cfg, s.Storage, nil, nil)
body := UserCreateBody{Email: "jane.doe@example.com"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
user := dto.CreateUserResponse{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.False(user.UserID.IsNil())
count, err := s.Storage.GetUserPersister().Count([]uuid.UUID{}, "", "")
s.NoError(err)
s.Equal(1, count)
email, err := s.Storage.GetEmailPersister().FindByAddress(body.Email)
s.NoError(err)
s.NotNil(email)
s.Empty(rec.Header().Get("X-Auth-Token"))
cookies := rec.Result().Cookies()
rec.Result().Cookies()
s.NotEmpty(cookies)
for _, cookie := range cookies {
if cookie.Name == "hanko" {
s.Regexp(".*\\..*\\..*", cookie.Value)
}
}
}
}
func (s *userSuite) TestUserHandler_Create_TokenInHeader() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
cfg := test.DefaultConfig
cfg.Session.EnableAuthTokenHeader = true
e := NewPublicRouter(&cfg, s.Storage, nil, nil)
body := UserCreateBody{Email: "jane.doe@example.com"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
user := dto.CreateUserResponse{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.False(user.UserID.IsNil())
count, err := s.Storage.GetUserPersister().Count([]uuid.UUID{}, "", "")
s.NoError(err)
s.Equal(1, count)
email, err := s.Storage.GetEmailPersister().FindByAddress(body.Email)
s.NoError(err)
s.NotNil(email)
s.True(!slices.ContainsFunc(rec.Result().Cookies(), func(cookie *http.Cookie) bool {
return strings.EqualFold(cookie.Name, cfg.Session.Cookie.Name)
}))
responseToken := rec.Header().Get("X-Auth-Token")
s.NotEmpty(responseToken)
s.Regexp(".*\\..*\\..*", responseToken)
}
}
func (s *userSuite) TestUserHandler_Create_CaseInsensitive() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
body := UserCreateBody{Email: "JANE.DOE@EXAMPLE.COM"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
user := dto.CreateUserResponse{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.False(user.UserID.IsNil())
count, err := s.Storage.GetUserPersister().Count([]uuid.UUID{}, "", "")
s.NoError(err)
s.Equal(1, count)
email, err := s.Storage.GetEmailPersister().FindByAddress(strings.ToLower(body.Email))
s.NoError(err)
s.NotNil(email)
}
}
func (s *userSuite) TestUserHandler_Create_UserExists() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
body := UserCreateBody{Email: "john.doe@example.com"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusConflict, rec.Code)
}
func (s *userSuite) TestUserHandler_Create_UserExists_CaseInsensitive() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
body := UserCreateBody{Email: "JOHN.DOE@EXAMPLE.COM"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusConflict, rec.Code)
}
func (s *userSuite) TestUserHandler_Create_InvalidEmail() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"email": 123"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusBadRequest, rec.Code)
}
func (s *userSuite) TestUserHandler_Create_EmailMissing() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(`{"bogus": 123}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusBadRequest, rec.Code)
}
func (s *userSuite) TestUserHandler_Create_AccountCreationDisabled() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
testConfig := test.DefaultConfig
testConfig.Account.AllowSignup = false
e := NewPublicRouter(&testConfig, s.Storage, nil, nil)
body := UserCreateBody{Email: "jane.doe@example.com"}
bodyJson, err := json.Marshal(body)
s.NoError(err)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyJson))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusForbidden, rec.Code)
}
func (s *userSuite) TestUserHandler_Get() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", userId.String()), nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
s.Equal(rec.Code, http.StatusOK)
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.NoError(err)
s.Equal(userId.String(), user.ID.String())
s.Equal(len(user.WebauthnCredentials), 0)
}
}
func (s *userSuite) TestUserHandler_GetUserWithWebAuthnCredential() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/users/%s", userId.String()), nil)
rec := httptest.NewRecorder()
req.AddCookie(cookie)
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
s.Equal(rec.Code, http.StatusOK)
user := models.User{}
err := json.Unmarshal(rec.Body.Bytes(), &user)
s.Require().NoError(err)
s.Equal(userId.String(), user.ID.String())
s.Equal(len(user.WebauthnCredentials), 1)
}
}
func (s *userSuite) TestUserHandler_Get_InvalidUserId() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, "/users/invalidUserId", nil)
req.Header.Set("Content-Type", "application/json")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusForbidden, rec.Code)
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_InvalidEmail() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "123"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusBadRequest, rec.Code)
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_InvalidJson() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`"email": "123}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusBadRequest, rec.Code)
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_UserNotFound() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "unknownAddress@example.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
s.Equal(http.StatusNotFound, rec.Code)
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "john.doe@example.com"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
response := struct {
UserId string `json:"id"`
Verified bool `json:"verified"`
}{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
s.Equal(userId, response.UserId)
s.Equal(true, response.Verified)
}
}
func (s *userSuite) TestUserHandler_GetUserIdByEmail_CaseInsensitive() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := "b5dd5267-b462-48be-b70d-bcd6f1bbe7a5"
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
req := httptest.NewRequest(http.MethodPost, "/user", strings.NewReader(`{"email": "JOHN.DOE@EXAMPLE.COM"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
response := struct {
UserId string `json:"id"`
Verified bool `json:"verified"`
}{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
s.Equal(userId, response.UserId)
s.Equal(true, response.Verified)
}
}
func (s *userSuite) TestUserHandler_Me() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user_with_webauthn_credential")
s.Require().NoError(err)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodGet, "/me", nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusOK, rec.Code) {
response := struct {
UserId string `json:"id"`
}{}
err = json.Unmarshal(rec.Body.Bytes(), &response)
s.NoError(err)
s.Equal(userId.String(), response.UserId)
}
}
func (s *userSuite) TestUserHandler_Logout() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
userId := uuid.FromStringOrNil("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusNoContent, rec.Code) {
cookie := rec.Header().Get("Set-Cookie")
s.NotEmpty(cookie)
split := strings.Split(cookie, ";")
s.Equal("Max-Age=0", strings.TrimSpace(split[2]))
}
}
func (s *userSuite) TestUserHandler_Delete() {
if testing.Short() {
s.T().Skip("skipping test in short mode.")
}
err := s.LoadFixtures("../test/fixtures/user")
s.Require().NoError(err)
userId, _ := uuid.FromString("b5dd5267-b462-48be-b70d-bcd6f1bbe7a5")
cfg := test.DefaultConfig
cfg.Account.AllowDeletion = true
e := NewPublicRouter(&cfg, s.Storage, nil, nil)
cookie, err := generateSessionCookie(s.Storage, userId)
s.Require().NoError(err)
req := httptest.NewRequest(http.MethodDelete, "/user", nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if s.Equal(http.StatusNoContent, rec.Code) {
cookie := rec.Header().Get("Set-Cookie")
s.NotEmpty(cookie)
split := strings.Split(cookie, ";")
s.Equal("Max-Age=0", strings.TrimSpace(split[2]))
}
count, err := s.Storage.GetUserPersister().Count([]uuid.UUID{}, "", "")
s.NoError(err)
s.Equal(0, count)
}