package test import ( "RenewCMS/api/pkg/apperr" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/user" "errors" "net/http" "strings" "testing" "time" ) func TestPasswordLengthLimit(t *testing.T) { validator.InitValidator() longest := strings.Repeat("p", user.MaxPasswordBytes) tooLong := longest + "p" multibyteTooLong := strings.Repeat("é", 37) t.Run("The domain accepts 72 bytes and refuses 73, counting bytes rather than characters", func(t *testing.T) { if err := user.ValidatePassword(longest); err != nil { t.Errorf("72 bytes: expected no error, got %v", err) } if err := user.ValidatePassword(tooLong); !errors.Is(err, user.ErrPasswordTooLong) { t.Errorf("73 bytes: expected ErrPasswordTooLong, got %v", err) } if err := user.ValidatePassword(multibyteTooLong); !errors.Is(err, user.ErrPasswordTooLong) { t.Errorf("37 two-byte characters: expected ErrPasswordTooLong, got %v", err) } }) t.Run("Registering with a password over 72 bytes answers 400 and creates nothing", func(t *testing.T) { userRepo := newMockUserRepo() mailRepo := newMockMailRepo() router := newRegistrationGateRouter(userRepo, mailRepo, "https://cms.example.com") rec := registerRequest(t, router, "alice", multibyteTooLong, "alice@example.com") if rec.Code != http.StatusBadRequest || strings.TrimSpace(rec.Body.String()) != apperr.PasswordTooLongErrorMessage { t.Errorf("expected 400 %q, got %d %q", apperr.PasswordTooLongErrorMessage, rec.Code, rec.Body.String()) } if len(userRepo.users) != 0 || len(mailRepo.SentEmails) != 0 { t.Errorf("expected no user and no mail, got %d users and %d mails", len(userRepo.users), len(mailRepo.SentEmails)) } }) t.Run("Registering with exactly 72 bytes still works", func(t *testing.T) { userRepo := newMockUserRepo() router := newRegistrationGateRouter(userRepo, newMockMailRepo(), "https://cms.example.com") if rec := registerRequest(t, router, "alice", longest, "alice@example.com"); rec.Code != http.StatusOK { t.Errorf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } }) t.Run("Resetting to a password over 72 bytes answers 400 and keeps the old password and code", func(t *testing.T) { userRepo := newAliceResetPasswordRepo(time.Now().Add(time.Hour)) router := newResetPasswordRouter(userRepo, &stubHasher{}) rec := sendResetPasswordRequest(router, `{"email": "alice@example.com", "code": "reset-code-1", "password": "`+tooLong+`"}`) if rec.Code != http.StatusBadRequest || strings.TrimSpace(rec.Body.String()) != apperr.PasswordTooLongErrorMessage { t.Errorf("expected 400 %q, got %d %q", apperr.PasswordTooLongErrorMessage, rec.Code, rec.Body.String()) } stored := userRepo.users[1] if stored.Password != "hashed:oldpassword" || stored.PasswordResetCode != "hashed:reset-code-1" { t.Errorf("expected the account untouched, got %+v", stored) } }) t.Run("A wrong reset code is reported as such even with an over-long password", func(t *testing.T) { userRepo := newAliceResetPasswordRepo(time.Now().Add(time.Hour)) router := newResetPasswordRouter(userRepo, &stubHasher{}) rec := sendResetPasswordRequest(router, `{"email": "alice@example.com", "code": "wrong", "password": "`+tooLong+`"}`) if rec.Code != http.StatusForbidden { t.Errorf("expected 403, got %d", rec.Code) } }) t.Run("Accepting an invite with a password over 72 bytes answers 400 and creates nothing", func(t *testing.T) { f := newAcceptInviteFixture(t, 100) f.seedPending() usersBefore := len(f.users.users) rec := f.accept(acceptBody(acceptCode, "newadmin", tooLong)) if rec.Code != http.StatusBadRequest || strings.TrimSpace(rec.Body.String()) != apperr.PasswordTooLongErrorMessage { t.Errorf("expected 400 %q, got %d %q", apperr.PasswordTooLongErrorMessage, rec.Code, rec.Body.String()) } if len(f.users.users) != usersBefore { t.Errorf("expected no user created") } }) }